package zerolog import ( "context" "log/slog" "time" ) // SlogHandler implements the slog.Handler interface using a zerolog.Logger // as the underlying log backend. This allows code that uses the standard // library's slog package to route log output through zerolog. type SlogHandler struct { logger Logger prefix string // group prefix for nested groups attrs []slog.Attr } // NewSlogHandler creates a new slog.Handler that writes log records to the // given zerolog.Logger. The handler maps slog levels to zerolog levels and // converts slog attributes to zerolog fields. func NewSlogHandler(logger Logger) *SlogHandler { return &SlogHandler{logger: logger} } // Enabled reports whether the handler handles records at the given level. // It mirrors Logger.should's level and writer checks (without sampling). func (h *SlogHandler) Enabled(_ context.Context, level slog.Level) bool { if h.logger.w == nil { return false } zl := slogToZerologLevel(level) if zl < GlobalLevel() { return false } return zl >= h.logger.level } // Handle handles the Record. It converts the slog.Record into a zerolog event // and writes it using the underlying zerolog.Logger. func (h *SlogHandler) Handle(ctx context.Context, record slog.Record) error { zlevel := slogToZerologLevel(record.Level) event := h.logger.WithLevel(zlevel) if event == nil { return nil } // Propagate slog context to the zerolog event so that hooks // relying on Event.GetCtx() (e.g. tracing) can access it. if ctx != nil { event = event.Ctx(ctx) } // Add pre-attached attrs from WithAttrs for _, a := range h.attrs { event = appendSlogAttr(event, a, h.prefix) } // Add attrs from the record itself record.Attrs(func(a slog.Attr) bool { event = appendSlogAttr(event, a, h.prefix) return true }) // Add timestamp from the slog record, but only if the logger doesn't // already have a timestampHook (added via .With().Timestamp()) to // avoid duplicate timestamp keys in the output. if !record.Time.IsZero() && !h.hasTimestampHook() { event.Time(TimestampFieldName, record.Time) } event.Msg(record.Message) return nil } // hasTimestampHook reports whether the logger has a timestampHook installed, // which would cause duplicate timestamp fields if we also emit record.Time. func (h *SlogHandler) hasTimestampHook() bool { for _, hook := range h.logger.hooks { if _, ok := hook.(timestampHook); ok { return true } } return false } // WithAttrs returns a new Handler with the given attributes pre-attached. // These attributes will be included in every subsequent log record. func (h *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } h2 := h.clone() h2.attrs = append(h2.attrs, attrs...) return h2 } // WithGroup returns a new Handler with the given group name. All subsequent // attributes will be nested under this group name in the output. func (h *SlogHandler) WithGroup(name string) slog.Handler { if name == "" { return h } h2 := h.clone() if h2.prefix != "" { h2.prefix = h2.prefix + "." + name } else { h2.prefix = name } return h2 } func (h *SlogHandler) clone() *SlogHandler { h2 := &SlogHandler{ logger: h.logger, prefix: h.prefix, } if len(h.attrs) > 0 { h2.attrs = make([]slog.Attr, len(h.attrs)) copy(h2.attrs, h.attrs) } return h2 } // slogToZerologLevel maps slog levels to zerolog levels. // // slog levels: Debug=-4, Info=0, Warn=4, Error=8 // zerolog levels: Trace=-1, Debug=0, Info=1, Warn=2, Error=3, Fatal=4, Panic=5 func slogToZerologLevel(level slog.Level) Level { switch { case level < slog.LevelDebug: return TraceLevel case level < slog.LevelInfo: return DebugLevel case level < slog.LevelWarn: return InfoLevel case level < slog.LevelError: return WarnLevel default: return ErrorLevel } } // zerologToSlogLevel maps zerolog levels to slog levels. func zerologToSlogLevel(level Level) slog.Level { switch level { case TraceLevel: return slog.LevelDebug - 4 case DebugLevel: return slog.LevelDebug case InfoLevel: return slog.LevelInfo case WarnLevel: return slog.LevelWarn case ErrorLevel: return slog.LevelError case FatalLevel: return slog.LevelError + 4 case PanicLevel: return slog.LevelError + 8 default: return slog.LevelInfo } } // joinPrefix concatenates a prefix and key with a dot separator. // It avoids allocations when either prefix or key is empty. func joinPrefix(prefix, key string) string { if prefix == "" { return key } if key == "" { return prefix } return prefix + "." + key } // appendSlogAttr appends a single slog.Attr to the zerolog event, handling // type-specific encoding to avoid reflection where possible. func appendSlogAttr(event *Event, attr slog.Attr, prefix string) *Event { if event == nil { return event } // Resolve the attribute to handle LogValuer types. // This handles slog.KindLogValuer implicitly by unwrapping // any values that implement slog.LogValuer to their resolved form. attr.Value = attr.Value.Resolve() // For group kinds, handle grouping before key concatenation if attr.Value.Kind() == slog.KindGroup { attrs := attr.Value.Group() if len(attrs) == 0 { return event } groupPrefix := joinPrefix(prefix, attr.Key) for _, ga := range attrs { event = appendSlogAttr(event, ga, groupPrefix) } return event } // Skip empty keys for non-group attributes if attr.Key == "" { return event } key := joinPrefix(prefix, attr.Key) val := attr.Value switch val.Kind() { case slog.KindString: event = event.Str(key, val.String()) case slog.KindInt64: event = event.Int64(key, val.Int64()) case slog.KindUint64: event = event.Uint64(key, val.Uint64()) case slog.KindFloat64: event = event.Float64(key, val.Float64()) case slog.KindBool: event = event.Bool(key, val.Bool()) case slog.KindDuration: event = event.Dur(key, val.Duration()) case slog.KindTime: event = event.Time(key, val.Time()) case slog.KindAny: v := val.Any() switch cv := v.(type) { case error: event = event.AnErr(key, cv) case time.Duration: event = event.Dur(key, cv) case time.Time: event = event.Time(key, cv) case []byte: event = event.Bytes(key, cv) default: event = event.Interface(key, v) } default: event = event.Interface(key, val.Any()) } return event } // Verify at compile time that SlogHandler satisfies the slog.Handler interface. var _ slog.Handler = (*SlogHandler)(nil)