- 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/
139 lines
3.6 KiB
Go
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]),
|
|
}
|
|
}
|