diff --git a/go.mod b/go.mod index d86a166..20ab9b2 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,8 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 go.uber.org/mock v0.6.0 go.uber.org/zap v1.28.0 - google.golang.org/grpc v1.81.1 + go.uber.org/zap/exp v0.3.0 + google.golang.org/grpc v1.82.1 ) require ( diff --git a/go.sum b/go.sum index 7de0f88..dc655af 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= @@ -130,8 +132,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1: google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/zap/slog.go b/zap/slog.go new file mode 100644 index 0000000..7701a89 --- /dev/null +++ b/zap/slog.go @@ -0,0 +1,27 @@ +package zap + +import ( + "io" + "log/slog" + + logpkg "github.com/LerianStudio/lib-observability/v2/log" + "go.uber.org/zap/exp/zapslog" +) + +// Slog adapts a log.Logger to a stdlib *slog.Logger, so it can be handed to +// libraries that accept an slog-compatible logger (for example +// lib-service-discovery's libsd.WithLogger) without exposing any +// lib-observability type on the boundary. +// +// When l is zap-backed — the production path, since czap.New returns *Logger — +// the returned *slog.Logger writes through the very same zap core, so its output +// stays unified with the rest of the service's logs. Any other implementation +// (the nil logger, gomock doubles) falls back to a discarding handler rather +// than panicking, mirroring the "unknown logger is silent" posture elsewhere. +func Slog(l logpkg.Logger) *slog.Logger { + if zl, ok := l.(*Logger); ok { + return slog.New(zapslog.NewHandler(zl.Raw().Core())) + } + + return slog.New(slog.NewTextHandler(io.Discard, nil)) +} diff --git a/zap/slog_test.go b/zap/slog_test.go new file mode 100644 index 0000000..f94cefa --- /dev/null +++ b/zap/slog_test.go @@ -0,0 +1,48 @@ +//go:build unit + +package zap + +import ( + "context" + "testing" + + logpkg "github.com/LerianStudio/lib-observability/v2/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "go.uber.org/zap/zaptest/observer" +) + +func TestSlog_WritesThroughZapCore(t *testing.T) { + core, observed := observer.New(zapcore.InfoLevel) + l := &Logger{logger: zap.New(core)} + + Slog(l).InfoContext(context.Background(), "resolved", "consul", "localhost:8500") + + require.Equal(t, 1, observed.Len()) + entry := observed.All()[0] + assert.Equal(t, "resolved", entry.Message) + assert.Equal(t, zapcore.InfoLevel, entry.Level) + assert.Equal(t, "localhost:8500", entry.ContextMap()["consul"]) +} + +func TestSlog_NonZapLoggerFallsBackWithoutPanic(t *testing.T) { + got := Slog(logpkg.NewNop()) + + require.NotNil(t, got) + assert.NotPanics(t, func() { + got.InfoContext(context.Background(), "discarded") + }) +} + +func TestSlog_NilConcreteLoggerIsSafe(t *testing.T) { + var l *Logger + + got := Slog(l) + + require.NotNil(t, got) + assert.NotPanics(t, func() { + got.InfoContext(context.Background(), "no panic on nil receiver") + }) +}