Watch at EaseStories. Answers. Perspective.

Technology · PRACTICAL GUIDE

Python logs every message twice? Audit the handler and propagation graph first

Tag one record, trace every handler to root, then separate repeated setup, framework defaults and multi-process capture before removing one duplicate edge.

Original diagram tracing one LogRecord through child, ancestor and root handlers.
Original illustration: trace the record to every handler before changing propagation. Watch at Ease

A duplicate line is not yet a diagnosis. One LogRecord can reach both a child handler and an ancestor handler; a request path can repeatedly add handlers to the same cached logger; a framework can own its own stream; or separate processes and an outer collector can produce two events that merely look identical.

The safe repair is to label one test record, record PID, logger and handler identity, then draw the path from emitter to every sink. Change exactly one ownership boundary and replay the original command.

Prove whether the event or only the line is duplicated

Emit a unique token such as LOG-PROBE-7f3a and include process ID, logger name, level and timestamp with enough precision. Capture console, file and collector output separately. Identical text without identity fields can be two records, while two differently formatted lines can be one record handled twice.

Repeat through the real command, reloader, worker launcher or service unit. A direct python app.py run is not evidence for a uvicorn, gunicorn, cron or container path.

Dump the logger-to-handler graph

For the emitting logger, record name, disabled, effective level, propagate and every direct handler with id(handler), class, level, stream or filename and formatter. Then walk parent links to root and repeat.

If the same handler object or two equivalent destinations appear on a child and an ancestor while propagation remains true, one LogRecord has two delivery paths. Keep the handler at the chosen highest owner or stop propagation at the deliberate boundary, but do not do both blindly.

Separate repeated setup from propagation

Record handler counts before import, after application startup, after one request and after a reload. A stable graph with both child and root handlers points to propagation. A count that grows after every request or constructor call points to non-idempotent configuration.

Move setup to one startup path or make it idempotent by reusing a known handler. Do not deduplicate only by handler class: two StreamHandlers may intentionally write to different streams, while two FileHandlers may resolve to the same file.

Respect framework and process ownership

Inspect framework loggers before calling basicConfig or dictConfig. Uvicorn documents separate parent, error and access branches; preserving access logs can require a different decision from application records. Compare the live graph with the current framework configuration rather than an old snippet.

Include PID and process name in the probe. For multiple workers, send records through QueueHandler to one listener or use an external collector; do not let workers independently rotate the same file. Also check whether stdout and stderr are both ingested by the same outer service.

Remove one edge and run an acceptance matrix

Choose one owner for each sink: application root, framework branch, queue listener or external collector. Remove only the duplicate handler, repeated setup call, unwanted propagation edge or duplicate collector route that the evidence proves. Preserve rollback.

Replay startup, one successful request, one application warning, one framework error and one access event. The probe must appear exactly once per intended sink, handler counts must remain stable after another request and reload, and required startup, error and access streams must still be present.

Original decision diagram separating ancestor propagation, repeated setup, framework and process duplication.
Original illustration: each duplicate branch needs a different repair. Watch at Ease