From fd08b36f857fee3d28fcf111f464f80a39d1f7db Mon Sep 17 00:00:00 2001 From: Tronje Krop Date: Wed, 6 Nov 2024 00:58:06 +0100 Subject: [PATCH] feat: add zerolog support (#16) Signed-off-by: Tronje Krop --- .github/workflows/build.yaml | 1 + Makefile | 4 +- VERSION | 2 +- config/config_test.go | 2 +- go.mod | 9 +- go.sum | 12 +- log/{format/logrus.go => buffer.go} | 77 +-- log/buffer_test.go | 306 +++++++++ log/doc.go | 4 + log/format/format.go | 187 ------ log/format/logrus_test.go | 825 ------------------------ log/log.go | 372 ++++++++--- log/log_test.go | 385 +++++++++--- log/logrus.go | 85 +++ log/logrus_test.go | 530 ++++++++++++++++ log/zerolog.go | 184 ++++++ log/zerolog_test.go | 932 ++++++++++++++++++++++++++++ 17 files changed, 2647 insertions(+), 1270 deletions(-) rename log/{format/logrus.go => buffer.go} (63%) create mode 100644 log/buffer_test.go create mode 100644 log/doc.go delete mode 100644 log/format/format.go delete mode 100644 log/format/logrus_test.go create mode 100644 log/logrus.go create mode 100644 log/logrus_test.go create mode 100644 log/zerolog.go create mode 100644 log/zerolog_test.go diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 404ed2b..ad568f6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,6 +8,7 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.23.2 + cache: false - name: Checkout code uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index 33115c2..302a55d 100644 --- a/Makefile +++ b/Makefile @@ -12,12 +12,12 @@ export GOPATH ?= $(shell $(GO) env GOPATH) export GOBIN ?= $(GOPATH)/bin # Setup go-make version to use desired build and config scripts. -GOMAKE_DEP ?= github.com/tkrop/go-make@v0.0.106 +GOMAKE_DEP ?= github.com/tkrop/go-make@v0.0.113 INSTALL_FLAGS ?= -mod=readonly -buildvcs=auto # Request targets from go-make targets target. TARGETS := $(shell command -v $(GOBIN)/go-make >/dev/null || \ $(GO) install $(INSTALL_FLAGS) $(GOMAKE_DEP) >/dev/stderr && \ - $(GOBIN)/go-make show-targets 2>/dev/null) + MAKEFLAGS="" $(GOBIN)/go-make show-targets 2>/dev/null) # Declare all targets phony to make them available for auto-completion. .PHONY:: $(TARGETS) diff --git a/VERSION b/VERSION index d169b2f..c5d54ec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.8 +0.0.9 diff --git a/config/config_test.go b/config/config_test.go index 53d3a2d..92cbeb9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -65,7 +65,7 @@ var testConfigParams = map[string]testConfigParam{ expect: test.Panic(config.NewErrConfig("loading file", "test", test.NewBuilder[viper.ConfigFileNotFoundError](). Set("locations", fmt.Sprintf("%s", configPaths)). - Set("name", "test").Get("").(error))), + Set("name", "test").Build())), }, "panic after unmarschal failure": { diff --git a/go.mod b/go.mod index c97d26e..cf5eb7e 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,18 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 - github.com/tkrop/go-testing v0.0.21 + github.com/tkrop/go-testing v0.0.22 gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/sys v0.26.0 // indirect +require ( + github.com/rogpeppe/go-internal v1.13.1 // indirect + golang.org/x/sys v0.26.0 // indirect +) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index db6221a..25a7547 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= @@ -34,8 +34,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -61,8 +61,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tkrop/go-testing v0.0.21 h1:lCSLCqsa0KUKOjGOd5euUIpmctSZy9eO7e1pwe2ym2A= -github.com/tkrop/go-testing v0.0.21/go.mod h1:RFUX2nk7n4QPUN0doC4R0KS6PqeaK1vWUDy3tBtjjKQ= +github.com/tkrop/go-testing v0.0.22 h1:zRxOdj4XAmafww6QtdkQlnqlZCnN14DbxSyZSWWjFx0= +github.com/tkrop/go-testing v0.0.22/go.mod h1:S9WAo/AbkqDLp1Jxu8Cd+fbmdgJmyDhv+CruOEBDlIY= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= diff --git a/log/format/logrus.go b/log/buffer.go similarity index 63% rename from log/format/logrus.go rename to log/buffer.go index b252f0c..9fe4593 100644 --- a/log/format/logrus.go +++ b/log/buffer.go @@ -1,58 +1,12 @@ -// Package format provides common log formatting based on logrus for services, -// jobs, and commands with integrated configuration loading. -package format +package log import ( - "bytes" "fmt" "io" - "maps" - "slices" - "sort" + "runtime" "strconv" - - log "github.com/sirupsen/logrus" ) -// Pretty formats logs into a pretty format. -type Pretty struct { - // TimeFormat is defining the time format used for printing timestamps. - TimeFormat string - // ColorMode is defining the color mode (default = ColorAuto). - ColorMode ColorMode - // OrderMode is defining the order mode. - OrderMode OrderMode - - // LevelNames is defining the names used for marking the different log - // levels. - LevelNames []string - // LevelColors is defining the colors used for marking the different log - // levels. - LevelColors []string -} - -// Format formats the log entry to a pretty format. -func (p *Pretty) Format(entry *log.Entry) ([]byte, error) { - buffer := NewBuffer(p, &bytes.Buffer{}) - buffer.WriteString(entry.Time.Format(p.TimeFormat)). - WriteByte(' ').WriteLevel(entry.Level).WriteCaller(entry). - WriteByte(' ').WriteString(entry.Message) - - for _, key := range p.getSortedKeys(entry.Data) { - buffer.WriteByte(' ').WriteData(key, entry.Data[key]) - } - return buffer.WriteByte('\n').Bytes() -} - -// getSortedKeys returns the keys of the given data. -func (p *Pretty) getSortedKeys(data log.Fields) []string { - keys := slices.Collect(maps.Keys(data)) - if p.OrderMode.CheckFlag(OrderOn) { - sort.Strings(keys) - } - return keys -} - // Buffer is the interface for writing bytes and strings. type BufferWriter interface { // WriteByte writes the given byte to the writer. @@ -62,12 +16,14 @@ type BufferWriter interface { // Bytes returns the current bytes of the writer. Bytes() []byte + // String returns the current string of the writer. + String() string } // Buffer is a buffer for the pretty formatter. type Buffer struct { // pretty is the pretty formatter of the buffer. - pretty *Pretty + pretty *Setup // buffer is the bytes buffer used for writing. buffer BufferWriter @@ -76,7 +32,7 @@ type Buffer struct { } // NewBuffer creates a new buffer for the pretty formatter. -func NewBuffer(p *Pretty, b BufferWriter) *Buffer { +func NewBuffer(p *Setup, b BufferWriter) *Buffer { return &Buffer{pretty: p, buffer: b} } @@ -122,7 +78,7 @@ func (b *Buffer) WriteColored(color, str string) *Buffer { } // WriteLevel writes the given log level to the buffer. -func (b *Buffer) WriteLevel(level log.Level) *Buffer { +func (b *Buffer) WriteLevel(level Level) *Buffer { if b.err != nil { return b } @@ -135,7 +91,7 @@ func (b *Buffer) WriteLevel(level log.Level) *Buffer { } // WriteField writes the given key with the given color to the buffer. -func (b *Buffer) WriteField(level log.Level, key string) *Buffer { +func (b *Buffer) WriteField(level Level, key string) *Buffer { if b.err != nil { return b } @@ -147,12 +103,11 @@ func (b *Buffer) WriteField(level log.Level, key string) *Buffer { } // WriteCaller writes the caller information to the buffer. -func (b *Buffer) WriteCaller(entry *log.Entry) *Buffer { - if b.err != nil || !entry.HasCaller() { +func (b *Buffer) WriteCaller(caller *runtime.Frame) *Buffer { + if b.err != nil || caller == nil { return b } - caller := entry.Caller return b.WriteByte(' ').WriteByte('['). WriteString(caller.File).WriteByte(':'). WriteString(strconv.Itoa(caller.Line)).WriteByte('#'). @@ -181,11 +136,10 @@ func (b *Buffer) WriteData(key string, value any) *Buffer { return b } - switch key { - case log.ErrorKey: - return b.WriteField(log.ErrorLevel, key). + if key == b.pretty.ErrorName { + return b.WriteField(ErrorLevel, key). WriteByte('=').WriteValue(value) - default: + } else { return b.WriteField(FieldLevel, key). WriteByte('=').WriteValue(value) } @@ -195,3 +149,8 @@ func (b *Buffer) WriteData(key string, value any) *Buffer { func (b *Buffer) Bytes() ([]byte, error) { return b.buffer.Bytes(), b.err } + +// String returns current string of the buffer with the current error. +func (b *Buffer) String() string { + return b.buffer.String() +} diff --git a/log/buffer_test.go b/log/buffer_test.go new file mode 100644 index 0000000..edd08fa --- /dev/null +++ b/log/buffer_test.go @@ -0,0 +1,306 @@ +package log_test + +import ( + "bytes" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/tkrop/go-config/log" + "github.com/tkrop/go-testing/mock" + "github.com/tkrop/go-testing/test" +) + +//revive:disable:line-length-limit // go:generate line length + +//go:generate mockgen -package=log_test -destination=mock_writer_test.go -source=buffer.go BufferWriter + +//revive:enable:line-length-limit + +// setupWriter sets up the writer for testing. +func setupWriter( + mocks *mock.Mocks, expect mock.SetupFunc, +) log.BufferWriter { + if expect != nil { + return mock.Get(mocks, NewMockBufferWriter) + } + return &bytes.Buffer{} +} + +type testBufferWriteParam struct { + colorMode log.ColorModeString + error error + setup func(*log.Buffer) + expect mock.SetupFunc + expectError error + expectString string +} + +var testBufferWriteParams = map[string]testBufferWriteParam{ + // Test write byte. + "write byte error": { + error: errAny, + setup: func(buffer *log.Buffer) { + buffer.WriteByte(' ') + }, + expectError: errAny, + }, + "write byte failure": { + setup: func(buffer *log.Buffer) { + buffer.WriteByte(' ') + }, + expect: mock.Chain(func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockBufferWriter).EXPECT().WriteByte(uint8(' ')). + DoAndReturn(mocks.Do(log.BufferWriter.WriteByte, errAny)) + }, func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockBufferWriter).EXPECT().Bytes(). + DoAndReturn(mocks.Do(log.BufferWriter.Bytes, []byte(""))) + }), + expectError: errAny, + }, + "write byte": { + setup: func(buffer *log.Buffer) { + buffer.WriteByte(' ') + }, + expectString: " ", + }, + + // Test write string. + "write string error": { + error: errAny, + setup: func(buffer *log.Buffer) { + buffer.WriteString("string") + }, + expectError: errAny, + }, + "write string failure": { + setup: func(buffer *log.Buffer) { + buffer.WriteString("string") + }, + expect: mock.Chain(func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockBufferWriter).EXPECT().WriteString("string"). + DoAndReturn(mocks.Do(log.BufferWriter.WriteString, 0, errAny)) + }, func(mocks *mock.Mocks) any { + return mock.Get(mocks, NewMockBufferWriter).EXPECT().Bytes(). + DoAndReturn(mocks.Do(log.BufferWriter.Bytes, []byte(""))) + }), + expectError: errAny, + }, + "write string": { + setup: func(buffer *log.Buffer) { + buffer.WriteString("string") + }, + expectString: "string", + }, + + // Test write colored. + "write colored error": { + error: errAny, + setup: func(buffer *log.Buffer) { + buffer.WriteColored(log.ColorField, "string") + }, + expectError: errAny, + }, + "write colored default": { + setup: func(buffer *log.Buffer) { + buffer.WriteColored(log.ColorField, "string") + }, + expectString: fieldC("string"), + }, + "write colored color-off": { + colorMode: log.ColorModeOff, + setup: func(buffer *log.Buffer) { + buffer.WriteColored(log.ColorField, "string") + }, + expectString: field("string"), + }, + "write colored color-on": { + colorMode: log.ColorModeOn, + setup: func(buffer *log.Buffer) { + buffer.WriteColored(log.ColorField, "string") + }, + expectString: fieldC("string"), + }, + + // Test write level. + "write level error": { + error: errAny, + setup: func(buffer *log.Buffer) { + buffer.WriteLevel(log.PanicLevel) + }, + expectError: errAny, + }, + "write level default": { + setup: func(buffer *log.Buffer) { + buffer.WriteLevel(log.PanicLevel) + }, + expectString: levelC(log.PanicLevel), + }, + "write level color-on": { + colorMode: log.ColorModeOn, + setup: func(buffer *log.Buffer) { + buffer.WriteLevel(log.PanicLevel) + }, + expectString: levelC(log.PanicLevel), + }, + "write level color-off": { + colorMode: log.ColorModeOff, + setup: func(buffer *log.Buffer) { + buffer.WriteLevel(log.PanicLevel) + }, + expectString: level(log.PanicLevel), + }, + + // Test write colored field. + "write field error": { + error: errAny, + setup: func(buffer *log.Buffer) { + buffer.WriteField(log.FieldLevel, "value") + }, + expectError: errAny, + }, + "write field default": { + setup: func(buffer *log.Buffer) { + buffer.WriteField(log.FieldLevel, "value") + }, + expectString: fieldC("value"), + }, + "write field color-on": { + colorMode: log.ColorModeOn, + setup: func(buffer *log.Buffer) { + buffer.WriteField(log.FieldLevel, "value") + }, + expectString: fieldC("value"), + }, + "write field color-off": { + colorMode: log.ColorModeOff, + setup: func(buffer *log.Buffer) { + buffer.WriteField(log.FieldLevel, "value") + }, + expectString: field("value"), + }, + + // Test write caller. + "write caller error": { + error: errAny, + setup: func(buffer *log.Buffer) { + buffer.WriteCaller(anyFrame) + }, + expectError: errAny, + }, + "write caller on": { + setup: func(buffer *log.Buffer) { + buffer.WriteCaller(anyFrame) + }, + expectString: " [file:123#function]", + }, + "write caller off": { + setup: func(buffer *log.Buffer) { + buffer.WriteCaller(nil) + }, + expectString: "", + }, + + // Test write value. + "write value error": { + error: errAny, + setup: func(buffer *log.Buffer) { + buffer.WriteValue("value") + }, + expectError: errAny, + }, + "write value string": { + setup: func(buffer *log.Buffer) { + buffer.WriteValue("value") + }, + expectString: "\"value\"", + }, + "write value int": { + setup: func(buffer *log.Buffer) { + buffer.WriteValue(123) + }, + expectString: "123", + }, + "write value float": { + setup: func(buffer *log.Buffer) { + buffer.WriteValue(123.456) + }, + expectString: "123.456", + }, + "write value complex": { + setup: func(buffer *log.Buffer) { + buffer.WriteValue(123.456 + 789i) + }, + expectString: "(123.456+789i)", + }, + "write value bool": { + setup: func(buffer *log.Buffer) { + buffer.WriteValue(true) + }, + expectString: "true", + }, + + // Test write data. + "write data error": { + error: errAny, + setup: func(buffer *log.Buffer) { + buffer.WriteData("key", "value") + }, + expectError: errAny, + }, + "write data default": { + setup: func(buffer *log.Buffer) { + buffer.WriteData("key", "value") + }, + expectString: dataC("key", "value"), + }, + "write data color-on error": { + setup: func(buffer *log.Buffer) { + buffer.WriteData(logrus.ErrorKey, errAny) + }, + expectString: dataC(logrus.ErrorKey, errAny.Error()), + }, + "write data color-on": { + colorMode: log.ColorModeOn, + setup: func(buffer *log.Buffer) { + buffer.WriteData("key", "value") + }, + expectString: dataC("key", "value"), + }, + "write data color-off": { + colorMode: log.ColorModeOff, + setup: func(buffer *log.Buffer) { + buffer.WriteData("key", "value") + }, + expectString: data("key", "value"), + }, +} + +func TestBufferWrite(t *testing.T) { + test.Map(t, testBufferWriteParams). + Run(func(t test.Test, param testBufferWriteParam) { + // Given + mocks := mock.NewMocks(t).Expect(param.expect) + pretty := &log.Setup{ + ColorMode: param.colorMode.Parse(true), + ErrorName: log.DefaultErrorName, + LevelNames: log.DefaultLevelNames, + LevelColors: log.DefaultLevelColors, + } + + buffer := log.NewBuffer(pretty, + setupWriter(mocks, param.expect)) + test.NewAccessor(buffer).Set("err", param.error) + + // When + param.setup(buffer) + result, err := buffer.Bytes() + + // Then + assert.Equal(t, param.expectError, err) + assert.Equal(t, param.expectString, string(result)) + if param.expect == nil { + assert.Equal(t, param.expectString, buffer.String()) + } + }) +} diff --git a/log/doc.go b/log/doc.go new file mode 100644 index 0000000..b881eaa --- /dev/null +++ b/log/doc.go @@ -0,0 +1,4 @@ +// Package log provides config and common log formatting for [logrus][logrus] +// and [zerolog][zerolog] loggers. It is designed to be used in applications, +// services, jobs, and commands with integrated configuration loading. +package log diff --git a/log/format/format.go b/log/format/format.go deleted file mode 100644 index dec38e3..0000000 --- a/log/format/format.go +++ /dev/null @@ -1,187 +0,0 @@ -package format - -import ( - "regexp" - - log "github.com/sirupsen/logrus" -) - -// Formatter is the formatter used for logging. -type Formatter string - -// Formatters. -const ( - // Pretty is the pretty formatter. - FormatterPretty Formatter = "pretty" - // Text is the text formatter. - FormatterText Formatter = "text" - // JSON is the JSON formatter. - FormatterJSON Formatter = "json" -) - -// Color codes for the different log levels. -const ( - // ColorRed is the color code for red. - ColorRed = "1;91" - // ColorGreen is the color code for green. - ColorGreen = "1;92" - // ColorYellow is the color code for yellow. - ColorYellow = "1;93" - // ColorGray is the color code for gray. - ColorBlue = "1;94" - // ColorMagenta is the color code for magenta. - ColorMagenta = "1;95" - // ColorCyan is the color code for cyan. - ColorCyan = "1;96" - // ColorGray is the color code for gray. - ColorGray = "1;37" - - // ColorPanic is the color code for panic. - ColorPanic = ColorRed - // ColorFatal is the color code for fatal. - ColorFatal = ColorRed - // ColorError is the color code for error. - ColorError = ColorRed - // ColorWarn is the color code for warn. - ColorWarn = ColorYellow - // ColorInfo is the color code for info. - ColorInfo = ColorCyan - // ColorDebug is the color code for debug. - ColorDebug = ColorBlue - // ColorTrace is the color code for trace. - ColorTrace = ColorMagenta - // ColorField is the color code for fields. - ColorField = ColorGray - - // FieldLevel is and extra log level used for field names. - FieldLevel log.Level = 7 - - // TImeFormat is defining default time format. - DefaultTimeFormat = "2006-01-02 15:04:05.999999" -) - -var ( - // DefaultLevelColors is the default color mapping for the log levels. - DefaultLevelColors = []string{ - ColorPanic, ColorFatal, ColorError, - ColorWarn, ColorInfo, ColorDebug, ColorTrace, ColorField, - } - - // DefaultLevelNames is the default name mapping for the log levels. - DefaultLevelNames = []string{ - "PANIC", "FATAL", "ERROR", "WARN", - "INFO", "DEBUG", "TRACE", "-", - } -) - -// ColorModeString is the color mode used for logging. -type ColorModeString string - -// Color mode strings. -const ( - // ColorOff disables the color mode. - ColorModeOff ColorModeString = "off" - // ColorOn enables the color mode. - ColorModeOn ColorModeString = "on" - // ColorAuto enables the automatic color mode. - ColorModeAuto ColorModeString = "auto" - // ColorLevel enables the color mode for log level. - ColorModeLevels ColorModeString = "levels" - // ColorFields enables the color mode for fields. - ColorModeFields ColorModeString = "fields" -) - -var splitRegex = regexp.MustCompile(`[|,:;]`) - -// Parse parses the color mode. -func (m ColorModeString) Parse(colorized bool) ColorMode { - mode := ColorUnset - for _, m := range splitRegex.Split(string(m), -1) { - switch ColorModeString(m) { - case ColorModeOff: - mode = ColorOff - case ColorModeOn: - mode = ColorOn - case ColorModeLevels: - mode |= ColorLevels - case ColorModeFields: - mode |= ColorFields - case ColorModeAuto: - fallthrough - default: - if colorized { - mode = ColorOn - } else { - mode = ColorOff - } - } - } - return mode -} - -// ColorMode is the color mode used for logging. -type ColorMode uint - -// Color modes. -const ( - // ColorDefault is the default color mode. - ColorDefault = ColorOn - // ColorUnset is the unset color mode (activates the default). - ColorUnset ColorMode = 0 - // ColorOff disables coloring of logs for all outputs files. - ColorOff ColorMode = 1 - // ColorOn enables coloring of logs for all outputs files. - ColorOn ColorMode = ColorFields | ColorLevels - // ColorLevels enables coloring for log levels entries only. - ColorLevels ColorMode = 2 - // ColorFields enables coloring for fields names only. - ColorFields ColorMode = 4 -) - -// CheckFlag checks if the given color mode flag is set. -func (m ColorMode) CheckFlag(flag ColorMode) bool { - return m&flag == flag -} - -// OrderModeString is the order mode used for logging. -type OrderModeString string - -// Order modes. -const ( - // OrderModeOff disables the order mode. - OrderModeOff OrderModeString = "off" - // OrderModeOn enables the order mode. - OrderModeOn OrderModeString = "on" -) - -// Parse parses the order mode. -func (m OrderModeString) Parse() OrderMode { - switch m { - case OrderModeOff: - return OrderOff - case OrderModeOn: - return OrderOn - default: - return OrderOn - } -} - -// OrderMode is the order mode used for logging. -type OrderMode uint - -// Order modes. -const ( - // OrderDefault is the default order mode. - OrderDefault = OrderOn - // OrderUnset is the unset order mode. - OrderUnset OrderMode = 0 - // OrderOff disables the order mode. - OrderOff OrderMode = 1 - // OrderOn enables the order mode. - OrderOn OrderMode = 2 -) - -// CheckFlag checks if the given order mode flag is set. -func (m OrderMode) CheckFlag(flag OrderMode) bool { - return m&flag == flag -} diff --git a/log/format/logrus_test.go b/log/format/logrus_test.go deleted file mode 100644 index c515014..0000000 --- a/log/format/logrus_test.go +++ /dev/null @@ -1,825 +0,0 @@ -package format_test - -import ( - "bytes" - "errors" - "runtime" - "testing" - "time" - - log "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - "github.com/tkrop/go-config/log/format" - "github.com/tkrop/go-testing/mock" - "github.com/tkrop/go-testing/test" -) - -//revive:disable:line-length-limit // go:generate line length - -//go:generate mockgen -package=format_test -destination=mock_writer_test.go -source=logrus.go BufferWriter - -//revive:enable:line-length-limit - -var ( - // TestTime is a fixed time for testing. - otime = "2024-10-01 23:07:13.891012345Z" - // TestTime is a fixed time for testing. - itime = "2024-10-01T23:07:13.891012345Z" - - // Arbitrary data for testing. - anyData = log.Fields{ - "key1": "value1", - "key2": "value2", - } - // Arbitrary frame for testing. - anyFrame = &runtime.Frame{ - File: "file", - Function: "function", - Line: 123, - } - // Arbitrary error for testing. - errAny = errors.New("any error") -) - -// setupTimeFormat sets up the time format for testing. -func setupTimeFormat(timeFormat string) string { - if timeFormat == "" { - return format.DefaultTimeFormat - } - return timeFormat -} - -// setupWriter sets up the writer for testing. -func setupWriter( - mocks *mock.Mocks, expect mock.SetupFunc, -) format.BufferWriter { - if expect != nil { - return mock.Get(mocks, NewMockBufferWriter) - } - return &bytes.Buffer{} -} - -// Helper functions for testing log levels without color. -func level(level log.Level) string { - return format.DefaultLevelNames[level] -} - -// Helper functions for testing log levels with color. -func levelC(level log.Level) string { - return "\x1b[" + format.DefaultLevelColors[level] + - "m" + format.DefaultLevelNames[level] + "\x1b[0m" -} - -// Helper functions for testing fields without color. -func fieldC(value string) string { - return "\x1b[" + format.ColorField + "m" + value + "\x1b[0m" -} - -// Helper functions for testing key-value data without color. -func data(key, value string) string { - return key + "=\"" + value + "\"" -} - -// Helper functions for testing key-value data with color. -func dataC(key, value string) string { - color := format.ColorField - if key == log.ErrorKey { - color = format.ColorError - } - return "\x1b[" + color + "m" + key + "\x1b[0m=\"" + value + "\"" -} - -type testPrettyFormatParam struct { - timeFormat string - noTerminal bool - colorMode format.ColorModeString - orderMode format.OrderModeString - entry *log.Entry - expect func(t test.Test, result string, err error) - expectError error - expectResult string -} - -var testPrettyFormatParams = map[string]testPrettyFormatParam{ - // Test levels with default. - "level panic default": { - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "panic message", - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " panic message\n", - }, - "level fatal default": { - entry: &log.Entry{ - Level: log.FatalLevel, - Message: "fatal message", - }, - expectResult: otime[0:26] + " " + - levelC(log.FatalLevel) + " fatal message\n", - }, - "level error default": { - entry: &log.Entry{ - Level: log.ErrorLevel, - Message: "error message", - }, - expectResult: otime[0:26] + " " + - levelC(log.ErrorLevel) + " error message\n", - }, - "level warn default": { - entry: &log.Entry{ - Level: log.WarnLevel, - Message: "warn message", - }, - expectResult: otime[0:26] + " " + - levelC(log.WarnLevel) + " warn message\n", - }, - "level info default": { - entry: &log.Entry{ - Level: log.InfoLevel, - Message: "info message", - }, - expectResult: otime[0:26] + " " + - levelC(log.InfoLevel) + " info message\n", - }, - "level debug default": { - entry: &log.Entry{ - Level: log.DebugLevel, - Message: "debug message", - }, - expectResult: otime[0:26] + " " + - levelC(log.DebugLevel) + " debug message\n", - }, - "level trace default": { - entry: &log.Entry{ - Level: log.TraceLevel, - Message: "trace message", - }, - expectResult: otime[0:26] + " " + - levelC(log.TraceLevel) + " trace message\n", - }, - - // Test levels with color. - "level panic color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "panic message", - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " panic message\n", - }, - "level fatal color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Level: log.FatalLevel, - Message: "fatal message", - }, - expectResult: otime[0:26] + " " + - levelC(log.FatalLevel) + " fatal message\n", - }, - "level error color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Level: log.ErrorLevel, - Message: "error message", - }, - expectResult: otime[0:26] + " " + - levelC(log.ErrorLevel) + " error message\n", - }, - "level warn color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Level: log.WarnLevel, - Message: "warn message", - }, - expectResult: otime[0:26] + " " + - levelC(log.WarnLevel) + " warn message\n", - }, - "level info color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Level: log.InfoLevel, - Message: "info message", - }, - expectResult: otime[0:26] + " " + - levelC(log.InfoLevel) + " info message\n", - }, - "level debug color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Level: log.DebugLevel, - Message: "debug message", - }, - expectResult: otime[0:26] + " " + - levelC(log.DebugLevel) + " debug message\n", - }, - "level trace color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Level: log.TraceLevel, - Message: "trace message", - }, - expectResult: otime[0:26] + " " + - levelC(log.TraceLevel) + " trace message\n", - }, - - // Test levels with color. - "level panic color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "panic message", - }, - expectResult: otime[0:26] + " " + - level(log.PanicLevel) + " panic message\n", - }, - "level fatal color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Level: log.FatalLevel, - Message: "fatal message", - }, - expectResult: otime[0:26] + " " + - level(log.FatalLevel) + " fatal message\n", - }, - "level error color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Level: log.ErrorLevel, - Message: "error message", - }, - expectResult: otime[0:26] + " " + - level(log.ErrorLevel) + " error message\n", - }, - "level warn color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Level: log.WarnLevel, - Message: "warn message", - }, - expectResult: otime[0:26] + " " + - level(log.WarnLevel) + " warn message\n", - }, - "level info color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Level: log.InfoLevel, - Message: "info message", - }, - expectResult: otime[0:26] + " " + - level(log.InfoLevel) + " info message\n", - }, - "level debug color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Level: log.DebugLevel, - Message: "debug message", - }, - expectResult: otime[0:26] + " " + - level(log.DebugLevel) + " debug message\n", - }, - "level trace color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Level: log.TraceLevel, - Message: "trace message", - }, - expectResult: otime[0:26] + " " + - level(log.TraceLevel) + " trace message\n", - }, - - // Test order key value data. - "data default": { - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " data message " + - dataC("key1", "value1") + " " + - dataC("key2", "value2") + "\n", - }, - "data ordered": { - orderMode: format.OrderModeOn, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " data message " + - dataC("key1", "value1") + " " + - dataC("key2", "value2") + "\n", - }, - "data unordered": { - orderMode: format.OrderModeOff, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expect: func(t test.Test, result string, err error) { - assert.Contains(t, result, otime[0:26]+" "+ - levelC(log.PanicLevel)+" "+"data message") - assert.Contains(t, result, dataC("key1", "value1")) - assert.Contains(t, result, dataC("key2", "value2")) - }, - }, - - // Test color modes. - "data color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expectResult: otime[0:26] + " " + - level(log.PanicLevel) + " data message " + - data("key1", "value1") + " " + - data("key2", "value2") + "\n", - }, - "data color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " data message " + - dataC("key1", "value1") + " " + - dataC("key2", "value2") + "\n", - }, - "data color-auto colorized": { - colorMode: format.ColorModeAuto, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " data message " + - dataC("key1", "value1") + " " + - dataC("key2", "value2") + "\n", - }, - "data color-auto not-colorized": { - colorMode: format.ColorModeAuto, - noTerminal: true, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - Logger: &log.Logger{Out: nil}, - }, - expectResult: otime[0:26] + " " + - level(log.PanicLevel) + " data message " + - data("key1", "value1") + " " + - data("key2", "value2") + "\n", - }, - "data color-levels": { - colorMode: format.ColorModeLevels, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " data message " + - data("key1", "value1") + " " + - data("key2", "value2") + "\n", - }, - "data color-fields": { - colorMode: format.ColorModeFields, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expectResult: otime[0:26] + " " + - level(log.PanicLevel) + " data message " + - dataC("key1", "value1") + " " + - dataC("key2", "value2") + "\n", - }, - "data color-levels+fields": { - colorMode: format.ColorModeLevels + "|" + format.ColorModeFields, - entry: &log.Entry{ - Message: "data message", - Data: anyData, - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " data message " + - dataC("key1", "value1") + " " + - dataC("key2", "value2") + "\n", - }, - - // Time format. - "time default": { - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "default time message", - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " " + - "default time message\n", - }, - "time short": { - timeFormat: "2006-01-02 15:04:05", - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "short time message", - }, - expectResult: otime[0:19] + " " + - levelC(log.PanicLevel) + " " + - "short time message\n", - }, - "time long": { - timeFormat: "2006-01-02 15:04:05.000000000", - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "long time message", - }, - expectResult: otime[0:29] + " " + - levelC(log.PanicLevel) + " " + - "long time message\n", - }, - - // Report caller. - "caller only": { - entry: &log.Entry{ - Message: "caller message", - Caller: anyFrame, - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " " + - "caller message\n", - }, - "caller report": { - entry: &log.Entry{ - Message: "caller report message", - Caller: anyFrame, - Logger: &log.Logger{ - ReportCaller: true, - }, - }, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " " + - "[file:123#function] caller report message\n", - }, - - // Test error. - "error output": { - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "error message", - Data: log.Fields{ - log.ErrorKey: errAny, - }, - }, - expectError: nil, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " error message " + - dataC("error", errAny.Error()) + "\n", - }, - "error output color-on": { - colorMode: format.ColorModeOn, - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "error message", - Data: log.Fields{ - log.ErrorKey: errAny, - }, - }, - expectError: nil, - expectResult: otime[0:26] + " " + - levelC(log.PanicLevel) + " error message " + - dataC("error", errAny.Error()) + "\n", - }, - "error output color-off": { - colorMode: format.ColorModeOff, - entry: &log.Entry{ - Level: log.PanicLevel, - Message: "error message", - Data: log.Fields{ - log.ErrorKey: errAny, - }, - }, - expectError: nil, - expectResult: otime[0:26] + " " + - level(log.PanicLevel) + " error message " + - data("error", errAny.Error()) + "\n", - }, -} - -func TestPrettyFormat(t *testing.T) { - test.Map(t, testPrettyFormatParams). - Run(func(t test.Test, param testPrettyFormatParam) { - // Given - pretty := &format.Pretty{ - TimeFormat: setupTimeFormat(param.timeFormat), - ColorMode: param.colorMode.Parse(!param.noTerminal), - OrderMode: param.orderMode.Parse(), - LevelNames: format.DefaultLevelNames, - LevelColors: format.DefaultLevelColors, - } - - if param.entry.Time == (time.Time{}) { - time, err := time.Parse(time.RFC3339Nano, itime) - assert.NoError(t, err) - param.entry.Time = time - } - - // When - result, err := pretty.Format(param.entry) - - // Then - if param.expect == nil { - assert.Equal(t, param.expectError, err) - assert.Equal(t, param.expectResult, string(result)) - } else { - param.expect(t, string(result), err) - } - }) -} - -type testBufferWriteParam struct { - colorMode format.ColorModeString - error error - setup func(*format.Buffer) - expect mock.SetupFunc - expectError error - expectString string -} - -var testBufferWriteParams = map[string]testBufferWriteParam{ - // Test write byte. - "write byte error": { - error: errAny, - setup: func(buffer *format.Buffer) { - buffer.WriteByte(' ') - }, - expectError: errAny, - }, - "write byte failure": { - setup: func(buffer *format.Buffer) { - buffer.WriteByte(' ') - }, - expect: mock.Chain(func(mocks *mock.Mocks) any { - return mock.Get(mocks, NewMockBufferWriter).EXPECT().WriteByte(uint8(' ')). - DoAndReturn(mocks.Do(format.BufferWriter.WriteByte, errAny)) - }, func(mocks *mock.Mocks) any { - return mock.Get(mocks, NewMockBufferWriter).EXPECT().Bytes(). - DoAndReturn(mocks.Do(format.BufferWriter.Bytes, []byte(""))) - }), - expectError: errAny, - }, - "write byte": { - setup: func(buffer *format.Buffer) { - buffer.WriteByte(' ') - }, - expectString: " ", - }, - - // Test write string. - "write string error": { - error: errAny, - setup: func(buffer *format.Buffer) { - buffer.WriteString("string") - }, - expectError: errAny, - }, - "write string failure": { - setup: func(buffer *format.Buffer) { - buffer.WriteString("string") - }, - expect: mock.Chain(func(mocks *mock.Mocks) any { - return mock.Get(mocks, NewMockBufferWriter).EXPECT().WriteString("string"). - DoAndReturn(mocks.Do(format.BufferWriter.WriteString, 0, errAny)) - }, func(mocks *mock.Mocks) any { - return mock.Get(mocks, NewMockBufferWriter).EXPECT().Bytes(). - DoAndReturn(mocks.Do(format.BufferWriter.Bytes, []byte(""))) - }), - expectError: errAny, - }, - "write string": { - setup: func(buffer *format.Buffer) { - buffer.WriteString("string") - }, - expectString: "string", - }, - - // Test write colored. - "write colored error": { - error: errAny, - setup: func(buffer *format.Buffer) { - buffer.WriteColored(format.ColorField, "string") - }, - expectError: errAny, - }, - "write colored default": { - setup: func(buffer *format.Buffer) { - buffer.WriteColored(format.ColorField, "string") - }, - expectString: fieldC("string"), - }, - "write colored color-off": { - colorMode: format.ColorModeOff, - setup: func(buffer *format.Buffer) { - buffer.WriteColored(format.ColorField, "string") - }, - expectString: "string", - }, - "write colored color-on": { - colorMode: format.ColorModeOn, - setup: func(buffer *format.Buffer) { - buffer.WriteColored(format.ColorField, "string") - }, - expectString: fieldC("string"), - }, - - // Test write level. - "write level error": { - error: errAny, - setup: func(buffer *format.Buffer) { - buffer.WriteLevel(log.PanicLevel) - }, - expectError: errAny, - }, - "write level default": { - setup: func(buffer *format.Buffer) { - buffer.WriteLevel(log.PanicLevel) - }, - expectString: levelC(log.PanicLevel), - }, - "write level color-on": { - colorMode: format.ColorModeOn, - setup: func(buffer *format.Buffer) { - buffer.WriteLevel(log.PanicLevel) - }, - expectString: levelC(log.PanicLevel), - }, - "write level color-off": { - colorMode: format.ColorModeOff, - setup: func(buffer *format.Buffer) { - buffer.WriteLevel(log.PanicLevel) - }, - expectString: level(log.PanicLevel), - }, - - // Test write colored field. - "write field error": { - error: errAny, - setup: func(buffer *format.Buffer) { - buffer.WriteField(format.FieldLevel, "value") - }, - expectError: errAny, - }, - "write field default": { - setup: func(buffer *format.Buffer) { - buffer.WriteField(format.FieldLevel, "value") - }, - expectString: fieldC("value"), - }, - "write field color-on": { - colorMode: format.ColorModeOn, - setup: func(buffer *format.Buffer) { - buffer.WriteField(format.FieldLevel, "value") - }, - expectString: fieldC("value"), - }, - "write field color-off": { - colorMode: format.ColorModeOff, - setup: func(buffer *format.Buffer) { - buffer.WriteField(format.FieldLevel, "value") - }, - expectString: "value", - }, - - // Test write caller. - "write caller error": { - error: errAny, - setup: func(buffer *format.Buffer) { - buffer.WriteCaller(&log.Entry{ - Caller: anyFrame, - Logger: &log.Logger{ - ReportCaller: true, - }, - }) - }, - expectError: errAny, - }, - "write caller on": { - setup: func(buffer *format.Buffer) { - buffer.WriteCaller(&log.Entry{ - Caller: anyFrame, - Logger: &log.Logger{ - ReportCaller: true, - }, - }) - }, - expectString: " [file:123#function]", - }, - "write caller off": { - setup: func(buffer *format.Buffer) { - buffer.WriteCaller(&log.Entry{ - Logger: &log.Logger{ - ReportCaller: false, - }, - }) - }, - expectString: "", - }, - - // Test write value. - "write value error": { - error: errAny, - setup: func(buffer *format.Buffer) { - buffer.WriteValue("value") - }, - expectError: errAny, - }, - "write value string": { - setup: func(buffer *format.Buffer) { - buffer.WriteValue("value") - }, - expectString: "\"value\"", - }, - "write value int": { - setup: func(buffer *format.Buffer) { - buffer.WriteValue(123) - }, - expectString: "123", - }, - "write value float": { - setup: func(buffer *format.Buffer) { - buffer.WriteValue(123.456) - }, - expectString: "123.456", - }, - "write value complex": { - setup: func(buffer *format.Buffer) { - buffer.WriteValue(123.456 + 789i) - }, - expectString: "(123.456+789i)", - }, - "write value bool": { - setup: func(buffer *format.Buffer) { - buffer.WriteValue(true) - }, - expectString: "true", - }, - - // Test write data. - "write data error": { - error: errAny, - setup: func(buffer *format.Buffer) { - buffer.WriteData("key", "value") - }, - expectError: errAny, - }, - "write data default": { - setup: func(buffer *format.Buffer) { - buffer.WriteData("key", "value") - }, - expectString: dataC("key", "value"), - }, - "write data color-on error": { - setup: func(buffer *format.Buffer) { - buffer.WriteData(log.ErrorKey, errAny) - }, - expectString: dataC(log.ErrorKey, errAny.Error()), - }, - "write data color-on": { - colorMode: format.ColorModeOn, - setup: func(buffer *format.Buffer) { - buffer.WriteData("key", "value") - }, - expectString: dataC("key", "value"), - }, - "write data color-off": { - colorMode: format.ColorModeOff, - setup: func(buffer *format.Buffer) { - buffer.WriteData("key", "value") - }, - expectString: data("key", "value"), - }, -} - -func TestBufferWrite(t *testing.T) { - test.Map(t, testBufferWriteParams). - Run(func(t test.Test, param testBufferWriteParam) { - // Given - mocks := mock.NewMocks(t).Expect(param.expect) - pretty := &format.Pretty{ - ColorMode: param.colorMode.Parse(true), - LevelNames: format.DefaultLevelNames, - LevelColors: format.DefaultLevelColors, - } - - buffer := format.NewBuffer(pretty, - setupWriter(mocks, param.expect)) - test.NewAccessor(buffer).Set("err", param.error) - - // When - param.setup(buffer) - result, err := buffer.Bytes() - - // Then - assert.Equal(t, param.expectError, err) - assert.Equal(t, param.expectString, string(result)) - }) -} diff --git a/log/log.go b/log/log.go index 20b83a4..9558805 100644 --- a/log/log.go +++ b/log/log.go @@ -1,77 +1,259 @@ -// Package log provides common log handling based on [logrus][logrus] for -// services, jobs, and commands with integrated configuration loading. package log import ( "io" "os" + "regexp" + "strings" - "github.com/sirupsen/logrus" "golang.org/x/term" +) - "github.com/tkrop/go-config/log/format" +// Log levels. +const ( + // LevelPanic is the panic log level. + LevelPanic string = "panic" + // LevelFatal is the fatal log level. + LevelFatal string = "fatal" + // LevelError is the error log level. + LevelError string = "error" + // LevelWarn is the warn log level. + LevelWarn string = "warn" + // LevelWarn is the warn log level. + LevelWarning string = "warning" + // LevelInfo is the info log level. + LevelInfo string = "info" + // LevelDebug is the debug log level. + LevelDebug string = "debug" + // LevelTrace is the trace log level. + LevelTrace string = "trace" ) -// Config common configuration for logging. -type Config struct { - // Level is defining the logger level (default `info`). - Level string `default:"info"` - // TImeFormat is defining the time format for timestamps. - TimeFormat string `default:"2006-01-02T15:04:05.999999"` - // Caller is defining whether the caller is logged (default `false`). - Caller bool `default:"false"` - // File is defining the file name used for the log output. - File string `default:"/dev/stderr"` - // ColorMode is defining the color mode used for logging. - ColorMode format.ColorModeString `default:"auto"` - // OrderMode is defining the order mode used for logging. - OrderMode format.OrderModeString `default:"on"` - // Formatter is defining the formatter used for logging. - Formatter format.Formatter `default:"pretty"` +// Level is the log level used for logging. +type Level int + +// Log levels. +const ( + // PanicLevel is the log level used for panics. + PanicLevel Level = 0 + // FatalLevel is the log level used for fatal errors. + FatalLevel Level = 1 + // ErrorLevel is the log level used for errors. + ErrorLevel Level = 2 + // WarnLevel is the log level used for warnings. + WarnLevel Level = 3 + // InfoLevel is the log level used for information. + InfoLevel Level = 4 + // DebugLevel is the log level used for debugging. + DebugLevel Level = 5 + // TraceLevel is the log level used for tracing. + TraceLevel Level = 6 + // FieldLevel is and extra log level used for field names. + FieldLevel Level = 7 +) + +// ParseLevel parses the log level string and returns the corresponding level. +func ParseLevel(level string) Level { + switch strings.ToLower(level) { + case LevelPanic: + return PanicLevel + case LevelFatal: + return FatalLevel + case LevelError: + return ErrorLevel + case LevelWarn, LevelWarning: + return WarnLevel + case LevelInfo: + return InfoLevel + case LevelDebug: + return DebugLevel + case LevelTrace: + return TraceLevel + default: + return InfoLevel + } } -// ColorMode is the color mode used for logging. -type ColorModeString format.ColorModeString +// Formatter is the formatter used for logging. +type Formatter string -// Color modes. +// Formatters. +const ( + // Pretty is the pretty formatter. + FormatterPretty Formatter = "pretty" + // Text is the text formatter. + FormatterText Formatter = "text" + // JSON is the JSON formatter. + FormatterJSON Formatter = "json" +) + +// Color codes for the different log levels. +const ( + // ColorRed is the color code for red. + ColorRed = "1;91" + // ColorGreen is the color code for green. + ColorGreen = "1;92" + // ColorYellow is the color code for yellow. + ColorYellow = "1;93" + // ColorGray is the color code for gray. + ColorBlue = "1;94" + // ColorMagenta is the color code for magenta. + ColorMagenta = "1;95" + // ColorCyan is the color code for cyan. + ColorCyan = "1;96" + // ColorGray is the color code for gray. + ColorGray = "1;37" + + // ColorPanic is the color code for panic. + ColorPanic = ColorRed + // ColorFatal is the color code for fatal. + ColorFatal = ColorRed + // ColorError is the color code for error. + ColorError = ColorRed + // ColorWarn is the color code for warn. + ColorWarn = ColorYellow + // ColorInfo is the color code for info. + ColorInfo = ColorCyan + // ColorDebug is the color code for debug. + ColorDebug = ColorBlue + // ColorTrace is the color code for trace. + ColorTrace = ColorMagenta + // ColorField is the color code for fields. + ColorField = ColorGray + + // TImeFormat is defining default time format. + DefaultTimeFormat = "2006-01-02 15:04:05.999999" +) + +var ( + // DefaultLevelColors is the default color mapping for the log levels. + DefaultLevelColors = []string{ + ColorPanic, ColorFatal, ColorError, + ColorWarn, ColorInfo, ColorDebug, ColorTrace, ColorField, + } + + // DefaultLevelNames is the default name mapping for the log levels. + DefaultLevelNames = []string{ + "PANIC", "FATAL", "ERROR", "WARN", + "INFO", "DEBUG", "TRACE", "-", + } + + // DefaultErrorName is the default name used for marking errors. + DefaultErrorName = "error" +) + +// ColorModeString is the color mode used for logging. +type ColorModeString string + +// Color mode strings. const ( // ColorOff disables the color mode. - ColorModeOff format.ColorModeString = format.ColorModeOff + ColorModeOff ColorModeString = "off" // ColorOn enables the color mode. - ColorModeOn format.ColorModeString = format.ColorModeOn + ColorModeOn ColorModeString = "on" // ColorAuto enables the automatic color mode. - ColorModeAuto format.ColorModeString = format.ColorModeAuto - // ColorLevels enables the color mode for log level. - ColorModeLevels format.ColorModeString = format.ColorModeLevels + ColorModeAuto ColorModeString = "auto" + // ColorLevel enables the color mode for log level. + ColorModeLevels ColorModeString = "levels" // ColorFields enables the color mode for fields. - ColorModeFields format.ColorModeString = format.ColorModeFields + ColorModeFields ColorModeString = "fields" ) -// OrderMode is the order mode used for logging. -type OrderModeString format.OrderModeString +var splitRegex = regexp.MustCompile(`[|,:;]`) + +// Parse parses the color mode. +func (m ColorModeString) Parse(colorized bool) ColorMode { + mode := ColorUnset + for _, m := range splitRegex.Split(string(m), -1) { + switch ColorModeString(m) { + case ColorModeOff: + mode = ColorOff + case ColorModeOn: + mode = ColorOn + case ColorModeLevels: + mode |= ColorLevels + case ColorModeFields: + mode |= ColorFields + case ColorModeAuto: + fallthrough + default: + if colorized { + mode = ColorOn + } else { + mode = ColorOff + } + } + } + return mode +} + +// ColorMode is the color mode used for logging. +type ColorMode int + +// Color modes. +const ( + // ColorDefault is the default color mode. + ColorDefault = ColorOn + // ColorUnset is the unset color mode (activates the default). + ColorUnset ColorMode = 0 + // ColorOff disables coloring of logs for all outputs files. + ColorOff ColorMode = 1 + // ColorOn enables coloring of logs for all outputs files. + ColorOn ColorMode = ColorFields | ColorLevels + // ColorLevels enables coloring for log levels entries only. + ColorLevels ColorMode = 2 + // ColorFields enables coloring for fields names only. + ColorFields ColorMode = 4 +) + +// CheckFlag checks if the given color mode flag is set. +func (m ColorMode) CheckFlag(flag ColorMode) bool { + return m&flag == flag +} + +// OrderModeString is the order mode used for logging. +type OrderModeString string // Order modes. const ( - OrderModeAuto format.OrderModeString = "" - // OrderOn enables the order mode. - OrderModeOn format.OrderModeString = format.OrderModeOn - // OrderOff disables the order mode. - OrderModeOff format.OrderModeString = format.OrderModeOff + // OrderModeOff disables the order mode. + OrderModeOff OrderModeString = "off" + // OrderModeOn enables the order mode. + OrderModeOn OrderModeString = "on" ) -// Formatter is the formatter used for logging output. -type Formatter format.Formatter +// Parse parses the order mode. +func (m OrderModeString) Parse() OrderMode { + switch m { + case OrderModeOff: + return OrderOff + case OrderModeOn: + return OrderOn + default: + return OrderOff + } +} + +// OrderMode is the order mode used for logging. +type OrderMode int -// Supported formatters. +// Order modes. const ( - // Pretty is setting up a pretty formatter. - FormatterPretty format.Formatter = format.FormatterPretty - // Text is setting up a text formatter. - FormatterText format.Formatter = format.FormatterText - // JSON is setting up a JSON formatter. - FormatterJSON format.Formatter = format.FormatterJSON + // OrderDefault is the default order mode. + OrderDefault = OrderOn + // OrderUnset is the unset order mode. + OrderUnset OrderMode = 0 + // OrderOff disables the order mode. + OrderOff OrderMode = 1 + // OrderOn enables the order mode. + OrderOn OrderMode = 2 ) +// CheckFlag checks if the given order mode flag is set. +func (m OrderMode) CheckFlag(flag OrderMode) bool { + return m&flag == flag +} + // IsTerminal checks whether the given writer is a terminal. func IsTerminal(writer io.Writer) bool { if file, ok := writer.(*os.File); ok { @@ -81,56 +263,58 @@ func IsTerminal(writer io.Writer) bool { return false } -// SetupRus is setting up the given logger using. It sets the formatter, the -// log level, and the report caller flag. If no logger is given, the standard -// logger is used. -func (c *Config) SetupRus(logger *logrus.Logger) *logrus.Logger { - // Uses the standard logger if no logger is given. - if logger == nil { - logger = logrus.StandardLogger() - } +// Config common configuration for logging. +type Config struct { + // Level is defining the logger level (default `info`). + Level string `default:"info"` + // TImeFormat is defining the time format for timestamps. + TimeFormat string `default:"2006-01-02 15:04:05.999999"` + // Caller is defining whether the caller is logged (default `false`). + Caller bool `default:"false"` + // File is defining the file name used for the log output. + File string `default:"/dev/stderr"` + // ColorMode is defining the color mode used for logging. + ColorMode ColorModeString `default:"auto"` + // OrderMode is defining the order mode used for logging. + OrderMode OrderModeString `default:"on"` + // Formatter is defining the formatter used for logging. + Formatter Formatter `default:"pretty"` - // Sets up the log output format. - switch c.Formatter { - case FormatterText: - mode := c.ColorMode.Parse(IsTerminal(logger.Out)) - logger.SetFormatter(&logrus.TextFormatter{ - TimestampFormat: c.TimeFormat, - FullTimestamp: true, - ForceColors: mode&format.ColorOn == format.ColorOn, - DisableColors: mode&format.ColorOff == format.ColorOff, - }) - case FormatterJSON: - logger.SetFormatter(&logrus.JSONFormatter{ - TimestampFormat: c.TimeFormat, - }) - case FormatterPretty: - fallthrough - default: - logger.SetFormatter(&format.Pretty{ - TimeFormat: c.TimeFormat, - ColorMode: c.ColorMode.Parse(IsTerminal(logger.Out)), - OrderMode: c.OrderMode.Parse(), - LevelNames: format.DefaultLevelNames, - LevelColors: format.DefaultLevelColors, - }) - } + // logger is the logger instance defined by the config. + logger any +} - // Helpful setting in certain debug situations. - logger.SetReportCaller(c.Caller) - - // Sets up the log level if given. - logLevel, err := logrus.ParseLevel(c.Level) - if err != nil { - logger.WithError(err).WithFields(logrus.Fields{ - "config": c.Level, - }).Info("failed setting log level") - } else { - logger.SetLevel(logLevel) - logger.WithFields(logrus.Fields{ - "level": c.Level, - }).Info("setting up log level") - } +// Setup is a data structure that contains all necessary setup information to +// format logs into a pretty format. +type Setup struct { + // TimeFormat is defining the time format used for printing timestamps. + TimeFormat string + // ColorMode is defining the color mode (default = ColorAuto). + ColorMode ColorMode + // OrderMode is defining the order mode. + OrderMode OrderMode + // Caller is defining whether the caller is reported. + Caller bool - return logger + // ErrorName is defining the name used for marking errors. + ErrorName string + // LevelNames is defining the names used for marking the different log + // levels. + LevelNames []string + // LevelColors is defining the colors used for marking the different log + // levels. + LevelColors []string +} + +// Setup creates a new pretty formatter config. +func (c *Config) Setup(writer io.Writer) *Setup { + return &Setup{ + TimeFormat: c.TimeFormat, + ColorMode: c.ColorMode.Parse(IsTerminal(writer)), + OrderMode: c.OrderMode.Parse(), + Caller: c.Caller, + ErrorName: DefaultErrorName, + LevelNames: DefaultLevelNames, + LevelColors: DefaultLevelColors, + } } diff --git a/log/log_test.go b/log/log_test.go index 68d6e90..620df3d 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -1,58 +1,200 @@ package log_test import ( - "bytes" - "testing" + "errors" + "runtime" + "strconv" "time" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - "github.com/tkrop/go-config/config" "github.com/tkrop/go-config/log" - "github.com/tkrop/go-config/log/format" - "github.com/tkrop/go-testing/test" ) const ( // Default log level in configuration. - DefaultLogLevel = "info" + defaultLogLevel = "info" // Default report caller flag in configuration. - DefaultLogCaller = false + defaultLogCaller = false +) + +var ( + // otime is a fixed output time string for testing. + otime = "2024-10-01 23:07:13.891012345Z" + // itime is a fixed input time string for testing. + itime = "2024-10-01T23:07:13.891012345Z" + // ttime is a fixed time stamp for testing. + ttime, terr = time.Parse(time.RFC3339Nano, itime) + // Arbitrary frame for testing. + anyFrame = &runtime.Frame{ + File: "file", + Function: "function", + Line: 123, + } + // Arbitrary error for testing. + errAny = errors.New("any error") ) -// DefaultLogTimeFormat contains the default timestamp format. -var DefaultLogTimeFormat = time.RFC3339Nano[0:26] +// caller returns the file and line of the caller. +func caller(offset int) string { + if _, file, line, ok := runtime.Caller(1); ok { + return file + ":" + strconv.Itoa(line+offset) + } + return "unknown" +} + +// Helper functions for testing log levels without color. +func level(level log.Level) string { + return log.DefaultLevelNames[level] +} + +// Helper functions for testing log levels with color. +func levelC(level log.Level) string { + return "\x1b[" + log.DefaultLevelColors[level] + + "m" + log.DefaultLevelNames[level] + "\x1b[0m" +} + +// Helper functions for testing fields without color. +func field(value string) string { + return value +} + +// Helper functions for testing fields with color. +func fieldC(value string) string { + return "\x1b[" + log.ColorField + "m" + value + "\x1b[0m" +} + +// Helper functions for testing key data without color. +func key(key string) string { + return key + "=" +} + +// Helper functions for testing key data with color. +func keyC(key string) string { + color := log.ColorField + if key == log.DefaultErrorName { + color = log.ColorError + } + return "\x1b[" + color + "m" + key + "\x1b[0m=" +} + +// Helper functions for testing key-value data without color. +func data(key, value string) string { + return key + "=\"" + value + "\"" +} + +// Helper functions for testing key-value data with color. +func dataC(key, value string) string { + color := log.ColorField + if key == log.DefaultErrorName { + color = log.ColorError + } + return "\x1b[" + color + "m" + key + "\x1b[0m=\"" + value + "\"" +} type setupParams struct { config *log.Config expectTimeFormat string expectLogLevel string expectLogCaller bool + expectColorMode log.ColorMode + expectOrderMode log.OrderMode } var testSetupParams = map[string]setupParams{ - "read default config no logger": { + "read default config": { config: &log.Config{}, - expectLogLevel: DefaultLogLevel, - expectTimeFormat: DefaultLogTimeFormat, - expectLogCaller: DefaultLogCaller, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, - "read default config": { - config: &log.Config{}, - expectLogLevel: DefaultLogLevel, - expectTimeFormat: DefaultLogTimeFormat, - expectLogCaller: DefaultLogCaller, + "log level panic": { + config: &log.Config{ + Level: log.LevelPanic, + }, + expectLogLevel: log.LevelPanic, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + }, + + "log level fatal": { + config: &log.Config{ + Level: log.LevelFatal, + }, + expectLogLevel: log.LevelFatal, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + }, + + "log level error": { + config: &log.Config{ + Level: log.LevelError, + }, + expectLogLevel: log.LevelError, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + }, + + "log level warn": { + config: &log.Config{ + Level: log.LevelWarn, + }, + expectLogLevel: log.LevelWarn, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, - "log level custom": { + "log level warning": { config: &log.Config{ - Level: "debug", + Level: log.LevelWarning, }, - expectLogLevel: "debug", - expectTimeFormat: DefaultLogTimeFormat, - expectLogCaller: DefaultLogCaller, + expectLogLevel: log.LevelWarning, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + }, + + "log level info": { + config: &log.Config{ + Level: log.LevelInfo, + }, + expectLogLevel: log.LevelInfo, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + }, + + "log level debug": { + config: &log.Config{ + Level: log.LevelDebug, + }, + expectLogLevel: log.LevelDebug, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + }, + + "log level trace": { + config: &log.Config{ + Level: log.LevelTrace, + }, + expectLogLevel: log.LevelTrace, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, "log level invalid": { @@ -60,101 +202,160 @@ var testSetupParams = map[string]setupParams{ Level: "detail", }, expectLogLevel: "info", - expectTimeFormat: DefaultLogTimeFormat, - expectLogCaller: DefaultLogCaller, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, "time format date": { config: &log.Config{ TimeFormat: "2024-12-31", }, - expectLogLevel: DefaultLogLevel, + expectLogLevel: defaultLogLevel, expectTimeFormat: "2024-12-31", - expectLogCaller: DefaultLogCaller, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, "caller enabled": { config: &log.Config{ Caller: true, }, - expectLogLevel: DefaultLogLevel, - expectTimeFormat: DefaultLogTimeFormat, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, expectLogCaller: true, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, - "formater text": { + "formatter text": { config: &log.Config{ - Formatter: format.FormatterText, + Formatter: log.FormatterText, }, - expectLogLevel: DefaultLogLevel, - expectTimeFormat: DefaultLogTimeFormat, - expectLogCaller: DefaultLogCaller, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, - "formater json": { + "formatter json": { config: &log.Config{ - Formatter: format.FormatterJSON, + Formatter: log.FormatterJSON, }, - expectLogLevel: DefaultLogLevel, - expectTimeFormat: DefaultLogTimeFormat, - expectLogCaller: DefaultLogCaller, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, - "formater pretty": { + "formatter pretty default": { config: &log.Config{ - Formatter: format.FormatterPretty, + Formatter: log.FormatterPretty, }, - expectLogLevel: DefaultLogLevel, - expectTimeFormat: DefaultLogTimeFormat, - expectLogCaller: DefaultLogCaller, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectLogCaller: defaultLogCaller, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, }, -} -func TestSetupRus(t *testing.T) { - test.Map(t, testSetupParams). - Run(func(t test.Test, param setupParams) { - // Given - logger := logrus.New() - logger.SetOutput(&bytes.Buffer{}) - config := config.New[config.Config]("TEST", "test"). - SetSubDefaults("log", param.config, false). - GetConfig(t.Name()) - - // When - config.Log.SetupRus(logger) - - // Then - switch param.config.Formatter { - case format.FormatterText: - assert.IsType(t, &logrus.TextFormatter{}, logger.Formatter) - assert.Equal(t, param.expectTimeFormat, - logger.Formatter.(*logrus.TextFormatter).TimestampFormat) - case format.FormatterJSON: - assert.IsType(t, &logrus.JSONFormatter{}, logger.Formatter) - assert.Equal(t, param.expectTimeFormat, - logger.Formatter.(*logrus.JSONFormatter).TimestampFormat) - case format.FormatterPretty: - assert.IsType(t, &format.Pretty{}, logger.Formatter) - assert.Equal(t, param.expectTimeFormat, - logger.Formatter.(*format.Pretty).TimeFormat) - default: - assert.IsType(t, &format.Pretty{}, logger.Formatter) - assert.Equal(t, param.expectTimeFormat, - logger.Formatter.(*format.Pretty).TimeFormat) - } - assert.Equal(t, param.expectLogLevel, logger.GetLevel().String()) - assert.Equal(t, param.expectLogCaller, logger.ReportCaller) - }) -} + "formatter pretty color-on": { + config: &log.Config{ + Formatter: log.FormatterPretty, + ColorMode: log.ColorModeOn, + }, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectColorMode: log.ColorOn, + expectOrderMode: log.OrderOn, + expectLogCaller: defaultLogCaller, + }, -func TestSetupNil(t *testing.T) { - // Given - config := config.New[config.Config]("TEST", "test"). - GetConfig(t.Name()) + "formatter pretty color-off": { + config: &log.Config{ + Formatter: log.FormatterPretty, + ColorMode: log.ColorModeOff, + }, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + expectLogCaller: defaultLogCaller, + }, - // When - config.Log.SetupRus(nil) + "formatter pretty color-levels": { + config: &log.Config{ + Formatter: log.FormatterPretty, + ColorMode: log.ColorModeLevels, + }, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectColorMode: log.ColorLevels, + expectOrderMode: log.OrderOn, + expectLogCaller: defaultLogCaller, + }, - // Then - assert.True(t, true) + "formatter pretty color-fields": { + config: &log.Config{ + Formatter: log.FormatterPretty, + ColorMode: log.ColorModeFields, + }, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectColorMode: log.ColorFields, + expectOrderMode: log.OrderOn, + expectLogCaller: defaultLogCaller, + }, + + "formatter pretty color-any": { + config: &log.Config{ + Formatter: log.FormatterPretty, + ColorMode: "any", + }, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + expectLogCaller: defaultLogCaller, + }, + + "formatter pretty order-on": { + config: &log.Config{ + Formatter: log.FormatterPretty, + OrderMode: log.OrderModeOn, + }, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOn, + expectLogCaller: defaultLogCaller, + }, + + "formatter pretty order-off": { + config: &log.Config{ + Formatter: log.FormatterPretty, + OrderMode: log.OrderModeOff, + }, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOff, + expectLogCaller: defaultLogCaller, + }, + + "formatter pretty order-any": { + config: &log.Config{ + Formatter: log.FormatterPretty, + OrderMode: "any", + }, + expectLogLevel: defaultLogLevel, + expectTimeFormat: log.DefaultTimeFormat, + expectColorMode: log.ColorOff, + expectOrderMode: log.OrderOff, + expectLogCaller: defaultLogCaller, + }, } diff --git a/log/logrus.go b/log/logrus.go new file mode 100644 index 0000000..5241a9a --- /dev/null +++ b/log/logrus.go @@ -0,0 +1,85 @@ +package log + +import ( + "bytes" + "io" + "maps" + "slices" + "sort" + + "github.com/sirupsen/logrus" +) + +// SetupRus is setting up and returning the given logger. It particular sets up +// the log level, the report caller flag, as well as the formatter with color +// and order mode. If no logger is given, the standard logger is set up. +func (c *Config) SetupRus(writer io.Writer, logger *logrus.Logger) *logrus.Logger { + // Uses the standard logger if no logger is given. + if logger == nil { + logger = logrus.StandardLogger() + } + + logger.SetOutput(writer) + // #nosec G115 // cannot happen. + logger.SetLevel(logrus.Level(ParseLevel(c.Level))) + logger.SetReportCaller(c.Caller) + + // Sets up the log output format. + switch c.Formatter { + case FormatterText: + color := c.ColorMode.Parse(IsTerminal(logger.Out)) + logger.SetFormatter(&logrus.TextFormatter{ + TimestampFormat: c.TimeFormat, + FullTimestamp: true, + ForceColors: color&ColorOn == ColorOn, + DisableColors: color&ColorOff == ColorOff, + }) + case FormatterJSON: + logger.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: c.TimeFormat, + }) + case FormatterPretty: + fallthrough + default: + logger.SetFormatter(NewLogRusPretty(c, writer)) + } + + return logger +} + +// LogRusPretty formats logs into a pretty format. +type LogRusPretty struct { + *Setup +} + +// NewLogRusPretty creates a new pretty formatter for logrus. +func NewLogRusPretty(c *Config, writer io.Writer) *LogRusPretty { + return &LogRusPretty{ + Setup: c.Setup(writer), + } +} + +// Format formats the log entry to a pretty format. +func (p *LogRusPretty) Format(entry *logrus.Entry) ([]byte, error) { + buffer := NewBuffer(p.Setup, &bytes.Buffer{}) + buffer.WriteString(entry.Time.Format(p.TimeFormat)). + WriteByte(' ').WriteLevel(Level(entry.Level)) + if entry.HasCaller() { + buffer.WriteCaller(entry.Caller) + } + buffer.WriteByte(' ').WriteString(entry.Message) + + for _, key := range p.getSortedKeys(entry.Data) { + buffer.WriteByte(' ').WriteData(key, entry.Data[key]) + } + return buffer.WriteByte('\n').Bytes() +} + +// getSortedKeys returns the keys of the given data. +func (p *LogRusPretty) getSortedKeys(data logrus.Fields) []string { + keys := slices.Collect(maps.Keys(data)) + if p.OrderMode.CheckFlag(OrderOn) { + sort.Strings(keys) + } + return keys +} diff --git a/log/logrus_test.go b/log/logrus_test.go new file mode 100644 index 0000000..78ad581 --- /dev/null +++ b/log/logrus_test.go @@ -0,0 +1,530 @@ +package log_test + +import ( + "os" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/tkrop/go-testing/test" + + "github.com/tkrop/go-config/config" + "github.com/tkrop/go-config/log" +) + +func TestSetupRus(t *testing.T) { + test.Map(t, testSetupParams). + Run(func(t test.Test, param setupParams) { + // Given + logger := logrus.New() + config := config.New[config.Config]("TEST", "test"). + SetSubDefaults("log", param.config, false). + GetConfig(t.Name()) + + // When + config.Log.SetupRus(os.Stderr, logger) + + // Then + switch param.config.Formatter { + case log.FormatterText: + assert.IsType(t, &logrus.TextFormatter{}, logger.Formatter) + format := logger.Formatter.(*logrus.TextFormatter) + assert.Equal(t, param.expectTimeFormat, format.TimestampFormat) + assert.Equal(t, param.expectColorMode.CheckFlag(log.ColorOn), + format.ForceColors) + case log.FormatterJSON: + assert.IsType(t, &logrus.JSONFormatter{}, logger.Formatter) + assert.Equal(t, param.expectTimeFormat, + logger.Formatter.(*logrus.JSONFormatter).TimestampFormat) + case log.FormatterPretty: + assert.IsType(t, &log.LogRusPretty{}, logger.Formatter) + pretty := logger.Formatter.(*log.LogRusPretty).Setup + assert.Equal(t, param.expectTimeFormat, pretty.TimeFormat) + assert.Equal(t, param.expectColorMode, pretty.ColorMode) + assert.Equal(t, param.expectOrderMode, pretty.OrderMode) + default: + assert.IsType(t, &log.LogRusPretty{}, logger.Formatter) + assert.Equal(t, param.expectTimeFormat, + logger.Formatter.(*log.LogRusPretty).TimeFormat) + } + + assert.Equal(t, log.ParseLevel(param.expectLogLevel), + log.ParseLevel(logger.GetLevel().String())) + assert.Equal(t, param.expectLogCaller, logger.ReportCaller) + assert.Equal(t, os.Stderr, logger.Out) + }) +} + +func TestSetupNil(t *testing.T) { + // Given + config := config.New[config.Config]("TEST", "test"). + GetConfig(t.Name()) + + // When + logger := config.Log.SetupRus(os.Stderr, nil) + + // Then + assert.True(t, true) + assert.Equal(t, logrus.StandardLogger(), logger) +} + +// Arbitrary data for testing. +var anyData = logrus.Fields{ + "key1": "value1", + "key2": "value2", +} + +type testPrettyLogRusParam struct { + noTerminal bool + config log.Config + entry *logrus.Entry + expect func(t test.Test, result string, err error) + expectResult string +} + +var testPrettyLogRusParams = map[string]testPrettyLogRusParam{ + // Test levels with default. + "level panic default": { + config: log.Config{Level: "panic"}, + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "panic message", + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " panic message\n", + }, + "level fatal default": { + config: log.Config{Level: "fatal"}, + entry: &logrus.Entry{ + Level: logrus.FatalLevel, + Message: "fatal message", + }, + expectResult: otime[0:26] + " " + + levelC(log.FatalLevel) + " fatal message\n", + }, + "level error default": { + config: log.Config{Level: "error"}, + entry: &logrus.Entry{ + Level: logrus.ErrorLevel, + Message: "error message", + }, + expectResult: otime[0:26] + " " + + levelC(log.ErrorLevel) + " error message\n", + }, + "level warn default": { + config: log.Config{Level: "warn"}, + entry: &logrus.Entry{ + Level: logrus.WarnLevel, + Message: "warn message", + }, + expectResult: otime[0:26] + " " + + levelC(log.WarnLevel) + " warn message\n", + }, + "level info default": { + config: log.Config{Level: "info"}, + entry: &logrus.Entry{ + Level: logrus.InfoLevel, + Message: "info message", + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " info message\n", + }, + "level debug default": { + config: log.Config{Level: "debug"}, + entry: &logrus.Entry{ + Level: logrus.DebugLevel, + Message: "debug message", + }, + expectResult: otime[0:26] + " " + + levelC(log.DebugLevel) + " debug message\n", + }, + "level trace default": { + config: log.Config{Level: "trace"}, + entry: &logrus.Entry{ + Level: logrus.TraceLevel, + Message: "trace message", + }, + expectResult: otime[0:26] + " " + + levelC(log.TraceLevel) + " trace message\n", + }, + + // Test levels with color. + "level panic color-on": { + config: log.Config{Level: "panic", ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "panic message", + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " panic message\n", + }, + "level fatal color-on": { + config: log.Config{Level: "fatal", ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Level: logrus.FatalLevel, + Message: "fatal message", + }, + expectResult: otime[0:26] + " " + + levelC(log.FatalLevel) + " fatal message\n", + }, + "level error color-on": { + config: log.Config{Level: "error", ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Level: logrus.ErrorLevel, + Message: "error message", + }, + expectResult: otime[0:26] + " " + + levelC(log.ErrorLevel) + " error message\n", + }, + "level warn color-on": { + config: log.Config{Level: "warn", ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Level: logrus.WarnLevel, + Message: "warn message", + }, + expectResult: otime[0:26] + " " + + levelC(log.WarnLevel) + " warn message\n", + }, + "level info color-on": { + config: log.Config{Level: "info", ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Level: logrus.InfoLevel, + Message: "info message", + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " info message\n", + }, + "level debug color-on": { + config: log.Config{Level: "debug", ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Level: logrus.DebugLevel, + Message: "debug message", + }, + expectResult: otime[0:26] + " " + + levelC(log.DebugLevel) + " debug message\n", + }, + "level trace color-on": { + config: log.Config{Level: "trace", ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Level: logrus.TraceLevel, + Message: "trace message", + }, + expectResult: otime[0:26] + " " + + levelC(log.TraceLevel) + " trace message\n", + }, + + // Test levels with color. + "level panic color-off": { + config: log.Config{Level: "panic", ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "panic message", + }, + expectResult: otime[0:26] + " " + + level(log.PanicLevel) + " panic message\n", + }, + "level fatal color-off": { + config: log.Config{Level: "fatal", ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Level: logrus.FatalLevel, + Message: "fatal message", + }, + expectResult: otime[0:26] + " " + + level(log.FatalLevel) + " fatal message\n", + }, + "level error color-off": { + config: log.Config{Level: "error", ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Level: logrus.ErrorLevel, + Message: "error message", + }, + expectResult: otime[0:26] + " " + + level(log.ErrorLevel) + " error message\n", + }, + "level warn color-off": { + config: log.Config{Level: "warning", ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Level: logrus.WarnLevel, + Message: "warn message", + }, + expectResult: otime[0:26] + " " + + level(log.WarnLevel) + " warn message\n", + }, + "level info color-off": { + config: log.Config{Level: "info", ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Level: logrus.InfoLevel, + Message: "info message", + }, + expectResult: otime[0:26] + " " + + level(log.InfoLevel) + " info message\n", + }, + "level debug color-off": { + config: log.Config{Level: "debug", ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Level: logrus.DebugLevel, + Message: "debug message", + }, + expectResult: otime[0:26] + " " + + level(log.DebugLevel) + " debug message\n", + }, + "level trace color-off": { + config: log.Config{Level: "trace", ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Level: logrus.TraceLevel, + Message: "trace message", + }, + expectResult: otime[0:26] + " " + + level(log.TraceLevel) + " trace message\n", + }, + + // Test order key value data. + "data default": { + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data ordered": { + config: log.Config{OrderMode: log.OrderModeOn}, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data unordered": { + config: log.Config{OrderMode: log.OrderModeOff}, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expect: func(t test.Test, result string, err error) { + assert.Contains(t, result, otime[0:26]+" "+ + levelC(log.PanicLevel)+" "+"data message") + assert.Contains(t, result, dataC("key1", "value1")) + assert.Contains(t, result, dataC("key2", "value2")) + }, + }, + + // Test color modes. + "data color-off": { + config: log.Config{ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expectResult: otime[0:26] + " " + + level(log.PanicLevel) + " data message " + + data("key1", "value1") + " " + + data("key2", "value2") + "\n", + }, + "data color-on": { + config: log.Config{ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data color-auto colorized": { + config: log.Config{ColorMode: log.ColorModeAuto}, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data color-auto not-colorized": { + noTerminal: true, + config: log.Config{ColorMode: log.ColorModeAuto}, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + Logger: &logrus.Logger{Out: nil}, + }, + expectResult: otime[0:26] + " " + + level(log.PanicLevel) + " data message " + + data("key1", "value1") + " " + + data("key2", "value2") + "\n", + }, + "data color-levels": { + config: log.Config{ColorMode: log.ColorModeLevels}, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " data message " + + data("key1", "value1") + " " + + data("key2", "value2") + "\n", + }, + "data color-fields": { + config: log.Config{ColorMode: log.ColorModeFields}, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expectResult: otime[0:26] + " " + + level(log.PanicLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data color-levels+fields": { + config: log.Config{ + ColorMode: log.ColorModeLevels + "|" + log.ColorModeFields, + }, + entry: &logrus.Entry{ + Message: "data message", + Data: anyData, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + + // Time format. + "time default": { + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "default time message", + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " " + + "default time message\n", + }, + "time short": { + config: log.Config{ + TimeFormat: "2006-01-02 15:04:05", + }, + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "short time message", + }, + expectResult: otime[0:19] + " " + + levelC(log.PanicLevel) + " " + + "short time message\n", + }, + "time long": { + config: log.Config{ + TimeFormat: "2006-01-02 15:04:05.000000000", + }, + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "long time message", + }, + expectResult: otime[0:29] + " " + + levelC(log.PanicLevel) + " " + + "long time message\n", + }, + + // Report caller. + "caller only": { + entry: &logrus.Entry{ + Message: "caller message", + Caller: anyFrame, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " " + + "caller message\n", + }, + "caller report": { + entry: &logrus.Entry{ + Message: "caller report message", + Caller: anyFrame, + Logger: &logrus.Logger{ + ReportCaller: true, + }, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " " + + "[file:123#function] caller report message\n", + }, + + // Test error. + "error output": { + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "error message", + Data: logrus.Fields{ + logrus.ErrorKey: errAny, + }, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " error message " + + dataC("error", errAny.Error()) + "\n", + }, + "error output color-on": { + config: log.Config{ColorMode: log.ColorModeOn}, + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "error message", + Data: logrus.Fields{ + logrus.ErrorKey: errAny, + }, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " error message " + + dataC("error", errAny.Error()) + "\n", + }, + "error output color-off": { + config: log.Config{ColorMode: log.ColorModeOff}, + entry: &logrus.Entry{ + Level: logrus.PanicLevel, + Message: "error message", + Data: logrus.Fields{ + logrus.ErrorKey: errAny, + }, + }, + expectResult: otime[0:26] + " " + + level(log.PanicLevel) + " error message " + + data("error", errAny.Error()) + "\n", + }, +} + +func TestPrettyLogRus(t *testing.T) { + test.Map(t, testPrettyLogRusParams). + Run(func(t test.Test, param testPrettyLogRusParam) { + // Given + config := config.New[config.Config]("X", "app"). + SetSubDefaults("log", param.config, true). + SetDefaults(func(r *config.Reader[config.Config]) { + r.SetDefault("log.level", "trace") + }).GetConfig("logrus") + pretty := config.Log.SetupRus(os.Stderr, logrus.New()).Formatter + pretty.(*log.LogRusPretty).Setup. + ColorMode = param.config.ColorMode.Parse(!param.noTerminal) + + if param.entry.Time == (time.Time{}) { + time, err := time.Parse(time.RFC3339Nano, itime) + assert.NoError(t, err) + param.entry.Time = time + } + + // When + result, err := pretty.Format(param.entry) + + // Then + if param.expect == nil { + assert.Equal(t, param.expectResult, string(result)) + } else { + param.expect(t, string(result), err) + } + }) +} diff --git a/log/zerolog.go b/log/zerolog.go new file mode 100644 index 0000000..6178172 --- /dev/null +++ b/log/zerolog.go @@ -0,0 +1,184 @@ +package log + +import ( + "bytes" + "fmt" + "io" + "strings" + "time" + + "github.com/rs/zerolog" +) + +// ParseZeroLevel parses the log level string and returns the corresponding +// zerolog level. +func (c *Config) ParseZeroLevel() zerolog.Level { + switch strings.ToLower(c.Level) { + case LevelPanic: + return zerolog.PanicLevel + case LevelFatal: + return zerolog.FatalLevel + case LevelError: + return zerolog.ErrorLevel + case LevelWarn, LevelWarning: + return zerolog.WarnLevel + case LevelInfo: + return zerolog.InfoLevel + case LevelDebug: + return zerolog.DebugLevel + case LevelTrace: + return zerolog.TraceLevel + default: + return zerolog.InfoLevel + } +} + +// SetupZero sets up the zerolog logger. It particular it sets up the log +// level, the report caller flag, as well as the formatter with color and order +// mode. +func (c *Config) SetupZero(writer io.Writer) *Config { + logger := zerolog.New(writer).Level(c.ParseZeroLevel()) + + switch c.Formatter { + case FormatterText: + color := c.ColorMode.Parse(IsTerminal(writer)) + logger = logger.Output(zerolog.ConsoleWriter{ + Out: writer, + NoColor: color == ColorOff, + TimeFormat: c.TimeFormat, + }) + case FormatterJSON: + logger = logger.Output(writer) + case FormatterPretty: + fallthrough + default: + logger = logger.Output(NewZeroLogPretty(c, writer)) + } + + context := logger.With().Timestamp() + if c.Caller { + context = context.Caller() + } + + c.logger = context.Logger() + + return c +} + +// Zero returns the zerolog logger. +func (c *Config) Zero() zerolog.Logger { + return c.logger.(zerolog.Logger) +} + +// ZeroLogPretty formats logs into a pretty format. +type ZeroLogPretty struct { + // Setup provides the setup for formatting logs. + *Setup + // ConsoleWriter is the console writer used for writing logs. + zerolog.ConsoleWriter +} + +func NewZeroLogPretty(c *Config, writer io.Writer) *ZeroLogPretty { + setup := c.Setup(writer) + return &ZeroLogPretty{ + Setup: setup, + ConsoleWriter: zerolog.ConsoleWriter{ + Out: writer, + TimeFormat: setup.TimeFormat, + FormatTimestamp: setup.FormatTimestamp, + FormatLevel: setup.FormatLevel, + FormatCaller: setup.FormatCaller, + FormatMessage: setup.FormatMessage, + FormatErrFieldName: setup.FormatErrFieldName, + FormatErrFieldValue: setup.FormatErrFieldValue, + FormatFieldName: setup.FormatFieldName, + FormatFieldValue: setup.FormatFieldValue, + }, + } +} + +func (s *Setup) FormatTimestamp(i any) string { + if timestamp, ok := i.(string); ok { + if ttime, err := time.Parse(time.RFC3339, timestamp); err == nil { + return ttime.Format(s.TimeFormat) + } + return timestamp + } + return fmt.Sprintf("%v", i) +} + +// Format formats the log entry. +func (s *Setup) FormatLevel(i any) string { + if level, ok := i.(string); ok { + level := ParseLevel(level) + buffer := NewBuffer(s, &bytes.Buffer{}) + if s.ColorMode.CheckFlag(ColorLevels) { + buffer.WriteColored(s.LevelColors[level], s.LevelNames[level]) + } else { + buffer.WriteString(s.LevelNames[level]) + } + return buffer.String() + } + return fmt.Sprintf("%v", i) +} + +// FormatCaller formats the caller. +func (s *Setup) FormatCaller(i any) string { + if !s.Caller { + return "" + } else if caller, ok := i.(string); ok { + return `[` + caller + `]` + } + return fmt.Sprintf("[%v]", i) +} + +// FormatMessage formats the message. +func (*Setup) FormatMessage(i any) string { + if message, ok := i.(string); ok { + return message + } + return fmt.Sprintf("%v", i) +} + +// FormatErrFieldName formats the error field name. +func (s *Setup) FormatErrFieldName(i any) string { + if name, ok := i.(string); ok { + buffer := NewBuffer(s, &bytes.Buffer{}) + if s.ColorMode.CheckFlag(ColorFields) { + buffer.WriteColored(ColorError, name) + } else { + buffer.WriteString(name) + } + return buffer.WriteByte('=').String() + } + return fmt.Sprintf("%v=", i) +} + +// FormatErrFieldValue formats the error field value. +func (*Setup) FormatErrFieldValue(i any) string { + if value, ok := i.(string); ok { + return value + } + return fmt.Sprintf("%v", i) +} + +// FormatFieldName formats the field name. +func (s *Setup) FormatFieldName(i any) string { + if field, ok := i.(string); ok { + buffer := NewBuffer(s, &bytes.Buffer{}) + if s.ColorMode.CheckFlag(ColorFields) { + buffer.WriteColored(ColorField, field) + } else { + buffer.WriteString(field) + } + return buffer.WriteByte('=').String() + } + return fmt.Sprintf("%v=", i) +} + +func (*Setup) FormatFieldValue(i any) string { + if value, ok := i.(string); ok { + return `"` + value + `"` + } + return fmt.Sprintf("\"%v\"", i) +} diff --git a/log/zerolog_test.go b/log/zerolog_test.go new file mode 100644 index 0000000..5fc59af --- /dev/null +++ b/log/zerolog_test.go @@ -0,0 +1,932 @@ +package log_test + +import ( + "bytes" + "os" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tkrop/go-testing/mock" + "github.com/tkrop/go-testing/test" + + "github.com/tkrop/go-config/config" + "github.com/tkrop/go-config/log" +) + +func TestSetupZero(t *testing.T) { + test.Map(t, testSetupParams). + Run(func(t test.Test, param setupParams) { + // Given + config := config.New[config.Config]("TEST", "test"). + SetSubDefaults("log", param.config, false). + GetConfig(t.Name()) + + // When + logger := config.Log.SetupZero(os.Stderr).Zero() + + // Then + assert.Equal(t, log.ParseLevel(param.expectLogLevel), + log.ParseLevel(logger.GetLevel().String())) + + // Check if the writer is set up correctly. + writer := test.NewAccessor(logger).Get("w") + require.IsType(t, zerolog.LevelWriterAdapter{}, writer) + adapter, ok := writer.(zerolog.LevelWriterAdapter) + require.True(t, ok) + + switch param.config.Formatter { + case log.FormatterJSON: + require.IsType(t, &os.File{}, adapter.Writer) + + case log.FormatterText: + require.IsType(t, zerolog.ConsoleWriter{}, adapter.Writer) + writer, ok := adapter.Writer.(zerolog.ConsoleWriter) + require.True(t, ok) + + assert.Equal(t, os.Stderr, writer.Out) + assert.Equal(t, param.expectTimeFormat, writer.TimeFormat) + assert.Equal(t, param.expectColorMode.CheckFlag(log.ColorOff), + writer.NoColor) + + case log.FormatterPretty: + fallthrough + default: + require.IsType(t, &log.ZeroLogPretty{}, adapter.Writer) + writer, ok := adapter.Writer.(*log.ZeroLogPretty) + require.True(t, ok) + + assert.Equal(t, os.Stderr, writer.Out) + assert.Equal(t, param.expectTimeFormat, writer.Setup.TimeFormat) + assert.Equal(t, param.expectTimeFormat, writer.ConsoleWriter.TimeFormat) + assert.Equal(t, param.expectColorMode, writer.Setup.ColorMode) + assert.Equal(t, param.expectOrderMode, writer.Setup.OrderMode) + } + + // Check if the hooks are set up with caller hook. + hooks := test.NewAccessor(logger).Get("hooks") + require.IsType(t, []zerolog.Hook{}, hooks) + hookSlice, ok := hooks.([]zerolog.Hook) + require.True(t, ok) + if param.expectLogCaller { + assert.Len(t, hookSlice, 2) + } else { + assert.Len(t, hookSlice, 1) + } + }) +} + +type testZeroLogParam struct { + config log.Config + noTerminal bool + setup func(zerolog.Logger) + expect mock.SetupFunc + expectResult string +} + +var testZeroLogParams = map[string]testZeroLogParam{ + // Test levels with default. + "level panic default": { + config: log.Config{Level: "panic"}, + setup: func(logger zerolog.Logger) { + logger.Panic().Msg("panic message") + }, + expect: test.Panic("panic message"), + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " panic message\n", + }, + // Fatal is not testable this way since it is calling `os.Exit``. It needs + // to be tested in spawned process instead. + // "level fatal default": { + // config: log.Config{Level: "fatal"}, + // setup: func(logger zerolog.Logger) { + // logger.Fatal().Msg("fatal message") + // }, + // expect: test.Panic("fatal message"), + // expectResult: otime[0:26] + " " + + // levelC(log.FatalLevel) + " fatal message\n", + // }, + "level error default": { + config: log.Config{Level: "error"}, + setup: func(logger zerolog.Logger) { + logger.Error().Msg("error message") + }, + expectResult: otime[0:26] + " " + + levelC(log.ErrorLevel) + " error message\n", + }, + "level warn default": { + config: log.Config{Level: "warn"}, + setup: func(logger zerolog.Logger) { + logger.Warn().Msg("warn message") + }, + expectResult: otime[0:26] + " " + + levelC(log.WarnLevel) + " warn message\n", + }, + "level info default": { + config: log.Config{Level: "info"}, + setup: func(logger zerolog.Logger) { + logger.Info().Msg("info message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " info message\n", + }, + "level debug default": { + config: log.Config{Level: "debug"}, + setup: func(logger zerolog.Logger) { + logger.Debug().Msg("debug message") + }, + expectResult: otime[0:26] + " " + + levelC(log.DebugLevel) + " debug message\n", + }, + "level trace default": { + config: log.Config{Level: "trace"}, + setup: func(logger zerolog.Logger) { + logger.Trace().Msg("trace message") + }, + expectResult: otime[0:26] + " " + + levelC(log.TraceLevel) + " trace message\n", + }, + + // Test levels with color. + "level panic color-on": { + config: log.Config{Level: "panic", ColorMode: log.ColorModeOn}, + setup: func(logger zerolog.Logger) { + logger.Panic().Msg("panic message") + }, + expect: test.Panic("panic message"), + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " panic message\n", + }, + // Fatal is not testable this way since it is calling `os.Exit``. It needs + // to be tested in spawned process instead. + // "level fatal color-on": { + // config: log.Config{Level: "fatal", ColorMode: log.ColorModeOn}, + // setup: func(logger zerolog.Logger) { + // logger.Fatal().Msg("fatal message") + // }, + // expect: test.Panic("fatal message"), + // expectResult: otime[0:26] + " " + + // levelC(log.FatalLevel) + " fatal message\n", + // }, + "level error color-on": { + config: log.Config{Level: "error", ColorMode: log.ColorModeOn}, + setup: func(logger zerolog.Logger) { + logger.Error().Msg("error message") + }, + expectResult: otime[0:26] + " " + + levelC(log.ErrorLevel) + " error message\n", + }, + "level warn color-on": { + config: log.Config{Level: "warn", ColorMode: log.ColorModeOn}, + setup: func(logger zerolog.Logger) { + logger.Warn().Msg("warn message") + }, + expectResult: otime[0:26] + " " + + levelC(log.WarnLevel) + " warn message\n", + }, + "level info color-on": { + config: log.Config{Level: "info", ColorMode: log.ColorModeOn}, + setup: func(logger zerolog.Logger) { + logger.Info().Msg("info message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " info message\n", + }, + "level debug color-on": { + config: log.Config{Level: "debug", ColorMode: log.ColorModeOn}, + setup: func(logger zerolog.Logger) { + logger.Debug().Msg("debug message") + }, + expectResult: otime[0:26] + " " + + levelC(log.DebugLevel) + " debug message\n", + }, + "level trace color-on": { + config: log.Config{Level: "trace", ColorMode: log.ColorModeOn}, + setup: func(logger zerolog.Logger) { + logger.Trace().Msg("trace message") + }, + expectResult: otime[0:26] + " " + + levelC(log.TraceLevel) + " trace message\n", + }, + + // Test levels with color. + "level panic color-off": { + config: log.Config{Level: "panic", ColorMode: log.ColorModeOff}, + expect: test.Panic("panic message"), + setup: func(logger zerolog.Logger) { + logger.Panic().Msg("panic message") + }, + expectResult: otime[0:26] + " " + + level(log.PanicLevel) + " panic message\n", + }, + // "level fatal color-off": { + // config: log.Config{Level: "fatal", ColorMode: log.ColorModeOff}, + // expect: test.Panic("fatal message"), + // setup: func(logger zerolog.Logger) { + // logger.Fatal().Msg("fatal message") + // }, + // expectResult: otime[0:26] + " " + + // level(log.FatalLevel) + " fatal message\n", + // }, + "level error color-off": { + config: log.Config{Level: "error", ColorMode: log.ColorModeOff}, + setup: func(logger zerolog.Logger) { + logger.Error().Msg("error message") + }, + expectResult: otime[0:26] + " " + + level(log.ErrorLevel) + " error message\n", + }, + "level warn color-off": { + config: log.Config{Level: "warning", ColorMode: log.ColorModeOff}, + setup: func(logger zerolog.Logger) { + logger.Warn().Msg("warn message") + }, + expectResult: otime[0:26] + " " + + level(log.WarnLevel) + " warn message\n", + }, + "level info color-off": { + config: log.Config{Level: "info", ColorMode: log.ColorModeOff}, + setup: func(logger zerolog.Logger) { + logger.Info().Msg("info message") + }, + expectResult: otime[0:26] + " " + + level(log.InfoLevel) + " info message\n", + }, + "level debug color-off": { + config: log.Config{Level: "debug", ColorMode: log.ColorModeOff}, + setup: func(logger zerolog.Logger) { + logger.Debug().Msg("debug message") + }, + expectResult: otime[0:26] + " " + + level(log.DebugLevel) + " debug message\n", + }, + "level trace color-off": { + config: log.Config{Level: "trace", ColorMode: log.ColorModeOff}, + setup: func(logger zerolog.Logger) { + logger.Trace().Msg("trace message") + }, + expectResult: otime[0:26] + " " + + level(log.TraceLevel) + " trace message\n", + }, + + // Test order key value data. + "data default": { + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data ordered": { + config: log.Config{OrderMode: log.OrderModeOn}, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key2", "value2"). + Str("key1", "value1").Msg("data message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data unordered": { + config: log.Config{OrderMode: log.OrderModeOff}, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + + // Test color modes. + "data color-off": { + config: log.Config{ColorMode: log.ColorModeOff}, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + level(log.InfoLevel) + " data message " + + data("key1", "value1") + " " + + data("key2", "value2") + "\n", + }, + "data color-on": { + config: log.Config{ColorMode: log.ColorModeOn}, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data color-auto colorized": { + config: log.Config{ColorMode: log.ColorModeAuto}, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data color-auto not-colorized": { + noTerminal: true, + config: log.Config{ColorMode: log.ColorModeAuto}, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + level(log.InfoLevel) + " data message " + + data("key1", "value1") + " " + + data("key2", "value2") + "\n", + }, + "data color-levels": { + config: log.Config{ColorMode: log.ColorModeLevels}, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " data message " + + data("key1", "value1") + " " + + data("key2", "value2") + "\n", + }, + "data color-fields": { + config: log.Config{ColorMode: log.ColorModeFields}, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + level(log.InfoLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + "data color-levels+fields": { + config: log.Config{ + ColorMode: log.ColorModeLevels + "|" + log.ColorModeFields, + }, + setup: func(logger zerolog.Logger) { + logger.Info().Str("key1", "value1"). + Str("key2", "value2").Msg("data message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " data message " + + dataC("key1", "value1") + " " + + dataC("key2", "value2") + "\n", + }, + + // Time format. + "time default": { + setup: func(logger zerolog.Logger) { + logger.Info().Msg("default time message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " " + + "default time message\n", + }, + "time short": { + config: log.Config{ + TimeFormat: "2006-01-02 15:04:05", + }, + setup: func(logger zerolog.Logger) { + logger.Info().Msg("short time message") + }, + expectResult: otime[0:19] + " " + + levelC(log.InfoLevel) + " " + + "short time message\n", + }, + "time long": { + config: log.Config{ + TimeFormat: "2006-01-02 15:04:05.000000000", + }, + setup: func(logger zerolog.Logger) { + logger.Info().Msg("long time message") + }, + expectResult: otime[0:29] + " " + + levelC(log.InfoLevel) + " " + + "long time message\n", + }, + + // Report caller. + "caller only": { + setup: func(logger zerolog.Logger) { + logger.Info().Caller(0).Msg("caller message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " " + "caller message\n", + }, + "caller report": { + config: log.Config{Caller: true}, + setup: func(logger zerolog.Logger) { + logger.Info().Msg("caller report message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " " + + "[" + caller(-4) + "] caller report message\n", + }, + + // Test error. + "error output": { + setup: func(logger zerolog.Logger) { + logger.Info().Err(errAny).Msg("error message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " error message " + + dataC("error", errAny.Error()) + "\n", + }, + "error output color-on": { + config: log.Config{ColorMode: log.ColorModeOn}, + setup: func(logger zerolog.Logger) { + logger.Info().Err(errAny).Msg("error message") + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " error message " + + dataC("error", errAny.Error()) + "\n", + }, + "error output color-off": { + config: log.Config{ColorMode: log.ColorModeOff}, + setup: func(logger zerolog.Logger) { + logger.Info().Err(errAny).Msg("error message") + }, + expectResult: otime[0:26] + " " + + level(log.InfoLevel) + " error message " + + data("error", errAny.Error()) + "\n", + }, +} + +func TestZeroLog(t *testing.T) { + assert.NoError(t, terr) + zerolog.TimeFieldFormat = time.RFC3339Nano + zerolog.TimestampFunc = func() time.Time { return ttime } + + test.Map(t, testZeroLogParams). + // Filter("level-panic-color-on", true). + Run(func(t test.Test, param testZeroLogParam) { + // Given + buffer := &bytes.Buffer{} + config := config.New[config.Config]("X", "app"). + SetSubDefaults("log", param.config, true). + SetDefaults(func(r *config.Reader[config.Config]) { + r.SetDefault("log.level", "trace") + }).GetConfig("zerolog") + logger := config.Log.SetupZero(buffer).Zero() + pretty := test.NewAccessor(logger).Get("w").(zerolog.LevelWriterAdapter). + Writer.(*log.ZeroLogPretty) + pretty.Setup.ColorMode = param.config.ColorMode.Parse(!param.noTerminal) + + if param.expect != nil { + // Then + mock.NewMocks(t).Expect(param.expect) + + defer func() { + assert.Equal(t, param.expectResult, buffer.String()) + if err := recover(); err != nil { + panic(err) + } + }() + } + + // When + param.setup(logger) + + // Then + assert.Equal(t, param.expectResult, buffer.String()) + }) +} + +type testSetupFormatParam struct { + config *log.Config + call func(*log.Setup) string + expect string +} + +var testSetupFormatParams = map[string]testSetupFormatParam{ + // Test time format. + "time default": { + config: &log.Config{ + TimeFormat: log.DefaultTimeFormat, + }, + call: func(s *log.Setup) string { + return s.FormatTimestamp(itime) + }, + expect: otime[0:26], + }, + "time short": { + config: &log.Config{ + TimeFormat: "2006-01-02 15:04:05", + }, + call: func(s *log.Setup) string { + return s.FormatTimestamp(itime) + }, + expect: otime[0:19], + }, + "time long": { + config: &log.Config{ + TimeFormat: "2006-01-02 15:04:05.000000000", + }, + call: func(s *log.Setup) string { + return s.FormatTimestamp(itime) + }, + expect: otime[0:29], + }, + "time invalid value": { + config: &log.Config{ + TimeFormat: time.RFC3339, + }, + call: func(s *log.Setup) string { + return s.FormatTimestamp("2024-12-31 23:59:59Z+07:00") + }, + expect: "2024-12-31 23:59:59Z+07:00", + }, + "time invalid type": { + config: &log.Config{ + TimeFormat: log.DefaultTimeFormat, + }, + call: func(s *log.Setup) string { + return s.FormatTimestamp(1) + }, + expect: "1", + }, + + // Test level format default. + "level panic default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel("panic") + }, + expect: level(log.PanicLevel), + }, + "level fatal default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel("fatal") + }, + expect: level(log.FatalLevel), + }, + "level error default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel("error") + }, + expect: level(log.ErrorLevel), + }, + "level warn default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel("warn") + }, + expect: level(log.WarnLevel), + }, + "level info default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel("info") + }, + expect: level(log.InfoLevel), + }, + "level debug default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel("debug") + }, + expect: level(log.DebugLevel), + }, + "level trace default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel("trace") + }, + expect: level(log.TraceLevel), + }, + "level unknown default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel("unknown") + }, + expect: level(log.InfoLevel), + }, + + // Test level format color-on. + "level panic color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatLevel("panic") + }, + expect: levelC(log.PanicLevel), + }, + "level fatal color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatLevel("fatal") + }, + expect: levelC(log.FatalLevel), + }, + "level error color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatLevel("error") + }, + expect: levelC(log.ErrorLevel), + }, + "level warn color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatLevel("warn") + }, + expect: levelC(log.WarnLevel), + }, + "level info color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatLevel("info") + }, + expect: levelC(log.InfoLevel), + }, + "level debug color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatLevel("debug") + }, + expect: levelC(log.DebugLevel), + }, + "level trace color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatLevel("trace") + }, + expect: levelC(log.TraceLevel), + }, + "level unknown color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatLevel("unknown") + }, + expect: levelC(log.InfoLevel), + }, + + // Test level format color-off. + "level panic color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatLevel("panic") + }, + expect: level(log.PanicLevel), + }, + "level fatal color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatLevel("fatal") + }, + expect: level(log.FatalLevel), + }, + "level error color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatLevel("error") + }, + expect: level(log.ErrorLevel), + }, + "level warn color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatLevel("warn") + }, + expect: level(log.WarnLevel), + }, + "level info color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatLevel("info") + }, + expect: level(log.InfoLevel), + }, + "level debug color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatLevel("debug") + }, + expect: level(log.DebugLevel), + }, + "level trace color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatLevel("trace") + }, + expect: level(log.TraceLevel), + }, + "level unknown color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatLevel("unknown") + }, + expect: level(log.InfoLevel), + }, + + "level invalid type": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatLevel(1) + }, + expect: "1", + }, + + // Test caller format. + "caller report off": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatCaller("caller") + }, + expect: "", + }, + "caller report on": { + config: &log.Config{Caller: true}, + call: func(s *log.Setup) string { + return s.FormatCaller("caller") + }, + expect: "[caller]", + }, + "caller invalid type": { + config: &log.Config{Caller: true}, + call: func(s *log.Setup) string { + return s.FormatCaller(1) + }, + expect: "[1]", + }, + + // Test message format. + "message default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatMessage("message") + }, + expect: "message", + }, + "message color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatMessage("message") + }, + expect: "message", + }, + "message color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatMessage("message") + }, + expect: "message", + }, + "message invalid type": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatMessage(1) + }, + expect: "1", + }, + + // Test error field name. + "error field name": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatErrFieldName("error") + }, + expect: key("error"), + }, + "error field name color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatErrFieldName("error") + }, + expect: keyC("error"), + }, + "error field name color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatErrFieldName("error") + }, + expect: key("error"), + }, + "error field name invalid type": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatErrFieldName(1) + }, + expect: key("1"), + }, + + // Test error field value. + "error field value": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatErrFieldValue("error") + }, + expect: "error", + }, + "error field value color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatErrFieldValue("error") + }, + expect: "error", + }, + "error field value color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatErrFieldValue("error") + }, + expect: "error", + }, + "error field value invalid type": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatErrFieldValue(1) + }, + expect: "1", + }, + + // Test field name. + "field name default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatFieldName("field") + }, + expect: key("field"), + }, + "field name color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatFieldName("field") + }, + expect: keyC("field"), + }, + "field name color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatFieldName("field") + }, + expect: key("field"), + }, + "field name invalid type": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatFieldName(1) + }, + expect: key("1"), + }, + + // Test field value. + "field value default": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatFieldValue("field") + }, + expect: `"field"`, + }, + "field value color-on": { + config: &log.Config{ColorMode: log.ColorModeOn}, + call: func(s *log.Setup) string { + return s.FormatFieldValue("field") + }, + expect: `"field"`, + }, + "field value color-off": { + config: &log.Config{ColorMode: log.ColorModeOff}, + call: func(s *log.Setup) string { + return s.FormatFieldValue("field") + }, + expect: `"field"`, + }, + "field value invalid type": { + config: &log.Config{}, + call: func(s *log.Setup) string { + return s.FormatFieldValue(1) + }, + expect: `"1"`, + }, +} + +func TestSetupFormat(t *testing.T) { + test.Map(t, testSetupFormatParams). + // Filter("level-panic", true). + RunSeq(func(t test.Test, param testSetupFormatParam) { + // Given + s := param.config.Setup(os.Stderr) + + // When + result := param.call(s) + + // Then + assert.Equal(t, param.expect, result) + }) +}