cicadel

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

VariableValues
CICADEL_LOG_FORMATjson (default) or text for local development
CICADEL_LOG_LEVELDEBUG, INFO, WARN or ERROR

Invalid values fail startup. The error does not print them.

For local development:

Terminal
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:

Go
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.LevelVar to change the level at runtime;
  • set AddSource to record caller locations;
  • set a ReplaceAttr hook to rewrite attributes.

#Ask your agent

Text
Read https://cicadel.fauzaanu.com/en/0.2.7/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:

Go
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 StopPropagation is 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 through HandlerConfig.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 credentials group is redacted as a whole.
  • NewLogger’s ReplaceAttr hook 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-ID header.
  • 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 statusLevel
Below 400INFO
4xxWARN
5xxERROR

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.ErrAbortHandler keeps 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

Go
db.Logger = observability.NewQueryLogger(logger, 200*time.Millisecond)

This logs GORM query timings without SQL text or query arguments.

QueryLevel
NormalDEBUG
Slower than the thresholdWARN
FailedERROR

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.

Type to search this version.