diff --git a/otelzap/README.md b/otelzap/README.md index 44d7d91..4a6adef 100644 --- a/otelzap/README.md +++ b/otelzap/README.md @@ -101,6 +101,8 @@ couple of [options](https://pkg.go.dev/github.com/uptrace/opentelemetry-go-extra number, and function name of the caller. Enabled by default. - `otelzap.WithStackTrace(true)` configures the logger to capture logs with a stack trace. Disabled by default. +- `otelzap.WithExtraFields(true)` configures the logger to add the given fields to structured log + messages and to span log events. - `otelzap.WithTraceIDField(true)` configures the logger to add `trace_id` field to structured log messages. This option is only useful with backends that don't support OTLP and instead parse log messages to extract structured information. diff --git a/otelzap/option.go b/otelzap/option.go index b3da57d..7d3a73c 100644 --- a/otelzap/option.go +++ b/otelzap/option.go @@ -42,6 +42,14 @@ func WithStackTrace(on bool) Option { } } +// WithExtraFields configures the logger to add the given extra fields to structured log messages +// and the span +func WithExtraFields(fields ...zapcore.Field) Option { + return func(l *Logger) { + l.extraFields = append(l.extraFields, fields...) + } +} + // WithTraceIDField configures the logger to add `trace_id` field to structured log messages. // // This option is only useful with backends that don't support OTLP and instead parse log diff --git a/otelzap/otelzap.go b/otelzap/otelzap.go index 23efcca..4e4ec8d 100644 --- a/otelzap/otelzap.go +++ b/otelzap/otelzap.go @@ -139,6 +139,10 @@ func (l *Logger) FatalContext(ctx context.Context, msg string, fields ...zapcore func (l *Logger) logFields( ctx context.Context, lvl zapcore.Level, msg string, fields []zapcore.Field, ) []zapcore.Field { + if len(l.extraFields) > 0 { + fields = append(fields, l.extraFields...) + } + if lvl < l.minLevel { return fields } @@ -148,7 +152,7 @@ func (l *Logger) logFields( return fields } - attrs := make([]attribute.KeyValue, 0, numAttr+len(fields)+len(l.extraFields)) + attrs := make([]attribute.KeyValue, 0, numAttr+len(fields)) for _, f := range fields { if f.Type == zapcore.NamespaceType { @@ -158,14 +162,6 @@ func (l *Logger) logFields( attrs = appendField(attrs, f) } - for _, f := range l.extraFields { - if f.Type == zapcore.NamespaceType { - // should this be a prefix? - continue - } - attrs = appendField(attrs, f) - } - l.log(span, lvl, msg, attrs) if l.withTraceID { @@ -355,21 +351,24 @@ func (s *SugaredLogger) Desugar() *Logger { // and the second as the field value. // // For example, -// sugaredLogger.With( -// "hello", "world", -// "failure", errors.New("oh no"), -// Stack(), -// "count", 42, -// "user", User{Name: "alice"}, -// ) +// +// sugaredLogger.With( +// "hello", "world", +// "failure", errors.New("oh no"), +// Stack(), +// "count", 42, +// "user", User{Name: "alice"}, +// ) +// // is the equivalent of -// unsugared.With( -// String("hello", "world"), -// String("failure", "oh no"), -// Stack(), -// Int("count", 42), -// Object("user", User{Name: "alice"}), -// ) +// +// unsugared.With( +// String("hello", "world"), +// String("failure", "oh no"), +// Stack(), +// Int("count", 42), +// Object("user", User{Name: "alice"}), +// ) // // Note that the keys in key-value pairs should be strings. In development, // passing a non-string key panics. In production, the logger is more @@ -606,7 +605,8 @@ func (s SugaredLoggerWithCtx) Fatalf(template string, args ...interface{}) { // pairs are treated as they are in With. // // When debug-level logging is disabled, this is much faster than -// s.With(keysAndValues).Debug(msg) +// +// s.With(keysAndValues).Debug(msg) func (s SugaredLoggerWithCtx) Debugw(msg string, keysAndValues ...interface{}) { keysAndValues = s.s.logKVs(s.ctx, zap.DebugLevel, msg, keysAndValues) s.s.skipCaller.Debugw(msg, keysAndValues...) diff --git a/otelzap/otelzap_test.go b/otelzap/otelzap_test.go index 3107ddd..3b53bfc 100644 --- a/otelzap/otelzap_test.go +++ b/otelzap/otelzap_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "go.uber.org/zap/zaptest/observer" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -346,6 +348,51 @@ func TestOtelZap(t *testing.T) { test.require(t, event) }) } + + t.Run("providing extra fields to be recorded on the span, and logged", func(t *testing.T) { + sr := tracetest.NewSpanRecorder() + provider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + tracer := provider.Tracer("test") + + ctx := context.Background() + ctx, span := tracer.Start(ctx, "main") + + core, observedLogs := observer.New(zap.InfoLevel) + logger := New(zap.New(core), WithMinLevel(zap.InfoLevel)) + loggerWithCtx := logger.Ctx(ctx).Clone(WithExtraFields( + zap.String("foo", "bar"), + zap.String("MyTraceIDKey", span.SpanContext().TraceID().String()), + )) + loggerWithCtx.Info("hello") + + span.End() + + spans := sr.Ended() + require.Equal(t, 1, len(spans)) + + events := spans[0].Events() + require.Equal(t, 1, len(events)) + + event := events[0] + require.Equal(t, "log", event.Name) + + m := attrMap(event.Attributes) + foo, ok := m["foo"] + require.True(t, ok) + require.Equal(t, "bar", foo.AsString()) + + _, ok = m["MyTraceIDKey"] + require.True(t, ok) + requireCodeAttrs(t, m) + + require.Equal(t, 1, observedLogs.Len()) + require.Equal(t, "hello", observedLogs.All()[0].Message) + require.Equal(t, zap.InfoLevel, observedLogs.All()[0].Level) + + contextMap := observedLogs.All()[0].ContextMap() + require.Equal(t, "bar", contextMap["foo"]) + require.Equal(t, span.SpanContext().TraceID().String(), contextMap["MyTraceIDKey"]) + }) } func requireCodeAttrs(t *testing.T, m map[attribute.Key]attribute.Value) {