Files
summercms/modules/diary/README.md
Jakub Zych 44291adcca refactor: framework modules log through diary components
- 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
2026-10-21 11:58:22 +02:00

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.For returns a *slog.Logger whose records carry the attribute component=<name>. It is safe to create in a package var block: before setup its records go to the process default logger's handler, after diary.Setup they reach the configured channels, and a later diary.Setup or diary.SetVerbosity applies to loggers created earlier.
  • Channels: console (styled lines on a terminal, logfmt otherwise, or JSON with format: json), stderr (logfmt or JSON, never styled), daily (one file per day), stack (several channels at once, nesting allowed) and null (discards), each with its own level.
  • Component levels: log.components sets a minimum level per component (river: warn, blog: debug); components without one use log.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.Setup with an error naming the key.
  • Console lines in bonfire's palette: time, level, the component as a fixed-width prefix, the message and key=value attributes. 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.log gives files storage/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 component diary on the console and disables the channel; it never fails the start-up.
  • Redaction: every channel leaf is wrapped by sunscreen.Wrap.
  • Runtime verbosity: diary.SetVerbosity lets 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 standard log package 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.