Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: zerolog slog adapter

+174 -8
+4 -8
cmd/server/main.go
··· 16 16 "time" 17 17 18 18 "arabica/internal/atproto" 19 + "arabica/internal/logging" 19 20 "arabica/internal/database/boltstore" 20 21 "arabica/internal/database/sqlitestore" 21 22 "arabica/internal/email" ··· 65 66 }) 66 67 } 67 68 68 - // Configure slog default handler to match zerolog format. 69 - // Third-party libraries (e.g. indigo OAuth) use slog, so without this 70 - // their output bypasses zerolog and breaks structured log parsing. 71 - if os.Getenv("LOG_FORMAT") == "json" { 72 - slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) 73 - } else { 74 - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, nil))) 75 - } 69 + // Bridge slog to zerolog so third-party libraries (e.g. indigo OAuth) 70 + // produce output consistent with the rest of the application. 71 + slog.SetDefault(slog.New(logging.NewZerologHandler(log.Logger))) 76 72 77 73 log.Info().Msg("Starting Arabica Coffee Tracker") 78 74
+113
internal/logging/slog.go
··· 1 + // Package logging provides log integration utilities. 2 + package logging 3 + 4 + import ( 5 + "context" 6 + "log/slog" 7 + 8 + "github.com/rs/zerolog" 9 + ) 10 + 11 + // ZerologHandler is a slog.Handler that routes slog output through zerolog, 12 + // ensuring libraries that use slog (e.g. indigo OAuth) produce output 13 + // consistent with the rest of the application. 14 + type ZerologHandler struct { 15 + logger zerolog.Logger 16 + attrs []slog.Attr 17 + group string 18 + } 19 + 20 + // NewZerologHandler returns a slog.Handler backed by the given zerolog.Logger. 21 + func NewZerologHandler(logger zerolog.Logger) *ZerologHandler { 22 + return &ZerologHandler{logger: logger} 23 + } 24 + 25 + func (h *ZerologHandler) Enabled(_ context.Context, level slog.Level) bool { 26 + return h.logger.GetLevel() <= slogToZerolog(level) 27 + } 28 + 29 + func (h *ZerologHandler) Handle(_ context.Context, r slog.Record) error { 30 + ev := h.logger.WithLevel(slogToZerolog(r.Level)) 31 + if ev == nil { 32 + return nil 33 + } 34 + 35 + // Add any pre-set attrs from WithAttrs. 36 + for _, a := range h.attrs { 37 + ev = addAttr(ev, h.group, a) 38 + } 39 + 40 + // Add attrs from the record itself. 41 + r.Attrs(func(a slog.Attr) bool { 42 + ev = addAttr(ev, h.group, a) 43 + return true 44 + }) 45 + 46 + ev.Msg(r.Message) 47 + return nil 48 + } 49 + 50 + func (h *ZerologHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 51 + newAttrs := make([]slog.Attr, len(h.attrs), len(h.attrs)+len(attrs)) 52 + copy(newAttrs, h.attrs) 53 + newAttrs = append(newAttrs, attrs...) 54 + return &ZerologHandler{logger: h.logger, attrs: newAttrs, group: h.group} 55 + } 56 + 57 + func (h *ZerologHandler) WithGroup(name string) slog.Handler { 58 + if name == "" { 59 + return h 60 + } 61 + prefix := name 62 + if h.group != "" { 63 + prefix = h.group + "." + name 64 + } 65 + newAttrs := make([]slog.Attr, len(h.attrs)) 66 + copy(newAttrs, h.attrs) 67 + return &ZerologHandler{logger: h.logger, attrs: newAttrs, group: prefix} 68 + } 69 + 70 + func slogToZerolog(level slog.Level) zerolog.Level { 71 + switch { 72 + case level >= slog.LevelError: 73 + return zerolog.ErrorLevel 74 + case level >= slog.LevelWarn: 75 + return zerolog.WarnLevel 76 + case level >= slog.LevelInfo: 77 + return zerolog.InfoLevel 78 + default: 79 + return zerolog.DebugLevel 80 + } 81 + } 82 + 83 + func addAttr(ev *zerolog.Event, group string, a slog.Attr) *zerolog.Event { 84 + key := a.Key 85 + if group != "" { 86 + key = group + "." + key 87 + } 88 + 89 + val := a.Value.Resolve() 90 + switch val.Kind() { 91 + case slog.KindString: 92 + return ev.Str(key, val.String()) 93 + case slog.KindInt64: 94 + return ev.Int64(key, val.Int64()) 95 + case slog.KindUint64: 96 + return ev.Uint64(key, val.Uint64()) 97 + case slog.KindFloat64: 98 + return ev.Float64(key, val.Float64()) 99 + case slog.KindBool: 100 + return ev.Bool(key, val.Bool()) 101 + case slog.KindDuration: 102 + return ev.Dur(key, val.Duration()) 103 + case slog.KindTime: 104 + return ev.Time(key, val.Time()) 105 + case slog.KindGroup: 106 + for _, ga := range val.Group() { 107 + ev = addAttr(ev, key, ga) 108 + } 109 + return ev 110 + default: 111 + return ev.Interface(key, val.Any()) 112 + } 113 + }
+57
internal/logging/slog_test.go
··· 1 + package logging 2 + 3 + import ( 4 + "bytes" 5 + "log/slog" 6 + "testing" 7 + 8 + "github.com/rs/zerolog" 9 + "github.com/stretchr/testify/assert" 10 + ) 11 + 12 + func TestZerologHandler_Handle(t *testing.T) { 13 + var buf bytes.Buffer 14 + logger := zerolog.New(&buf) 15 + slogLogger := slog.New(NewZerologHandler(logger)) 16 + 17 + slogLogger.Warn("auth server request failed", "request", "token-refresh", "statusCode", 400) 18 + 19 + out := buf.String() 20 + assert.Contains(t, out, `"level":"warn"`) 21 + assert.Contains(t, out, `"message":"auth server request failed"`) 22 + assert.Contains(t, out, `"request":"token-refresh"`) 23 + assert.Contains(t, out, `"statusCode":400`) 24 + } 25 + 26 + func TestZerologHandler_Enabled(t *testing.T) { 27 + logger := zerolog.New(nil).Level(zerolog.WarnLevel) 28 + h := NewZerologHandler(logger) 29 + 30 + assert.True(t, h.Enabled(nil, slog.LevelWarn)) 31 + assert.True(t, h.Enabled(nil, slog.LevelError)) 32 + assert.False(t, h.Enabled(nil, slog.LevelInfo)) 33 + assert.False(t, h.Enabled(nil, slog.LevelDebug)) 34 + } 35 + 36 + func TestZerologHandler_WithAttrs(t *testing.T) { 37 + var buf bytes.Buffer 38 + logger := zerolog.New(&buf) 39 + slogLogger := slog.New(NewZerologHandler(logger)).With("component", "oauth") 40 + 41 + slogLogger.Info("test message") 42 + 43 + out := buf.String() 44 + assert.Contains(t, out, `"component":"oauth"`) 45 + assert.Contains(t, out, `"message":"test message"`) 46 + } 47 + 48 + func TestZerologHandler_WithGroup(t *testing.T) { 49 + var buf bytes.Buffer 50 + logger := zerolog.New(&buf) 51 + slogLogger := slog.New(NewZerologHandler(logger)).WithGroup("auth") 52 + 53 + slogLogger.Info("test", "method", "dpop") 54 + 55 + out := buf.String() 56 + assert.Contains(t, out, `"auth.method":"dpop"`) 57 + }