Dash0 Raises $35 Million Series A to Build the First AI-Native Observability Platform

Last updated: January 11, 2026

Understanding Log4j Log Levels in Practice

If you operate Java services in production, Log4j is likely somewhere in the stack, either directly or through a framework that depends on it. Despite its age and ubiquity, its log levels are still frequently misunderstood and misused, often leading to noisy logs, missing signals, or alerts that fire too late to matter.

Log levels are how you communicate intent to future operators, on call engineers, and automated systems. Every time you pick a level, you're answering a subtle but important question: how serious is this, and what should someone do about it later?

This article walks through Log4j log levels in practical terms, how they map to modern observability standards, and how to configure and use them without falling into the common traps that make Java logs expensive and hard to trust.

Log4j log levels in order

Log4j defines six primary log levels, ordered below from least to most severe. Each level represents a different expectation about frequency, urgency, and actionability:

LevelWhen to use it
TRACEExtremely detailed execution flow used for deep diagnostics.
DEBUGInternal state and decision making for troubleshooting.
INFONormal, expected application behavior under healthy conditions.
WARNThe service is an an unusual state that may require attention if it continues.
ERRORAn operation failed and functionality was impacted.
FATALAn unrecoverable condition that forces the application to shut down.

Used correctly, Log4j levels create a clear separation between routine behavior and real problems. The more severe the level, the less often it should appear and the faster someone should care.

How to set the log level of an event

In Log4j, the log level is chosen when you emit the log. Most applications use the level specific methods provided by the logger, which keeps intent explicit and readable:

java
123456789101112
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class OrderService {
private static final Logger logger = LogManager.getLogger(OrderService.class);
public void placeOrder(String orderId) {
logger.trace("Entering placeOrder with orderId={}", orderId);
logger.debug("Validating order {}", orderId);
logger.info("Order {} placed successfully", orderId);
}
}

A practical way to think about using Log4j levels is as follows:

  • TRACE and DEBUG explain how the system works internally. They are usually disabled in production and enabled temporarily when investigating an issue.
  • INFO describes what the system is doing under normal conditions. It should be the majority of your logs.
  • WARN signals risk. It means the system still works, but something important is drifting away from the happy path.
  • ERROR means a request or task failed, even though the process keeps running.
  • FATAL is reserved for explaining unrecoverable conditions that caused the application instance to terminate.

By following these simple guidelines, most level decisions become obvious. Routine behavior stays routine, and real problems stand out instead of hiding in noise.

Log4j default log level behavior

If no explicit configuration is provided, Log4j defaults to a default log level of ERROR. In practice, most frameworks override this early, but it is important to understand what the active minimum level is in your environment.

With a root level of INFO, anything below it is discarded:

xml
123
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
java
1234
logger.debug("This will not be logged");
logger.info("This will be logged");
logger.warn("This will be logged");
logger.error("This will be logged");

Think of the configured level as a hard floor. Events below it are never created, which means they have almost zero runtime cost.

Configuring log levels in Log4j 2

Most applications configure Log4j using XML, YAML, or properties files. XML remains common in production systems.

A simple log4j2.xml configuration might look like this:

xml
12345678910111213
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{ISO8601} %-5level %logger - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

This setup keeps logs focused on operational signal while suppressing debug noise by default.

Overriding log levels for specific packages

One of Log4j’s most useful features is per package or per class level overrides. This lets you increase detail in a narrow area without flooding the entire application.

xml
123456789
<Loggers>
<Logger name="com.myapp.payments" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>

With this configuration:

  • Payment related code logs at DEBUG
  • Everything else remains at INFO
  • Log volume stays predictable

This pattern is especially valuable when debugging production issues in large codebases.

Mapping Log4j levels to OpenTelemetry severity

Log4j’s level model aligns cleanly with OpenTelemetry’s standardized severity categories, making it straightforward to export logs without losing intent:

Log4j levelOpenTelemetry SeverityTextOTel SeverityNumber range
TRACETRACE1-4
DEBUGDEBUG5-8
INFOINFO9-12
WARNWARN13-16
ERRORERROR17-20
FATALFATAL21-24

When Log4j logs are exported through proper OpenTelemetry instrumentation, this mapping preserves severity semantics across languages and platforms, which is critical in distributed systems.

Performance considerations with debug logging

Log4j does not evaluate log events below the active level, but any work you do before the call still happens. This matters in hot paths like request handlers and tight loops.

Avoid patterns like this:

java
1
logger.debug("Computed result {}", expensiveComputation());

Even if DEBUG is disabled, expensiveComputation() still runs. In high volume paths, guard explicitly:

java
123
if (logger.isDebugEnabled()) {
logger.debug("Computed result {}", expensiveComputation());
}

You do not need this everywhere. Use it where log volume and computation cost make the overhead visible.

Making Log4j levels actionable with Dash0

In production services, the hard part of troubleshooting is rarely collecting logs but out which ones matter when everything is noisy and something is already broken.

Dash0 keeps Log4j’s severity model intact and uses it to drive investigation flow. You start with the small number of ERROR and WARN events that indicate real impact, then expand outward to INFO and DEBUG only for the specific requests involved.

Because Dash0 is OpenTelemetry-native, Log4j logs are tied directly to the requests that produced them. That means you can move from a failed log line to the exact execution path across threads and services, instead of grepping through millions of unrelated entries.

Final thoughts

Log4j log levels are simple, but they carry a lot of weight. Used deliberately, they give your logs a clear operational shape. Used carelessly, they turn into background noise that nobody trusts.

Treat log levels as part of your system design, not a cosmetic detail. If you do, Log4j will scale with your application and still be useful when things go wrong.

Try Dash0 today to see Log4j logs in the context they were produced, with severity guiding what you look at first instead of drowning you in noise.

Authors
Ayooluwa Isaiah
Ayooluwa Isaiah