log: fix bug, expand test coverage and slightly optimize (#847)

* log: fix bugs expand coverage and optimise

* log: fix linter issues

* log: fix linter issue and pack methods in same file

* log: drop defer

* logger: move global check inside getfields  and remove unused test function

* logger: Increase note thanks @gloriouscode

* logger: wrap error with writer type

* logger: change variable name

* logger: change variable names and remove validsublogger func as it doesn't add functionality over a standard map call

* logs: error when unsupported output is applied on setup calls

* logs: add glorious suggestion

* logger: add protection to reduce olympic gold medal races

* logger: fix linter issues

* log: glorious niterinos
This commit is contained in:
Ryan O'Hara-Reid
2021-11-30 16:43:27 +11:00
committed by GitHub
parent f266bd14f5
commit ac692b04f4
27 changed files with 767 additions and 329 deletions

View File

@@ -30,7 +30,11 @@ const (
func TestMain(m *testing.M) {
if verbose {
testhelpers.EnableVerboseTestOutput()
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error
testhelpers.PostgresTestDatabase = testhelpers.GetConnectionDetails()

View File

@@ -64,9 +64,12 @@ func main() {
var bt *backtest.BackTest
var cfg *config.Config
logConfig := log.GenDefaultSettings()
log.GlobalLogConfig = &logConfig
log.SetupGlobalLogger()
log.GlobalLogConfig = log.GenDefaultSettings()
err = log.SetupGlobalLogger()
if err != nil {
fmt.Printf("Could not setup global logger. Error: %v.\n", err)
os.Exit(1)
}
cfg, err = config.ReadConfigFromFile(configPath)
if err != nil {

View File

@@ -101,11 +101,14 @@ func main() {
flag.BoolVar(&create, "create", false, "specifies whether to automatically create trello list, card and checklist in a given board")
flag.Parse()
var err error
c := log.GenDefaultSettings()
log.RWM.Lock()
log.GlobalLogConfig = &c
log.GlobalLogConfig = log.GenDefaultSettings()
log.RWM.Unlock()
log.SetupGlobalLogger()
err = log.SetupGlobalLogger()
if err != nil {
fmt.Printf("Could not setup global logger. Error: %v.\n", err)
os.Exit(1)
}
configData, err = readFileData(jsonFile)
if err != nil {
log.Error(log.Global, err)

View File

@@ -29,7 +29,7 @@ func TestMain(m *testing.M) {
c := log.GenDefaultSettings()
c.Enabled = convert.BoolPtr(true)
log.RWM.Lock()
log.GlobalLogConfig = &c
log.GlobalLogConfig = c
log.RWM.Unlock()
log.Infoln(log.Global, "set verbose to true for more detailed output")
var err error

View File

@@ -1267,7 +1267,7 @@ func (c *Config) CheckLoggerConfig() error {
defer m.Unlock()
if c.Logging.Enabled == nil || c.Logging.Output == "" {
c.Logging = log.GenDefaultSettings()
c.Logging = *log.GenDefaultSettings()
}
if c.Logging.AdvancedSettings.ShowLogSystemName == nil {

View File

@@ -30,7 +30,11 @@ var (
func TestMain(m *testing.M) {
if verbose {
testhelpers.EnableVerboseTestOutput()
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error

View File

@@ -33,7 +33,11 @@ var (
func TestMain(m *testing.M) {
if verbose {
testhelpers.EnableVerboseTestOutput()
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error
testhelpers.PostgresTestDatabase = testhelpers.GetConnectionDetails()

View File

@@ -30,7 +30,11 @@ var (
func TestMain(m *testing.M) {
if verbose {
testhelpers.EnableVerboseTestOutput()
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error
testhelpers.PostgresTestDatabase = testhelpers.GetConnectionDetails()

View File

@@ -30,7 +30,11 @@ var (
func TestMain(m *testing.M) {
if verbose {
testhelpers.EnableVerboseTestOutput()
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error

View File

@@ -28,7 +28,11 @@ func TestMain(m *testing.M) {
}
if verbose {
testhelpers.EnableVerboseTestOutput()
err = testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
t := m.Run()

View File

@@ -33,7 +33,11 @@ var (
func TestMain(m *testing.M) {
if verbose {
testhelpers.EnableVerboseTestOutput()
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error
testhelpers.PostgresTestDatabase = testhelpers.GetConnectionDetails()

View File

@@ -29,7 +29,11 @@ var (
func TestMain(m *testing.M) {
if verbose {
testhelpers.EnableVerboseTestOutput()
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error

View File

@@ -118,14 +118,15 @@ func migrateDB(db *sql.DB) error {
}
// EnableVerboseTestOutput enables debug output for SQL queries
func EnableVerboseTestOutput() {
c := log.GenDefaultSettings()
func EnableVerboseTestOutput() error {
log.RWM.Lock()
log.GlobalLogConfig = &c
log.GlobalLogConfig = log.GenDefaultSettings()
log.RWM.Unlock()
log.SetupGlobalLogger()
if err := log.SetupGlobalLogger(); err != nil {
return err
}
DBLogger := database.Logger{}
boil.DebugMode = true
boil.DebugWriter = DBLogger
return nil
}

View File

@@ -84,12 +84,18 @@ func NewFromSettings(settings *Settings, flagSet map[string]bool) (*Engine, erro
b.Config, err = loadConfigWithSettings(settings, flagSet)
if err != nil {
return nil, fmt.Errorf("failed to load config. Err: %s", err)
return nil, fmt.Errorf("failed to load config. Err: %w", err)
}
if *b.Config.Logging.Enabled {
gctlog.SetupGlobalLogger()
gctlog.SetupSubLoggers(b.Config.Logging.SubLoggers)
err = gctlog.SetupGlobalLogger()
if err != nil {
return nil, fmt.Errorf("failed to setup global logger. %w", err)
}
err = gctlog.SetupSubLoggers(b.Config.Logging.SubLoggers)
if err != nil {
return nil, fmt.Errorf("failed to setup sub loggers. %w", err)
}
gctlog.Infoln(gctlog.Global, "Logger initialised.")
}
@@ -99,12 +105,12 @@ func NewFromSettings(settings *Settings, flagSet map[string]bool) (*Engine, erro
err = utils.AdjustGoMaxProcs(settings.GoMaxProcs)
if err != nil {
return nil, fmt.Errorf("unable to adjust runtime GOMAXPROCS value. Err: %s", err)
return nil, fmt.Errorf("unable to adjust runtime GOMAXPROCS value. Err: %w", err)
}
b.gctScriptManager, err = gctscript.NewManager(&b.Config.GCTScript)
if err != nil {
return nil, fmt.Errorf("failed to create script manager. Err: %s", err)
return nil, fmt.Errorf("failed to create script manager. Err: %w", err)
}
b.ExchangeManager = SetupExchangeManager()

View File

@@ -3,6 +3,7 @@ package exchange
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
@@ -29,11 +30,13 @@ const (
)
func TestMain(m *testing.M) {
c := log.GenDefaultSettings()
log.RWM.Lock()
log.GlobalLogConfig = &c
log.GlobalLogConfig = log.GenDefaultSettings()
log.RWM.Unlock()
log.SetupGlobalLogger()
if err := log.SetupGlobalLogger(); err != nil {
fmt.Println("Cannot setup global logger. Error:", err)
os.Exit(1)
}
os.Exit(m.Run())
}

View File

@@ -2,6 +2,7 @@ package kline
import (
"errors"
"fmt"
"io/ioutil"
"math/rand"
"os"
@@ -511,7 +512,11 @@ func TestItem_SortCandlesByTimestamp(t *testing.T) {
func setupTest(t *testing.T) {
t.Helper()
if verbose {
testhelpers.EnableVerboseTestOutput()
err := testhelpers.EnableVerboseTestOutput()
if err != nil {
fmt.Printf("failed to enable verbose test output: %v", err)
os.Exit(1)
}
}
var err error

View File

@@ -33,7 +33,7 @@ func TestMain(m *testing.M) {
c := log.GenDefaultSettings()
c.Enabled = convert.BoolPtr(false)
log.RWM.Lock()
log.GlobalLogConfig = &c
log.GlobalLogConfig = c
log.RWM.Unlock()
os.Exit(m.Run())
}

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
"strings"
"time"
)
@@ -13,28 +12,8 @@ var (
errSubLoggerAlreadyregistered = errors.New("sub logger already registered")
)
// NewSubLogger allows for a new sub logger to be registered.
func NewSubLogger(name string) (*SubLogger, error) {
if name == "" {
return nil, errEmptyLoggerName
}
name = strings.ToUpper(name)
if _, ok := SubLoggers[name]; ok {
return nil, errSubLoggerAlreadyregistered
}
return registerNewSubLogger(name), nil
}
// SetOutput overrides the default output with a new writer
func (s *SubLogger) SetOutput(o io.Writer) {
RWM.Lock()
defer RWM.Unlock()
s.output = o
}
func newLogger(c *Config) *Logger {
return &Logger{
func newLogger(c *Config) Logger {
return Logger{
Timestamp: c.AdvancedSettings.TimeStampFormat,
Spacer: c.AdvancedSettings.Spacer,
ErrorHeader: c.AdvancedSettings.Headers.Error,
@@ -50,29 +29,28 @@ func (l *Logger) newLogEvent(data, header, slName string, w io.Writer) error {
return errors.New("io.Writer not set")
}
e, ok := eventPool.Get().(*Event)
pool, ok := eventPool.Get().(*[]byte)
if !ok {
return errors.New("unable to type asset event")
return errors.New("unable to type assert slice of bytes pointer")
}
e.output = w
e.data = append(e.data, []byte(header)...)
if l.ShowLogSystemName {
e.data = append(e.data, l.Spacer...)
e.data = append(e.data, slName...)
}
e.data = append(e.data, l.Spacer...)
if l.Timestamp != "" {
e.data = time.Now().AppendFormat(e.data, l.Timestamp)
}
e.data = append(e.data, l.Spacer...)
e.data = append(e.data, []byte(data)...)
if data == "" || data[len(data)-1] != '\n' {
e.data = append(e.data, '\n')
}
_, err := e.output.Write(e.data)
e.data = e.data[:0]
eventPool.Put(e)
*pool = append(*pool, header...)
if l.ShowLogSystemName {
*pool = append(*pool, l.Spacer...)
*pool = append(*pool, slName...)
}
*pool = append(*pool, l.Spacer...)
if l.Timestamp != "" {
*pool = time.Now().AppendFormat(*pool, l.Timestamp)
}
*pool = append(*pool, l.Spacer...)
*pool = append(*pool, data...)
if data == "" || data[len(data)-1] != '\n' {
*pool = append(*pool, '\n')
}
_, err := w.Write(*pool)
*pool = (*pool)[:0]
eventPool.Put(pool)
return err
}
@@ -82,30 +60,25 @@ func CloseLogger() error {
return GlobalLogFile.Close()
}
func validSubLogger(s string) (bool, *SubLogger) {
if v, found := SubLoggers[s]; found {
return true, v
}
return false, nil
}
// Level retries the current sublogger levels
func Level(s string) (*Levels, error) {
found, logger := validSubLogger(s)
func Level(name string) (Levels, error) {
RWM.RLock()
defer RWM.RUnlock()
subLogger, found := SubLoggers[name]
if !found {
return nil, fmt.Errorf("logger %v not found", s)
return Levels{}, fmt.Errorf("logger %s not found", name)
}
return &logger.Levels, nil
return subLogger.levels, nil
}
// SetLevel sets sublogger levels
func SetLevel(s, level string) (*Levels, error) {
found, logger := validSubLogger(s)
func SetLevel(s, level string) (Levels, error) {
RWM.Lock()
defer RWM.Unlock()
subLogger, found := SubLoggers[s]
if !found {
return nil, fmt.Errorf("logger %v not found", s)
return Levels{}, fmt.Errorf("sub logger %v not found", s)
}
logger.Levels = splitLevel(level)
return &logger.Levels, nil
subLogger.SetLevels(splitLevel(level))
return subLogger.levels, nil
}

View File

@@ -1,63 +1,75 @@
package log
import (
"errors"
"fmt"
"io"
)
var (
errWriterAlreadyLoaded = errors.New("io.Writer already loaded")
errWriterNotFound = errors.New("io.Writer not found")
)
// Add appends a new writer to the multiwriter slice
func (mw *multiWriter) Add(writer io.Writer) {
func (mw *multiWriter) Add(writer io.Writer) error {
mw.mu.Lock()
defer mw.mu.Unlock()
for i := range mw.writers {
if mw.writers[i] == writer {
return errWriterAlreadyLoaded
}
}
mw.writers = append(mw.writers, writer)
mw.mu.Unlock()
return nil
}
// Remove removes existing writer from multiwriter slice
func (mw *multiWriter) Remove(writer io.Writer) {
func (mw *multiWriter) Remove(writer io.Writer) error {
mw.mu.Lock()
var removeIDs []int
defer mw.mu.Unlock()
for i := range mw.writers {
if mw.writers[i] == writer {
removeIDs = append(removeIDs, i)
if mw.writers[i] != writer {
continue
}
}
for x := range removeIDs {
mw.writers[x] = mw.writers[len(mw.writers)-1]
mw.writers[i] = mw.writers[len(mw.writers)-1]
mw.writers[len(mw.writers)-1] = nil
mw.writers = mw.writers[:len(mw.writers)-1]
return nil
}
mw.mu.Unlock()
return errWriterNotFound
}
// Write concurrent safe Write for each writer
func (mw *multiWriter) Write(p []byte) (n int, err error) {
func (mw *multiWriter) Write(p []byte) (int, error) {
type data struct {
n int
err error
}
mw.mu.RLock()
defer mw.mu.RUnlock()
results := make(chan data, len(mw.writers))
for _, wr := range mw.writers {
go func(w io.Writer, p []byte, ch chan data) {
n, err = w.Write(p)
mw.mu.RLock()
defer mw.mu.RUnlock()
for x := range mw.writers {
go func(w io.Writer, p []byte, ch chan<- data) {
n, err := w.Write(p)
if err != nil {
ch <- data{n, err}
ch <- data{n, fmt.Errorf("%T %w", w, err)}
return
}
if n != len(p) {
ch <- data{n, io.ErrShortWrite}
ch <- data{n, fmt.Errorf("%T %w", w, io.ErrShortWrite)}
return
}
ch <- data{n, nil}
}(wr, p, results)
}(mw.writers[x], p, results)
}
for range mw.writers {
// NOTE: These results do not necessarily reflect the current io.writer
// due to the go scheduler and writer finishing at different times, the
// response coming from the channel might not match up with the for loop
// writer.
d := <-results
if d.err != nil {
return d.n, d.err
@@ -67,8 +79,13 @@ func (mw *multiWriter) Write(p []byte) (n int, err error) {
}
// MultiWriter make and return a new copy of multiWriter
func MultiWriter(writers ...io.Writer) io.Writer {
w := make([]io.Writer, len(writers))
copy(w, writers)
return &multiWriter{writers: w}
func MultiWriter(writers ...io.Writer) (*multiWriter, error) {
mw := &multiWriter{}
for x := range writers {
err := mw.Add(writers[x])
if err != nil {
return nil, err
}
}
return mw, nil
}

View File

@@ -1,6 +1,7 @@
package log
import (
"errors"
"fmt"
"os"
"path/filepath"
@@ -9,16 +10,20 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/file"
)
var (
errExceedsMaxFileSize = errors.New("exceeds max file size")
errFileNameIsEmpty = errors.New("filename is empty")
)
// Write implementation to satisfy io.Writer handles length check and rotation
func (r *Rotate) Write(output []byte) (n int, err error) {
r.mu.Lock()
defer r.mu.Unlock()
outputLen := int64(len(output))
if outputLen > r.maxSize() {
return 0, fmt.Errorf(
"write length %v exceeds max file size %v", outputLen, r.maxSize(),
"write length %v %w %v", outputLen, errExceedsMaxFileSize, r.maxSize(),
)
}
@@ -40,13 +45,11 @@ func (r *Rotate) Write(output []byte) (n int, err error) {
n, err = r.output.Write(output)
r.size += int64(n)
return n, err
}
func (r *Rotate) openOrCreateFile(n int64) error {
logFile := filepath.Join(LogPath, r.FileName)
info, err := os.Stat(logFile)
if err != nil {
if os.IsNotExist(err) {
@@ -73,6 +76,9 @@ func (r *Rotate) openOrCreateFile(n int64) error {
}
func (r *Rotate) openNew() error {
if r.FileName == "" {
return fmt.Errorf("cannot open new file: %w", errFileNameIsEmpty)
}
name := filepath.Join(LogPath, r.FileName)
_, err := os.Stat(name)
@@ -93,7 +99,6 @@ func (r *Rotate) openNew() error {
r.output = file
r.size = 0
return nil
}
@@ -118,17 +123,12 @@ func (r *Rotate) rotateFile() (err error) {
if err != nil {
return
}
err = r.openNew()
if err != nil {
return
}
return nil
return r.openNew()
}
func (r *Rotate) maxSize() int64 {
if r.MaxSize == 0 {
return int64(defaultMaxSize * megabyte)
return defaultMaxSize * megabyte
}
return r.MaxSize * int64(megabyte)
return r.MaxSize * megabyte
}

View File

@@ -6,8 +6,8 @@ import (
)
const (
defaultMaxSize = 250
megabyte = 1024 * 1024
defaultMaxSize int64 = 250
megabyte int64 = 1024 * 1024
)
// Rotate struct for each instance of Rotate

View File

@@ -1,40 +1,50 @@
package log
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/thrasher-corp/gocryptotrader/common/convert"
)
func getWriters(s *SubLoggerConfig) io.Writer {
mw := MultiWriter()
m := mw.(*multiWriter) // nolint // type assert not required
var (
errSubloggerConfigIsNil = errors.New("sublogger config is nil")
errUnhandledOutputWriter = errors.New("unhandled output writer")
)
func getWriters(s *SubLoggerConfig) (io.Writer, error) {
if s == nil {
return nil, errSubloggerConfigIsNil
}
var writers []io.Writer
outputWriters := strings.Split(s.Output, "|")
for x := range outputWriters {
switch outputWriters[x] {
var writer io.Writer
switch strings.ToLower(outputWriters[x]) {
case "stdout", "console":
m.Add(os.Stdout)
writer = os.Stdout
case "stderr":
m.Add(os.Stderr)
writer = os.Stderr
case "file":
if FileLoggingConfiguredCorrectly {
m.Add(GlobalLogFile)
writer = GlobalLogFile
}
default:
m.Add(ioutil.Discard)
// Note: Do not want to add a ioutil.discard here as this adds
// additional routines for every write for no reason.
return nil, fmt.Errorf("%w: %s", errUnhandledOutputWriter, outputWriters[x])
}
writers = append(writers, writer)
}
return m
return MultiWriter(writers...)
}
// GenDefaultSettings return struct with known sane/working logger settings
func GenDefaultSettings() Config {
return Config{
func GenDefaultSettings() *Config {
return &Config{
Enabled: convert.BoolPtr(true),
SubLoggerConfig: SubLoggerConfig{
Level: "INFO|DEBUG|WARN|ERROR",
@@ -59,34 +69,40 @@ func GenDefaultSettings() Config {
}
}
func configureSubLogger(logger, levels string, output io.Writer) error {
found, logPtr := validSubLogger(logger)
func configureSubLogger(subLogger, levels string, output io.Writer) error {
RWM.Lock()
defer RWM.Unlock()
logPtr, found := SubLoggers[subLogger]
if !found {
return fmt.Errorf("logger %v not found", logger)
return fmt.Errorf("sub logger %v not found", subLogger)
}
logPtr.output = output
logPtr.Levels = splitLevel(levels)
SubLoggers[logger] = logPtr
logPtr.SetOutput(output)
logPtr.SetLevels(splitLevel(levels))
SubLoggers[subLogger] = logPtr
return nil
}
// SetupSubLoggers configure all sub loggers with provided configuration values
func SetupSubLoggers(s []SubLoggerConfig) {
func SetupSubLoggers(s []SubLoggerConfig) error {
for x := range s {
output := getWriters(&s[x])
err := configureSubLogger(strings.ToUpper(s[x].Name), s[x].Level, output)
output, err := getWriters(&s[x])
if err != nil {
continue
return err
}
err = configureSubLogger(strings.ToUpper(s[x].Name), s[x].Level, output)
if err != nil {
return err
}
}
return nil
}
// SetupGlobalLogger setup the global loggers with the default global config values
func SetupGlobalLogger() {
func SetupGlobalLogger() error {
RWM.Lock()
defer RWM.Unlock()
if FileLoggingConfiguredCorrectly {
GlobalLogFile = &Rotate{
FileName: GlobalLogConfig.LoggerFileConfig.FileName,
@@ -96,12 +112,15 @@ func SetupGlobalLogger() {
}
for x := range SubLoggers {
SubLoggers[x].Levels = splitLevel(GlobalLogConfig.Level)
SubLoggers[x].output = getWriters(&GlobalLogConfig.SubLoggerConfig)
SubLoggers[x].SetLevels(splitLevel(GlobalLogConfig.Level))
writers, err := getWriters(&GlobalLogConfig.SubLoggerConfig)
if err != nil {
return err
}
SubLoggers[x].SetOutput(writers)
}
logger = newLogger(GlobalLogConfig)
RWM.Unlock()
return nil
}
func splitLevel(level string) (l Levels) {
@@ -121,16 +140,16 @@ func splitLevel(level string) (l Levels) {
return
}
func registerNewSubLogger(logger string) *SubLogger {
temp := SubLogger{
name: strings.ToUpper(logger),
func registerNewSubLogger(subLogger string) *SubLogger {
temp := &SubLogger{
name: strings.ToUpper(subLogger),
output: os.Stdout,
levels: splitLevel("INFO|WARN|DEBUG|ERROR"),
}
temp.Levels = splitLevel("INFO|WARN|DEBUG|ERROR")
SubLoggers[logger] = &temp
return &temp
RWM.Lock()
SubLoggers[subLogger] = temp
RWM.Unlock()
return temp
}
// register all loggers at package init()

View File

@@ -3,7 +3,9 @@ package log
import (
"bytes"
"errors"
"io"
"io/ioutil"
"log"
"os"
"strings"
"testing"
@@ -12,11 +14,25 @@ import (
)
func TestMain(m *testing.M) {
setupTestLoggers()
os.Exit(m.Run())
err := setupTestLoggers()
if err != nil {
log.Fatal("cannot set up test loggers", err)
}
tempDir, err := ioutil.TempDir(os.TempDir(), "")
if err != nil {
log.Fatal("Cannot create temporary file", err)
}
log.Println("temp dir created at:", tempDir)
LogPath = tempDir
r := m.Run()
err = os.Remove(tempDir)
if err != nil {
log.Println("failed to remove temp file:", tempDir)
}
os.Exit(r)
}
func setupTestLoggers() {
func setupTestLoggers() error {
logTest := Config{
Enabled: convert.BoolPtr(true),
SubLoggerConfig: SubLoggerConfig{
@@ -36,7 +52,7 @@ func setupTestLoggers() {
},
SubLoggers: []SubLoggerConfig{
{
Name: "TEST",
Name: "lOg",
Level: "INFO|DEBUG|WARN|ERROR",
Output: "stdout",
}},
@@ -44,11 +60,13 @@ func setupTestLoggers() {
RWM.Lock()
GlobalLogConfig = &logTest
RWM.Unlock()
SetupGlobalLogger()
SetupSubLoggers(logTest.SubLoggers)
if err := SetupGlobalLogger(); err != nil {
return err
}
return SetupSubLoggers(logTest.SubLoggers)
}
func SetupDisabled() {
func SetupDisabled() error {
logTest := Config{
Enabled: convert.BoolPtr(false),
}
@@ -56,8 +74,10 @@ func SetupDisabled() {
GlobalLogConfig = &logTest
RWM.Unlock()
SetupGlobalLogger()
SetupSubLoggers(logTest.SubLoggers)
if err := SetupGlobalLogger(); err != nil {
return err
}
return SetupSubLoggers(logTest.SubLoggers)
}
func BenchmarkInfo(b *testing.B) {
@@ -67,43 +87,151 @@ func BenchmarkInfo(b *testing.B) {
}
}
func SetupTestDisabled(t *testing.T) {
t.Helper()
SetupDisabled()
}
func TestAddWriter(t *testing.T) {
mw := MultiWriter()
m := mw.(*multiWriter) // nolint // type assert not required
t.Parallel()
_, err := MultiWriter(ioutil.Discard, ioutil.Discard)
if !errors.Is(err, errWriterAlreadyLoaded) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWriterAlreadyLoaded)
}
m.Add(ioutil.Discard)
m.Add(os.Stdin)
m.Add(os.Stdout)
mw, err := MultiWriter()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
err = mw.Add(ioutil.Discard)
if err != nil {
t.Fatal(err)
}
err = mw.Add(os.Stdin)
if err != nil {
t.Fatal(err)
}
err = mw.Add(os.Stdout)
if err != nil {
t.Fatal(err)
}
if total := len(m.writers); total != 3 {
if total := len(mw.writers); total != 3 {
t.Errorf("expected m.Writers to be 3 %v", total)
}
}
func TestRemoveWriter(t *testing.T) {
mw := MultiWriter()
m := mw.(*multiWriter) // nolint // type assert not required
t.Parallel()
mw, err := MultiWriter()
if err != nil {
t.Fatal(err)
}
err = mw.Add(ioutil.Discard)
if err != nil {
t.Fatal(err)
}
err = mw.Add(os.Stdin)
if err != nil {
t.Fatal(err)
}
err = mw.Add(os.Stdout)
if err != nil {
t.Fatal(err)
}
total := len(mw.writers)
err = mw.Remove(os.Stdin)
if err != nil {
t.Fatal(err)
}
err = mw.Remove(os.Stdout)
if err != nil {
t.Fatal(err)
}
err = mw.Remove(&bytes.Buffer{})
if !errors.Is(err, errWriterNotFound) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWriterNotFound)
}
m.Add(ioutil.Discard)
m.Add(os.Stdin)
m.Add(os.Stdout)
if len(mw.writers) != total-2 {
t.Errorf("expected m.Writers to be %v got %v", total-2, len(mw.writers))
}
}
total := len(m.writers)
type WriteShorter struct{}
m.Remove(os.Stdin)
m.Remove(os.Stdout)
func (w *WriteShorter) Write(p []byte) (int, error) {
return 1, nil
}
if len(m.writers) != total-2 {
t.Errorf("expected m.Writers to be %v got %v", total-2, len(m.writers))
type WriteError struct{}
func (w *WriteError) Write(p []byte) (int, error) {
return 0, errWriteError
}
var errWriteError = errors.New("write error")
func TestMultiWriterWrite(t *testing.T) {
t.Parallel()
mw, err := MultiWriter(ioutil.Discard, &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
payload := "woooooooooooooooooooooooooooooooooooow"
l, err := mw.Write([]byte(payload))
if err != nil {
t.Fatal(err)
}
if l != len(payload) {
t.Fatal("unexpected return")
}
mw, err = MultiWriter(&WriteShorter{}, ioutil.Discard)
if err != nil {
t.Fatal(err)
}
_, err = mw.Write([]byte(payload))
if !errors.Is(err, io.ErrShortWrite) {
t.Fatalf("received: '%v' but expected: '%v'", err, io.ErrShortWrite)
}
mw, err = MultiWriter(&WriteError{}, ioutil.Discard)
if err != nil {
t.Fatal(err)
}
_, err = mw.Write([]byte(payload))
if !errors.Is(err, errWriteError) {
t.Fatalf("received: '%v' but expected: '%v'", err, errWriteError)
}
}
func TestGetWriters(t *testing.T) {
t.Parallel()
_, err := getWriters(nil)
if !errors.Is(err, errSubloggerConfigIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errSubloggerConfigIsNil)
}
outputWriters := "stDout|stderr|filE"
_, err = getWriters(&SubLoggerConfig{Output: outputWriters})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
outputWriters = "stdout|stderr|file|noobs"
_, err = getWriters(&SubLoggerConfig{Output: outputWriters})
if !errors.Is(err, errUnhandledOutputWriter) {
t.Fatalf("received: '%v' but expected: '%v'", err, errUnhandledOutputWriter)
}
}
func TestGenDefaultSettings(t *testing.T) {
t.Parallel()
if cfg := GenDefaultSettings(); cfg.Enabled == nil {
t.Fatal("unexpected items in struct")
}
}
func TestLevel(t *testing.T) {
t.Parallel()
_, err := Level("LOG")
if err != nil {
t.Errorf("Failed to get log %s levels skipping", err)
@@ -116,6 +244,7 @@ func TestLevel(t *testing.T) {
}
func TestSetLevel(t *testing.T) {
t.Parallel()
newLevel, err := SetLevel("LOG", "ERROR")
if err != nil {
t.Skipf("Failed to get log %s levels skipping", err)
@@ -135,32 +264,21 @@ func TestSetLevel(t *testing.T) {
}
}
func TestValidSubLogger(t *testing.T) {
b, logPtr := validSubLogger("LOG")
if !b {
t.Skip("validSubLogger() should return found, pointer if valid logger found")
}
if logPtr == nil {
t.Error("validSubLogger() should return a pointer and not nil")
}
}
func TestCloseLogger(t *testing.T) {
t.Parallel()
if err := CloseLogger(); err != nil {
t.Errorf("CloseLogger() failed %v", err)
}
}
func TestConfigureSubLogger(t *testing.T) {
t.Parallel()
err := configureSubLogger("LOG", "INFO", os.Stdin)
if err != nil {
t.Skipf("configureSubLogger() returned unexpected error %v", err)
}
if (Global.Levels != Levels{
Info: true,
Debug: false,
}) {
levels := Global.GetLevels()
if (levels != Levels{Info: true, Debug: false}) {
t.Error("configureSubLogger() incorrectly configure subLogger")
}
if Global.name != "LOG" {
@@ -169,6 +287,7 @@ func TestConfigureSubLogger(t *testing.T) {
}
func TestSplitLevel(t *testing.T) {
t.Parallel()
levelsInfoDebug := splitLevel("INFO|DEBUG")
expected := Levels{
@@ -184,7 +303,9 @@ func TestSplitLevel(t *testing.T) {
}
func BenchmarkInfoDisabled(b *testing.B) {
SetupDisabled()
if err := SetupDisabled(); err != nil {
b.Fatal(err)
}
b.ResetTimer()
for n := 0; n < b.N; n++ {
@@ -207,8 +328,11 @@ func BenchmarkInfoln(b *testing.B) {
}
func TestNewLogEvent(t *testing.T) {
t.Parallel()
w := &bytes.Buffer{}
RWM.Lock()
err := logger.newLogEvent("out", "header", "SUBLOGGER", w)
RWM.Unlock()
if err != nil {
t.Fatal(err)
}
@@ -217,43 +341,209 @@ func TestNewLogEvent(t *testing.T) {
t.Error("newLogEvent() failed expected output got empty string")
}
RWM.Lock()
err = logger.newLogEvent("out", "header", "SUBLOGGER", nil)
RWM.Unlock()
if err == nil {
t.Error("Error expected with output is set to nil")
}
}
func TestInfo(t *testing.T) {
t.Parallel()
w := &bytes.Buffer{}
sl := registerNewSubLogger("TESTYMCTESTALOT")
sl.Levels = splitLevel("INFO|WARN|DEBUG|ERROR")
sl.output = w
sl := registerNewSubLogger("TESTYMCTESTALOTINFO")
sl.SetLevels(splitLevel("INFO|WARN|DEBUG|ERROR"))
sl.SetOutput(w)
Info(sl, "Hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
sl.output = nil
w.Reset()
_, err := SetLevel("TESTYMCTESTALOT", "INFO")
Infof(sl, "%s", "hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
Infoln(sl, "hello", "hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
_, err := SetLevel("TESTYMCTESTALOTINFO", "")
if err != nil {
t.Fatal(err)
}
Info(sl, "HelloHello")
if w.String() != "" {
t.Error("Expected output buffer to be empty but wrote to output", w.String())
}
Infoln(sl, "HelloHello")
if w.String() != "" {
t.Error("Expected output buffer to be empty but wrote to output", w.String())
}
}
func TestDebug(t *testing.T) {
t.Parallel()
w := &bytes.Buffer{}
sl := registerNewSubLogger("TESTYMCTESTALOTDEBUG")
sl.SetLevels(splitLevel("INFO|WARN|DEBUG|ERROR"))
sl.SetOutput(w)
Debug(sl, "Hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
Debugf(sl, "%s", "hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
Debugln(sl, "hello", "hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
_, err := SetLevel("TESTYMCTESTALOTDEBUG", "")
if err != nil {
t.Fatal(err)
}
Debug(sl, "HelloHello")
if w.String() != "" {
t.Error("Expected output buffer to be empty but Debug wrote to output")
t.Error("Expected output buffer to be empty but wrote to output", w.String())
}
Debugln(sl, "HelloHello")
if w.String() != "" {
t.Error("Expected output buffer to be empty but wrote to output", w.String())
}
}
func TestWarn(t *testing.T) {
t.Parallel()
w := &bytes.Buffer{}
sl := registerNewSubLogger("TESTYMCTESTALOTWARN")
sl.SetLevels(splitLevel("INFO|WARN|DEBUG|ERROR"))
sl.SetOutput(w)
Warn(sl, "Hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
Warnf(sl, "%s", "hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
Warnln(sl, "hello", "hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
_, err := SetLevel("TESTYMCTESTALOTWARN", "")
if err != nil {
t.Fatal(err)
}
Warn(sl, "HelloHello")
if w.String() != "" {
t.Error("Expected output buffer to be empty but wrote to output", w.String())
}
Warnln(sl, "HelloHello")
if w.String() != "" {
t.Error("Expected output buffer to be empty but wrote to output", w.String())
}
}
func TestError(t *testing.T) {
t.Parallel()
w := &bytes.Buffer{}
sl := registerNewSubLogger("TESTYMCTESTALOTERROR")
sl.SetLevels(splitLevel("INFO|WARN|DEBUG|ERROR"))
sl.SetOutput(w)
Error(sl, "Hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
Errorf(sl, "%s", "hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
Errorln(sl, "hello", "hello")
if w.String() == "" {
t.Error("expected Info() to write output to buffer")
}
w.Reset()
_, err := SetLevel("TESTYMCTESTALOTERROR", "")
if err != nil {
t.Fatal(err)
}
Error(sl, "HelloHello")
if w.String() != "" {
t.Error("Expected output buffer to be empty but wrote to output", w.String())
}
Errorln(sl, "HelloHello")
if w.String() != "" {
t.Error("Expected output buffer to be empty but wrote to output", w.String())
}
}
func TestSubLoggerName(t *testing.T) {
t.Parallel()
w := &bytes.Buffer{}
registerNewSubLogger("sublogger")
RWM.Lock()
err := logger.newLogEvent("out", "header", "SUBLOGGER", w)
RWM.Unlock()
if err != nil {
t.Fatal(err)
}
@@ -261,9 +551,13 @@ func TestSubLoggerName(t *testing.T) {
t.Error("Expected SUBLOGGER in output")
}
RWM.Lock()
logger.ShowLogSystemName = false
RWM.Unlock()
w.Reset()
RWM.Lock()
err = logger.newLogEvent("out", "header", "SUBLOGGER", w)
RWM.Unlock()
if err != nil {
t.Fatal(err)
}
@@ -273,6 +567,7 @@ func TestSubLoggerName(t *testing.T) {
}
func TestNewSubLogger(t *testing.T) {
t.Parallel()
_, err := NewSubLogger("")
if !errors.Is(err, errEmptyLoggerName) {
t.Fatalf("received: %v but expected: %v", err, errEmptyLoggerName)
@@ -290,3 +585,57 @@ func TestNewSubLogger(t *testing.T) {
t.Fatalf("received: %v but expected: %v", err, errSubLoggerAlreadyregistered)
}
}
func BenchmarkNewLogEvent(b *testing.B) {
var bro bytes.Buffer
l := Logger{Spacer: " "}
for i := 0; i < b.N; i++ {
_ = l.newLogEvent("somedata", "header", "sublog", &bro)
}
}
func TestRotateWrite(t *testing.T) {
t.Parallel()
empty := Rotate{Rotate: convert.BoolPtr(true), FileName: "test.txt"}
payload := make([]byte, defaultMaxSize*megabyte+1)
_, err := empty.Write(payload)
if !errors.Is(err, errExceedsMaxFileSize) {
t.Fatalf("received: %v but expected: %v", err, errExceedsMaxFileSize)
}
empty.MaxSize = 1
payload = make([]byte, 1*megabyte+1)
_, err = empty.Write(payload)
if !errors.Is(err, errExceedsMaxFileSize) {
t.Fatalf("received: %v but expected: %v", err, errExceedsMaxFileSize)
}
// test write
payload = make([]byte, 1*megabyte-1)
_, err = empty.Write(payload)
if !errors.Is(err, nil) {
t.Fatalf("received: %v but expected: %v", err, nil)
}
// test rotate
payload = make([]byte, 1*megabyte)
_, err = empty.Write(payload)
if !errors.Is(err, nil) {
t.Fatalf("received: %v but expected: %v", err, nil)
}
}
func TestOpenNew(t *testing.T) {
t.Parallel()
empty := Rotate{}
err := empty.openNew()
if !errors.Is(err, errFileNameIsEmpty) {
t.Fatalf("received: %v but expected: %v", err, errFileNameIsEmpty)
}
empty.FileName = "wow.txt"
err = empty.openNew()
if !errors.Is(err, nil) {
t.Fatalf("received: %v but expected: %v", err, nil)
}
}

View File

@@ -10,10 +10,12 @@ const (
spacer = " | "
// DefaultMaxFileSize for logger rotation file
DefaultMaxFileSize int64 = 100
defaultCapacityForSliceOfBytes = 100
)
var (
logger = &Logger{}
logger = Logger{}
// FileLoggingConfiguredCorrectly flag set during config check if file logging meets requirements
FileLoggingConfiguredCorrectly bool
// GlobalLogConfig holds global configuration options for logger
@@ -23,9 +25,8 @@ var (
eventPool = &sync.Pool{
New: func() interface{} {
return &Event{
data: make([]byte, 0, 80),
}
sliceOBytes := make([]byte, 0, defaultCapacityForSliceOfBytes)
return &sliceOBytes
},
}
@@ -85,20 +86,6 @@ type Levels struct {
Info, Debug, Warn, Error bool
}
// SubLogger defines a sub logger can be used externally for packages wanted to
// leverage GCT library logger features.
type SubLogger struct {
name string
Levels
output io.Writer
}
// Event holds the data sent to the log and which multiwriter to send to
type Event struct {
data []byte
output io.Writer
}
type multiWriter struct {
writers []io.Writer
mu sync.RWMutex

View File

@@ -7,28 +7,27 @@ import (
// Info takes a pointer subLogger struct and string sends to newLogEvent
func Info(sl *SubLogger, data string) {
fields := getFields(sl)
if fields == nil {
return
}
if !fields.info {
fields := sl.getFields()
if fields == nil || !fields.info {
return
}
displayError(fields.logger.newLogEvent(data, fields.logger.InfoHeader, fields.name, fields.output))
displayError(fields.logger.newLogEvent(data,
fields.logger.InfoHeader,
fields.name,
fields.output))
}
// Infoln takes a pointer subLogger struct and interface sends to newLogEvent
func Infoln(sl *SubLogger, v ...interface{}) {
fields := getFields(sl)
if fields == nil {
fields := sl.getFields()
if fields == nil || !fields.info {
return
}
if !fields.info {
return
}
displayError(fields.logger.newLogEvent(fmt.Sprintln(v...), fields.logger.InfoHeader, fields.name, fields.output))
displayError(fields.logger.newLogEvent(fmt.Sprintln(v...),
fields.logger.InfoHeader,
fields.name,
fields.output))
}
// Infof takes a pointer subLogger struct, string & interface formats and sends to Info()
@@ -38,28 +37,27 @@ func Infof(sl *SubLogger, data string, v ...interface{}) {
// Debug takes a pointer subLogger struct and string sends to multiwriter
func Debug(sl *SubLogger, data string) {
fields := getFields(sl)
if fields == nil {
fields := sl.getFields()
if fields == nil || !fields.debug {
return
}
if !fields.debug {
return
}
displayError(fields.logger.newLogEvent(data, fields.logger.DebugHeader, fields.name, fields.output))
displayError(fields.logger.newLogEvent(data,
fields.logger.DebugHeader,
fields.name,
fields.output))
}
// Debugln takes a pointer subLogger struct, string and interface sends to newLogEvent
func Debugln(sl *SubLogger, v ...interface{}) {
fields := getFields(sl)
if fields == nil {
return
}
if !fields.debug {
fields := sl.getFields()
if fields == nil || !fields.debug {
return
}
displayError(fields.logger.newLogEvent(fmt.Sprintln(v...), fields.logger.DebugHeader, fields.name, fields.output))
displayError(fields.logger.newLogEvent(fmt.Sprintln(v...),
fields.logger.DebugHeader,
fields.name,
fields.output))
}
// Debugf takes a pointer subLogger struct, string & interface formats and sends to Info()
@@ -69,28 +67,26 @@ func Debugf(sl *SubLogger, data string, v ...interface{}) {
// Warn takes a pointer subLogger struct & string and sends to newLogEvent()
func Warn(sl *SubLogger, data string) {
fields := getFields(sl)
if fields == nil {
fields := sl.getFields()
if fields == nil || !fields.warn {
return
}
if !fields.warn {
return
}
displayError(fields.logger.newLogEvent(data, fields.logger.WarnHeader, fields.name, fields.output))
displayError(fields.logger.newLogEvent(data,
fields.logger.WarnHeader,
fields.name,
fields.output))
}
// Warnln takes a pointer subLogger struct & interface formats and sends to newLogEvent()
func Warnln(sl *SubLogger, v ...interface{}) {
fields := getFields(sl)
if fields == nil {
fields := sl.getFields()
if fields == nil || !fields.warn {
return
}
if !fields.warn {
return
}
displayError(fields.logger.newLogEvent(fmt.Sprintln(v...), fields.logger.WarnHeader, fields.name, fields.output))
displayError(fields.logger.newLogEvent(fmt.Sprintln(v...),
fields.logger.WarnHeader,
fields.name,
fields.output))
}
// Warnf takes a pointer subLogger struct, string & interface formats and sends to Warn()
@@ -100,28 +96,26 @@ func Warnf(sl *SubLogger, data string, v ...interface{}) {
// Error takes a pointer subLogger struct & interface formats and sends to newLogEvent()
func Error(sl *SubLogger, data ...interface{}) {
fields := getFields(sl)
if fields == nil {
fields := sl.getFields()
if fields == nil || !fields.error {
return
}
if !fields.error {
return
}
displayError(fields.logger.newLogEvent(fmt.Sprint(data...), fields.logger.ErrorHeader, fields.name, fields.output))
displayError(fields.logger.newLogEvent(fmt.Sprint(data...),
fields.logger.ErrorHeader,
fields.name,
fields.output))
}
// Errorln takes a pointer subLogger struct, string & interface formats and sends to newLogEvent()
func Errorln(sl *SubLogger, v ...interface{}) {
fields := getFields(sl)
if fields == nil {
fields := sl.getFields()
if fields == nil || !fields.error {
return
}
if !fields.error {
return
}
displayError(fields.logger.newLogEvent(fmt.Sprintln(v...), fields.logger.ErrorHeader, fields.name, fields.output))
displayError(fields.logger.newLogEvent(fmt.Sprintln(v...),
fields.logger.ErrorHeader,
fields.name,
fields.output))
}
// Errorf takes a pointer subLogger struct, string & interface formats and sends to Debug()
@@ -134,35 +128,3 @@ func displayError(err error) {
log.Printf("Logger write error: %v\n", err)
}
}
func enabled() bool {
RWM.Lock()
defer RWM.Unlock()
if GlobalLogConfig == nil || GlobalLogConfig.Enabled == nil {
return false
}
if *GlobalLogConfig.Enabled {
return true
}
return false
}
func getFields(sl *SubLogger) *logFields {
if !enabled() {
return nil
}
if sl == nil {
return nil
}
RWM.RLock()
defer RWM.RUnlock()
return &logFields{
info: sl.Info,
warn: sl.Warn,
debug: sl.Debug,
error: sl.Error,
name: sl.name,
output: sl.output,
logger: *logger,
}
}

66
log/sublogger.go Normal file
View File

@@ -0,0 +1,66 @@
package log
import (
"io"
"strings"
)
// NewSubLogger allows for a new sub logger to be registered.
func NewSubLogger(name string) (*SubLogger, error) {
if name == "" {
return nil, errEmptyLoggerName
}
name = strings.ToUpper(name)
RWM.RLock()
if _, ok := SubLoggers[name]; ok {
RWM.RUnlock()
return nil, errSubLoggerAlreadyregistered
}
RWM.RUnlock()
return registerNewSubLogger(name), nil
}
// SetOutput overrides the default output with a new writer
func (sl *SubLogger) SetOutput(o io.Writer) {
sl.mtx.Lock()
sl.output = o
sl.mtx.Unlock()
}
// SetLevels overrides the default levels with new levels; levelception
func (sl *SubLogger) SetLevels(newLevels Levels) {
sl.mtx.Lock()
sl.levels = newLevels
sl.mtx.Unlock()
}
// GetLevels returns current functional log levels
func (sl *SubLogger) GetLevels() Levels {
sl.mtx.RLock()
defer sl.mtx.RUnlock()
return sl.levels
}
func (sl *SubLogger) getFields() *logFields {
RWM.RLock()
defer RWM.RUnlock()
if sl == nil ||
(GlobalLogConfig != nil &&
GlobalLogConfig.Enabled != nil &&
!*GlobalLogConfig.Enabled) {
return nil
}
sl.mtx.RLock()
defer sl.mtx.RUnlock()
return &logFields{
info: sl.levels.Info,
warn: sl.levels.Warn,
debug: sl.levels.Debug,
error: sl.levels.Error,
name: sl.name,
output: sl.output,
logger: logger,
}
}

View File

@@ -1,6 +1,9 @@
package log
import "io"
import (
"io"
"sync"
)
// Global vars related to the logger package
var (
@@ -34,6 +37,15 @@ var (
Fill *SubLogger
)
// SubLogger defines a sub logger can be used externally for packages wanted to
// leverage GCT library logger features.
type SubLogger struct {
name string
levels Levels
output io.Writer
mtx sync.RWMutex
}
// logFields is used to store data in a non-global and thread-safe manner
// so logs cannot be modified mid-log causing a data-race issue
type logFields struct {