# Log Levels Explained: DEBUG, INFO, WARN, ERROR & Best Practices

> A complete guide to log levels: what DEBUG, INFO, WARN, ERROR, TRACE, and FATAL actually mean, how log level hierarchy and filtering work, and best practices for choosing the right level in every environment.

Source: https://openobserve.ai/blog/log-levels-explained/
Published: 2026-09-08
Authors: Simran Kumari
Category: How To
Tags: Logging, Observability, Monitoring, DevOps, SRE

---

Every logging library ships with the same handful of levels: DEBUG, INFO, WARN, ERROR, and usually TRACE and FATAL too. They look simple enough that most teams never think about them until one of two things happens: production is on fire and the one log line that would explain why is buried under a wall of INFO noise, or an on-call engineer gets paged at 3 a.m. for an ERROR that turns out to be completely expected behavior.

Both problems come from the same root cause: log levels were assigned without a shared, consistent standard for what each one actually means. This guide covers what each level is for, how the severity hierarchy and filtering actually work, how the same concepts map across Python, Java, and Node.js, and the best practices that keep log levels useful instead of just noisy.

## What Are Log Levels?

A log level (also called a log severity) is a label attached to every log line that says how important or urgent that line is. Instead of treating every log message the same way, a log level lets you:

- **Filter** what gets written or displayed, e.g. show everything in development, but only WARN and above in production
- **Route** logs differently, e.g. send ERROR-level logs to a paging system while INFO logs just go to storage
- **Query** logs by severity later, e.g. "show me every ERROR from the checkout service in the last hour"
- **Alert** on the signal that actually matters, instead of drowning it in volume

Every mainstream logging library (Python's `logging`, Java's Log4j2 and SLF4J, Node's Winston and Pino, Go's `log/slog`, and the Unix `syslog` standard) implements some version of the same idea: a small, ordered set of severities that every log line gets assigned to.

## The Log Level Hierarchy: From Least to Most Severe

Log levels form a hierarchy, and that hierarchy is the entire mechanism behind "log level filtering." When you set a minimum level, say, INFO, you see that level and everything **more severe** than it (WARN, ERROR, FATAL), but nothing **less severe** (DEBUG, TRACE) gets written or shown at all.

| Level | Severity | What it generally means |
| --- | --- | --- |
| TRACE | Lowest | Extremely fine-grained, step-by-step execution detail. Rarely enabled outside deep, active debugging. |
| DEBUG | Low | Diagnostic detail useful while developing or troubleshooting, not needed in normal operation. |
| INFO | Normal | Confirmation that things are working as expected; the standard "system is healthy" audit trail. |
| WARN (WARNING) | Elevated | Something unexpected happened, but the system recovered or is still working; worth a look, not an emergency. |
| ERROR | High | An operation failed and needs attention; something didn't work the way it was supposed to. |
| FATAL / CRITICAL | Highest | The application, or a critical part of it, cannot continue running. |

## DEBUG: What It's For and When to Use It

DEBUG exists for the questions you only ask while actively developing or troubleshooting: what value did this variable hold, which branch of the code executed, what did the request payload actually look like before it got transformed.

```python
logging.debug("Cache lookup for key=%s: %s", cache_key, "hit" if found else "miss")
```

DEBUG logs are almost always too verbose and too detailed for normal production traffic. They're also a common source of accidental data leaks, since it's tempting to log entire objects, request bodies, or query parameters at DEBUG "because no one will see it in production anyway." Treat that assumption as false: DEBUG logs still get written, ingested, and stored if the level is left on, so the same sensitive-data rules apply to DEBUG as to every other level.

## INFO: What It's For and When to Use It

INFO is the default operating level for most production systems. It records the normal, expected events that make up an audit trail of what the application did: a service started, a request completed, a scheduled job finished, a configuration was loaded.

```python
logging.info("Order %s placed successfully for customer %s", order_id, customer_id)
```

A useful test for whether something belongs at INFO: if it happened and nothing needs to change as a result, it's INFO. The moment a human might need to act on it, it belongs at WARN or above.

