Dash0 Acquires Lumigo to Expand Agentic Observability Across AWS and Serverless

Last updated: January 31, 2026

Java Log Levels: How to Use Them in Practice

Java has no shortage of logging frameworks, but long before Log4j, Logback, or SLF4J entered the picture, the JDK itself shipped with a logging API: java.util.logging, often shortened to JUL.

It's not flashy, and it's not especially ergonomic, but it is everywhere. If you've ever debugged a JVM in production, you've most certainly encountered JUL logs, either directly or as the lowest layer under another framework.

Understanding Java log levels at this level is still worth your time. JUL’s model shaped how logging works across the Java ecosystem, and many abstractions still map back to it under the hood.

This article explains Java log levels as defined by java.util.logging, how they behave at runtime, and how to use them in a way that keeps logs useful instead of noisy.

Java log levels in order

java.util.logging defines a fixed set of log levels, each represented by a Level object with an associated numeric value. They're ordered from least to most severe:

LevelValueTypical use case
FINEST300Step by step execution detail such as loop iterations, branch paths,
or method entry and exit.
FINER400Intermediate state and derived values used to make decisions.
FINE500High level diagnostic context explaining why the system behaved a
certain way.
CONFIG700Configuration and startup related information.
INFO800Normal, expected application behavior or significant business events
WARNING900An unusual situation that may require attention if it continues.
SEVERE1000A failure that prevented an operation from completing successfully.

There are also two special levels, but these are not used in application code. They exist only to configure logging behavior:

  • ALL: Enables every log message
  • OFF: Disables logging entirely

In practice, most applications use a smaller subset: FINE, INFO, WARNING, and SEVERE. They cover the majority of real world needs, with the other levels reserved only for specialized use cases.

Setting the log level for an event

In JUL, you obtain a Logger by name, usually the fully qualified class name, and log messages at a chosen level through the corresponding level method:

java
1234567891011121314151617
import java.util.logging.Logger;
import java.util.logging.Level;
public class OrderService {
private static final Logger logger =
Logger.getLogger(OrderService.class.getName());
public void placeOrder(String orderId) {
logger.finest("Entered placeOrder() method, initializing local variables");
logger.finer("Parsed request headers and extracted correlationId=abc123");
logger.fine("Calculated order total=149.99 after discounts");
logger.config("Loaded payment provider configuration from environment");
logger.info("Order submitted successfully orderId=" + orderId);
logger.warning("Retrying payment request after transient timeout orderId=" + orderId);
logger.severe("Payment processing failed permanently orderId=" + orderId);
}
}

You can also use the log() method with a Level constant. This is required when you need to attach a Throwable, for example:

java
12345
try {
throw new IllegalArgumentException("Order ID must not be null");
} catch (Exception ex) {
logger.log(Level.SEVERE, "Failed to process order", ex);
}

logger.severe() will not work here because it does not have an overload that accepts a Throwable.

How to choose log levels in Java

JUL’s level names are more granular than many modern frameworks, which can make them feel abstract. A useful way to reason about them is by intent, not by name:

  • FINEST, FINER, FINE: Use these to surface internal details that help you understand system behavior when actively troubleshooting an issue. They should never be enabled by default in production.

  • CONFIG: Use this for configuration and startup details like environment values, feature flags, and related decisions.

  • INFO: Use this for routine, expected, or significant events that describe normal operation of your services.

  • WARNING: This is like a check engine light for your services. It signals a significant issue that hasn't caused a failure yet, but likely will if it continues.

  • SEVERE: Covers both "error" and "fatal" style events where an operation failed or the system entered an unrecoverable state.

The level you choose should answer a simple operational question: how serious is this, and who should care if it shows up?

This way, your logs stay quiet during normal operation and surface the right signals immediately when something goes wrong.

Customizing the default log level behavior

Out of the box, the JDK configures the default level of the root logger at INFO. This means:

  • INFO, WARNING, and SEVERE are logged.
  • CONFIG, FINE, FINER, and FINEST are suppressed.

You can see this behavior immediately from the previous example:

text
123456
Jan 30, 2026 2:25:25 PM OrderService placeOrder
INFO: Order submitted successfully orderId=ORD-123
Jan 30, 2026 2:25:25 PM OrderService placeOrder
WARNING: Retrying payment request after transient timeout orderId=ORD-123
Jan 30, 2026 2:25:25 PM OrderService placeOrder
SEVERE: Payment processing failed permanently orderId=ORD-123

JUL is configured using a logging.properties file, typically located at $JAVA_HOME/conf/logging.properties or provided via the -Djava.util.logging.config.file:

