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 // (console, stderr, daily and null) is wrapped by sunscreen.Wrap and gated by // its own level; a stack fans out through slog.NewMultiHandler. A relative // daily path resolves against Options.Root. 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 DriverStderr: var leaf slog.Handler if ch.Format == FormatJSON { leaf = slog.NewJSONHandler(b.opts.Stderr, hopts) } else { leaf = slog.NewTextHandler(b.opts.Stderr, hopts) } return sunscreen.Wrap(leaf), nil case DriverNull: return sunscreen.Wrap(slog.DiscardHandler), 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) }