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:
main.pyimport logging
from encore.logs import ConsoleHandler
logging.basicConfig(level=logging.INFO, handlers=[ConsoleHandler()])
Then log as usual, with a logger per module:
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:
ConsoleHandler(level=logging.WARNING)
It writes each record as one JSON line, for example:
{"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:
# 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:
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, 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.
ERRORis for an operation that failed unexpectedly and couldn't complete. Expected outcomes, such as a declined payment or failed validation, are not errors.WARNINGis for something unexpected that the app recovered from, such as falling back to cached data or succeeding on retry.INFOis for meaningful, relatively low-volume events, such as a state change or a completed job.DEBUGis 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 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:
$ 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