package logging import ( "context" "fmt" "log/slog" "sync" types "github.com/pocketbase/pocketbase/tools/types" ) type HookHandlerOptions struct { Level slog.Leveler WriteFunc func(ctx context.Context, record *Record) error } var _ slog.Handler = (*HookHandler)(nil) type HookHandler struct { mutex *sync.Mutex parent *HookHandler options *HookHandlerOptions group string attrs []slog.Attr } func NewHookHandler(opts *HookHandlerOptions) *HookHandler { if opts == nil { opts = &HookHandlerOptions{} } h := &HookHandler{ mutex: &sync.Mutex{}, options: opts, } if h.options.WriteFunc == nil { panic("`options.WriteFunc` is nil") } if h.options.Level == nil { h.options.Level = slog.LevelInfo } return h } func (h *HookHandler) Enabled(ctx context.Context, level slog.Level) bool { return level >= h.options.Level.Level() } func (h *HookHandler) WithGroup(name string) slog.Handler { if name == "" { return h } return &HookHandler{ parent: h, mutex: h.mutex, options: h.options, group: name, } } func (h *HookHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } return &HookHandler{ parent: h, mutex: h.mutex, options: h.options, attrs: attrs, } } func (h *HookHandler) Handle(ctx context.Context, r slog.Record) error { if h.group != "" { h.mutex.Lock() attrs := make([]any, 0, len(h.attrs)+r.NumAttrs()) for _, a := range h.attrs { attrs = append(attrs, a) } h.mutex.Unlock() r.Attrs(func(a slog.Attr) bool { attrs = append(attrs, a) return true }) r = slog.NewRecord(r.Time, r.Level, r.Message, r.PC) r.AddAttrs(slog.Group(h.group, attrs...)) } else if len(h.attrs) > 0 { r = r.Clone() h.mutex.Lock() r.AddAttrs(h.attrs...) h.mutex.Unlock() } if h.parent != nil { return h.parent.Handle(ctx, r) } data := make(map[string]any, r.NumAttrs()) r.Attrs(func(a slog.Attr) bool { if err := h.resolveAttr(data, a); err != nil { return false } return true }) log := &Record{ Time: r.Time, Message: r.Message, Data: types.JSONMap[any](data), } switch r.Level { case slog.LevelDebug: log.Level = LevelDebug case slog.LevelInfo: log.Level = LevelInfo case slog.LevelWarn: log.Level = LevelWarn case slog.LevelError: log.Level = LevelError default: log.Level = Level(fmt.Sprintf("LV(%d)", r.Level)) } if err := h.writeRecord(ctx, log); err != nil { return err } return nil } func (h *HookHandler) SetLevel(level slog.Level) { h.mutex.Lock() h.options.Level = level h.mutex.Unlock() } func (h *HookHandler) writeRecord(ctx context.Context, r *Record) error { if h.parent != nil { return h.parent.writeRecord(ctx, r) } return h.options.WriteFunc(ctx, r) } func (h *HookHandler) resolveAttr(data map[string]any, attr slog.Attr) error { attr.Value = attr.Value.Resolve() if attr.Equal(slog.Attr{}) { return nil } switch attr.Value.Kind() { case slog.KindGroup: { attrs := attr.Value.Group() if len(attrs) == 0 { return nil } groupData := make(map[string]any, len(attrs)) for _, subAttr := range attrs { h.resolveAttr(groupData, subAttr) } if len(groupData) > 0 { data[attr.Key] = groupData } } default: { switch v := attr.Value.Any().(type) { case error: data[attr.Key] = v.Error() default: data[attr.Key] = v } } } return nil }