Files
summercms/modules/diary/component.go
Jakub Zych e903cc32be feat(diary): component loggers through a redacting console and daily stack
- New diary module: For, Bootstrap, Setup, SetVerbosity, Reset, Close, ParseConfig
- Proxy component handler safe in package vars; styled console handler in bonfire's palette
- Daily writer: one file per day, 0640, lazy open, retention, disables itself on open failure
- Every channel leaf wrapped by sunscreen.Wrap; stack via slog.NewMultiHandler
- bonfire: ColorMode, Palette, DefaultPalette, DetectProfile (lipgloss v2, colorprofile)
- Generated main runs diary.Bootstrap first and diary.Setup after compass.Load
- Ignore /storage/ and /examples/hello/storage/
2026-10-21 11:41:48 +02:00

139 lines
3.6 KiB
Go

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]),
}
}