- Each module logs through diary.For(<package>): cabana, lagoon, attach, cooler, postcard, beachcomber, lighthouse, centrifugo, flare, phrasebook, wristband, bouncer - App-published *slog.Logger overrides stay first; messages and attributes unchanged - sunscreen: InstallDefault documented as the compatibility entry point; diary wraps every channel - READMEs list diary as a dependency; logging docs: plugins use diary.For
9.9 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. River logs as component river (warn by default), GORM as db (through lagoon), and each framework module under its package name: cabana, lagoon, attach, conga, cooler, postcard, beachcomber, lighthouse, centrifugo, flare, phrasebook, wristband and bouncer. Modules that let the application publish its own *slog.Logger (conga, cooler, postcard, beachcomber, lighthouse, flare) still prefer it.
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),stderr(logfmt or JSON, never styled),daily(one file per day),stack(several channels at once, nesting allowed) andnull(discards), each with its own level. - Component levels:
log.componentssets a minimum level per component (river: warn,blog: debug); components without one uselog.level. - Environment overrides:
SUMMER_LOG__variables overlay the defaults key by key, and string values parse where YAML gives numbers or lists (SUMMER_LOG__CHANNELS__DAILY__DAYS=30,SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,daily). - Validation: an unknown driver, format or level, a stack naming a missing channel or containing itself, and a relative daily path that leaves the application root fail
diary.Setupwith an error naming the key. - 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 (only files of that name pattern). A relative path resolves against the application root. A directory that cannot be written logs one warning from componentdiaryon the console and disables the channel; it never fails the start-up. - 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 (summer new writes the same file, plus http: info):
# 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
The defaults also define stderr (driver: stderr, format: logfmt) and null (driver: null).
| 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, and of records without a component. debug, info, warn (or warning) and error, in any letter case. |
log.channels.<name>.driver |
required for a new channel | console, stderr, daily, stack or null. |
log.channels.<name>.level |
none | The channel's own minimum level. |
log.channels.<name>.format |
auto for console, logfmt for stderr, json for daily |
auto (console only: styled on a terminal, logfmt otherwise), json or logfmt. |
log.channels.<name>.path |
storage/logs/summer.log for daily |
The daily file path; a relative path resolves against the application root and may not leave it. |
log.channels.<name>.days |
14 for daily |
Retention in days, a whole number of at least 0; 0 keeps every file. |
log.channels.<name>.channels |
[console, daily] for stack |
A stack's children, as a YAML list or a comma-separated string. |
log.components.<name> |
river: warn |
The minimum level of a component. Component names cannot contain dots. |
Environment variables use the SUMMER_LOG__ prefix with a double underscore between path segments: SUMMER_LOG__COMPONENTS__RIVER=error.
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. TestConfigEnvSmoke parses SUMMER_LOG__ overrides given as strings and checks that every invalid value fails with an error naming its key. TestDailyDegradesSmoke points the daily channel at a read-only directory and checks that it warns once and the console keeps logging. The tests write to temporary directories and need no external services.