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:
157
modules/diary/channels.go
Normal file
157
modules/diary/channels.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package diary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
"git.golem15.com/golem15/summercms/modules/sunscreen"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
)
|
||||
|
||||
// noFloor is the level of a channel without its own level: every record the
|
||||
// component floors let through reaches it.
|
||||
const noFloor = slog.Level(-1 << 30)
|
||||
|
||||
// consoleLevel is a console channel's runtime level: base is the configured
|
||||
// level, which quiet raises to warn.
|
||||
type consoleLevel struct {
|
||||
lv *slog.LevelVar
|
||||
base slog.Level
|
||||
}
|
||||
|
||||
func (c consoleLevel) apply(quiet bool) {
|
||||
l := c.base
|
||||
if quiet && l < slog.LevelWarn {
|
||||
l = slog.LevelWarn
|
||||
}
|
||||
c.lv.Set(l)
|
||||
}
|
||||
|
||||
// channels is the built handler tree of one configuration.
|
||||
type channels struct {
|
||||
root slog.Handler
|
||||
consoles []consoleLevel
|
||||
closers []io.Closer
|
||||
source *atomic.Bool
|
||||
}
|
||||
|
||||
type channelBuilder struct {
|
||||
cfg Config
|
||||
opts Options
|
||||
out *channels
|
||||
visiting map[string]bool
|
||||
warnTo slog.Handler
|
||||
}
|
||||
|
||||
// buildChannels builds the channel named by cfg.Default. Every leaf is
|
||||
// wrapped by sunscreen.Wrap and gated by its own level; a stack fans out
|
||||
// through slog.NewMultiHandler.
|
||||
func buildChannels(cfg Config, opts Options) (*channels, error) {
|
||||
b := &channelBuilder{
|
||||
cfg: cfg,
|
||||
opts: opts,
|
||||
out: &channels{source: new(atomic.Bool)},
|
||||
visiting: map[string]bool{},
|
||||
}
|
||||
b.out.source.Store(opts.Verbosity >= 3)
|
||||
root, err := b.build(cfg.Default)
|
||||
if err != nil {
|
||||
for _, c := range b.out.closers {
|
||||
_ = c.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
b.out.root = root
|
||||
for _, c := range b.out.consoles {
|
||||
c.apply(opts.Quiet)
|
||||
}
|
||||
return b.out, nil
|
||||
}
|
||||
|
||||
func (b *channelBuilder) build(name string) (slog.Handler, error) {
|
||||
ch, ok := b.cfg.Channels[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("diary: channel %q is not defined in %s", name, ChannelsKey)
|
||||
}
|
||||
if b.visiting[name] {
|
||||
return nil, fmt.Errorf("diary: %s.%s.channels includes itself", ChannelsKey, name)
|
||||
}
|
||||
b.visiting[name] = true
|
||||
defer delete(b.visiting, name)
|
||||
|
||||
lv := new(slog.LevelVar)
|
||||
base := noFloor
|
||||
if ch.HasLevel {
|
||||
base = ch.Level
|
||||
}
|
||||
lv.Set(base)
|
||||
hopts := &slog.HandlerOptions{Level: lv}
|
||||
|
||||
switch ch.Driver {
|
||||
case DriverConsole:
|
||||
w := b.opts.Stderr
|
||||
var leaf slog.Handler
|
||||
switch ch.Format {
|
||||
case FormatJSON:
|
||||
leaf = slog.NewJSONHandler(w, hopts)
|
||||
case FormatLogfmt:
|
||||
leaf = slog.NewTextHandler(w, hopts)
|
||||
default:
|
||||
profile := bonfire.DetectProfile(w, b.opts.Color, b.opts.Environ)
|
||||
if profile > colorprofile.NoTTY {
|
||||
leaf = newConsoleHandler(w, ConsoleOptions{Level: lv, Profile: profile}, b.out.source)
|
||||
} else {
|
||||
leaf = slog.NewTextHandler(w, hopts)
|
||||
}
|
||||
}
|
||||
b.out.consoles = append(b.out.consoles, consoleLevel{lv: lv, base: base})
|
||||
wrapped := sunscreen.Wrap(leaf)
|
||||
if b.warnTo == nil {
|
||||
b.warnTo = wrapped
|
||||
}
|
||||
return wrapped, nil
|
||||
case DriverDaily:
|
||||
path := ch.Path
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(b.opts.Root, filepath.Clean(path))
|
||||
}
|
||||
dw := newDailyWriter(path, ch.Days, b.opts.Now, b.warn)
|
||||
b.out.closers = append(b.out.closers, dw)
|
||||
var leaf slog.Handler
|
||||
if ch.Format == FormatLogfmt {
|
||||
leaf = slog.NewTextHandler(dw, hopts)
|
||||
} else {
|
||||
leaf = slog.NewJSONHandler(dw, hopts)
|
||||
}
|
||||
return sunscreen.Wrap(leaf), nil
|
||||
case DriverStack:
|
||||
children := make([]slog.Handler, 0, len(ch.Channels))
|
||||
for _, child := range ch.Channels {
|
||||
h, err := b.build(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
children = append(children, h)
|
||||
}
|
||||
return slog.NewMultiHandler(children...), nil
|
||||
}
|
||||
return nil, fmt.Errorf("diary: %s.%s.driver %q is not supported", ChannelsKey, name, ch.Driver)
|
||||
}
|
||||
|
||||
// warn reports a daily file that cannot be opened: one WARN record from
|
||||
// component diary on the first console channel, or on a plain stderr
|
||||
// handler when the configuration has no console channel.
|
||||
func (b *channelBuilder) warn(path string, err error) {
|
||||
h := b.warnTo
|
||||
if h == nil {
|
||||
h = sunscreen.Wrap(slog.NewTextHandler(b.opts.Stderr, nil))
|
||||
}
|
||||
r := slog.NewRecord(b.opts.Now(), slog.LevelWarn, "daily log file disabled", 0)
|
||||
r.AddAttrs(slog.String("path", path), slog.String("error", err.Error()))
|
||||
_ = h.WithAttrs([]slog.Attr{slog.String(ComponentKey, "diary")}).Handle(context.Background(), r)
|
||||
}
|
||||
Reference in New Issue
Block a user