# 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=`. 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: ```go 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: ```text 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: ```go 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: ```yaml # 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..driver` | | `console`, `daily` or `stack`. | | `log.channels..level` | none | The channel's own minimum level. | | `log.channels..format` | per driver | `auto` (console only), `json` or `logfmt`; console defaults to `auto`, daily to `json`. | | `log.channels..path` | | The daily file path, relative to the application root. | | `log.channels..days` | `0` | Daily retention in days; `0` keeps every file. | | `log.channels..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 ```sh 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.