- Spinner, gradient progress bar and the multi-row process view ported from SSU to bubbletea v2 (input off, no signal handler, command context); boxed tables through lipgloss/table - Prompts use huh fields and a single-select port of SSU's selector with its confirmation screen on an interactive terminal; line prompts off a terminal and under --no-ansi are unchanged (piped passwords still work) - -q hides decoration but keeps data and errors; -n returns defaults and Secret fails with a hint on a terminal stdin; --no-ansi and non-TTY runs print today's plain lines - Helpers: GlobalsOf, VerbosityOf, IsQuiet, Interactive, ConfirmProceed, Process, Current, Failed, PrintAbove, ItemProgress, ErrAborted - diary's styled console lines print above a running live view
160 lines
11 KiB
Markdown
160 lines
11 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 reads the console global options first (`bonfire.PeekGlobals`), calls `diary.Bootstrap` with them, which installs a console channel before configuration is read, and `diary.Setup` right after `compass` loads `config/`. Once the command line is parsed, `diary.ApplyGlobals` re-applies `-v`, `-q` and `--ansi`/`--no-ansi`.
|
|
|
|
## 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. While a bonfire live view (spinner, progress bar, process view) runs, styled lines print above it through `bonfire.PrintAbove`.
|
|
- 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.
|
|
- Console global options: `diary.FromGlobals` turns `-v`/`-vv`/`-vvv`, `-q` and `--ansi`/`--no-ansi` into `diary.Options`, and `diary.ApplyGlobals` applies them to the running setup (the generated main registers it with `bonfire.OnGlobals`).
|
|
- 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) int {
|
|
g := bonfire.PeekGlobals(args)
|
|
diary.Bootstrap(os.Stderr, diary.FromGlobals(g))
|
|
cfg, err := compass.Open(compass.Options{Dir: "config", Env: g.Env})
|
|
if err != nil {
|
|
return bonfire.Fail(os.Stderr, "acme", err, g)
|
|
}
|
|
if err := diary.Setup(cfg, diary.FromGlobals(g)); err != nil {
|
|
return bonfire.Fail(os.Stderr, "acme", err, g)
|
|
}
|
|
// ...
|
|
root, err := bonfire.NewRoot("acme", commands, out,
|
|
bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))
|
|
// ...
|
|
}
|
|
```
|
|
|
|
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.FromGlobals` | Returns the `diary.Options` (verbosity, quiet, color mode) of the console global options in a `bonfire.Globals`. |
|
|
| `diary.ApplyGlobals` | Applies parsed `bonfire.Globals` to the running setup: verbosity and quiet, and the console color mode when it changed. |
|
|
| `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`):
|
|
|
|
```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
|
|
```
|
|
|
|
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
|
|
|
|
```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. `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.
|