Python Structured Logging vs Plain Text Logs in Production APIs
Quick answer
Quick answer: structured logging writes each log entry as key-value data, usually JSON, so log systems can filter and aggregate by field. In production APIs it...
Quick answer: structured logging writes each log entry as key-value data, usually JSON, so log systems can filter and aggregate by field. In production APIs it beats plain text because you can search by request ID, user, or status without parsing strings. Log to stdout, attach a request ID to every entry, and never log secrets or sensitive personal data.
Why Is Structured Logging Better Than Plain Text?
A plain text line like a timestamp, a level, and a sentence is easy to read in a terminal, but hard for machines. To answer a question such as how many requests to one endpoint failed with a 500 in the last hour, you must write fragile regular expressions that break whenever someone rewords a message. Structured entries expose fields directly, so queries are reliable and cheap.
Fields also make correlation possible. If every entry carries a request ID, you can retrieve everything that happened during one request across handlers, services, and background tasks. In distributed systems, adding a trace ID lets you follow a request across services. Without these fields, debugging production incidents turns into guesswork over timestamps.
Plain text still has a place. In local development, human-readable output is more pleasant. A common approach is to switch the formatter by environment, so developers see readable lines and production emits JSON from the same logging calls.
How Do You Implement It With the Standard Library?
Python's logging module supports custom formatters, so you do not need a third-party package to emit JSON. A formatter converts each record into a dictionary and serializes it. Configure it once at startup, preferably with dictConfig, and have every module use logging.getLogger(__name__).
import json, logging
class JsonFormatter(logging.Formatter):
def format(self, record):
entry = {
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
for key in ("request_id", "user_id", "status"):
if hasattr(record, key):
entry[key] = getattr(record, key)
if record.exc_info:
entry["exc"] = self.formatException(record.exc_info)
return json.dumps(entry)
Pass extra context with the extra argument on logging calls, or better, inject it automatically. A request ID stored in a context variable can be added to every record by a logging filter, so developers do not have to pass it manually through every function. Context variables work correctly with both threads and asyncio tasks, which makes them a good fit for web services.
What Practices Keep Logs Useful and Safe?
Good structure is not enough without discipline about content. These rules prevent the most common problems:
- Never log passwords, tokens, API keys, full card numbers, or sensitive personal data. Redact or omit them at the source.
- Use levels consistently: debug for development detail, info for normal events, warning for recoverable problems, error for failures.
- Use logger.exception inside except blocks so the stack trace is included.
- Pass arguments lazily, as in a message with placeholders and separate values, so formatting work is skipped when the level is disabled.
- Write to stdout in containers and let the platform collect it, instead of managing log files inside the container.
Keep field names stable, because dashboards and alerts depend on them. Avoid putting large payloads in logs, and consider sampling very high-volume debug messages. Logging costs CPU and storage, and verbose logs on a hot path can slow your API.
Be deliberate about volume and cost. Logging every request body or every database query at info level quickly produces enormous amounts of data and a large bill. Log what you need to answer operational questions, keep debug output behind a level switch, and set retention periods so old logs are deleted automatically.
Frequently Asked Questions
Should production logs be JSON?
Usually yes, if you use a log platform that indexes fields. JSON makes filtering and alerting reliable. For small setups reading logs by eye, plain text may be enough.
How do I add a request ID to every log line?
Generate it in middleware, store it in a context variable, and add it to records with a logging filter or formatter. That avoids passing it through function arguments.
What log level should I use in production?
Info is a common default, with debug enabled temporarily for troubleshooting. Adjust per logger so noisy libraries do not flood your logs.
Do I need a third-party logging library?
Not necessarily. The standard library covers formatting, filters, and handlers. Third-party packages add convenience, so evaluate them against your needs.
Summary
Use structured JSON logs in production, add request IDs through context variables, and keep sensitive data out. Configure logging once, log to stdout, and use consistent levels and stable field names. Check the Python logging documentation for configuration details in your version.