diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index bfc1308..404ed2b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -5,9 +5,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: 1.23.1 + go-version: 1.23.2 - name: Checkout code uses: actions/checkout@v4 @@ -16,7 +16,7 @@ jobs: env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} LANG: en_US.UTF-8 - # using --trace has a side effect on the the test output. + shell: 'script -q -e -c "bash {0}"' run: make --trace all - name: Send coverage report @@ -30,9 +30,9 @@ jobs: contents: write steps: - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: 1.23.1 + go-version: 1.23.2 - name: Checkout code uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index cada2a2..b7b1e95 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ 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.104 +GOMAKE_DEP ?= github.com/tkrop/go-make@v0.0.105 INSTALL_FLAGS ?= -mod=readonly -buildvcs=auto # Request targets from go-make targets target. TARGETS := $(shell command -v $(GOBIN)/go-make >/dev/null || \ diff --git a/README.md b/README.md index c8fb9b0..33bfeed 100644 --- a/README.md +++ b/README.md @@ -132,16 +132,17 @@ in the `SetSubDefaults` method as follows: ## Logger setup -The [`go-config`][go-config] framework supports to set up a [logrus][logrus] -`Logger`_out-of-the-box using the following two approaches: +The [`go-config`][go-config] framework supports to set up a `Logger` in +[zerolog][zerolog] and [logrus][logrus] using the provided standard options +out-of-the-box as follows: ```go - logger := config.SetupLogger(log.New()) - logger := config.Log.Setup(log.New()) + logger := config.Log.Setup[Rus|Zero](log.New(...)|nil) ``` If no logger is provided, the standard logger is configured and returned. +[zerolog]: [logrus]: diff --git a/VERSION b/VERSION index bbdeab6..1750564 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.5 +0.0.6 diff --git a/config/config.go b/config/config.go index ebc3f64..4c45e10 100644 --- a/config/config.go +++ b/config/config.go @@ -8,11 +8,12 @@ import ( "os" "strings" + log "github.com/sirupsen/logrus" "github.com/spf13/viper" "github.com/tkrop/go-config/info" "github.com/tkrop/go-config/internal/reflect" - "github.com/tkrop/go-config/log" + clog "github.com/tkrop/go-config/log" ) // ErrConfig is a common error to indicate a configuration error. @@ -31,12 +32,7 @@ type Config struct { // Info default build information. Info *info.Info // Log default logger setup. - Log *log.Config -} - -// SetupLogger is a convenience method to setup the logger. -func (c *Config) SetupLogger(logger *log.Logger) *log.Logger { - return c.Log.Setup(logger) + Log *clog.Config } // Reader common config reader based on viper. diff --git a/config/config_test.go b/config/config_test.go index 6b4b372..53d3a2d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -10,7 +10,6 @@ import ( "github.com/tkrop/go-config/config" "github.com/tkrop/go-config/internal/filepath" - "github.com/tkrop/go-config/log" "github.com/tkrop/go-testing/mock" "github.com/tkrop/go-testing/test" ) @@ -102,22 +101,3 @@ func TestConfig(t *testing.T) { assert.Equal(t, param.expectLogLevel, reader.GetString("log.level")) }) } - -func TestSetupLogger(t *testing.T) { - t.Parallel() - - // Given - logger := log.New() - config := config.New[config.Config]("TC", "test"). - SetDefaults(func(r *config.Reader[config.Config]) { - r.AddConfigPath("fixtures") - r.SetDefault("log.level", "trace") - }). - GetConfig(t.Name()) - - // When - config.SetupLogger(logger) - - // Then - assert.Equal(t, log.TraceLevel, logger.GetLevel()) -} diff --git a/go.mod b/go.mod index 03d6063..6466743 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,17 @@ module github.com/tkrop/go-config -go 1.23.1 +go 1.23.2 require ( github.com/golang/mock v1.6.0 + github.com/mattn/go-tty v0.0.7 github.com/mitchellh/mapstructure v1.5.0 + github.com/rs/zerolog v1.33.0 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.20 + github.com/tkrop/go-testing v0.0.21 + golang.org/x/sys v0.26.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -17,6 +20,8 @@ require ( github.com/fsnotify/fsnotify v1.7.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 + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect @@ -27,8 +32,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect + golang.org/x/text v0.19.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index dc9e5c2..b03599a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -6,6 +7,7 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk 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/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= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -18,15 +20,27 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= +github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/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= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -49,17 +63,15 @@ 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.19 h1:3wTJX0TepvkhEC5pz6bSrfZxCqiJDkh3ofJO41hhBus= -github.com/tkrop/go-testing v0.0.19/go.mod h1:BFsOhvdwOvNKO8nqAEEMAYEeim3nXjvOzREs6lobJ/8= -github.com/tkrop/go-testing v0.0.20 h1:2zKyBlmmagWpgkoFQUDuD9Vwz35DcC6D+U3VyIACd6o= -github.com/tkrop/go-testing v0.0.20/go.mod h1:BFsOhvdwOvNKO8nqAEEMAYEeim3nXjvOzREs6lobJ/8= +github.com/tkrop/go-testing v0.0.21 h1:lCSLCqsa0KUKOjGOd5euUIpmctSZy9eO7e1pwe2ym2A= +github.com/tkrop/go-testing v0.0.21/go.mod h1:RFUX2nk7n4QPUN0doC4R0KS6PqeaK1vWUDy3tBtjjKQ= 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= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw= +golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -72,13 +84,16 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/internal/reflect/walker_test.go b/internal/reflect/walker_test.go index c53f95b..5c24579 100644 --- a/internal/reflect/walker_test.go +++ b/internal/reflect/walker_test.go @@ -8,7 +8,7 @@ import ( "github.com/tkrop/go-testing/test" ) -//revive:disable:line-length-limit // go generate line length. +//revive:disable:line-length-limit // go:generate line length. //go:generate mockgen -package=reflect_test -destination=mock_callback_test.go -source=walker_test.go Callback diff --git a/log/format/format.go b/log/format/format.go new file mode 100644 index 0000000..4e99f4f --- /dev/null +++ b/log/format/format.go @@ -0,0 +1,185 @@ +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() ColorMode { + mode := ColorUnset + for _, m := range splitRegex.Split(string(m), -1) { + switch ColorModeString(m) { + case ColorModeOff: + mode = ColorOff + case ColorModeOn: + mode = ColorOn + case ColorModeAuto: + mode = ColorAuto + case ColorModeLevels: + mode |= ColorLevels + case ColorModeFields: + mode |= ColorFields + default: + mode = ColorDefault + } + } + return mode +} + +// ColorMode is the color mode used for logging. +type ColorMode uint + +// Color modes. +const ( + // ColorDefault is the default color mode. + ColorDefault = ColorAuto + // 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 = 2 + // ColorAuto enables the automatic coloring for tty outputs files. + ColorAuto ColorMode = 4 + // ColorLevels enables coloring for log levels entries only. + ColorLevels ColorMode = 8 + // ColorFields enables coloring for fields names only. + ColorFields ColorMode = 16 +) + +// 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.go b/log/format/logrus.go new file mode 100644 index 0000000..a5f0832 --- /dev/null +++ b/log/format/logrus.go @@ -0,0 +1,231 @@ +// Package format provides common log formatting based on logrus for services, +// jobs, and commands with integrated configuration loading. +package format + +import ( + "bytes" + "io" + "maps" + "os" + "slices" + "sort" + "strconv" + "sync" + + log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" +) + +// 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 + + // initOnce is used to initialize the formatter only once. + initOnce sync.Once +} + +// Init initializes the pretty formatter. +func (p *Pretty) Init(out io.Writer) *Pretty { + // Set public default fields. + if p.TimeFormat == "" { + p.TimeFormat = DefaultTimeFormat + } + if p.ColorMode == ColorUnset { + p.ColorMode = ColorDefault + } + if p.OrderMode == OrderOff { + p.OrderMode = OrderDefault + } + + // Set default level names and colors. + if len(p.levelNames) == 0 { + p.levelNames = DefaultLevelNames + } + if len(p.levelColors) == 0 { + p.levelColors = DefaultLevelColors + } + + if p.ColorMode&ColorAuto == ColorAuto { + if IsTerminal(out) { + p.ColorMode |= ColorOn + } + } + if p.ColorMode&ColorOn == ColorOn { + p.ColorMode |= ColorLevels | ColorFields + } + return p +} + +// Format formats the log entry to a pretty format. +func (p *Pretty) Format(entry *log.Entry) ([]byte, error) { + p.initOnce.Do(func() { p.Init(entry.Logger.Out) }) + + 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.getKeys(entry.Data) { + buffer.WriteByte(' ').WriteData(key, entry.Data[key]) + } + + return buffer.Bytes() +} + +// getKeys returns the keys of the given data. +func (p *Pretty) getKeys(data log.Fields) []string { + keys := slices.Collect(maps.Keys(data)) + if p.OrderMode.CheckFlag(OrderOn) { + sort.Strings(keys) + } + return keys +} + +// IsTerminal checks whether the given writer is a terminal. +func IsTerminal(writer io.Writer) bool { + if file, ok := writer.(*os.File); ok { + // #nosec G115 // is a safe conversion for files. + _, err := unix.IoctlGetTermios(int(file.Fd()), unix.TCGETS) + return err == nil + } + return false +} + +// Buffer is the interface for writing bytes and strings. +type BufferWriter interface { + // WriteByte writes the given byte to the writer. + io.ByteWriter + // WriteString writes the given string to the writer. + io.StringWriter + + // Bytes returns the current bytes of the writer. + Bytes() []byte +} + +// Buffer is a buffer for the pretty formatter. +type Buffer struct { + // pretty is the pretty formatter of the buffer. + pretty *Pretty + // buffer is the bytes buffer used for writing. + buffer BufferWriter + + // err is the error occurred during writing. + err error +} + +// NewBuffer creates a new buffer for the pretty formatter. +func NewBuffer(p *Pretty, b BufferWriter) *Buffer { + return &Buffer{pretty: p, buffer: b} +} + +// WriteByte writes the given byte to the buffer. +// +//nolint:govet // Intentional deviation from the go vet check. +func (b *Buffer) WriteByte(byt byte) *Buffer { + if b.err != nil { + return b + } + + if err := b.buffer.WriteByte(byt); err != nil { + b.err = err + } + return b +} + +// WriteString writes the given string to the buffer. +func (b *Buffer) WriteString(str string) *Buffer { + if b.err != nil { + return b + } + + if _, err := b.buffer.WriteString(str); err != nil { + b.err = err + } + return b +} + +// WriteColored writes the given text with the given color to the buffer. +func (b *Buffer) WriteColored(color, str string) *Buffer { + if b.err != nil { + return b + } + + // Check if color mode is disabled. + if b.pretty.ColorMode == ColorOff { + return b.WriteString(str) + } + + return b.WriteString("\x1b[").WriteString(color).WriteByte('m'). + WriteString(str).WriteString("\x1b[0m") +} + +// WriteLevel writes the given log level to the buffer. +func (b *Buffer) WriteLevel(level log.Level) *Buffer { + if b.err != nil { + return b + } + + if b.pretty.ColorMode.CheckFlag(ColorLevels) { + return b.WriteColored(b.pretty.levelColors[level], + b.pretty.levelNames[level]) + } + return b.WriteString(b.pretty.levelNames[level]) +} + +// WriteField writes the given key with the given color to the buffer. +func (b *Buffer) WriteField(level log.Level, key string) *Buffer { + if b.err != nil { + return b + } + + if b.pretty.ColorMode.CheckFlag(ColorFields) { + return b.WriteColored(b.pretty.levelColors[level], key) + } + return b.WriteString(key) +} + +// WriteCaller writes the caller information to the buffer. +func (b *Buffer) WriteCaller(entry *log.Entry) *Buffer { + if b.err != nil || !entry.HasCaller() { + return b + } + + caller := entry.Caller + return b.WriteByte(' ').WriteByte('['). + WriteString(caller.File).WriteByte(':'). + WriteString(strconv.Itoa(caller.Line)).WriteByte('#'). + WriteString(caller.Function).WriteByte(']') +} + +// WriteData writes the data to the buffer. +func (b *Buffer) WriteData(key string, value any) *Buffer { + if b.err != nil { + return b + } + + switch key { + case log.ErrorKey: + return b.WriteField(log.ErrorLevel, key). + WriteByte('=').WriteString(value.(error).Error()) + default: + return b.WriteField(FieldLevel, key). + WriteByte('=').WriteString(value.(string)) + } +} + +// Bytes returns current bytes of the buffer with the current error. +func (b *Buffer) Bytes() ([]byte, error) { + return b.buffer.Bytes(), b.err +} diff --git a/log/format/logrus_test.go b/log/format/logrus_test.go new file mode 100644 index 0000000..8332bff --- /dev/null +++ b/log/format/logrus_test.go @@ -0,0 +1,796 @@ +package format_test + +import ( + "bytes" + "errors" + "runtime" + "testing" + "time" + + "github.com/mattn/go-tty" + 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") +) + +// 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 + 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", + }, + "level fatal default": { + entry: &log.Entry{ + Level: log.FatalLevel, + Message: "fatal message", + }, + expectResult: otime[0:26] + " " + + levelC(log.FatalLevel) + " fatal message", + }, + "level error default": { + entry: &log.Entry{ + Level: log.ErrorLevel, + Message: "error message", + }, + expectResult: otime[0:26] + " " + + levelC(log.ErrorLevel) + " error message", + }, + "level warn default": { + entry: &log.Entry{ + Level: log.WarnLevel, + Message: "warn message", + }, + expectResult: otime[0:26] + " " + + levelC(log.WarnLevel) + " warn message", + }, + "level info default": { + entry: &log.Entry{ + Level: log.InfoLevel, + Message: "info message", + }, + expectResult: otime[0:26] + " " + + levelC(log.InfoLevel) + " info message", + }, + "level debug default": { + entry: &log.Entry{ + Level: log.DebugLevel, + Message: "debug message", + }, + expectResult: otime[0:26] + " " + + levelC(log.DebugLevel) + " debug message", + }, + "level trace default": { + entry: &log.Entry{ + Level: log.TraceLevel, + Message: "trace message", + }, + expectResult: otime[0:26] + " " + + levelC(log.TraceLevel) + " trace message", + }, + + // 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", + }, + "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", + }, + "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", + }, + "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", + }, + "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", + }, + "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", + }, + "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", + }, + + // 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", + }, + "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", + }, + "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", + }, + "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", + }, + "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", + }, + "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", + }, + "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", + }, + + // 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"), + }, + "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"), + }, + "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"), + }, + "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"), + }, + "data color-auto": { + 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"), + }, + "data color-auto no-tty": { + colorMode: format.ColorModeAuto, + 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"), + }, + "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"), + }, + "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"), + }, + "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"), + }, + + // Time format. + "time default": { + entry: &log.Entry{ + Level: log.PanicLevel, + Message: "default time message", + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " " + + "default time message", + }, + "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", + }, + "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", + }, + + // Report caller. + "caller only": { + entry: &log.Entry{ + Message: "caller message", + Caller: anyFrame, + }, + expectResult: otime[0:26] + " " + + levelC(log.PanicLevel) + " " + + "caller message", + }, + "caller report": { + entry: &log.Entry{ + Message: "caller report message", + Caller: anyFrame, + Logger: &log.Logger{ + ReportCaller: true, + }, + }, + expectResult: otime[0:26] + " " + + level(log.PanicLevel) + " " + + "[file:123#function] caller report message", + }, + + // 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()), + }, + "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()), + }, + "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()), + }, +} + +func TestPrettyFormat(t *testing.T) { + tty, err := tty.Open() + assert.NoError(t, err) + + test.Map(t, testPrettyFormatParams). + Run(func(t test.Test, param testPrettyFormatParam) { + // Given + pretty := &format.Pretty{ + TimeFormat: param.timeFormat, + ColorMode: param.colorMode.Parse(), + OrderMode: param.orderMode.Parse(), + } + + if param.entry.Time == (time.Time{}) { + time, err := time.Parse(time.RFC3339Nano, itime) + assert.NoError(t, err) + param.entry.Time = time + } + if param.entry.Logger == nil { + param.entry.Logger = log.New() + param.entry.Logger.Out = tty.Output() + } + + // 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) + } + }). + Cleanup(func() { + tty.Close() + }) +} + +func setupWriter(mocks *mock.Mocks, expect mock.SetupFunc) format.BufferWriter { + var writer format.BufferWriter + if expect != nil { + writer = mock.Get(mocks, NewMockBufferWriter) + } else { + writer = &bytes.Buffer{} + } + return writer +} + +type testBufferWriteParam struct { + pretty *format.Pretty + error error + setup func(test.Test, *format.Buffer) + expect mock.SetupFunc + expectError error + expectString string +} + +var testBufferWriteParams = map[string]testBufferWriteParam{ + // Test write byte. + "write byte error": { + error: errAny, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteByte(' ') + }, + expectError: errAny, + }, + "write byte failure": { + setup: func(t test.Test, 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(t test.Test, buffer *format.Buffer) { + buffer.WriteByte(' ') + }, + expectString: " ", + }, + + // Test write string. + "write string error": { + error: errAny, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteString("string") + }, + expectError: errAny, + }, + "write string failure": { + setup: func(t test.Test, 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(t test.Test, buffer *format.Buffer) { + buffer.WriteString("string") + }, + expectString: "string", + }, + + // Test write colored. + "write colored error": { + error: errAny, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteColored(format.ColorField, "string") + }, + expectError: errAny, + }, + "write colored default": { + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteColored(format.ColorField, "string") + }, + expectString: fieldC("string"), + }, + "write colored color-off": { + pretty: &format.Pretty{ + ColorMode: format.ColorOff, + }, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteColored(format.ColorField, "string") + }, + expectString: "string", + }, + "write colored color-on": { + pretty: &format.Pretty{ + ColorMode: format.ColorOn, + }, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteColored(format.ColorField, "string") + }, + expectString: fieldC("string"), + }, + + // Test write level. + "write level error": { + error: errAny, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteLevel(log.PanicLevel) + }, + expectError: errAny, + }, + "write level default": { + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteLevel(log.PanicLevel) + }, + expectString: levelC(log.PanicLevel), + }, + "write level color-on": { + pretty: &format.Pretty{ + ColorMode: format.ColorOn, + }, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteLevel(log.PanicLevel) + }, + expectString: levelC(log.PanicLevel), + }, + "write level color-off": { + pretty: &format.Pretty{ + ColorMode: format.ColorOff, + }, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteLevel(log.PanicLevel) + }, + expectString: level(log.PanicLevel), + }, + + // Test write colored field. + "write field error": { + error: errAny, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteField(format.FieldLevel, "value") + }, + expectError: errAny, + }, + "write field default": { + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteField(format.FieldLevel, "value") + }, + expectString: fieldC("value"), + }, + "write field color-on": { + pretty: &format.Pretty{ + ColorMode: format.ColorOn, + }, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteField(format.FieldLevel, "value") + }, + expectString: fieldC("value"), + }, + "write field color-off": { + pretty: &format.Pretty{ + ColorMode: format.ColorOff, + }, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteField(format.FieldLevel, "value") + }, + expectString: "value", + }, + + // Test write caller. + "write caller error": { + error: errAny, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteCaller(&log.Entry{ + Caller: anyFrame, + Logger: &log.Logger{ + ReportCaller: true, + }, + }) + }, + expectError: errAny, + }, + "write caller on": { + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteCaller(&log.Entry{ + Caller: anyFrame, + Logger: &log.Logger{ + ReportCaller: true, + }, + }) + }, + expectString: " [file:123#function]", + }, + "write caller off": { + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteCaller(&log.Entry{ + Logger: &log.Logger{ + ReportCaller: false, + }, + }) + }, + expectString: "", + }, + + // Test write data. + "write data error": { + error: errAny, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteData("key", "value") + }, + expectError: errAny, + }, + "write data default": { + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteData("key", "value") + }, + expectString: dataC("key", "value"), + }, + "write data color-on error": { + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteData(log.ErrorKey, errAny) + }, + expectString: dataC(log.ErrorKey, errAny.Error()), + }, + "write data color-on": { + pretty: &format.Pretty{ + ColorMode: format.ColorOn, + }, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteData("key", "value") + }, + expectString: dataC("key", "value"), + }, + "write data color-off": { + pretty: &format.Pretty{ + ColorMode: format.ColorOff, + }, + setup: func(t test.Test, buffer *format.Buffer) { + buffer.WriteData("key", "value") + }, + expectString: data("key", "value"), + }, +} + +func TestBufferWrite(t *testing.T) { + tty, err := tty.Open() + assert.NoError(t, err) + + test.Map(t, testBufferWriteParams). + Run(func(t test.Test, param testBufferWriteParam) { + // Given + mocks := mock.NewMocks(t).Expect(param.expect) + if param.pretty == nil { + param.pretty = &format.Pretty{} + } + param.pretty.Init(tty.Output()) + buffer := format.NewBuffer(param.pretty, + setupWriter(mocks, param.expect)) + test.NewAccessor(buffer).Set("err", param.error) + + // When + param.setup(t, buffer) + result, err := buffer.Bytes() + + // Then + assert.Equal(t, param.expectError, err) + assert.Equal(t, param.expectString, string(result)) + }). + Cleanup(func() { + tty.Close() + }) +} diff --git a/log/log.go b/log/log.go index 1af0622..4030010 100644 --- a/log/log.go +++ b/log/log.go @@ -3,176 +3,112 @@ package log import ( - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" + + "github.com/tkrop/go-config/log/format" ) // Config common configuration for logging. type Config struct { // Level is defining the logger level (default `info`). Level string `default:"info"` - // TImeFormat is defining the logger time format. + // 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 for logger output. + // 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"` } -// Exported log types to be used in the application. -type ( - // Logger is the logrus logger. - Logger = log.Logger - // Entry is the logrus entry. - Entry = log.Entry - // Fields is the logrus fields. - Fields = log.Fields - // Level is the logrus level. - Level = log.Level - // Hook is the logrus hook. - Hook = log.Hook - - // Formatter is the logrus formatter. - Formatter = log.Formatter - // TextFormatter is the logrus text formatter. - TextFormatter = log.TextFormatter - // JSONFormatter is the logrus JSON formatter. - JSONFormatter = log.JSONFormatter +// ColorMode is the color mode used for logging. +type ColorModeString format.ColorModeString + +// Color modes. +const ( + // ColorOff disables the color mode. + ColorOff format.ColorModeString = format.ColorModeOff + // ColorOn enables the color mode. + ColorOn format.ColorModeString = format.ColorModeOn + // ColorAuto enables the automatic color mode. + ColorAuto format.ColorModeString = format.ColorModeAuto + // ColorLevels enables the color mode for log level. + ColorLevels format.ColorModeString = format.ColorModeLevels + // ColorFields enables the color mode for fields. + ColorFields format.ColorModeString = format.ColorModeFields ) -//revive:disable:max-public-structs // export log types - -// Exported log functions to be used in the application. -var ( - // New creates a new logger. - New = log.New - // StandardLogger returns the standard logger. - StandardLogger = log.StandardLogger - // ParseLevel parses a log level. - ParseLevel = log.ParseLevel - // GetLevel returns the current log level. - GetLevel = log.GetLevel - // SetLevel sets the log level of the logger. - SetLevel = log.SetLevel - // IsLevelEnabled checks if the log level is enabled. - IsLevelEnabled = log.IsLevelEnabled - - // SetOutput sets the output of the logger. - SetOutput = log.SetOutput - // SetFormatter sets the formatter of the logger. - SetFormatter = log.SetFormatter - // SetReportCaller sets the report caller flag of the logger. - SetReportCaller = log.SetReportCaller - // AddHook adds a hook to the logger. - AddHook = log.AddHook - - // WithTime adds the current time to the entry. - WithTime = log.WithTime - // WithContext adds the context to the entry. - WithContext = log.WithContext - // WithError adds the error to the entry. - WithError = log.WithError - // WithField adds a field to the entry. - WithField = log.WithField - // WithFields adds fields to the entry. - WithFields = log.WithFields - - // Tracef logs a message at level Trace. - Tracef = log.Tracef - // Debugf logs a message at level Debug. - Debugf = log.Debugf - // Infof logs a message at level Info. - Infof = log.Infof - // Printf logs a message at level Info. - Printf = log.Printf - // Warnf logs a message at level Warn. - Warnf = log.Warnf - // Warningf logs a message at level Warn. - Warningf = log.Warningf - // Errorf logs a message at level Error. - Errorf = log.Errorf - // Fatalf logs a message at level Fatal. - Fatalf = log.Fatalf +// OrderMode is the order mode used for logging. +type OrderModeString format.OrderModeString - // Traceln logs a message at level Trace. - Traceln = log.Traceln - // Debugln logs a message at level Debug. - Debugln = log.Debugln - // Infoln logs a message at level Info. - Infoln = log.Infoln - // Println logs a message at level Info. - Println = log.Println - // Warnln logs a message at level Warn. - Warnln = log.Warnln - // Warningln logs a message at level Warn. - Warningln = log.Warningln - // Errorln logs a message at level Error. - Errorln = log.Errorln - // Panicln logs a message at level Panic. - Panicln = log.Panicln - // Fatalln logs a message at level Fatal. - Fatalln = log.Fatalln - - // Trace logs a message at level Trace. - Trace = log.Trace - // Debug logs a message at level Debug. - Debug = log.Debug - // Info logs a message at level Info. - Info = log.Info - // Print logs a message at level Info. - Print = log.Print - // Warn logs a message at level Warn. - Warn = log.Warn - // Warning logs a message at level Warn. - Warning = log.Warning - // Error logs a message at level Error. - Error = log.Error - // Panic logs a message at level Panic. - Panic = log.Panic - // Fatal logs a message at level Fatal. - Fatal = log.Fatal +// Order modes. +const ( + OrderAuto format.OrderModeString = "" + // OrderOn enables the order mode. + OrderOn format.OrderModeString = format.OrderModeOn + // OrderOff disables the order mode. + OrderOff format.OrderModeString = format.OrderModeOff +) - // TraceLevel is the log level Trace. - TraceLevel = log.TraceLevel - // DebugLevel is the log level Debug. - DebugLevel = log.DebugLevel - // InfoLevel is the log level Info. - InfoLevel = log.InfoLevel - // WarnLevel is the log level Warn. - WarnLevel = log.WarnLevel - // ErrorLevel is the log level Error. - ErrorLevel = log.ErrorLevel - // PanicLevel is the log level Panic. - PanicLevel = log.PanicLevel - // FatalLevel is the log level Fatal. - FatalLevel = log.FatalLevel +// Formatter is the formatter used for logging output. +type Formatter format.Formatter + +// Supported formatters. +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 ) -// Setup 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 +// 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) Setup(logger *Logger) *Logger { +func (c *Config) SetupRus(logger *logrus.Logger) *logrus.Logger { // Uses the standard logger if no logger is given. if logger == nil { - logger = StandardLogger() + logger = logrus.StandardLogger() } - // Sets up the text formatter with the given time format. - if c.TimeFormat != "" { - logger.SetFormatter(&TextFormatter{ + // Sets up the log output format. + switch c.Formatter { + case FormatterText: + mode := c.ColorMode.Parse() + logger.SetFormatter(&logrus.TextFormatter{ TimestampFormat: c.TimeFormat, - ForceColors: true, + 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(), }) } // Sets up the log level if given. - logLevel, err := ParseLevel(c.Level) + logLevel, err := logrus.ParseLevel(c.Level) if err != nil { - logger.WithError(err).WithFields(Fields{ + logger.WithError(err).WithFields(logrus.Fields{ "config": c.Level, }).Info("failed setting log level") } else { logger.SetLevel(logLevel) - WithFields(Fields{ + logger.WithFields(logrus.Fields{ "level": c.Level, }).Info("setting up log level") } diff --git a/log/log_test.go b/log/log_test.go index 36082b1..8c37d62 100644 --- a/log/log_test.go +++ b/log/log_test.go @@ -4,10 +4,12 @@ import ( "testing" "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" ) @@ -29,21 +31,21 @@ type setupParams struct { } var testSetupParams = map[string]setupParams{ - "read default log config no logger": { + "read default config no logger": { config: &log.Config{}, expectLogLevel: DefaultLogLevel, expectTimeFormat: DefaultLogTimeFormat, expectLogCaller: DefaultLogCaller, }, - "read default log config": { + "read default config": { config: &log.Config{}, expectLogLevel: DefaultLogLevel, expectTimeFormat: DefaultLogTimeFormat, expectLogCaller: DefaultLogCaller, }, - "change log level debug": { + "log level custom": { config: &log.Config{ Level: "debug", }, @@ -52,7 +54,7 @@ var testSetupParams = map[string]setupParams{ expectLogCaller: DefaultLogCaller, }, - "invalid log level debug": { + "log level invalid": { config: &log.Config{ Level: "detail", }, @@ -61,7 +63,7 @@ var testSetupParams = map[string]setupParams{ expectLogCaller: DefaultLogCaller, }, - "change time format date": { + "time format date": { config: &log.Config{ TimeFormat: "2024-12-31", }, @@ -70,7 +72,7 @@ var testSetupParams = map[string]setupParams{ expectLogCaller: DefaultLogCaller, }, - "change caller to true": { + "caller enabled": { config: &log.Config{ Caller: true, }, @@ -78,23 +80,66 @@ var testSetupParams = map[string]setupParams{ expectTimeFormat: DefaultLogTimeFormat, expectLogCaller: true, }, + + "formater text": { + config: &log.Config{ + Formatter: format.FormatterText, + }, + expectLogLevel: DefaultLogLevel, + expectTimeFormat: DefaultLogTimeFormat, + expectLogCaller: DefaultLogCaller, + }, + + "formater json": { + config: &log.Config{ + Formatter: format.FormatterJSON, + }, + expectLogLevel: DefaultLogLevel, + expectTimeFormat: DefaultLogTimeFormat, + expectLogCaller: DefaultLogCaller, + }, + + "formater pretty": { + config: &log.Config{ + Formatter: format.FormatterPretty, + }, + expectLogLevel: DefaultLogLevel, + expectTimeFormat: DefaultLogTimeFormat, + expectLogCaller: DefaultLogCaller, + }, } func TestSetup(t *testing.T) { test.Map(t, testSetupParams). Run(func(t test.Test, param setupParams) { // Given - logger := log.New() + logger := logrus.New() config := config.New[config.Config]("TEST", "test"). SetSubDefaults("log", param.config, false). GetConfig(t.Name()) // When - config.SetupLogger(logger) + config.Log.SetupRus(logger) // Then - assert.Equal(t, param.expectTimeFormat, - logger.Formatter.(*log.TextFormatter).TimestampFormat) + 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) }) @@ -106,7 +151,7 @@ func TestSetupNil(t *testing.T) { GetConfig(t.Name()) // When - config.SetupLogger(nil) + config.Log.SetupRus(nil) // Then assert.True(t, true)