Logslog levelsseverityconventiondynamic levelsnoise

Log Levels Are a Convention, Not a Standard

Nothing in any specification says what warn means. What it means is whatever your team decided, written down or not — and when it was never written down, everything becomes info, the error rate becomes unmeasurable, and the level field stops carrying information.

Follow the diagnosis

Frame the diagnosis

Performance work starts from a symptom and a signal — never from a resource dashboard.

Diagnostic question
What does each level mean in this codebase, and does the whole team agree well enough for a query on `level` to mean anything?
Symptom
Filtering to `level=error` produces both real outages and routine validation failures, so nobody filters on it. Meanwhile a genuine failure is sitting at `info` because the author was not sure.
Signal
The ratio of lines per level and its stability over time. The misleading signal is the raw error-line count, which reflects logging habits at least as much as system health.
SymptomSignalMeasurementHypothesisEvidenceRoot CauseChangeValidationRegression Check

Four levels, and the only question that decides them

The useful discriminator is not severity in the abstract — it is who needs to act, and when. error means a human must look at this, ideally soon. warn means something recoverable happened that is worth knowing if a pattern forms, but nobody is woken up. info records the normal operation of the system so a request can be reconstructed afterwards. debug is detail that is too voluminous to keep on by default.

Applied consistently, this makes level queryable. rate(level="error") becomes a real health signal, and an alert on it means something. Applied inconsistently, the field degenerates: half the errors are at info because they felt routine, half the warn lines are actually normal, and the only honest query is one that ignores level entirely and filters on event instead (see Structured Logging: Fields a Program Can Read).

The specific trap worth naming is the expected failure. A validation rejection, a 404, a retry that succeeded on the second attempt — these are the system working correctly. Logging them at error is the fastest way to make the error level meaningless, because their volume dwarfs real failures and trains everyone to ignore the level.

A level convention that survives contact with a real team
LevelDecision ruleExampleCommon misuse
errorA human must investigate; something is broken that we intended to workPayment provider unreachable after all retriesExpected validation failures, 404s, user typos
warnRecoverable, but a pattern here would matterRetry succeeded on attempt 2; falling back to a replicaAnything nobody would ever act on, which is just info
infoNormal operation worth reconstructing laterRequest completed, job finished, config loaded at startupPer-iteration loop detail, which is debug
debugDetail too voluminous to keep on by defaultFull request bodies, per-item processing stepsLeft enabled in production "temporarily"

Everything-is-info, and the error rate that lies

The most common end state is that almost everything is info. It happens gradually and for a reasonable-sounding reason each time: an author is unsure, info is the safe default, nobody objects in review. Once the ratio is far enough gone, level conveys nothing and every query has to filter on event names instead.

The mirror image is just as damaging: routine failures logged at error inflate the error line rate by an order of magnitude, so an alert on that rate either fires constantly or is set so high it never fires. This is the log-shaped version of the problem Alert Fatigue: The Page Nobody Reads describes — a signal that cried wolf until nobody looks.

The corrective is cheap. Write the decision rules down, put them in the shared logging helper's documentation, and check the level distribution periodically. A healthy service usually shows info dominating, warn a small fraction, and error genuinely rare — and if the shape shifts sharply after a deploy, that is a signal worth reading alongside "What Changed?" — Deploy Markers and the Invisible Deploys.

Level distribution as its own health signalILLUSTRATIVE
SignalValueWhat it tells youVerdict
Healthy service: info / warn / error94% / 5% / 1%Level carries information; an alert on error rate is meaningfulnormal
Degenerate service: info / warn / error99.8% / 0.1% / 0.1%Almost everything is info — level is not a usable filter heresuspect
Inflated-error service: info / warn / error61% / 3% / 36%Routine failures are being logged as errors; no threshold on this rate can worksmoking gun
Error rate before/after deploy1.0% → 12%Either a real regression or a logging-level change — check the event breakdown before concludingsuspect
Debug lines in production4.1% of volumeDebug was enabled and not turned off; a cost and privacy issue bothsuspect

Changing the level without a deploy

During an incident you frequently want more detail from one component, right now. The choice between "ship a deploy to enable debug logging" and "give up and guess" is a bad one, especially since the deploy itself restarts processes and may clear the very state you were investigating.

Runtime-adjustable levels solve this: a control that raises verbosity for a specific logger, ideally scoped to one service, one component, or a sampled fraction of requests. Scoping matters because global debug on a busy service can multiply log volume by ten or more and create a second incident in the log pipeline (see The Log Bill and What It Is Buying).

Two safeguards make this safe to hand to on-call. A time-boxed default, so elevated verbosity expires automatically rather than being forgotten. And an audit record of who changed what, since debug output is exactly where sensitive values are most likely to appear (see What You Just Wrote Into a Log Half the Company Can Read).

