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/
This commit is contained in:
138
modules/diary/component.go
Normal file
138
modules/diary/component.go
Normal file
@@ -0,0 +1,138 @@
|
||||
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]),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user