# Logging

In this recipe you set up structured JSON logs with per-request correlation, one file per severity level, and an HTTP endpoint to read them back. The goal is that every log line is parseable, traceable to the request that produced it, and inspectable without SSHing into the server.

`configure_logging` installs a JSON handler on the root logger that emits one-line JSON records carrying the active request ID. `LogUtils` is a thin facade that adds level methods accepting structured `**fields`.

```python
from tempest_fastapi_sdk import LogUtils, configure_logging
from tempest_fastapi_sdk.core import get_request_id

# Imperative — call once during bootstrap.
configure_logging(level="INFO", json_output=True)

# Facade — handy for service-wide singletons.
log = LogUtils("app.users", level="INFO")
log.info("user_created", user_id=str(user.id), email=user.email)
log.warning("login_throttled", ip="1.2.3.4", attempts=5)

try:
    risky()
except RuntimeError:
    log.exception("risky_failed", op="reconcile")  # appends traceback

# Surface the correlation ID outside the log line if needed.
request_id = get_request_id()
```

JSON output (single line — formatted here for readability):

```json
{
  "timestamp": "2026-05-16T20:14:33.412Z",
  "level": "INFO",
  "logger": "app.users",
  "message": "user_created",
  "request_id": "d83e4b0c-7c2f-4bd6-aaa1-7d4f6cf5e5e9",
  "user_id": "9c1a5b2d-...",
  "email": "ana@example.com"
}
```

The middleware accepts a custom header name (`RequestIDMiddleware(app, header_name="X-Correlation-ID")`); the same header is echoed back on every response.


## Per-level files + isolated `500.log`

**By default the SDK writes to stdout AND to `logs/`** (one JSON file per level) at the same time. Each file receives **only its own level** (exact match — an `ERROR` never lands in `warning.log`), so every severity becomes an isolated, greppable stream.

```python
from tempest_fastapi_sdk import configure_logging

# Defaults — stdout + logs/{debug,info,warning,error,critical,500}.log
configure_logging(level="INFO")

# Custom directory
configure_logging(level="INFO", log_dir="/var/log/myapp")

# Disable file output (stdout-only — handy for serverless / read-only FS)
configure_logging(level="INFO", file_output=False)

# Disable stdout (sidecar tails from disk)
configure_logging(level="INFO", stdout=False)
```

!!! warning "Don't disable both"
    `configure_logging(stdout=False, file_output=False)` raises
    `ValueError` — silencing every handler leaves the application
    blind.

!!! check "File logging is best-effort — it never crashes startup"
    If `log_dir` cannot be created or its files cannot be opened
    (read-only filesystem, missing write permission, hardened container,
    serverless, CI), the SDK **skips** the file handlers, emits a warning
    (to the logger when stdout is on, otherwise straight to `stderr`) and
    keeps running with stdout only — instead of dying at import with
    `PermissionError: [Errno 13] ... 'logs'`. Pass `file_output=False` to
    opt out of file logging explicitly.

On disk:

```text
logs/
├── debug.log      # only DEBUG records
├── info.log       # only INFO records
├── warning.log    # only WARNING records
├── error.log      # only ERROR records (a 500 lands here too)
├── critical.log   # only CRITICAL records
└── 500.log        # only uncaught-500 records (isolated)
```

!!! danger "500s are grave — that's why they get their own file"
    The catch-all handler registered by `register_exception_handlers`
    flags every uncaught exception with the `http_500=True` extra.
    `configure_logging(log_dir=...)` routes those records to a dedicated
    `500.log` **in addition** to `error.log`. The gravest failure is
    never buried among the other errors.

!!! tip "Always in the logs, never in the body"
    The traceback goes to the files/terminal via logging — **not** to the
    response body. A 500 body is just the generic envelope
    (`{"detail": "Internal server error", "code": "INTERNAL_SERVER_ERROR"}`).
    See [HTTP layer](http.md) for the `log_traceback` /
    `include_traceback` flags.

!!! note "Files are always JSON"
    File handlers use `JSONFormatter` regardless of `json_output`, so the
    `/logs` endpoint can parse them back. `json_output` only controls the
    stdout format.

In the scaffold the directory comes from `LOG_DIR` (defaults to
`"logs"`; set it empty to disable file logging). Add `logs/` to your
`.gitignore`.


## Reading logs over HTTP — `make_logs_router`

`make_logs_router` mounts `GET /logs`, which parses the on-disk JSON files and returns a paginated `BasePaginationSchema[LogEntrySchema]` (newest first).

```python
from tempest_fastapi_sdk import make_logs_router

app.include_router(
    make_logs_router(log_dir="logs", token_secret=settings.TOKEN_SECRET),
)
```

!!! warning "Protect the endpoint in production"
    The payload exposes tracebacks and request metadata. The endpoint is
    gated by a shared-secret `X-Token` header via
    `make_token_dependency`. An empty `TOKEN_SECRET` **disables** the
    check (dev only) — never expose `/logs` unauthenticated in
    production.

Query examples:

```bash
# Latest 20 records across every level
curl -H "X-Token: $TOKEN_SECRET" "http://localhost:8000/logs"

# Only the isolated 500s, page 1, 50 per page
curl -H "X-Token: $TOKEN_SECRET" "http://localhost:8000/logs?source=500&page_size=50"

# Errors mentioning "timeout" in a time window
curl -H "X-Token: $TOKEN_SECRET" \
  "http://localhost:8000/logs?source=error&q=timeout&start=2026-05-31T00:00:00Z"
```

Query parameters:

| Parameter | Values | Description |
| --- | --- | --- |
| `source` | `all` (default), `debug`, `info`, `warning`, `error`, `critical`, `500` | Which file to read. `all` merges every level; `500` returns only the isolated 500s. |
| `q` | text | Case-insensitive substring match on the message. |
| `start` / `end` | ISO-8601 | Limit records to a time window. |
| `page` / `page_size` | integers | Pagination (1-indexed). |

!!! check "Recap"
    - `configure_logging(log_dir=...)` → stdout **+** one file per level.
    - Exact-level routing: each file holds only its own severity.
    - `500.log` isolates uncaught 500s (the `http_500` marker).
    - `make_logs_router` serves those files, paginated and authenticated.

