Skip to content

Commit f8df448

Browse files
committed
backend: format slog durations as human friendly string
1 parent 906b549 commit f8df448

2 files changed

Lines changed: 79 additions & 14 deletions

File tree

backend/pkg/logger/logger.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,25 +90,26 @@ func createHandler(cfg *config) slog.Handler {
9090
Level: cfg.level,
9191
}
9292

93-
// If custom timestamp format is specified, use ReplaceAttr to customize the time field
94-
if cfg.timestampFormat != "" {
95-
handlerOpts.ReplaceAttr = func(_ []string, a slog.Attr) slog.Attr {
96-
if a.Key == slog.TimeKey {
97-
return formatTimestamp(a, cfg.timestampFormat)
98-
}
99-
return a
93+
// Use ReplaceAttr to customize attribute formatting:
94+
// - Durations are formatted as human-readable strings (e.g., "6h0m0s")
95+
// - Timestamps use custom format if specified
96+
handlerOpts.ReplaceAttr = func(_ []string, a slog.Attr) slog.Attr {
97+
// Format durations as human-readable strings instead of nanoseconds
98+
if a.Value.Kind() == slog.KindDuration {
99+
return slog.String(a.Key, a.Value.Duration().String())
100100
}
101+
// Format timestamps if custom format is specified
102+
if cfg.timestampFormat != "" && a.Key == slog.TimeKey {
103+
return formatTimestamp(a, cfg.timestampFormat)
104+
}
105+
return a
101106
}
102107

103-
// Create handler based on format
104-
switch cfg.format {
105-
case FormatText:
108+
// Create handler based on format (default is JSON)
109+
if cfg.format == FormatText {
106110
return slog.NewTextHandler(cfg.output, handlerOpts)
107-
case FormatJSON:
108-
return slog.NewJSONHandler(cfg.output, handlerOpts)
109-
default:
110-
return slog.NewJSONHandler(cfg.output, handlerOpts)
111111
}
112+
return slog.NewJSONHandler(cfg.output, handlerOpts)
112113
}
113114

114115
// formatTimestamp formats a time attribute according to the specified format.

backend/pkg/logger/logger_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"log/slog"
1717
"strings"
1818
"testing"
19+
"time"
1920

2021
"github.com/stretchr/testify/assert"
2122
"github.com/stretchr/testify/require"
@@ -345,3 +346,66 @@ func TestFormatOverride(t *testing.T) {
345346
})
346347
}
347348
}
349+
350+
func TestDurationFormatting(t *testing.T) {
351+
tests := []struct {
352+
name string
353+
duration time.Duration
354+
expectedString string
355+
}{
356+
{
357+
name: "hours",
358+
duration: 6 * time.Hour,
359+
expectedString: "6h0m0s",
360+
},
361+
{
362+
name: "minutes and seconds",
363+
duration: 5*time.Minute + 30*time.Second,
364+
expectedString: "5m30s",
365+
},
366+
{
367+
name: "milliseconds",
368+
duration: 150 * time.Millisecond,
369+
expectedString: "150ms",
370+
},
371+
{
372+
name: "complex duration",
373+
duration: 2*time.Hour + 30*time.Minute + 45*time.Second,
374+
expectedString: "2h30m45s",
375+
},
376+
}
377+
378+
for _, tt := range tests {
379+
t.Run(tt.name, func(t *testing.T) {
380+
var buf bytes.Buffer
381+
logger := NewSlogLogger(
382+
WithOutput(&buf),
383+
WithFormat(FormatJSON),
384+
)
385+
386+
logger.Info("test message", slog.Duration("duration", tt.duration))
387+
388+
// Parse the JSON output
389+
var logEntry map[string]any
390+
err := json.Unmarshal(buf.Bytes(), &logEntry)
391+
require.NoError(t, err)
392+
393+
// Duration should be formatted as human-readable string, not nanoseconds
394+
assert.Equal(t, tt.expectedString, logEntry["duration"], "duration should be human-readable")
395+
})
396+
}
397+
}
398+
399+
func TestDurationFormattingTextFormat(t *testing.T) {
400+
var buf bytes.Buffer
401+
logger := NewSlogLogger(
402+
WithOutput(&buf),
403+
WithFormat(FormatText),
404+
)
405+
406+
logger.Info("test message", slog.Duration("duration", 6*time.Hour))
407+
408+
output := buf.String()
409+
// Text format should also show human-readable duration
410+
assert.Contains(t, output, "duration=6h0m0s")
411+
}

0 commit comments

Comments
 (0)