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

```sh
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 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.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.

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