Logging
Cicadel logs with Go’s log/slog. You get named loggers, handlers, filters, levels
and output formats, all configured in Go.
The generated project owns its logging setup in project/logging.go. The default
is JSON to stderr.
#Configure with environment variables
| Variable | Values |
|---|---|
CICADEL_LOG_FORMAT | json (default) or text for local development |
CICADEL_LOG_LEVEL | DEBUG, INFO, WARN or ERROR |
Invalid values fail startup. The error does not print them.
For local development:
export CICADEL_LOG_FORMAT=text
export CICADEL_LOG_LEVEL=DEBUG
go run manage.go runserver#Create 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)NewLogger returns a standard slog.Logger. You can also:
- pass a
slog.LevelVarto change the level at runtime; - set
AddSourceto record caller locations; - set a
ReplaceAttrhook to rewrite attributes.
#Ask your agent
Read https://cicadel.fauzaanu.com/en/0.2.8/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 loggers
Use a System when apps need their own levels, filters, several 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)#How names resolve
- A name inherits from its closest configured dot-separated ancestor, then from
Root. - The nearest configured level applies. Each destination also checks its own level.
- Handlers on ancestors receive propagated events unless
StopPropagationis set. - A shared handler runs once per event, even when several levels reference it.
#Filters and custom handlers
- Logger filters come from the nearest configuration. Handler filters apply per destination.
- Filters receive redacted records. They return true to keep an event and must be safe for concurrent use.
- Custom formatters and exporters implement
slog.Handler. Supply them throughHandlerConfig.Handler.
#Validation and ownership
The configuration checks names, formats, nil filters and handler references before use. It copies configuration containers. You still own the level variables, writers, handlers and filter closures you reference.
Drain requests before you close outputs.
Config.OnError observes handler write and delivery errors. Its callback must not
log through the same failing logging system. Slog’s convenience methods do not
return output errors themselves.
#Sensitive data
Cicadel redacts credential attributes before filtering and output. This covers
nested groups, With attributes and resolved LogValuer values.
- Redacted 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
credentialsgroup is redacted as a whole. NewLogger’sReplaceAttrhook runs between redaction passes. So renamed sensitive keys are still protected.
Warning
Redaction works on structured fields only. It cannot make free-form messages, error strings, custom string payloads or arbitrary objects safe. Never put credentials into log messages. Never dump request, settings or model objects.
Error logs should name the operation and safe metadata. Leave out raw provider responses and database query arguments.
#Requests
observability.Middleware(logger, observe) logs every request:
- It generates a fresh request ID and sets the
X-Request-IDheader. - It logs method, status, bytes and duration.
- It ignores incoming IDs, paths, query strings, headers and bodies.
- It keeps standard response interfaces through httpsnoop.
| Response status | Level |
|---|---|
| Below 400 | INFO |
| 4xx | WARN |
| 5xx | ERROR |
observability.Logger(ctx, logger) attaches the same request ID to your own
application events. Pass the same logger to the middleware.
#Panics
- Panics are logged with Go call stacks. The panic value and local variables are never logged.
- Uncommitted responses get a generic 500. Committed responses are not rewritten.
http.ErrAbortHandlerkeeps standard HTTP abort semantics.
Observer callbacks receive status, timing, bytes and a Panicked flag. They run
synchronously, so they must be fast, safe for concurrent use and must not panic.
Distributed trace propagation needs an explicit exporter or middleware integration.
#Database queries
db.Logger = observability.NewQueryLogger(logger, 200*time.Millisecond)This logs GORM query timings without SQL text or query arguments.
| Query | Level |
|---|---|
| Normal | DEBUG |
| Slower than the threshold | WARN |
| Failed | ERROR |
ErrRecordNotFound counts as a normal outcome. GORM’s LogMode is honored. The
accounts starter turns this adapter on. Other applications opt in explicitly.
#Log files
observability.OpenRotatingFile(path, maxBytes, backups) returns a synchronized
writer. It keeps numbered backups, creates files with mode 0600 and needs an
explicit Close.
- Rotation happens between complete writes. One large record can exceed the size limit.
- Use one writer per path in one process.
- No background goroutine is started for rotation.
Tip
With several replicas, log to stdout and use your platform’s log collector.
#Error email
observability.NewEmailHandler(sender, from, recipients, interval) creates a
handler for ERROR and above, like Django’s AdminEmailHandler.
- It sends a redacted JSON record through the configured mail transport.
- Each send has a five-second timeout.
- A shared suppression interval prevents alert storms.
- It attaches no request dumps or local variables.
- Delivery is synchronous. Use a custom queue handler to keep network latency out of requests.
Set OnError to observe failed alerts without recursive logging. The framework
never chooses alert recipients and sends nothing when you construct the handler.
#What is tested
observability/logging_test.go covers routing, propagation, filters, live levels,
grouped attributes, credential privacy, correlated panic events, query privacy,
file rotation, concurrent writes and captured email alerts.
This is a practical logging foundation. It does not claim full compatibility with Python’s logging API or automatic integration with third-party exporters.