package diary import ( "context" "log/slog" "os" "slices" "sync/atomic" "git.golem15.com/golem15/summercms/modules/sunscreen" ) // componentHandler is the handler behind every diary logger. It holds only a // component name and the attributes and groups bound with With and // WithGroup; the channels it writes to are looked up when a record is // logged. A logger created in a package var block, before Setup, therefore // routes through the channels Setup installs later, and through a new set // after the next Setup or SetVerbosity. type componentHandler struct { name string ops []handlerOp cache *atomic.Pointer[boundHandler] } // handlerOp is one WithAttrs or WithGroup call, replayed in order onto the // live root handler. type handlerOp struct { group string attrs []slog.Attr } type boundHandler struct { gen uint64 h slog.Handler } func newComponentHandler(name string) *componentHandler { return &componentHandler{name: name, cache: new(atomic.Pointer[boundHandler])} } // fallbackHandler writes to stderr through sunscreen when there is no diary // state and the process default is itself a diary logger. var fallbackHandler = sunscreen.Wrap(slog.NewTextHandler(os.Stderr, nil)) // processDefault is the handler records go to before diary is set up: the // process default logger's handler, unless that is a diary handler, which // would recurse. func processDefault() slog.Handler { h := slog.Default().Handler() if _, ok := h.(*componentHandler); ok { return fallbackHandler } return h } func (h *componentHandler) Enabled(ctx context.Context, l slog.Level) bool { st := current.Load() if st == nil { return processDefault().Enabled(ctx, l) } return l >= st.floor(h.name) && st.root.Enabled(ctx, l) } func (h *componentHandler) Handle(ctx context.Context, r slog.Record) error { st := current.Load() if st == nil { return h.bind(processDefault()).Handle(ctx, r) } b := h.cache.Load() if b == nil || b.gen != st.gen { b = &boundHandler{gen: st.gen, h: h.bind(st.root)} h.cache.Store(b) } return b.h.Handle(ctx, r) } // bind applies the component attribute, then the recorded operations, to // base. The component goes first so it stays a top-level attribute even // when groups are open. func (h *componentHandler) bind(base slog.Handler) slog.Handler { out := base if h.name != "" { out = out.WithAttrs([]slog.Attr{slog.String(ComponentKey, h.name)}) } for _, op := range h.ops { if op.group != "" { out = out.WithGroup(op.group) } else { out = out.WithAttrs(op.attrs) } } return out } func (h *componentHandler) hasGroups() bool { for _, op := range h.ops { if op.group != "" { return true } } return false } // WithAttrs records attrs. A top-level string attribute keyed "component" // rebinds the component name instead, so slog.Default().With("component", // "x") behaves like For("x"). func (h *componentHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } c := &componentHandler{name: h.name, ops: slices.Clip(h.ops), cache: new(atomic.Pointer[boundHandler])} rest := attrs if !h.hasGroups() { rest = make([]slog.Attr, 0, len(attrs)) for _, a := range attrs { if a.Key == ComponentKey && a.Value.Kind() == slog.KindString { c.name = a.Value.String() continue } rest = append(rest, a) } } if len(rest) > 0 { c.ops = append(c.ops, handlerOp{attrs: slices.Clone(rest)}) } return c } func (h *componentHandler) WithGroup(name string) slog.Handler { if name == "" { return h } return &componentHandler{ name: h.name, ops: append(slices.Clip(h.ops), handlerOp{group: name}), cache: new(atomic.Pointer[boundHandler]), } }