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

145 lines
7.6 KiB
Markdown

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