Files
summercms/modules/diary/README.md
Jakub Zych e903cc32be feat(diary): component loggers through a redacting console and daily stack
- 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/
2026-10-21 11:41:48 +02:00

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.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), daily (one file per day) and stack (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=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. A relative path resolves against the application root.
  • 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:

# 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.