Logging ¶
Cicadel uses Go log/slog with configurable named loggers, handlers, filters,
levels and output formats. The generated project owns its logging configuration
in project/logging.go; JSON to stderr is the default. Set CICADEL_LOG_FORMAT
to text for local development and CICADEL_LOG_LEVEL to DEBUG, INFO, WARN or ERROR.
Invalid settings fail startup without printing their values.
For local development:
export CICADEL_LOG_FORMAT=text
export CICADEL_LOG_LEVEL=DEBUG
go run manage.go runserverFor a standalone logger, import os and github.com/fauzaanu/cicadel/observability:
logger, err := observability.NewLogger(observability.Logging{
Output: os.Stderr,
Format: "json",
})
if err != nil {
return err
}
logger.InfoContext(ctx, "note created", "note_id", noteID)Ask your agent ¶
Read https://cicadel.fauzaanu.com/en/dev/topics/logging/index.md.
Configure application logging for [events]. Use structured fields and the
existing project logger. Include request correlation, exclude secrets and
personal payloads, and verify the expected output in a focused test.Named configuration ¶
Use a System when applications need independent logger/handler levels, filters,
multiple destinations or propagation rules:
level := new(slog.LevelVar)
level.Set(slog.LevelInfo)
logs, err := observability.NewLogging(observability.Config{
Handlers: map[string]observability.HandlerConfig{
"console": {Output: os.Stderr, Format: "json", Level: level},
"errors": {Output: errorFile, Format: "text", Level: slog.LevelError},
},
Root: observability.LoggerConfig{
Level: level, Handlers: []string{"console"},
},
Loggers: map[string]observability.LoggerConfig{
"application": {Handlers: []string{"errors"}},
"application.audit": {
Handlers: []string{"errors"}, StopPropagation: true,
},
},
})
// Handle err before using logs.
logger := logs.Logger("application.accounts")
logger.InfoContext(ctx, "account enabled", "account_id", userID)Names inherit from the closest configured dot-separated ancestor, then Root.
The nearest configured level applies; each destination also checks its own level.
Handlers on ancestors receive propagated events unless StopPropagation is set.
A shared handler is invoked once per event even if referenced at several levels.
Logger filters come from the nearest configuration; handler filters apply to
each destination. Filters receive redacted records, return true to keep an event
and must be concurrency-safe. Custom formatters/exporters implement slog.Handler
and are supplied through HandlerConfig.Handler.
Configuration validates names, formats, nil filters and handler references before
use, and copies configuration containers. Referenced level variables, writers,
handlers and filter closures remain owned by the caller. Drain requests before
closing outputs. Config.OnError observes handler write/delivery errors; its
callback must not log through the same failing logging system. Slog itself does
not return output errors from its convenience logging methods.
Sensitive data ¶
Credential attributes are redacted before filtering and output, including nested groups, With attributes and resolved LogValuer values. Keys include password, password_hash, token, csrf_token, authorization, cookies, secret, API keys, SMTP password and database URL; matching ignores case, hyphens, dots and underscores. A credentials group is redacted as a whole. NewLogger’s ReplaceAttr hook runs between redaction passes so renamed sensitive keys are also protected.
Redaction is a structured-field contract. It cannot make arbitrary free-form messages, error strings, custom string payloads or arbitrary objects safe. Never interpolate credentials into log messages or dump request/settings/model objects. Error logs should identify the operation and safe metadata rather than include raw provider responses or database query arguments.
Request, exception and query events ¶
observability.Middleware(logger, observe) generates a fresh request ID, adds
X-Request-ID and logs method/status/bytes/duration. It ignores incoming IDs,
paths, query strings, headers and bodies. Responses below 400 use INFO; 4xx use
WARN; 5xx use ERROR. observability.Logger(ctx, logger) attaches the same ID to
application events. Standard response interfaces are preserved through httpsnoop.
Panics are logged with Go call stacks, never the panic value or local variables.
Uncommitted responses receive a generic 500; committed responses are not rewritten.
http.ErrAbortHandler retains standard HTTP abort semantics. Observer callbacks
receive status/timing/bytes and a Panicked flag. They run synchronously and must
be fast, concurrency-safe and non-panicking. Distributed trace propagation is
an explicit exporter/middleware integration.
db.Logger = observability.NewQueryLogger(logger, 200*time.Millisecond) enables
GORM timing events without SQL text or query arguments. Normal queries use DEBUG,
slow queries WARN and failures ERROR. ErrRecordNotFound is treated as a normal
outcome. GORM’s LogMode is honored. The accounts starter enables this adapter;
other applications can opt in explicitly.
Files and error email ¶
observability.OpenRotatingFile(path, maxBytes, backups) returns a synchronized
writer with numbered backups, mode-0600 creation and explicit Close. Rotation
occurs between complete writes; one large record can exceed the configured size.
Use one writer per path in one process. For multiple replicas, prefer stdout and
a platform log collector. No background goroutine is started for file rotation.
observability.NewEmailHandler(sender, from, recipients, interval) creates a
custom handler for ERROR and above. It sends a redacted JSON record through the
configured mail transport with a five-second timeout and a shared suppression
interval to prevent alert storms. It does not attach request dumps or locals.
Delivery is synchronous; use a custom queue handler when network latency must be
removed from requests. Configure OnError to observe failed alert delivery without
recursive logging. The framework never chooses alert recipients or sends an
alert at construction time.
The tests in observability/logging_test.go cover routing, propagation, filters,
live levels, grouped attributes, credential privacy, correlated panic events,
query privacy, file rotation, concurrent writes and captured email alerts. These
contracts provide a practical logging foundation; they do not claim complete
Python logging API compatibility or automatic third-party exporter integration.