Skip to content

Commit 65b5014

Browse files
authored
Logger: make sure to pass along base loggers context when using With (#113)
* Logger: make sure to pass along base loggers context when using With * fixed otel context not propagating issue * suggested improvements * improvement in test
1 parent b79ddfd commit 65b5014

File tree

5 files changed

+300
-7
lines changed

5 files changed

+300
-7
lines changed

logger/console_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,29 @@ func TestConsoleLoggerWithMetadata(t *testing.T) {
146146
assert.Contains(t, output, `"key2":"value2"`)
147147
}
148148

149+
func TestConsoleLoggerWithMergesMetadata(t *testing.T) {
150+
logger := NewConsoleLogger().(*consoleLogger)
151+
152+
baseLogger := logger.With(map[string]interface{}{
153+
"base_key": "base_value",
154+
"shared": "from_base",
155+
}).(*consoleLogger)
156+
157+
extendedLogger := baseLogger.With(map[string]interface{}{
158+
"extra_key": "extra_value",
159+
"shared": "from_extended",
160+
}).(*consoleLogger)
161+
162+
output := captureOutput(func() {
163+
extendedLogger.Info("Test message")
164+
})
165+
166+
assert.Contains(t, output, "Test message")
167+
assert.Contains(t, output, `"base_key":"base_value"`)
168+
assert.Contains(t, output, `"extra_key":"extra_value"`)
169+
assert.Contains(t, output, `"shared":"from_extended"`)
170+
}
171+
149172
func TestConsoleLoggerSinkTraceLevel(t *testing.T) {
150173
logger := NewConsoleLogger().(*consoleLogger)
151174
logger.SetLogLevel(LevelTrace)

logger/json.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,16 @@ func (c *jsonLogger) WithPrefix(prefix string) Logger {
104104

105105
func (c *jsonLogger) With(newFields map[string]interface{}) Logger {
106106
clone := c.clone()
107-
if trace, ok := newFields["trace"].(string); ok {
107+
for k, v := range newFields {
108+
clone.metadata[k] = v
109+
}
110+
if trace, ok := clone.metadata["trace"].(string); ok {
108111
clone.traceID = trace
109-
delete(newFields, "trace")
112+
delete(clone.metadata, "trace")
110113
}
111-
if comp, ok := newFields["component"].(string); ok {
114+
if comp, ok := clone.metadata["component"].(string); ok {
112115
clone.component = comp
113-
delete(newFields, "component")
114-
}
115-
for k, v := range newFields {
116-
clone.metadata[k] = v
116+
delete(clone.metadata, "component")
117117
}
118118
if c.child != nil {
119119
clone.child = c.child.With(newFields)

logger/json_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,32 @@ func TestJSONLoggerWith(t *testing.T) {
148148
assert.Equal(t, float64(42), metadata["key2"]) // JSON numbers are float64
149149
}
150150

151+
func TestJSONLoggerWithMergesMetadata(t *testing.T) {
152+
sink := &testSink{}
153+
logger := NewJSONLoggerWithSink(sink, LevelTrace)
154+
155+
baseLogger := logger.With(map[string]interface{}{
156+
"base_key": "base_value",
157+
"shared": "from_base",
158+
})
159+
160+
extendedLogger := baseLogger.With(map[string]interface{}{
161+
"extra_key": "extra_value",
162+
"shared": "from_extended",
163+
})
164+
165+
extendedLogger.Info("Test message")
166+
167+
var parsed map[string]interface{}
168+
err := json.Unmarshal(sink.buf, &parsed)
169+
assert.NoError(t, err)
170+
171+
metadata := parsed["metadata"].(map[string]interface{})
172+
assert.Equal(t, "base_value", metadata["base_key"])
173+
assert.Equal(t, "extra_value", metadata["extra_key"])
174+
assert.Equal(t, "from_extended", metadata["shared"])
175+
}
176+
151177
func TestJSONLoggerLog(t *testing.T) {
152178
sink := &testSink{}
153179

logger/otel.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func (o *otelLogger) clone() *otelLogger {
5050
logLevel: o.logLevel,
5151
otelLogger: o.otelLogger,
5252
child: o.child,
53+
context: o.context,
5354
}
5455
}
5556

@@ -59,8 +60,14 @@ func toLogValue(unknown interface{}) otelLog.Value {
5960
return otelLog.StringValue(v)
6061
case int:
6162
return otelLog.IntValue(v)
63+
case int32:
64+
return otelLog.IntValue(int(v))
65+
case int64:
66+
return otelLog.Int64Value(v)
6267
case bool:
6368
return otelLog.BoolValue(v)
69+
case float32:
70+
return otelLog.Float64Value(float64(v))
6471
case float64:
6572
return otelLog.Float64Value(v)
6673
case []byte:

logger/otel_test.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package logger
2+
3+
import (
4+
"context"
5+
"sync"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"go.opentelemetry.io/otel/log"
11+
"go.opentelemetry.io/otel/log/embedded"
12+
"go.opentelemetry.io/otel/log/noop"
13+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
14+
"go.opentelemetry.io/otel/trace"
15+
)
16+
17+
func TestOtelLoggerWithMergesMetadata(t *testing.T) {
18+
noopLogger := noop.NewLoggerProvider().Logger("test")
19+
20+
logger := &otelLogger{
21+
metadata: make(map[string]log.Value),
22+
logLevel: LevelTrace,
23+
otelLogger: noopLogger,
24+
context: context.Background(),
25+
}
26+
27+
baseLogger := logger.With(map[string]interface{}{
28+
"base_key": "base_value",
29+
"shared": "from_base",
30+
}).(*otelLogger)
31+
32+
extendedLogger := baseLogger.With(map[string]interface{}{
33+
"extra_key": "extra_value",
34+
"shared": "from_extended",
35+
}).(*otelLogger)
36+
37+
// Verify baseLogger.metadata stayed unchanged (immutability)
38+
assert.Equal(t, 2, len(baseLogger.metadata))
39+
assert.Equal(t, "base_value", baseLogger.metadata["base_key"].AsString())
40+
assert.Equal(t, "from_base", baseLogger.metadata["shared"].AsString())
41+
42+
assert.Equal(t, 3, len(extendedLogger.metadata))
43+
assert.Equal(t, "base_value", extendedLogger.metadata["base_key"].AsString())
44+
assert.Equal(t, "extra_value", extendedLogger.metadata["extra_key"].AsString())
45+
assert.Equal(t, "from_extended", extendedLogger.metadata["shared"].AsString())
46+
}
47+
48+
type mockOtelLogger struct {
49+
embedded.Logger
50+
mu sync.Mutex
51+
contexts []context.Context
52+
}
53+
54+
func (m *mockOtelLogger) Emit(ctx context.Context, record log.Record) {
55+
m.mu.Lock()
56+
defer m.mu.Unlock()
57+
m.contexts = append(m.contexts, ctx)
58+
}
59+
60+
func (m *mockOtelLogger) Enabled(ctx context.Context, param log.EnabledParameters) bool {
61+
return true
62+
}
63+
64+
func (m *mockOtelLogger) getContexts() []context.Context {
65+
m.mu.Lock()
66+
defer m.mu.Unlock()
67+
return append([]context.Context{}, m.contexts...)
68+
}
69+
70+
func TestOtelLoggerWithContextPreservesSpanContext(t *testing.T) {
71+
mockLogger := &mockOtelLogger{}
72+
73+
tp := sdktrace.NewTracerProvider()
74+
tracer := tp.Tracer("test-tracer")
75+
76+
ctx, span := tracer.Start(context.Background(), "test-span")
77+
defer span.End()
78+
79+
logger := &otelLogger{
80+
metadata: make(map[string]log.Value),
81+
logLevel: LevelTrace,
82+
otelLogger: mockLogger,
83+
context: ctx,
84+
}
85+
86+
logger.Info("test message")
87+
88+
contexts := mockLogger.getContexts()
89+
require.Len(t, contexts, 1)
90+
91+
spanContext := trace.SpanContextFromContext(contexts[0])
92+
assert.True(t, spanContext.IsValid())
93+
assert.Equal(t, span.SpanContext().TraceID(), spanContext.TraceID())
94+
assert.Equal(t, span.SpanContext().SpanID(), spanContext.SpanID())
95+
}
96+
97+
func TestOtelLoggerWithContextMultipleTimes(t *testing.T) {
98+
mockLogger := &mockOtelLogger{}
99+
100+
tp := sdktrace.NewTracerProvider()
101+
tracer := tp.Tracer("test-tracer")
102+
103+
ctx1, span1 := tracer.Start(context.Background(), "span-1")
104+
defer span1.End()
105+
106+
ctx2, span2 := tracer.Start(ctx1, "span-2")
107+
defer span2.End()
108+
109+
ctx3, span3 := tracer.Start(ctx2, "span-3")
110+
defer span3.End()
111+
112+
baseLogger := &otelLogger{
113+
metadata: make(map[string]log.Value),
114+
logLevel: LevelTrace,
115+
otelLogger: mockLogger,
116+
context: ctx1,
117+
}
118+
119+
logger2 := baseLogger.WithContext(ctx2)
120+
logger3 := logger2.WithContext(ctx3)
121+
122+
logger3.Info("test message")
123+
124+
contexts := mockLogger.getContexts()
125+
require.Len(t, contexts, 1)
126+
127+
spanContext := trace.SpanContextFromContext(contexts[0])
128+
assert.True(t, spanContext.IsValid())
129+
assert.Equal(t, span3.SpanContext().TraceID(), spanContext.TraceID())
130+
assert.Equal(t, span3.SpanContext().SpanID(), spanContext.SpanID())
131+
}
132+
133+
func TestOtelLoggerWithContextChainedLoggers(t *testing.T) {
134+
mockLogger1 := &mockOtelLogger{}
135+
mockLogger2 := &mockOtelLogger{}
136+
137+
tp := sdktrace.NewTracerProvider()
138+
tracer := tp.Tracer("test-tracer")
139+
140+
ctx, span := tracer.Start(context.Background(), "test-span")
141+
defer span.End()
142+
143+
childLogger := &otelLogger{
144+
metadata: make(map[string]log.Value),
145+
logLevel: LevelTrace,
146+
otelLogger: mockLogger2,
147+
context: context.Background(),
148+
}
149+
150+
parentLogger := &otelLogger{
151+
metadata: make(map[string]log.Value),
152+
logLevel: LevelTrace,
153+
otelLogger: mockLogger1,
154+
context: context.Background(),
155+
child: childLogger,
156+
}
157+
158+
loggerWithCtx := parentLogger.WithContext(ctx)
159+
loggerWithCtx.Info("test message")
160+
161+
contexts1 := mockLogger1.getContexts()
162+
require.Len(t, contexts1, 1)
163+
spanContext1 := trace.SpanContextFromContext(contexts1[0])
164+
assert.True(t, spanContext1.IsValid())
165+
assert.Equal(t, span.SpanContext().TraceID(), spanContext1.TraceID())
166+
167+
contexts2 := mockLogger2.getContexts()
168+
require.Len(t, contexts2, 1)
169+
spanContext2 := trace.SpanContextFromContext(contexts2[0])
170+
assert.True(t, spanContext2.IsValid())
171+
assert.Equal(t, span.SpanContext().TraceID(), spanContext2.TraceID())
172+
}
173+
174+
func TestOtelLoggerWithContextPreservesOriginalLogger(t *testing.T) {
175+
mockLogger := &mockOtelLogger{}
176+
177+
tp := sdktrace.NewTracerProvider()
178+
tracer := tp.Tracer("test-tracer")
179+
180+
ctx1, span1 := tracer.Start(context.Background(), "span-1")
181+
defer span1.End()
182+
183+
ctx2, span2 := tracer.Start(ctx1, "span-2")
184+
defer span2.End()
185+
186+
baseLogger := &otelLogger{
187+
metadata: make(map[string]log.Value),
188+
logLevel: LevelTrace,
189+
otelLogger: mockLogger,
190+
context: ctx1,
191+
}
192+
193+
newLogger := baseLogger.WithContext(ctx2)
194+
195+
baseLogger.Info("original context")
196+
newLogger.Info("new context")
197+
198+
contexts := mockLogger.getContexts()
199+
require.Len(t, contexts, 2)
200+
201+
spanContext1 := trace.SpanContextFromContext(contexts[0])
202+
assert.Equal(t, span1.SpanContext().SpanID(), spanContext1.SpanID())
203+
204+
spanContext2 := trace.SpanContextFromContext(contexts[1])
205+
assert.Equal(t, span2.SpanContext().SpanID(), spanContext2.SpanID())
206+
}
207+
208+
func TestOtelLoggerWithContextAndMetadata(t *testing.T) {
209+
mockLogger := &mockOtelLogger{}
210+
211+
tp := sdktrace.NewTracerProvider()
212+
tracer := tp.Tracer("test-tracer")
213+
214+
ctx, span := tracer.Start(context.Background(), "test-span")
215+
defer span.End()
216+
217+
baseLogger := &otelLogger{
218+
metadata: make(map[string]log.Value),
219+
logLevel: LevelTrace,
220+
otelLogger: mockLogger,
221+
context: context.Background(),
222+
}
223+
224+
loggerWithMetadata := baseLogger.With(map[string]interface{}{
225+
"key": "value",
226+
})
227+
228+
loggerWithCtx := loggerWithMetadata.WithContext(ctx)
229+
loggerWithCtx.Info("test message")
230+
231+
contexts := mockLogger.getContexts()
232+
require.Len(t, contexts, 1)
233+
234+
spanContext := trace.SpanContextFromContext(contexts[0])
235+
assert.True(t, spanContext.IsValid())
236+
assert.Equal(t, span.SpanContext().TraceID(), spanContext.TraceID())
237+
}

0 commit comments

Comments
 (0)