- Wrap redacts sensitive keys at any depth and scrubs Bearer, sk- and x-api-key shapes - InstallDefault is the first statement of the generated run; hello main regenerated - surf test pins that recovered panics echo no credential - sunscreen README, root modules row and the logging docs page
3.1 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Logging | Log through log/slog with sunscreen, the redacting handler every generated main installs, and keep tokens, keys and request bodies out of log records. | services | 105 |
Logging
SummerCMS logs through the standard library's log/slog. Where a WinterCMS plugin calls Log::error and relies on a Monolog tap to scrub credentials, a SummerCMS plugin logs through a *slog.Logger, and sunscreen does the scrubbing.
The redacting default logger
Every application main that summer build generates calls sunscreen.InstallDefault(os.Stderr) as the first statement of run, before the configuration is loaded and before any plugin is activated. From then on slog.Default, the package-level slog functions and the standard log package all write through a text handler wrapped by sunscreen.Wrap.
The handler rewrites every record before it is written:
- The value of an attribute whose key is one of
sunscreen.RedactedKeysbecomes[REDACTED], at any group depth and whatever the letter case:api_key,apikey,authorization,bearer,password,secret,token,webhook_secret,admin_passwordand theopenai_api_key,anthropic_api_keyandperplexity_api_keyvariants. - The message, string values, error values and the formatted text of other values are passed through
sunscreen.Scrub, which replacesBearer <token>,sk-style API keys andx-api-key: <value>. slog.LogValuervalues are resolved first, and maps such ashttp.Headerare redacted key by key.
// Drop the time so the output is stable; a real application keeps it.
plain := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if len(groups) == 0 && a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})
logger := slog.New(sunscreen.Wrap(plain))
logger.Info("vendor call failed",
"api_key", "live-key-123",
slog.Group("request", slog.String("Authorization", "Bearer abc.def")),
"err", errors.New("401 for key sk-abcdefghijklmnopqrstuvwx"),
)
// Output:
// level=INFO msg="vendor call failed" api_key=[REDACTED] request.Authorization=[REDACTED] err="401 for key sk-[REDACTED]"
Logging from a plugin
A plugin resolves a *slog.Logger from the application with backpack.Lookup and falls back to slog.Default when none is published, as the framework modules do. Either way the record goes through the redacting handler.
Redaction is a safety net for mistakes, not a way to log secrets on purpose. Log identifiers and outcomes: the record id, the vendor, the status code, how long it took. Never log request arguments, tokens, credentials or request and response bodies, which can carry personal data the patterns do not recognise.
Panics
The router recovers a panicking handler and answers with an opaque 500: the JSON groups send {"error":true,"message":"Internal server error"} and the raw groups an empty body. The panic value never reaches the client. See Routing.