Skip to content

Commit 3bbfaa0

Browse files
committed
make log fields generic
1 parent 77f6e57 commit 3bbfaa0

File tree

5 files changed

+80
-50
lines changed

5 files changed

+80
-50
lines changed

pkg/observability/log/field.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package log
2+
3+
import (
4+
"fmt"
5+
"time"
6+
)
7+
8+
// Field is a key-value pair for structured logging.
9+
// This type is backend-agnostic — adapters convert it to their native field type.
10+
type Field struct {
11+
Key string
12+
Value any
13+
}
14+
15+
// Convenience constructors for common field types.
16+
17+
func String(key, value string) Field { return Field{Key: key, Value: value} }
18+
func Int(key string, value int) Field { return Field{Key: key, Value: value} }
19+
func Int64(key string, value int64) Field { return Field{Key: key, Value: value} }
20+
func Float64(key string, value float64) Field { return Field{Key: key, Value: value} }
21+
func Bool(key string, value bool) Field { return Field{Key: key, Value: value} }
22+
func Err(err error) Field { return Field{Key: "error", Value: err} }
23+
func Duration(key string, value time.Duration) Field { return Field{Key: key, Value: value} }
24+
func Any(key string, value any) Field { return Field{Key: key, Value: value} }
25+
26+
// Stringer returns the string representation of a Field.
27+
func (f Field) String() string {
28+
return fmt.Sprintf("%s=%v", f.Key, f.Value)
29+
}

pkg/observability/log/logger.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ package log
22

33
import (
44
"context"
5-
"log/slog"
65
)
76

