diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 6239219..cc290c0 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -27,7 +27,7 @@ jobs: uses: golangci/golangci-lint-action@v4.0.0 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.56.2 + version: v1.61.0 # Optional: working directory, useful for monorepos # working-directory: somedir @@ -45,4 +45,4 @@ jobs: # skip-pkg-cache: true # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - # skip-build-cache: true \ No newline at end of file + # skip-build-cache: true diff --git a/.golangci.yml b/.golangci.yml index 379ab5f..6bc89fb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,52 +20,48 @@ linters-settings: linters: enable-all: true disable: + - copyloopvar + - depguard - dupl # some code duplication is traded for 1 alloc - - lll - - maligned + - dupword + - errname + - exportloopref + - execinquery + - exhaustruct + - forbidigo + - forcetypeassert + - gci - gochecknoglobals - gomnd - - wrapcheck + - intrange + - lll + - mnd + - nonamedreturns - paralleltest - - forbidigo - - exhaustivestruct - - interfacer # deprecated - - forcetypeassert - - scopelint # deprecated - - ifshort # too many false positives - - golint # deprecated - - varnamelen + - tagalign - tagliatelle - - errname - - ireturn - - exhaustruct - - nonamedreturns - - nosnakecase - - structcheck - - varcheck - - deadcode - testableexamples - - dupword - - depguard - - tagalign + - varnamelen + - wrapcheck issues: exclude-use-default: false exclude-rules: - linters: - - gomnd + - dupl + - err113 + - fatcontext + - funlen - goconst - goerr113 + - gomnd + - mnd - noctx - - funlen - - dupl - structcheck - - unused - unparam - - nosnakecase + - unused path: "_test.go" - linters: - errcheck # Error checking omitted for brevity. - gosec path: "example_" - diff --git a/Makefile b/Makefile index 79a4626..e71cfe3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -#GOLANGCI_LINT_VERSION := "v1.56.2" # Optional configuration to pinpoint golangci-lint version. +GOLANGCI_LINT_VERSION := "v1.61.0" # The head of Makefile determines location of dev-go to include standard targets. GO ?= go diff --git a/README.md b/README.md index 843e798..2f7f46e 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,14 @@ logger.Error(ctx, "something failed", logger.Important(ctx, "logged because is important") logger.Info(ctxd.WithDebug(ctx), "logged because of forced DEBUG mode") -logger.AtomicLevel.SetLevel(zap.DebugLevel) +logger.SetLevelEnabler(zapcore.DebugLevel) logger.Info(ctx, "logged because logger level was changed to DEBUG") // Output: -// ERROR zapctxd/example_test.go:23 something failed {"baz": 1, "quux": 2.2, "foo": "bar"} -// INFO zapctxd/example_test.go:28 logged because is important {"foo": "bar"} -// INFO zapctxd/example_test.go:29 logged because of forced DEBUG mode {"foo": "bar"} -// INFO zapctxd/example_test.go:32 logged because logger level was changed to DEBUG {"foo": "bar"} +// ERROR zapctxd/example_test.go:26 something failed {"baz": 1, "quux": 2.2, "foo": "bar"} +// INFO zapctxd/example_test.go:31 logged because is important {"foo": "bar"} +// INFO zapctxd/example_test.go:32 logged because of forced DEBUG mode {"foo": "bar"} +// INFO zapctxd/example_test.go:35 logged because logger level was changed to DEBUG {"foo": "bar"} ``` ## See Also diff --git a/example_test.go b/example_test.go index 8c9ad91..050201f 100644 --- a/example_test.go +++ b/example_test.go @@ -4,8 +4,10 @@ import ( "context" "github.com/bool64/ctxd" - "github.com/bool64/zapctxd" "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/bool64/zapctxd" ) func ExampleNew() { @@ -29,12 +31,12 @@ func ExampleNew() { logger.Important(ctx, "logged because is important") logger.Info(ctxd.WithDebug(ctx), "logged because of forced DEBUG mode") - logger.AtomicLevel.SetLevel(zap.DebugLevel) + logger.SetLevelEnabler(zapcore.DebugLevel) logger.Info(ctx, "logged because logger level was changed to DEBUG") // Output: - // ERROR zapctxd/example_test.go:24 something failed {"baz": 1, "quux": 2.2, "foo": "bar"} - // INFO zapctxd/example_test.go:29 logged because is important {"foo": "bar"} - // INFO zapctxd/example_test.go:30 logged because of forced DEBUG mode {"foo": "bar"} - // INFO zapctxd/example_test.go:33 logged because logger level was changed to DEBUG {"foo": "bar"} + // ERROR zapctxd/example_test.go:26 something failed {"baz": 1, "quux": 2.2, "foo": "bar"} + // INFO zapctxd/example_test.go:31 logged because is important {"foo": "bar"} + // INFO zapctxd/example_test.go:32 logged because of forced DEBUG mode {"foo": "bar"} + // INFO zapctxd/example_test.go:35 logged because logger level was changed to DEBUG {"foo": "bar"} } diff --git a/go.mod b/go.mod index 236d33a..c31df69 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.19 require ( github.com/bool64/ctxd v1.2.1 - github.com/bool64/dev v0.2.34 - github.com/stretchr/testify v1.8.1 + github.com/bool64/dev v0.2.36 + github.com/stretchr/testify v1.9.0 github.com/swaggest/assertjson v1.9.0 go.uber.org/zap v1.27.0 ) diff --git a/go.sum b/go.sum index 230fcca..2ab4110 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/bool64/ctxd v1.2.1 h1:hARFteq0zdn4bwfmxLhak3fXFuvtJVKDH2X29VV/2ls= github.com/bool64/ctxd v1.2.1/go.mod h1:ZG6QkeGVLTiUl2mxPpyHmFhDzFZCyocr9hluBV3LYuc= github.com/bool64/dev v0.2.34 h1:P9n315P8LdpxusnYQ0X7MP1CZXwBK5ae5RZrd+GdSZE= github.com/bool64/dev v0.2.34/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/dev v0.2.36 h1:yU3bbOTujoxhWnt8ig8t94PVmZXIkCaRj9C57OtqJBY= +github.com/bool64/dev v0.2.36/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,6 +37,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/swaggest/usecase v1.2.0 h1:cHVFqxIbHfyTXp02JmWXk+ZADaSa87UZP+b3qL5Nz90= diff --git a/logger.go b/logger.go index aca5527..1b6c6bd 100644 --- a/logger.go +++ b/logger.go @@ -17,13 +17,16 @@ var _ ctxd.Logger = &Logger{} // Logger is a contextualized zap logger. type Logger struct { + // Deprecated: Use SetLevelEnabler instead. AtomicLevel zap.AtomicLevel - callerSkip bool - encoder zapcore.Encoder - sugared *zap.SugaredLogger - debug *zap.SugaredLogger - options []zap.Option - out zapcore.WriteSyncer + + callerSkip bool + encoder zapcore.Encoder + levelEnabler zapcore.LevelEnabler + sugared *zap.SugaredLogger + debug *zap.SugaredLogger + options []zap.Option + out zapcore.WriteSyncer } // Config is log configuration. @@ -64,9 +67,9 @@ func New(cfg Config, options ...zap.Option) *Logger { } l := Logger{ - AtomicLevel: zap.NewAtomicLevelAt(level), - out: out, - options: append(cfg.ZapOptions, options...), + levelEnabler: zap.NewAtomicLevelAt(level), + out: out, + options: append(cfg.ZapOptions, options...), } if cfg.DevMode { @@ -101,11 +104,25 @@ func New(cfg Config, options ...zap.Option) *Logger { return &l } +// WrapZapLoggers creates contextualized logger with provided zap loggers. +func WrapZapLoggers(sugared, debug *zap.Logger, encoder zapcore.Encoder, options ...zap.Option) *Logger { + sugared = sugared.WithOptions(options...) + debug = debug.WithOptions(options...) + + return &Logger{ + levelEnabler: sugared.Core(), + sugared: sugared.Sugar(), + debug: debug.Sugar(), + encoder: encoder, + options: options, + } +} + func (l *Logger) make() { l.sugared = zap.New(zapcore.NewCore( l.encoder, l.out, - l.AtomicLevel, + loggerLevelEnabler(l), ), l.options...).Sugar() l.debug = zap.New(zapcore.NewCore( @@ -115,6 +132,15 @@ func (l *Logger) make() { ), l.options...).Sugar() } +// SetLevelEnabler sets level enabler. +func (l *Logger) SetLevelEnabler(enabler zapcore.LevelEnabler) { + if _, ok := l.levelEnabler.(zapcore.Core); ok { + panic("cannot set level enabler when logger is created with zap loggers") + } + + l.levelEnabler = enabler +} + // SkipCaller adapts logger for wrapping by increasing skip caller counter. func (l *Logger) SkipCaller() *Logger { if !l.callerSkip { @@ -130,7 +156,7 @@ func (l *Logger) SkipCaller() *Logger { } // Debug implements ctxd.Logger. -func (l *Logger) Debug(ctx context.Context, msg string, keysAndValues ...interface{}) { +func (l *Logger) Debug(ctx context.Context, msg string, keysAndValues ...any) { z := l.get(ctx, zap.DebugLevel) if z == nil { return @@ -142,7 +168,7 @@ func (l *Logger) Debug(ctx context.Context, msg string, keysAndValues ...interfa ) if len(fv) > 0 { - kv = make([]interface{}, 0, len(fv)+len(kv)) + kv = make([]any, 0, len(fv)+len(kv)) kv = append(kv, keysAndValues...) kv = append(kv, fv...) @@ -164,7 +190,7 @@ func (l *Logger) Debug(ctx context.Context, msg string, keysAndValues ...interfa z.Debugw(msg, kv...) } -func expandError(kv []interface{}, se ctxd.StructuredError, i int) []interface{} { +func expandError(kv []any, se ctxd.StructuredError, i int) []any { kv[i] = se.Error() tuples := se.Tuples() @@ -189,7 +215,7 @@ func expandError(kv []interface{}, se ctxd.StructuredError, i int) []interface{} } // Info implements ctxd.Logger. -func (l *Logger) Info(ctx context.Context, msg string, keysAndValues ...interface{}) { +func (l *Logger) Info(ctx context.Context, msg string, keysAndValues ...any) { z := l.get(ctx, zap.InfoLevel) if z == nil { return @@ -201,7 +227,7 @@ func (l *Logger) Info(ctx context.Context, msg string, keysAndValues ...interfac ) if len(fv) > 0 { - kv = make([]interface{}, 0, len(fv)+len(kv)) + kv = make([]any, 0, len(fv)+len(kv)) kv = append(kv, keysAndValues...) kv = append(kv, fv...) @@ -224,7 +250,7 @@ func (l *Logger) Info(ctx context.Context, msg string, keysAndValues ...interfac } // Important implements ctxd.Logger. -func (l *Logger) Important(ctx context.Context, msg string, keysAndValues ...interface{}) { +func (l *Logger) Important(ctx context.Context, msg string, keysAndValues ...any) { z := l.get(ctxd.WithDebug(ctx), zap.InfoLevel) if z == nil { return @@ -236,7 +262,7 @@ func (l *Logger) Important(ctx context.Context, msg string, keysAndValues ...int ) if len(fv) > 0 { - kv = make([]interface{}, 0, len(fv)+len(kv)) + kv = make([]any, 0, len(fv)+len(kv)) kv = append(kv, keysAndValues...) kv = append(kv, fv...) @@ -259,7 +285,7 @@ func (l *Logger) Important(ctx context.Context, msg string, keysAndValues ...int } // Warn implements ctxd.Logger. -func (l *Logger) Warn(ctx context.Context, msg string, keysAndValues ...interface{}) { +func (l *Logger) Warn(ctx context.Context, msg string, keysAndValues ...any) { z := l.get(ctx, zap.WarnLevel) if z == nil { return @@ -271,7 +297,7 @@ func (l *Logger) Warn(ctx context.Context, msg string, keysAndValues ...interfac ) if len(fv) > 0 { - kv = make([]interface{}, 0, len(fv)+len(kv)) + kv = make([]any, 0, len(fv)+len(kv)) kv = append(kv, keysAndValues...) kv = append(kv, fv...) @@ -294,7 +320,7 @@ func (l *Logger) Warn(ctx context.Context, msg string, keysAndValues ...interfac } // Error implements ctxd.Logger. -func (l *Logger) Error(ctx context.Context, msg string, keysAndValues ...interface{}) { +func (l *Logger) Error(ctx context.Context, msg string, keysAndValues ...any) { z := l.get(ctx, zap.ErrorLevel) if z == nil { return @@ -306,7 +332,7 @@ func (l *Logger) Error(ctx context.Context, msg string, keysAndValues ...interfa ) if len(fv) > 0 { - kv = make([]interface{}, 0, len(fv)+len(kv)) + kv = make([]any, 0, len(fv)+len(kv)) kv = append(kv, keysAndValues...) kv = append(kv, fv...) @@ -330,7 +356,7 @@ func (l *Logger) Error(ctx context.Context, msg string, keysAndValues ...interfa func (l *Logger) get(ctx context.Context, level zapcore.Level) *zap.SugaredLogger { z := l.sugared - if !l.AtomicLevel.Enabled(level) { + if !l.levelEnabler.Enabled(level) { z = nil } @@ -345,9 +371,9 @@ func (l *Logger) get(ctx context.Context, level zapcore.Level) *zap.SugaredLogge writer := ctxd.LogWriter(ctx) if writer != nil { - level := zap.DebugLevel + level := zapcore.LevelEnabler(zap.DebugLevel) if !isDebug { - level = l.AtomicLevel.Level() + level = l.levelEnabler } ws, ok := writer.(zapcore.WriteSyncer) @@ -368,7 +394,7 @@ func (l *Logger) get(ctx context.Context, level zapcore.Level) *zap.SugaredLogge var _ ctxd.LoggerProvider = &Logger{} // CtxdLogger provides contextualized logger. -func (l *Logger) CtxdLogger() ctxd.Logger { +func (l *Logger) CtxdLogger() ctxd.Logger { //nolint: ireturn return l } @@ -376,3 +402,9 @@ func (l *Logger) CtxdLogger() ctxd.Logger { func (l *Logger) ZapLogger() *zap.Logger { return l.sugared.Desugar() } + +func loggerLevelEnabler(l *Logger) zap.LevelEnablerFunc { + return func(lvl zapcore.Level) bool { + return l.levelEnabler.Enabled(lvl) + } +} diff --git a/logger_test.go b/logger_test.go index 0db3224..a3784ac 100644 --- a/logger_test.go +++ b/logger_test.go @@ -8,11 +8,12 @@ import ( "testing" "github.com/bool64/ctxd" - "github.com/bool64/zapctxd" "github.com/stretchr/testify/assert" "github.com/swaggest/assertjson" "go.uber.org/zap" "go.uber.org/zap/zapcore" + + "github.com/bool64/zapctxd" ) func TestLogger(t *testing.T) { @@ -24,6 +25,7 @@ func TestLogger(t *testing.T) { ctx = ctxd.WithLogWriter(ctx, w) type ctxInt int + // Put some pressure on context. for i := 0; i < 20; i++ { ctx = context.WithValue(ctx, ctxInt(i), i) @@ -54,8 +56,8 @@ func TestLogger(t *testing.T) { c.Debug(ctx, "hello!", "foo", 1, - "def", ctxd.DeferredJSON(func() interface{} { return 123 }), - "defstr", ctxd.DeferredJSON(func() interface{} { return "abc" }), + "def", ctxd.DeferredJSON(func() any { return 123 }), + "defstr", ctxd.DeferredJSON(func() any { return "abc" }), ) assertjson.Equal(t, []byte( @@ -80,6 +82,7 @@ func TestLogger_concurrency(t *testing.T) { logger.Error(ctx, "hello") }() } + wg.Wait() assert.Equal(t, @@ -119,7 +122,7 @@ func TestLogger_Importantw_dev(t *testing.T) { c.Info(ctx, "hello!", "foo", 1) c.Important(ctx, "account created", "foo", 1) - assert.Equal(t, "\tINFO\tzapctxd/logger_test.go:120\taccount created\t{\"foo\": 1}\n", w.String()) + assert.Equal(t, "\tINFO\tzapctxd/logger_test.go:123\taccount created\t{\"foo\": 1}\n", w.String()) } func TestLogger_ColoredOutput_dev(t *testing.T) { @@ -138,7 +141,7 @@ func TestLogger_ColoredOutput_dev(t *testing.T) { c.Info(ctx, "hello!", "foo", 1) c.Important(ctx, "account created", "foo", 1) - assert.Equal(t, "\t\u001B[34mINFO\u001B[0m\tzapctxd/logger_test.go:139\taccount created\t{\"foo\": 1}\n", w.String()) + assert.Equal(t, "\t\u001B[34mINFO\u001B[0m\tzapctxd/logger_test.go:142\taccount created\t{\"foo\": 1}\n", w.String()) } func TestNew_atomic_dev(t *testing.T) { @@ -154,7 +157,7 @@ func TestNew_atomic_dev(t *testing.T) { ctx := context.Background() for _, lvl := range []zapcore.Level{zap.ErrorLevel, zap.WarnLevel, zap.InfoLevel, zap.DebugLevel} { - c.AtomicLevel.SetLevel(lvl) + c.SetLevelEnabler(lvl) c.Debug(ctx, "msg", "lvl", lvl) c.Info(ctx, "msg", "lvl", lvl) @@ -163,20 +166,20 @@ func TestNew_atomic_dev(t *testing.T) { c.Important(ctx, "msg", "lvl", lvl, "important", true) } - assert.Equal(t, ` ERROR zapctxd/logger_test.go:162 msg {"lvl": "error"} - INFO zapctxd/logger_test.go:163 msg {"lvl": "error", "important": true} - WARN zapctxd/logger_test.go:161 msg {"lvl": "warn"} - ERROR zapctxd/logger_test.go:162 msg {"lvl": "warn"} - INFO zapctxd/logger_test.go:163 msg {"lvl": "warn", "important": true} - INFO zapctxd/logger_test.go:160 msg {"lvl": "info"} - WARN zapctxd/logger_test.go:161 msg {"lvl": "info"} - ERROR zapctxd/logger_test.go:162 msg {"lvl": "info"} - INFO zapctxd/logger_test.go:163 msg {"lvl": "info", "important": true} - DEBUG zapctxd/logger_test.go:159 msg {"lvl": "debug"} - INFO zapctxd/logger_test.go:160 msg {"lvl": "debug"} - WARN zapctxd/logger_test.go:161 msg {"lvl": "debug"} - ERROR zapctxd/logger_test.go:162 msg {"lvl": "debug"} - INFO zapctxd/logger_test.go:163 msg {"lvl": "debug", "important": true} + assert.Equal(t, ` ERROR zapctxd/logger_test.go:165 msg {"lvl": "error"} + INFO zapctxd/logger_test.go:166 msg {"lvl": "error", "important": true} + WARN zapctxd/logger_test.go:164 msg {"lvl": "warn"} + ERROR zapctxd/logger_test.go:165 msg {"lvl": "warn"} + INFO zapctxd/logger_test.go:166 msg {"lvl": "warn", "important": true} + INFO zapctxd/logger_test.go:163 msg {"lvl": "info"} + WARN zapctxd/logger_test.go:164 msg {"lvl": "info"} + ERROR zapctxd/logger_test.go:165 msg {"lvl": "info"} + INFO zapctxd/logger_test.go:166 msg {"lvl": "info", "important": true} + DEBUG zapctxd/logger_test.go:162 msg {"lvl": "debug"} + INFO zapctxd/logger_test.go:163 msg {"lvl": "debug"} + WARN zapctxd/logger_test.go:164 msg {"lvl": "debug"} + ERROR zapctxd/logger_test.go:165 msg {"lvl": "debug"} + INFO zapctxd/logger_test.go:166 msg {"lvl": "debug", "important": true} `, w.String(), w.String()) } @@ -192,7 +195,7 @@ func TestNew_atomic(t *testing.T) { ctx := context.Background() for _, lvl := range []zapcore.Level{zap.ErrorLevel, zap.WarnLevel, zap.InfoLevel, zap.DebugLevel} { - c.AtomicLevel.SetLevel(lvl) + c.SetLevelEnabler(lvl) c.Debug(ctx, "msg", "lvl", lvl) c.Info(ctx, "msg", "lvl", lvl) @@ -231,6 +234,37 @@ func TestLogger_ZapLogger(t *testing.T) { assert.Equal(t, `{"level":"error","time":"","msg":"oops","error":"failed"}`+"\n", w.String()) } +func TestLogger_SetLevelEnabler_FailedToHandleZapLoggers(t *testing.T) { + t.Parallel() + + w := bytes.NewBuffer(nil) + enc := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) + sl := zap.New(zapcore.NewCore(enc, zapcore.AddSync(w), zapcore.InfoLevel)) + + l := zapctxd.WrapZapLoggers(sl, sl, enc) + + assert.Panics(t, func() { + l.SetLevelEnabler(zapcore.DebugLevel) + }) +} + +func TestLogger_SetLevelEnabler_Success(t *testing.T) { + t.Parallel() + + w := bytes.NewBuffer(nil) + + l := zapctxd.New(zapctxd.Config{ + Level: zap.InfoLevel, + StripTime: true, + DevMode: true, + Output: w, + }) + + assert.NotPanics(t, func() { + l.SetLevelEnabler(zapcore.DebugLevel) + }) +} + func TestLogger_SkipCaller(t *testing.T) { w := bytes.NewBuffer(nil) @@ -249,9 +283,9 @@ func TestLogger_SkipCaller(t *testing.T) { do() - assert.Equal(t, ` INFO zapctxd/logger_test.go:245 hello {"k": "v"} - INFO zapctxd/logger_test.go:250 world {"k": "v"} - INFO zapctxd/logger_test.go:247 hello {"k": "v"} + assert.Equal(t, ` INFO zapctxd/logger_test.go:279 hello {"k": "v"} + INFO zapctxd/logger_test.go:284 world {"k": "v"} + INFO zapctxd/logger_test.go:281 hello {"k": "v"} `, w.String()) assert.NotNil(t, zapctxd.New(zapctxd.Config{}).SkipCaller())