Structured Logging with JSON

RoadmapsPython

Overview

Structured logging means emitting logs as machine-readable data structures (like `JSON`) rather than plain text strings. Instead of logging a raw string: logger.error("User 123 failed login from 192.168.1.1") You emit a dictionary of queryable data: logger.error("login_failed", extra={"user_id": 123, "ip": "192.168.1.1"}) Think of it like filling out a spreadsheet with specific columns instead of writing a free-form diary entry; machines can easily filter and `aggregate` the spreadsheet. When building structured logs, you must avoid key collisions with Python's built-in `LogRecord` attributes. Passing a reserved key in your `extra` dictionary will raise an exception at runtime. # Raises KeyError: "Attempt to overwrite 'name' in LogRecord" logger.info("Server started", extra={"name": "auth-service"})

Plain text logs require brittle regular expressions (`regex`) to parse in log aggregation tools (like `Datadog` or `Splunk`). `JSON` logs are instantly parsed into queryable fields, making production debugging exponentially faster.

Where used: FastAPI, CloudWatch, Microservices

Why learn this

Code walkthrough

import logging
import json
import sys

class CustomJSONFormatter(logging.Formatter):
    def format(self, record):
        return json.dumps({
            'lvl': record.levelname,
            'msg': record.getMessage()
        })

logger = logging.getLogger('app')
logger.setLevel(logging.WARNING)
h = logging.StreamHandler(sys.stdout)
h.setFormatter(CustomJSONFormatter())
logger.addHandler(h)

logger.warning('Disk low')

Focus: return json.dumps({

Common mistakes

Glossary

machine-readable
Data formatted in a specific way so that computers and software can easily process and understand it. Example: `{"event": "login", "user_id": 42}`.
aggregate
To collect and combine many separate pieces of data into a single, summarized view or total. Example: `aggregate(logs)`.

Recall questions

Understanding checks

What is printed to the console?

{"level": "INFO", "msg": "Login", "user": 42}

The `JSONFormatter` extracts `levelname` and `msg` from the `LogRecord`, as well as any custom attributes passed via `extra` (which Python attaches to the record), and returns a `JSON` string.

Why does this log `{"msg": "Hello %s"}` instead of `{"msg": "Hello World"}`?

It accesses `record.msg` (the raw string with '%s') instead of calling `record.getMessage()`, which performs the string interpolation.

The `LogRecord` stores the raw message template and arguments separately. You must call `getMessage()` to safely merge them before `JSON` encoding.

Practice tasks

Add request ID to JSON logs

Given a working `JSON` formatter, modify the `format` method so that it also includes a `request_id` field in the `JSON` output, but ONLY if `req_id` was passed in the logger's `extra` dictionary. (Hint: use `hasattr(record, 'req_id')`).

Continue learning

Previous: Loggers, Handlers, and Formatters

Previous: Dictionaries: operations and use cases

Return to Python Roadmap