From 0e7afaf89a1c4fd676282a2f3aee316fc9451364 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Wed, 19 Mar 2025 15:16:26 -0500 Subject: [PATCH] Demonstrate zap logger wrapped core concept --- bridges/otelzap/core.go | 84 +++++++++++++++++++++++++++++++++ bridges/otelzap/example_test.go | 40 ++++++++++++++++ bridges/otelzap/go.mod | 5 ++ bridges/otelzap/go.sum | 10 ++++ 4 files changed, 139 insertions(+) diff --git a/bridges/otelzap/core.go b/bridges/otelzap/core.go index e3564247efb..4e1e1c68837 100644 --- a/bridges/otelzap/core.go +++ b/bridges/otelzap/core.go @@ -225,6 +225,90 @@ func (o *Core) Write(ent zapcore.Entry, fields []zapcore.Field) error { return nil } +// WrappedCore is a [zapcore.Core] that wraps a core and sends logging records to OpenTelemetry. +type WrappedCore struct { + delegate zapcore.Core + provider log.LoggerProvider +} + +// Compile-time check *Core implements zapcore.Core. +var _ zapcore.Core = (*WrappedCore)(nil) + +// Compile-time check *Core implements zapcore.CheckWriteHook. +var _ zapcore.CheckWriteHook = (*WrappedCore)(nil) + +func NewWrappedCore(delegate zapcore.Core, provider log.LoggerProvider) *WrappedCore { + return &WrappedCore{ + delegate: delegate, + provider: provider, + } +} + +func (o *WrappedCore) Enabled(level zapcore.Level) bool { + return o.delegate.Enabled(level) +} + +func (o *WrappedCore) With(fields []zapcore.Field) zapcore.Core { + // TODO: figure out if / how to interact with otel context + return o.delegate.With(fields) +} + +func (o *WrappedCore) clone() *WrappedCore { + return &WrappedCore{ + delegate: o.delegate, + provider: o.provider, + } +} + +func (o *WrappedCore) Sync() error { + return o.delegate.Sync() +} + +func (o *WrappedCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + return o.delegate.Check(ent, ce).After(ent, o) +} + +func (o *WrappedCore) OnWrite(ent *zapcore.CheckedEntry, fields []zapcore.Field) { + ctx := context.Background() + r := log.Record{} + r.SetTimestamp(ent.Time) + r.SetBody(log.StringValue(ent.Message)) + r.SetSeverity(convertLevel(ent.Level)) + r.SetSeverityText(ent.Level.String()) + + if ent.Caller.Defined { + r.AddAttributes( + log.String(string(semconv.CodeFilepathKey), ent.Caller.File), + log.Int(string(semconv.CodeLineNumberKey), ent.Caller.Line), + log.String(string(semconv.CodeFunctionKey), ent.Caller.Function), + ) + } + if ent.Stack != "" { + r.AddAttributes(log.String(string(semconv.CodeStacktraceKey), ent.Stack)) + } + if len(fields) > 0 { + context, attrbuf := convertField(fields) + if context != nil { + ctx = context + } + r.AddAttributes(attrbuf...) + } + + var logger log.Logger + if ent.LoggerName != "" { + logger = o.provider.Logger(ent.LoggerName) + // TODO: figure out what to do with context + } else { + logger = o.provider.Logger("unknown") + } + logger.Emit(ctx, r) + +} + +func (o *WrappedCore) Write(ent zapcore.Entry, fields []zapcore.Field) error { + return o.delegate.Write(ent, fields) +} + func convertField(fields []zapcore.Field) (context.Context, []log.KeyValue) { var ctx context.Context enc := newObjectEncoder(len(fields)) diff --git a/bridges/otelzap/example_test.go b/bridges/otelzap/example_test.go index 54fa02a57ee..7315135f90e 100644 --- a/bridges/otelzap/example_test.go +++ b/bridges/otelzap/example_test.go @@ -5,7 +5,10 @@ package otelzap_test import ( "context" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/sdk/log" "os" + "testing" "go.opentelemetry.io/contrib/bridges/otelzap" "go.opentelemetry.io/otel/log/noop" @@ -14,6 +17,43 @@ import ( "go.uber.org/zap/zapcore" ) +func TestAltAppender(t *testing.T) { + // Configure otel log provider, which uses simple processor and stdout exporter for simplicity + logExporter, err := stdoutlog.New() + if err != nil { + t.Error(err) + return + } + provider := log.NewLoggerProvider( + log.WithProcessor(log.NewSimpleProcessor(logExporter)), + ) + if err != nil { + t.Error(err) + return + } + + // Configure a zap core using whatever configuration you typically use + core := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(os.Stdout), zapcore.WarnLevel) + // Wrap the core in an otel wrapper, which bridges all logs the core decides to write (i.e. all logs for which core.Write is invoked) to the otel log provider + wrappedCore := otelzap.NewWrappedCore(core, provider) + // Create a logger from the wrapped core, and proceed as usual + logger := zap.New(wrappedCore).Named("my.logger") + + logger.Warn("message level warn") + logger.Info("message level info") + + // Output: + // {"level":"warn","ts":1742414003.3163369,"msg":"message level warn"} + // {"Timestamp":"2025-03-19T14:53:23.316337-05:00","ObservedTimestamp":"2025-03-19T14:53:23.316363-05:00","Severity":13,"SeverityText":"warn","Body":{"Type":"String","Value":"message level warn"},"Attributes":[],"TraceID":"00000000000000000000000000000000","SpanID":"0000000000000000","TraceFlags":"00","Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"unknown_service:___TestAltAppender_in_go_opentelemetry_io_contrib_bridges_otelzap.test"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.35.0"}}],"Scope":{"Name":"unknown","Version":"","SchemaURL":"","Attributes":{}},"DroppedAttributes":0} + + // Notes: + // - Only the warn level log is written, since the core specifies zapcore.WarnLevel + // - The log is written to stdout twice: + // - Once by the core's configured stdout writer w/ JSON encoded + // - Second by the otel log provider's stdout log exporter. + // - Typically, the otel log provider would be configured with a batch processor and otlp exporter, which would result in the log being written to stdout once, and OTLP once. +} + func Example() { // Use a working LoggerProvider implementation instead e.g. use go.opentelemetry.io/otel/sdk/log. provider := noop.NewLoggerProvider() diff --git a/bridges/otelzap/go.mod b/bridges/otelzap/go.mod index 467ad5074d3..107a3f29eaf 100644 --- a/bridges/otelzap/go.mod +++ b/bridges/otelzap/go.mod @@ -5,7 +5,9 @@ go 1.23.0 require ( github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 go.opentelemetry.io/otel/log v0.11.0 + go.opentelemetry.io/otel/sdk/log v0.11.0 go.uber.org/zap v1.27.0 ) @@ -13,10 +15,13 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/bridges/otelzap/go.sum b/bridges/otelzap/go.sum index 8e0de803c9c..9cf8efbb6cb 100644 --- a/bridges/otelzap/go.sum +++ b/bridges/otelzap/go.sum @@ -7,6 +7,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -21,10 +23,16 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0 h1:k6KdfZk72tVW/QVZf60xlDziDvYAePj5QHwoQvrB2m8= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.11.0/go.mod h1:5Y3ZJLqzi/x/kYtrSrPSx7TFI/SGsL7q2kME027tH6I= go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/log v0.11.0 h1:7bAOpjpGglWhdEzP8z0VXc4jObOiDEwr3IYbhBnjk2c= +go.opentelemetry.io/otel/sdk/log v0.11.0/go.mod h1:dndLTxZbwBstZoqsJB3kGsRPkpAgaJrWfQg3lhlHFFY= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -33,6 +41,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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=