87
type Logger interface {
9-
Log(ctx context.Context, level Level, msg string, fields ...slog.Attr)
10-
Debug(msg string, fields ...slog.Attr)
11-
Info(msg string, fields ...slog.Attr)
12-
Warn(msg string, fields ...slog.Attr)
13-
Error(msg string, fields ...slog.Attr)
14-
Fatal(msg string, fields ...slog.Attr)
15-
WithFields(fields ...slog.Attr) Logger
8+
Log(ctx context.Context, level Level, msg string, fields ...Field)
9+
Debug(msg string, fields ...Field)
10+
Info(msg string, fields ...Field)
11+
Warn(msg string, fields ...Field)
12+
Error(msg string, fields ...Field)
13+
Fatal(msg string, fields ...Field)
14+
WithFields(fields ...Field) Logger
1615
WithError(err error) Logger
1716
Named(name string) Logger
1817
WithLevel(level Level) Logger

pkg/observability/log/noop_adapter.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package log
22

33
import (
44
"context"
5-
"log/slog"
65
)
76

87
type NoopLogger struct{}
@@ -17,31 +16,31 @@ func (l *NoopLogger) Level() Level {
1716
return DebugLevel
1817
}
1918

20-
func (l *NoopLogger) Log(_ context.Context, _ Level, _ string, _ ...slog.Attr) {
19+
func (l *NoopLogger) Log(_ context.Context, _ Level, _ string, _ ...Field) {
2120
// No-op
2221
}
2322

24-
func (l *NoopLogger) Debug(_ string, _ ...slog.Attr) {
23+
func (l *NoopLogger) Debug(_ string, _ ...Field) {
2524
// No-op
2625
}
2726

28-
func (l *NoopLogger) Info(_ string, _ ...slog.Attr) {
27+
func (l *NoopLogger) Info(_ string, _ ...Field) {
2928
// No-op
3029
}
3130

32-
func (l *NoopLogger) Warn(_ string, _ ...slog.Attr) {
31+
func (l *NoopLogger) Warn(_ string, _ ...Field) {
3332
// No-op
3433
}
3534

36-
func (l *NoopLogger) Error(_ string, _ ...slog.Attr) {
35+
func (l *NoopLogger) Error(_ string, _ ...Field) {
3736
// No-op
3837
}
3938

40-
func (l *NoopLogger) Fatal(_ string, _ ...slog.Attr) {
39+
func (l *NoopLogger) Fatal(_ string, _ ...Field) {
4140
// No-op
4241
}
4342

44-
func (l *NoopLogger) WithFields(_ ...slog.Attr) Logger {
43+
func (l *NoopLogger) WithFields(_ ...Field) Logger {
4544
return l
4645
}
4746

pkg/observability/log/slog_adapter.go

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package log
22

33
import (
44
"context"
5+
"fmt"
56
"log/slog"
67
)
78

@@ -21,42 +22,40 @@ func (l *SlogLogger) Level() Level {
2122
return l.level
2223
}
2324

24-
func (l *SlogLogger) Log(ctx context.Context, level Level, msg string, fields ...slog.Attr) {
25+
func (l *SlogLogger) Log(ctx context.Context, level Level, msg string, fields ...Field) {
2526
slogLevel := convertLevel(level)
26-
l.logger.LogAttrs(ctx, slogLevel, msg, fields...)
27+
l.logger.LogAttrs(ctx, slogLevel, msg, fieldsToSlogAttrs(fields)...)
2728
}
2829

29-
func (l *SlogLogger) Debug(msg string, fields ...slog.Attr) {
30+
func (l *SlogLogger) Debug(msg string, fields ...Field) {
3031
l.Log(context.Background(), DebugLevel, msg, fields...)
3132
}
3233

33-
func (l *SlogLogger) Info(msg string, fields ...slog.Attr) {
34+
func (l *SlogLogger) Info(msg string, fields ...Field) {
3435
l.Log(context.Background(), InfoLevel, msg, fields...)
3536
}
3637

37-
func (l *SlogLogger) Warn(msg string, fields ...slog.Attr) {
38+
func (l *SlogLogger) Warn(msg string, fields ...Field) {
3839
l.Log(context.Background(), WarnLevel, msg, fields...)
3940
}
4041

41-
func (l *SlogLogger) Error(msg string, fields ...slog.Attr) {
42+
func (l *SlogLogger) Error(msg string, fields ...Field) {
4243
l.Log(context.Background(), ErrorLevel, msg, fields...)
4344
}
4445

45-
func (l *SlogLogger) Fatal(msg string, fields ...slog.Attr) {
46+
func (l *SlogLogger) Fatal(msg string, fields ...Field) {
4647
l.Log(context.Background(), FatalLevel, msg, fields...)
47-
// Sync to ensure the log message is flushed before panic
4848
_ = l.Sync()
4949
panic("fatal: " + msg)
5050
}
5151

52-
func (l *SlogLogger) WithFields(fields ...slog.Attr) Logger {
53-
fieldKvPairs := make([]any, 0, len(fields)*2)
54-
for _, attr := range fields {
55-
k, v := attr.Key, attr.Value
56-
fieldKvPairs = append(fieldKvPairs, k, v.Any())
52+
func (l *SlogLogger) WithFields(fields ...Field) Logger {
53+
args := make([]any, 0, len(fields)*2)
54+
for _, f := range fields {
55+
args = append(args, f.Key, f.Value)
5756
}
5857
return &SlogLogger{
59-
logger: l.logger.With(fieldKvPairs...),
58+
logger: l.logger.With(args...),
6059
level: l.level,
6160
}
6261
}
@@ -86,10 +85,17 @@ func (l *SlogLogger) WithLevel(level Level) Logger {
8685
}
8786

8887
func (l *SlogLogger) Sync() error {
89-
// Slog does not require syncing
9088
return nil
9189
}
9290

91+
func fieldsToSlogAttrs(fields []Field) []slog.Attr {
92+
attrs := make([]slog.Attr, 0, len(fields))
93+
for _, f := range fields {
94+
attrs = append(attrs, slog.Any(f.Key, f.Value))
95+
}
96+
return attrs
97+
}
98+
9399
func convertLevel(level Level) slog.Level {
94100
switch level {
95101
case DebugLevel:
@@ -106,3 +112,8 @@ func convertLevel(level Level) slog.Level {
106112
return slog.LevelInfo
107113
}
108114
}
115+
116+
// FormatValue formats any value as a string, used by adapters for display.
117+
func FormatValue(v any) string {
118+
return fmt.Sprintf("%v", v)
119+
}

pkg/observability/log/slog_adapter_test.go

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestSlogLogger_ConvenienceMethods(t *testing.T) {
4949

5050
tests := []struct {
5151
name string
52-
logFunc func(string, ...slog.Attr)
52+
logFunc func(string, ...Field)
5353
level string
5454
}{
5555
{"Debug", logger.Debug, "DEBUG"},
@@ -61,7 +61,7 @@ func TestSlogLogger_ConvenienceMethods(t *testing.T) {
6161
for _, tt := range tests {
6262
t.Run(tt.name, func(t *testing.T) {
6363
buf.Reset()
64-
tt.logFunc("test message", slog.String("key", "value"))
64+
tt.logFunc("test message", String("key", "value"))
6565

6666
output := buf.String()
6767
assert.Contains(t, output, "test message")
@@ -76,12 +76,10 @@ func TestSlogLogger_Fatal(t *testing.T) {
7676
slogger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
7777
logger := NewSlogLogger(slogger, DebugLevel)
7878

79-
// Fatal should panic after logging
8079
assert.Panics(t, func() {
8180
logger.Fatal("fatal message")
8281
})
8382

84-
// Verify the message was logged before panic
8583
assert.Contains(t, buf.String(), "fatal message")
8684
}
8785

@@ -90,10 +88,9 @@ func TestSlogLogger_WithFields(t *testing.T) {
9088
slogger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
9189
logger := NewSlogLogger(slogger, InfoLevel)
9290

93-
// Add fields and log
9491
loggerWithFields := logger.WithFields(
95-
slog.String("service", "test-service"),
96-
slog.Int("port", 8080),
92+
String("service", "test-service"),
93+
Int("port", 8080),
9794
)
9895

9996
loggerWithFields.Info("message with fields")
@@ -105,7 +102,6 @@ func TestSlogLogger_WithFields(t *testing.T) {
105102
assert.Contains(t, output, "port")
106103
assert.Contains(t, output, "8080")
107104

108-
// Original logger should not have the fields
109105
buf.Reset()
110106
logger.Info("message without fields")
111107
output = buf.String()
@@ -147,10 +143,8 @@ func TestSlogLogger_WithLevel(t *testing.T) {
147143
slogger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
148144
logger := NewSlogLogger(slogger, InfoLevel)
149145

150-
// New logger with debug level
151146
debugLogger := logger.WithLevel(DebugLevel)
152147

153-
// Verify levels are correct
154148
assert.Equal(t, InfoLevel, logger.Level())
155149
assert.Equal(t, DebugLevel, debugLogger.Level())
156150
}
@@ -160,7 +154,6 @@ func TestSlogLogger_Sync(t *testing.T) {
160154
slogger := slog.New(slog.NewTextHandler(buf, nil))
161155
logger := NewSlogLogger(slogger, InfoLevel)
162156

163-
// Sync should not error for slog (no-op)
164157
err := logger.Sync()
165158
assert.NoError(t, err)
166159
}
@@ -170,10 +163,9 @@ func TestSlogLogger_Chaining(t *testing.T) {
170163
slogger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
171164
logger := NewSlogLogger(slogger, DebugLevel)
172165

173-
// Chain multiple operations
174166
chainedLogger := logger.
175167
Named("service").
176-
WithFields(slog.String("version", "1.0")).
168+
WithFields(String("version", "1.0")).
177169
WithLevel(InfoLevel)
178170

179171
chainedLogger.Info("chained message")
@@ -222,7 +214,7 @@ func TestSlogLogger_LogWithContext(t *testing.T) {
222214
logger := NewSlogLogger(slogger, DebugLevel)
223215

224216
ctx := context.Background()
225-
logger.Log(ctx, InfoLevel, "context message", slog.String("trace_id", "abc123"))
217+
logger.Log(ctx, InfoLevel, "context message", String("trace_id", "abc123"))
226218

227219
output := buf.String()
228220
assert.Contains(t, output, "context message")
@@ -235,7 +227,7 @@ func TestSlogLogger_WithFields_PreservesLevel(t *testing.T) {
235227
slogger := slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
236228
logger := NewSlogLogger(slogger, WarnLevel)
237229

238-
withFields := logger.WithFields(slog.String("key", "value"))
230+
withFields := logger.WithFields(String("key", "value"))
239231

240232
assert.Equal(t, WarnLevel, withFields.Level())
241233

@@ -262,10 +254,10 @@ func TestSlogLogger_MultipleFields(t *testing.T) {
262254
logger := NewSlogLogger(slogger, DebugLevel)
263255

264256
logger.Info("multi-field message",
265-
slog.String("string_field", "value"),
266-
slog.Int("int_field", 42),
267-
slog.Bool("bool_field", true),
268-
slog.Float64("float_field", 3.14),
257+
String("string_field", "value"),
258+
Int("int_field", 42),
259+
Bool("bool_field", true),
260+
Float64("float_field", 3.14),
269261
)
270262

271263
output := buf.String()

0 commit comments

Comments
 (0)