Open any application log and you'll see the same kind of message tagged with different labels: , , , .
Why not just write down everything that happens, in one uniform stream?
This post looks at what log levels actually do, and at the companion problem every long-running app eventually faces: keeping a log file from growing forever (rotation).
A Log Level Is a Filtering Threshold Note: logging means recording what happened while a program runs, so it can be reviewed later in a file or on screen.
Python's standard module defines five levels: Level Numeric value Meaning 10 Fine-grained detail for tracing exactly what the code did 20 Normal progress record 30 Something unexpected, but processing continues 40 An operation actually failed 50 The application itself can no longer continue The key point is that this number isn't just a label — it's a threshold used for filtering.
Set and only messages at 20 or above (INFO, WARNING, ERROR, CRITICAL) get written out; anything at DEBUG (10) is silently dropped.
In other words, level design isn't only about deciding what to record — it's about being able to dial the visible detail up or down later without touching the code.
In normal operation you watch INFO and above; when something goes wrong, you temporarily drop the threshold to DEBUG to see the fine-grained trace.
How This App Actually Configures It At the top of , log output is routed to two handlers: Because the threshold is , DEBUG-level messages produce no output during normal operation.
Across the codebase there are 19 calls — statements written to stay quiet by default and only become useful once someone deliberately lowers the threshold during an investigation.
That's the practical payoff of level filtering: you can leave detailed diagnostic statements embedded in the code permanently, without needing to add them later, and without them cluttering the log under normal conditions.
What the Actual Level Distribution Reveals Counting calls across the same codebase gives: : 135 calls : 114 calls : 28 calls : 0 calls INFO dominates because the app processes multiple WordPress sites in sequence during a maintenance run, and each step of that sequence needs a progress record.
WARNING is the next largest category, and a good example is in , where detects incomplete SMTP settings: Notice this uses , not .
Even if the notification email can't be sent, the rest of the maintenance run — backups, updates, rollback decisions — can still proceed; nothing has actually failed. is reserved for cases where an operation genuinely did fail (a WP-CLI update command erroring out, an exception during mail delivery, and similar), which is why it's the smallest of the three at 28 occurrences. doesn't appear at all, and that's not an oversight — it follows from how the app is designed.
Each site is processed independently: if one site's maintenance run fails, that site alone gets rolled back and the run moves on to the next site.
There's essentially no scenario where the whole application needs to be treated as unable to continue.
On top of that, urgent notifications to the user aren't handled through the log level at all — they go through a separate channel, .
Deciding what gets written to the log and deciding what the user needs to be told are two different concerns, and the second one lives in application logic, not in log-level plumbing.
Rotation: Making "Keep Recording Forever" Safe Logs get more useful the longer they accumulate, but an unbounded log file will eventually fill the disk. solves this: triggers a rotation once the file reaches 10MB; caps how many rotated generations are kept.
When the active file hits the size limit, it's renamed and a fresh empty file takes over.
The next time the limit is hit, becomes , and so on — once a generation would exceed , it's deleted.
That fixes the maximum disk footprint at — about 60MB in this app's case — no matter how long the process keeps running.
For a desktop app meant to stay running over long peri