package logger import ( "context" "log/slog" "strings" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) type slogZapHandler struct { logger *zap.Logger attrs []slog.Attr groups []string } func newSlogZapHandler(logger *zap.Logger) slog.Handler { if logger == nil { logger = zap.NewNop() } return &slogZapHandler{ logger: logger, attrs: make([]slog.Attr, 0, 8), groups: make([]string, 0, 4), } } func (h *slogZapHandler) Enabled(_ context.Context, level slog.Level) bool { switch { case level >= slog.LevelError: return h.logger.Core().Enabled(LevelError) case level >= slog.LevelWarn: return h.logger.Core().Enabled(LevelWarn) case level <= slog.LevelDebug: return h.logger.Core().Enabled(LevelDebug) default: return h.logger.Core().Enabled(LevelInfo) } } func (h *slogZapHandler) Handle(_ context.Context, record slog.Record) error { fields := make([]zap.Field, 0, len(h.attrs)+record.NumAttrs()+3) fields = append(fields, slogAttrsToZapFields(h.groups, h.attrs)...) record.Attrs(func(attr slog.Attr) bool { fields = append(fields, slogAttrToZapField(h.groups, attr)) return true }) entry := h.logger.With(fields...) switch { case record.Level >= slog.LevelError: entry.Error(record.Message) case record.Level >= slog.LevelWarn: entry.Warn(record.Message) case record.Level <= slog.LevelDebug: entry.Debug(record.Message) default: entry.Info(record.Message) } return nil } func (h *slogZapHandler) WithAttrs(attrs []slog.Attr) slog.Handler { next := *h next.attrs = append(append([]slog.Attr{}, h.attrs...), attrs...) return &next } func (h *slogZapHandler) WithGroup(name string) slog.Handler { name = strings.TrimSpace(name) if name == "" { return h } next := *h next.groups = append(append([]string{}, h.groups...), name) return &next } func slogAttrsToZapFields(groups []string, attrs []slog.Attr) []zap.Field { fields := make([]zap.Field, 0, len(attrs)) for _, attr := range attrs { fields = append(fields, slogAttrToZapField(groups, attr)) } return fields } func slogAttrToZapField(groups []string, attr slog.Attr) zap.Field { if len(groups) > 0 { attr.Key = strings.Join(append(append([]string{}, groups...), attr.Key), ".") } value := attr.Value.Resolve() switch value.Kind() { case slog.KindBool: return zap.Bool(attr.Key, value.Bool()) case slog.KindInt64: return zap.Int64(attr.Key, value.Int64()) case slog.KindUint64: return zap.Uint64(attr.Key, value.Uint64()) case slog.KindFloat64: return zap.Float64(attr.Key, value.Float64()) case slog.KindDuration: return zap.Duration(attr.Key, value.Duration()) case slog.KindTime: return zap.Time(attr.Key, value.Time()) case slog.KindString: return zap.String(attr.Key, value.String()) case slog.KindGroup: groupFields := make([]zap.Field, 0, len(value.Group())) for _, nested := range value.Group() { groupFields = append(groupFields, slogAttrToZapField(nil, nested)) } return zap.Object(attr.Key, zapObjectFields(groupFields)) case slog.KindAny: if t, ok := value.Any().(time.Time); ok { return zap.Time(attr.Key, t) } return zap.Any(attr.Key, value.Any()) default: return zap.String(attr.Key, value.String()) } } type zapObjectFields []zap.Field func (z zapObjectFields) MarshalLogObject(enc zapcore.ObjectEncoder) error { for _, field := range z { field.AddTo(enc) } return nil }