Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions bridges/otelzap/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,90 @@
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)

Check warning on line 253 in bridges/otelzap/core.go

View check run for this annotation

Codecov / codecov/patch

bridges/otelzap/core.go#L251-L253

Added lines #L251 - L253 were not covered by tests
}

func (o *WrappedCore) clone() *WrappedCore {
return &WrappedCore{
delegate: o.delegate,
provider: o.provider,
}

Check warning on line 260 in bridges/otelzap/core.go

View check run for this annotation

Codecov / codecov/patch

bridges/otelzap/core.go#L256-L260

Added lines #L256 - L260 were not covered by tests
}

func (o *WrappedCore) Sync() error {
return o.delegate.Sync()

Check warning on line 264 in bridges/otelzap/core.go

View check run for this annotation

Codecov / codecov/patch

bridges/otelzap/core.go#L263-L264

Added lines #L263 - L264 were not covered by tests
}

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),
)
}

Check warning on line 285 in bridges/otelzap/core.go

View check run for this annotation

Codecov / codecov/patch

bridges/otelzap/core.go#L280-L285

Added lines #L280 - L285 were not covered by tests
if ent.Stack != "" {
r.AddAttributes(log.String(string(semconv.CodeStacktraceKey), ent.Stack))
}

Check warning on line 288 in bridges/otelzap/core.go

View check run for this annotation

Codecov / codecov/patch

bridges/otelzap/core.go#L287-L288

Added lines #L287 - L288 were not covered by tests
if len(fields) > 0 {
context, attrbuf := convertField(fields)
if context != nil {
ctx = context
}
r.AddAttributes(attrbuf...)

Check warning on line 294 in bridges/otelzap/core.go

View check run for this annotation

Codecov / codecov/patch

bridges/otelzap/core.go#L290-L294

Added lines #L290 - L294 were not covered by tests
}

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")
}

Check warning on line 303 in bridges/otelzap/core.go

View check run for this annotation

Codecov / codecov/patch

bridges/otelzap/core.go#L302-L303

Added lines #L302 - L303 were not covered by tests
logger.Emit(ctx, r)

}

func (o *WrappedCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
return o.delegate.Write(ent, fields)

Check warning on line 309 in bridges/otelzap/core.go

View check run for this annotation

Codecov / codecov/patch

bridges/otelzap/core.go#L308-L309

Added lines #L308 - L309 were not covered by tests
}

func convertField(fields []zapcore.Field) (context.Context, []log.KeyValue) {
var ctx context.Context
enc := newObjectEncoder(len(fields))
Expand Down
40 changes: 40 additions & 0 deletions bridges/otelzap/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions bridges/otelzap/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,23 @@ 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
)

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
)
10 changes: 10 additions & 0 deletions bridges/otelzap/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down
Loading