Scoped, time-boxed verbosity — safe enough for on-call to use unsupervised
1# Bad: global, permanent, no audit
2set_log_level("debug")
3
4# Better: scoped to one component, expires on its own
5set_log_level(
6 logger = "checkout.payment", # not the whole service
7 level = "debug",
8 duration = "15m", # auto-reverts; cannot be forgotten
9 sample = 0.05, # 5% of requests, not all of them
10 reason = "INC-4417 provider timeouts",
11 actor = "oncall@example.com", # audited: debug output may contain sensitive values
12)
13
14# Volume check before enabling globally:
15# info-level volume ~12 GB/day
16# estimated at debug ~140 GB/day
17# ingest quota 25 GB/day
18# -> global debug would drop data for 4 hours. Scope it.

Key points

  • No specification defines the levels; the only thing that gives them meaning is a written team convention.
  • Decide the level by who must act and when, not by how bad the event feels in the abstract.
  • Logging expected failures at error destroys the error level's usefulness faster than anything else.
  • The level distribution is itself a signal — a sharp change after a deploy is worth investigating.
  • Runtime-adjustable levels should be scoped, sampled, time-boxed and audited, or they create a second incident.

Follow the diagnosis

The causal chain, hop by hop — and the readings that invite the wrong conclusion.

  1. 1
    Author → log call: unsure which level applies, picks info as the safe default.
  2. 2
    Log call → distribution: over months the info share climbs toward 100% and error becomes rare and arbitrary.
  3. 3
    Distribution → query: filtering on level no longer separates real problems from normal operation.
  4. 4
    Query → alerting: an alert on error-line rate is either constantly firing or set so high it never does.
  5. 5
    Alerting → responder: the responder stops trusting the level field and filters on event names instead, which only works for events that have stable names.
What this evidence makes people conclude — wrongly
  • "Error count doubled, we have a regression" — check whether a deploy changed what gets logged at error before concluding anything.
  • "No errors in the logs, so nothing is wrong" — the failure may be sitting at info because the author was unsure.
  • "We should log more at error so problems are visible" — that dilutes the level and makes every real error less visible.
  • "Debug is off in production" — verify it; a temporary enablement left on is common and shows up as both cost and privacy exposure.

Measure, fix, validate

An optimization is not finished until the metric that motivated it has moved.

How to measure it
  • • Compute the share of lines at each level per service and compare against the team's stated intent.
  • • Count distinct `event` values at `error` — a long tail of routine events means the level has been diluted.
  • • Check for debug-level lines in production, which usually means a temporary change was never reverted.
  • • Estimate log volume at debug before enabling it, and compare against the ingest quota.
What actually fixes it
  • • Write the level convention down with a decision rule per level and put it where the logging helper is documented.
  • • Move expected failures (validation, 404s, successful retries) down to `info` or `warn` and keep `error` for things needing investigation.
  • • Add scoped, sampled, time-boxed runtime level control so incidents do not require a deploy.
  • • Review the level distribution periodically and treat sharp changes as a signal, not as noise.
How you know it worked
  • • Confirm the level distribution moves toward the intended shape and that error-level `event` names are all genuinely actionable.
  • • Verify an alert on error rate now correlates with real incidents rather than with traffic volume.
  • • Test that elevated verbosity auto-reverts after its window and that the change was audited.
What it costs
  • • A strict convention slows down writing log calls slightly and needs enforcement to survive team growth.
  • • Runtime level control is another control-plane surface with its own auth, audit and failure modes.
  • • Moving expected failures off `error` means a genuinely novel failure may be under-noticed if it lands at `warn`.
Stop it coming back
  • Alert if debug-level volume appears in production above a small threshold.
  • Include level choice in code review for new log calls, especially anything at error.
  • Track the error-level event cardinality; growth means routine events are creeping back in.

Accuracy

Performance numbers are conditional. These are the conditions.

What these numbers depend on
  • ILLUSTRATIVELevel distributions, the volume estimates and the ingest quota are invented to give the ratios a shape. Healthy distributions vary enormously by service type — a batch worker and an API server look nothing alike.
  • ENVIRONMENT-SPECIFICAvailable levels, their names and whether runtime adjustment exists are properties of the logging library and platform. Some stacks add trace, fatal or numeric levels with different conventions again.

Misconceptions

Claim
“Log levels are standardized.”
Reality
The names are conventional; the meanings are not defined anywhere authoritative. Two teams in the same company routinely disagree about warn, which is why the convention has to be written down to exist at all.
Claim
“When in doubt, log at error so it is not missed.”
Reality
That is precisely how the error level dies. Once routine events outnumber real failures at error, no threshold works and the real failures are less visible than before.
Claim
“Debug logging is free if nobody reads it.”
Reality
It is charged on ingest and storage whether or not it is read, and debug output is the most likely place for tokens and personal data to appear (see What You Just Wrote Into a Log Half the Company Can Read).