- 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/
7.6 KiB
diary
Application logging modelled on Laravel's logging config: named channels (console, stderr, daily files, stacks), per-component levels and redaction on every channel.
import "git.golem15.com/golem15/summercms/modules/diary"
Name: A beach diary records each day; the log keeps one file per day.
Overview
diary owns the process logger of a SummerCMS application. config/log.yaml names a default channel, a global level and a map of channels, the way Laravel's config/logging.php does: a console channel that prints styled lines on a terminal, daily files with retention, and a stack that fans a record out to several channels. Code logs through named component loggers (diary.For), so every record carries the plugin or module it came from. Every channel is wrapped by sunscreen.Wrap, so credentials are redacted whichever channel a record reaches.
The application main that summer build generates calls diary.Bootstrap as its first statement, which installs a console channel before configuration is read, and diary.Setup right after compass loads config/.
Role in the framework
The single owner of the process logger. It builds on compass (configuration), sunscreen (redaction) and bonfire (the console palette and color detection). Framework modules and plugins only ask it for a component logger; they never configure handlers themselves.
Use cases:
- Keep a daily JSON log file per instance without extra setup, next to readable console output.
- Turn one noisy component down (or one under investigation up) without touching code.
- Send logs to stderr as logfmt or JSON when the process runs under systemd or Docker.
Features
- Component loggers:
diary.Forreturns a*slog.Loggerwhose records carry the attributecomponent=<name>. It is safe to create in a packagevarblock: before setup its records go to the process default logger's handler, afterdiary.Setupthey reach the configured channels, and a laterdiary.Setupordiary.SetVerbosityapplies to loggers created earlier. - Channels:
console(styled lines on a terminal, logfmt otherwise, or JSON withformat: json),daily(one file per day) andstack(several channels at once), each with its own level. - Console lines in bonfire's palette: time, level, the component as a fixed-width prefix, the message and
key=valueattributes. Control characters other than tab are removed from messages and values, so logged data cannot inject terminal escape sequences. - Daily files: the path
storage/logs/summer.loggives filesstorage/logs/summer-2026-10-21.log, created mode 0640 in directories created 0750, opened on the first record, rotated at local midnight, and deleted after the retention in days. A relative path resolves against the application root. - Redaction: every channel leaf is wrapped by
sunscreen.Wrap. - Runtime verbosity:
diary.SetVerbositylets info (1) or debug (2) through for every component and adds source locations on styled lines (3); quiet raises only the console channels to warn. - The process default logger (
slog.Default) and the standardlogpackage log through the configured channels without a component.
Usage
A plugin or module creates its logger once:
var logger = diary.For("blog")
func publish(ctx context.Context, id int) {
logger.InfoContext(ctx, "post published", "id", id)
}
With the default configuration this prints on a terminal:
12:04:31 INFO blog post published id=7
and writes warnings and errors as JSON to storage/logs/summer-YYYY-MM-DD.log.
The generated main installs it:
func run(args []string, out io.Writer) error {
diary.Bootstrap(os.Stderr, diary.Options{})
cfg, err := compass.Load("config")
if err != nil {
return err
}
if err := diary.Setup(cfg, diary.Options{}); err != nil {
return err
}
// ...
}
Tests that set diary up restore the previous process logger with t.Cleanup(diary.Reset).
API reference
| Identifier | Description |
|---|---|
diary.For |
Returns the *slog.Logger of a component; safe in package var blocks. |
diary.Bootstrap |
Installs a console channel on a writer before configuration is read and makes diary the process default logger. |
diary.Setup |
Parses log.* from a compass configuration, builds the channels and installs them; returns configuration errors. |
diary.SetVerbosity |
Changes verbosity and quiet at runtime. |
diary.Verbosity |
Returns the current verbosity. |
diary.Reset |
Drops the setup, closes its files and restores the process default logger captured at the first setup. |
diary.Close |
Closes the open daily files; later records open them again. |
diary.Options |
Setup options: Verbosity, Quiet, Color, Root, Stderr, Environ and Now. |
diary.ParseConfig |
Reads log.* over diary.DefaultConfig. |
diary.DefaultConfig |
The framework defaults. |
diary.Config |
Parsed configuration: Default, Level, Channels and Components. |
diary.ChannelConfig |
One channel: Driver, Format, Path, Level, HasLevel, Days and Channels. |
diary.NewConsoleHandler |
The styled console slog.Handler, for programs that build their own logger. |
diary.ConsoleOptions |
Console handler options: Level, Profile and AddSource. |
diary.ComponentKey |
The attribute key of the component, component. |
diary.DriverConsole, diary.DriverStderr, diary.DriverDaily, diary.DriverStack, diary.DriverNull |
Channel driver names. |
diary.FormatAuto, diary.FormatJSON, diary.FormatLogfmt |
Channel formats. |
diary.DefaultKey, diary.LevelKey, diary.ChannelsKey, diary.ComponentsKey |
The configuration keys log.default, log.level, log.channels and log.components. |
Configuration
Without a config/log.yaml an application uses these defaults:
# config/log.yaml
default: stack
level: info
channels:
stack:
driver: stack
channels: [console, daily]
console:
driver: console
format: auto
daily:
driver: daily
path: storage/logs/summer.log
days: 14
format: json
level: warn
components:
river: warn
| Key | Default | Meaning |
|---|---|---|
log.default |
stack |
The channel every record goes to. |
log.level |
info |
The minimum level of every component without its own level. |
log.channels.<name>.driver |
console, daily or stack. |
|
log.channels.<name>.level |
none | The channel's own minimum level. |
log.channels.<name>.format |
per driver | auto (console only), json or logfmt; console defaults to auto, daily to json. |
log.channels.<name>.path |
The daily file path, relative to the application root. | |
log.channels.<name>.days |
0 |
Daily retention in days; 0 keeps every file. |
log.channels.<name>.channels |
A stack's children. |
Dependencies
- SummerCMS modules:
bonfire(palette and color detection),compass(configuration),sunscreen(redaction). - Third-party:
charm.land/lipgloss/v2,github.com/charmbracelet/colorprofile(decided in.planning/notes/2026-10-21-console-charm-stack.md). - Standard library:
context,errors,fmt,io,io/fs,log,log/slog,maps,os,path/filepath,runtime,slices,strconv,strings,sync,sync/atomic,time,unicode.
Testing
go test ./modules/diary/...
TestDiaryTracerSmoke loads a config/log.yaml through compass, logs one record from a component logger and checks that it reaches the console and the daily file redacted. The tests write to temporary directories and need no external services.