diff --git a/internal/pkg/logging/handler.go b/internal/pkg/logging/handler.go new file mode 100644 index 00000000..5e932d1f --- /dev/null +++ b/internal/pkg/logging/handler.go @@ -0,0 +1,188 @@ +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(options HookHandlerOptions) *HookHandler { + h := &HookHandler{ + mutex: &sync.Mutex{}, + options: &options, + } + + 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 +} diff --git a/internal/pkg/logging/level.go b/internal/pkg/logging/level.go new file mode 100644 index 00000000..2af1c2f4 --- /dev/null +++ b/internal/pkg/logging/level.go @@ -0,0 +1,31 @@ +package logging + +import "log/slog" + +type Level string + +const ( + LevelDebug Level = "DEBUG" + LevelInfo Level = "INFO" + LevelWarn Level = "WARN" + LevelError Level = "ERROR" +) + +func (l Level) String() string { + return string(l) +} + +func (l Level) Level() slog.Level { + switch l { + case LevelDebug: + return slog.LevelDebug + case LevelInfo: + return slog.LevelInfo + case LevelWarn: + return slog.LevelWarn + case LevelError: + return slog.LevelError + default: + return slog.Level(-1) + } +} diff --git a/internal/pkg/logging/record.go b/internal/pkg/logging/record.go new file mode 100644 index 00000000..9980489b --- /dev/null +++ b/internal/pkg/logging/record.go @@ -0,0 +1,14 @@ +package logging + +import ( + "time" + + types "github.com/pocketbase/pocketbase/tools/types" +) + +type Record struct { + Time time.Time + Level Level + Message string + Data types.JSONMap[any] +}