## WARN (WARNING): What It's For and When to Use It

WARN sits in the space between "everything is fine" and "something is broken." It covers situations that are abnormal but not yet failures: a retried request that eventually succeeded, a deprecated API being called, a fallback code path being taken, disk usage crossing a soft threshold.

```python
logging.warning("Payment gateway timed out, retrying (attempt %d/3)", attempt)
```

WARN is the level teams most often get wrong in both directions: using it so rarely that real early-warning signals never surface, or using it so often for routine conditions that it becomes background noise no one reads, which is functionally the same problem as having no WARN level at all.

## ERROR: What It's For and When to Use It

ERROR means an operation actually failed: an unhandled exception, a database write that didn't commit, an upstream API call that returned nothing usable, a request the application could not complete for the caller.

```python
logging.error("Failed to charge customer %s: %s", customer_id, str(exc))
```

ERROR is generally what alerting and paging get built on top of, which makes it the most important level to keep honest. If ERROR gets used for conditions that are actually expected and handled, like a normal validation failure or a resource that legitimately doesn't exist, on-call engineers stop trusting ERROR alerts, and real failures start getting ignored along with the noise.

## Beyond the Big Four: TRACE and FATAL/CRITICAL

Most logging libraries define two more levels beyond DEBUG, INFO, WARN, and ERROR:

- **TRACE** sits below DEBUG: even more granular, typically used for tracing execution flow line by line inside a specific function or library. Most teams never enable it outside targeted debugging of a specific, hard-to-reproduce issue.
- **FATAL** (or **CRITICAL**, depending on the library) sits above ERROR: reserved for failures severe enough that the application, or a critical subsystem of it, cannot keep running, such as failing to acquire a required resource at startup or a corrupted state that makes it unsafe to continue.

The Unix `syslog` standard (RFC 5424) uses a more granular, eight-level scale that predates most application logging libraries: Emergency, Alert, Critical, Error, Warning, Notice, Informational, and Debug. Infrastructure and network tooling still commonly speaks in these terms even when the applications behind them use the simpler five- or six-level scale.

## How Log Levels Differ Across Languages and Frameworks

The concept is universal, but the exact names, defaults, and even the numbering differ by ecosystem, which causes real confusion in polyglot environments.

| Concept | Python `logging` | Log4j2 / SLF4J (Java) | Winston (Node.js) | Syslog (RFC 5424) |
| --- | --- | --- | --- | --- |
| Most severe | CRITICAL (50) | FATAL* | error (0) | Emergency (0), Alert (1), Critical (2) |
| Failure | ERROR (40) | ERROR | error (0) | Error (3) |
| Recoverable / abnormal | WARNING (30) | WARN | warn (1) | Warning (4) |
| Normal operation | INFO (20) | INFO | info (2) | Notice (5), Informational (6) |
| Diagnostic detail | DEBUG (10) | DEBUG | debug (5) | Debug (7) |
| Finest-grained | NOTSET (rare) | TRACE | silly (6) | Not defined |

\*Log4j2 kept `FATAL` for backward compatibility, but its own documentation recommends `ERROR` for most application-level failures.

The detail that trips people up most often: **Winston's default npm levels number in the opposite direction from Python and Log4j.** In Winston, `error` is `0` and `debug` is `5`, meaning a *lower* number is *more* severe. In Python's `logging` module and Log4j, it's the reverse: a *higher* number is *more* severe. Mixing these mental models up is a common source of misconfigured log-level thresholds when a team works across both a Python/Java backend and a Node.js service.

## Log Level Best Practices

<div style="left: 0; width: 100%; height: 0; position: relative; padding-bottom: 56.25%;"><iframe src="https://www.youtube.com/embed/TA-6GcZsNfE?rel=0" style="top: 0; left: 0; width: 100%; height: 100%; position: absolute; border: 0;" allowfullscreen scrolling="no" allow="accelerometer *; clipboard-write *; encrypted-media *; gyroscope *; picture-in-picture *; web-share *;" referrerpolicy="strict-origin"></iframe></div>