A minimal configuration that sets the global level looks like this:

properties
1
.level = INFO

To enable debug style logging globally:

properties
1
.level = FINE

Handlers also have their own levels. If a handler is more restrictive than the logger, it will still filter messages out.

properties
123
handlers = java.util.logging.ConsoleHandler
.level = FINE
java.util.logging.ConsoleHandler.level = INFO

In this setup, FINE logs are created but only INFO logs and above are sent to the console.

Overriding levels for specific packages and classes

One of the most important features of java.util.logging is per logger configuration. It allows you to increase verbosity for a narrow slice of your codebase without flooding the rest of the system with noise.

In practice, that means you can override levels for packages, individual classes, or any custom logger name you define. For example:

properties
123
.level = INFO
com.myapp.payments.level = FINE
com.myapp.payments.OrderService.level = FINE

With this configuration:

  • All code under com.myapp.payments can emit FINE logs,
  • OrderService can emit FINE logs even if its package does not,
  • Everything else remains at INFO

Note that a more specific name always overrides a less specific one, making it easy to increase verbosity for a single subsystem or class during an investigation.

Avoiding performance pitfalls with verbose logging

Like most logging systems, JUL drops log records below the active level. What it cannot avoid is the work done before the log call itself.

This matters in hot paths such as request handlers, tight loops, or high-throughput operations:

java
1
logger.fine("Computed result: " + expensiveComputation());

Even when FINE is disabled, expensiveComputation() still executes and the string is still built. In performance-sensitive code, guard these calls explicitly:

java
123
if (logger.isLoggable(Level.FINE)) {
logger.fine("Computed result: " + expensiveComputation());
}

You do not need this everywhere; only use it where log volume and computation cost actually matter.

Logging exceptions correctly

JUL supports logging exceptions directly, but always pass the throwable instead of stringifying it yourself.

java
12345
try {
throw new IllegalArgumentException("Order ID must not be null");
} catch (Exception ex) {
logger.log(Level.SEVERE, "Failed to process order " + order.getId(), ex);
}

This preserves the full stack trace and associates it with the log record, instead of burying it in a string where tools cannot reliably parse it:

text
12345
Jan 30, 2026 3:42:27 PM OrderService placeOrder
SEVERE: Failed to process order
java.lang.IllegalArgumentException: Order ID must not be null
at OrderService.placeOrder(OrderService.java:26)
at OrderService.main(OrderService.java:35)

As a rule of thumb:

  • Use WARNING when an error occurred but the system recovered
  • Use SEVERE when an operation failed or data was put at risk

If you would page someone for it, it should not be logged below SEVERE.

How Java log levels map to OpenTelemetry

When JUL logs are exported through an OpenTelemetry pipeline, their levels are normalized into OpenTelemetry’s standard severity model to ensure consistent behavior across diverse logging systems.

A typical mapping looks like this:

java.util.loggingSeverityTextSeverityNumber
FINESTTRACE1–4
FINERTRACE1–4
FINEDEBUG5–8
CONFIGINFO9–12
INFOINFO9–12
WARNINGWARN13–16
SEVEREERROR17–20
SEVEREFATAL21–24

This normalization matters in distributed systems, where logs from various language ecosystems must be analyzed side by side in a universally understandable way.

Making Java log levels actionable with Dash0

In production, you rarely start an investigation by reading every log line.

A spike in SEVERE usually means something failed outright, while a gradual increase in WARNING often points to a component drifting toward failure. INFO provides operational context, and the FINE levels are only used when you need to understand how the code behaved for a specific execution.

Log levels in Dash0

Dash0 treats these levels as a first-class signal, not just a filter. Higher severity events naturally rise to the surface, so you can focus on what failed first, then expand outward without drowning in noise.

Because Dash0 is OpenTelemetry-native, Java logs keep their severity semantics and can be correlated with traces. You can move from a SEVERE log to the exact request that produced it, then pull in lower-level detail only for that execution.

The outcome is simple: you begin with a small set of high-severity events, follow the execution path they belong to, and surface additional detail on demand without turning on verbose logging everywhere.

Final thoughts

java.util.logging is not the most pleasant API, but its level model is solid and still underpins much of the Java ecosystem.

When levels are used deliberately, routine behavior fades into the background, warnings draw attention, and failures are unmistakable. More detail can be surfaced instantly when needed, without increasing noise or cost.

Treat log levels as part of your system design, even when working at the lowest level of the stack. When things go wrong, that discipline pays off fast.

Try Dash0 today to see Java logs in the context they were produced and make log levels work the way they were intended.

Authors
Ayooluwa Isaiah
Ayooluwa Isaiah