- serve prints a startup summary on a terminal (version, environment, URL, admin, database host and name, pending migrations, plugins, drivers) and logs it as a 'startup summary' record plus 'listening on <addr>' - on SIGINT/SIGTERM serve drains, prints and logs a shutdown report and returns nil (exit 0), or the drain errors (exit 1) - the request log is on by default under diary component http with request_id; the route pattern, never the path, is logged - lagoon.PendingMigrations, conga.Worker.Stats, diary.HideOnTTY, bonfire.IsTerminal and bonfire.Details back the summary and report
172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|
|
if _, styled := leaf.(*consoleHandler); !styled && isTerminal(w) {
|
|
leaf = ttyFilter{leaf}
|
|
}
|
|
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)
|
|
}
|