Python Logging Practices
Written by coxley
on
Every project logs a little differently. Partly because there are a dozen ways to configure, partly due to consistency generally being hard. These practices have served my teams best out of the alternatives I've tried. You can separate logging from API contracts without sacrificing what you need.
Intro
Every project logs a little differently. Partly because there are a dozen ways to configure, partly due to consistency generally being hard. These practices have served my teams best out of the alternatives I've tried.
You can separate logging from API contracts without sacrificing what you need.
Naming
Like modules, loggers are hierarchical. Their names should follow the import
path of where they live. Use __name__
everywhere to get this for free.
Follow this guideline for both libraries and CLI entry-points. Technically the name of the main module's logger doesn’t matter, but inconsistencies breed.
As a rule of thumb, avoid using the root logger. Attach handlers or filters — but don’t use.
Hierarchy
These settings are available to loggers:
- Min Level
- Handlers
- Filters
Log names, split by period, make up the hierarchy.Each log bubbles up, visiting each handler, until stopped by a filter or the minimum level. (This flow is outlined in the official docs)
The hierarchy makes it possible to do things like:
- Exclude noisy logs from an import
- Include verbose logs from an import
- Format and print all program logs to
stderr
Avoid self.logger
All calls to logging.getLogger(name)
will return the same instance. Storing
in an instance variable pollutes the logging call-site — which should be
frequent!
"But I need request-specific details!" — if that was your reaction to this, keep reading to the following section! It was admittedly harder to do before context variables were mainline, but we should use them now that they are.
Adding Context
We can add context to loggers in a few ways, but this one is my favorite. It optimizes for:
- Zero special logger instances
- Fewest number of Handlers (1)
- No changes at logging call-site
For more information on contextvars
, see: https://docs.python.org/3/library/contextvars.html
Logging Over the Network
Use a queue with a flusher thread.
For every log issued, all handlers run serially in the same thread as the call-site. Uploading messages to a database in-line turns microseconds into 20ms, 100ms, 500ms, and so on.
A project I worked on did this once. Jobs that landed in Europe went from a few seconds to finish to 10+ minutes. We figured out it was due to 500-1000ms round-trips for each log message.
For asyncio
in the flusher thread, use janus.Queue
.
It’s both asyncio
-compatible and thread-safe:
https://gist.github.com/coxley/14a1e3d98898132fa89c4e025cfa840f
Duplicate Logs in Terminal
When logs print more than once, there are too many StreamHandlers
attached.
Use logging.removeHandler
for a deterministic starting point.
Don’t attach StreamHandler
within libraries. Configure the root logger from
the program entry-point.