# Logging

> Structured logs with Python's standard logging module

Encore.py works with Python's standard `logging` module. Install Encore's `ConsoleHandler`, and each log record is written as one structured line in Encore's log format, with its fields and the IDs of the trace and span it was logged in. Encore collects these lines, so you can see each one with the request or message it belongs to.

## Setting up logging

Add `ConsoleHandler` to the root logger when your app starts:

```python
-- main.py --
import logging

from encore.logs import ConsoleHandler

logging.basicConfig(level=logging.INFO, handlers=[ConsoleHandler()])
```

Then log as usual, with a logger per module:

```python
import logging

log = logging.getLogger(__name__)

log.info("order created", extra={"order_id": order.id, "total": 14900, "currency": "SEK"})
```

`ConsoleHandler` writes to stderr by default. It takes an optional `stream` and `level`:

```python
ConsoleHandler(level=logging.WARNING)
```

It writes each record as one JSON line, for example:

```json
{"time":"2026-10-19T09:41:07.123456Z","level":"info","message":"order created","logger":"orders.service","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","sampled":true,"code.file.path":"/app/orders/service.py","code.function.name":"create_order","code.line.number":42,"currency":"SEK","order_id":"ord_123","total":14900}
```

Each line has the time, level, message, logger name, where it was logged from (`code.*`), the `extra` fields, and the trace and span IDs of the current span. `ConsoleHandler` ignores logging formatters: the line's format is Encore's.

Output from `print`, and lines written by other handlers, is still collected, but as plain text without fields or trace IDs.

## Structured fields

Pass variable data as fields with `extra` rather than formatting it into the message:

```python
# Prefer
log.info("order created", extra={"order_id": order_id, "customer_id": customer_id})

# Avoid
log.info(f"order {order_id} created for customer {customer_id}")
```

A message that's the same for every equivalent event can be grouped and counted, and fields can be searched and filtered without parsing the message.

Field values are written as JSON. Strings, numbers, booleans, `None`, lists and dicts keep their shape; any other value, such as a `Decimal` or a `datetime`, is written as its `str()`. Python's `logging` refuses `extra` keys that clash with `LogRecord` attributes, such as `name`, `message` or `args`, so pick field names that don't.

## Logging exceptions

Log an exception with `log.exception(...)` inside an `except` block, or with `exc_info=True`:

```python
try:
    await charge_customer(customer_id, amount)
except PaymentError:
    log.exception("payment authorization failed", extra={"customer_id": customer_id})
    raise
```

The line records the exception's type, message and stack trace as `exception.type`, `exception.message` and `exception.stacktrace`. Formatting the exception into the message with `str(err)` loses the type and stack trace.

Log a failure once, at the layer that decides whether to retry, fall back or give up. Lower layers should let the exception propagate rather than logging it and raising it again.

## Trace and span IDs

A record logged while an OpenTelemetry span is current carries that span's trace and span IDs. This is what ties a log line to its request in Encore's traces. With [tracing set up](/docs/py/observability/tracing), records logged while your web framework handles a request carry the request's span, and records logged inside your own spans carry those.

The span is read when the record is handled, so set up tracing as well as logging to get the IDs. Records logged outside any span have no trace IDs.

## Encore's own log lines

The Encore runtime logs under the `encore.runtime` logger, so its lines follow your logging configuration: with `ConsoleHandler` on the root logger, they're written in the same format as your own, and your log level filters them like any other logger's.

## Log levels

Python's levels map onto Encore's: `DEBUG`, `INFO`, `WARNING`, `ERROR` and `CRITICAL`. Set the minimum level with `logging.basicConfig(level=...)`, or per logger with `setLevel`.

- `ERROR` is for an operation that failed unexpectedly and couldn't complete. Expected outcomes, such as a declined payment or failed validation, are not errors.
- `WARNING` is for something unexpected that the app recovered from, such as falling back to cached data or succeeding on retry.
- `INFO` is for meaningful, relatively low-volume events, such as a state change or a completed job.
- `DEBUG` is for detail that helps investigate behavior but isn't needed to operate the app.

## What to log

Log what the trace doesn't already show. Encore traces database queries, Redis commands, object storage operations and Pub/Sub messages, and your web framework's instrumentation traces requests. Lines like "querying database" or "request completed" duplicate the trace.

Do log business events and decisions, with the identifiers needed to investigate them: an order being rejected and why, a fallback being used, a batch completing with its counts. When you only need to know how often something happens, use a [metric](/docs/py/observability/metrics) instead.

## Sensitive data

Encore writes log fields as they are, without redacting them. Never log passwords, API keys, tokens, session IDs, authorization headers, secret values or payment details, and avoid logging whole request or response objects, which often contain them. Prefer internal identifiers, such as a user ID, over personal data, such as an email address.

## Viewing logs

`encore run` shows your services' logs as they happen, grouped with the request or message that caused them. `encore logs` shows the logs of a run afterwards, from the app's local telemetry store, so the app doesn't need to be running:

```shell
$ encore logs                    # the latest session
$ encore logs --svc orders       # only entries involving the orders service
$ encore logs --since 10m        # only entries that began in the last 10 minutes
$ encore logs --errors           # only entries in which something failed
```