- **Set different levels per environment.** DEBUG or TRACE in local development, INFO or WARN by default in staging and production. Don't ship the same threshold everywhere.
- **Make the level dynamically configurable.** The most valuable time to see DEBUG output is during an active incident, and redeploying just to change a log level costs you the minutes that matter most. Most modern logging libraries and platforms support changing the level at runtime.
- **Never log sensitive data, regardless of level.** DEBUG is not an exemption from your data-handling policy. Passwords, tokens, full payment details, and personal data shouldn't appear in logs at any level.
- **Use structured logging so level is a real field, not just a string prefix.** A `level` field you can filter and query on (`level="ERROR"`) is far more useful than parsing `[ERROR]` out of a plain-text line.
- **Reserve ERROR for genuine failures.** An expected, handled condition (a normal 404, a validation failure with a clear user-facing message) is not an ERROR. Save it for things that actually need attention.
- **Keep levels consistent across services.** In a microservices architecture, a shared logging wrapper or library saves you from five teams having five different definitions of what counts as a WARN.
- **Alert on ERROR and sustained WARN spikes, not on INFO or DEBUG volume.** If your paging system fires on the same signal your dashboards are built on, you'll either under-alert or drown yourself in noise.
- **Sample or rate-limit high-volume DEBUG and TRACE logs instead of leaving them fully off.** A 1-in-100 sample of DEBUG output during normal operation can be enough to catch a pattern without paying to ingest and store all of it.
- **Include enough context at every level to make it actionable.** A request ID, trace ID, or tenant ID attached to a WARN or ERROR line means you don't have to reproduce the issue just to understand it.

## Common Log Level Mistakes to Avoid

- **Logging everything as INFO** because no one took the time to decide what belonged where. This defeats the entire purpose of having levels.
- **Using ERROR for expected, handled conditions**, which trains on-call engineers to ignore ERROR-based alerts over time.
- **Leaving DEBUG enabled in production indefinitely**, quietly inflating both infrastructure cost and the risk of sensitive data ending up in logs.
- **Inconsistent levels across services**, so the same type of event is a WARN in one service and an ERROR in another, making cross-service correlation unreliable.
- **No way to change log levels without a redeploy**, which means the most useful diagnostic tool you have is unavailable during the exact moments you need it most.

## Querying and Alerting on Log Levels in OpenObserve

None of the above matters much if your logging platform can't actually filter and alert on level cheaply and quickly. In OpenObserve, log level is just another structured field, so filtering by severity is a plain query rather than a regex over raw text:

```sql
SELECT * FROM app_logs WHERE level = 'ERROR' ORDER BY _timestamp DESC LIMIT 100
```

From there, the same best practices above translate directly into platform features: set an [alert](https://openobserve.ai/docs/) on ERROR rate or a sustained WARN spike instead of on raw log volume, use [log searching and filtering](/blog/log-searching-and-filtering/) to jump straight to the severity that matters, and rely on object-storage-backed retention to keep DEBUG-level detail affordable to store for the (rare) times you actually need to go back and look at it.

## Conclusion

Log levels are a small piece of configuration with an outsized effect on whether your logs are actually useful during an incident. Getting DEBUG, INFO, WARN, and ERROR right isn't about memorizing definitions, it's about agreeing, across your team, on the one question that decides which level a line belongs to: does this need someone to look at it, and how urgently? Get that consistent, make the threshold changeable without a redeploy, and the rest of your logging setup gets meaningfully easier to trust.

**Related reading:** [Structured Logging Best Practices](/blog/structured-logging-best-practices/), [Microservices Observability: Logs, Metrics, and Traces](/blog/microservices-observability-logs-metrics-traces/), [Avoiding Alert Fatigue](/blog/alert-fatigue/), [Best Log Analysis Tools](/blog/best-log-analysis-tools/), and [Integrating Log4j2 with OpenObserve](/blog/integrating-log4j2-with-openobserve/).
