10 Java logging practices for production services

Configure useful Java logs with SLF4J, JSON layouts, scoped MDC, measured async behavior, preserved exceptions, and verified collection in Guance.

Best practices
Illustration of application logs flowing through a collection pipeline

A Java logging setup has to do more than print messages. It needs to preserve request context, keep failures diagnosable, behave predictably under load, and produce records the collection pipeline can use.

These ten practices cover the application API, backend configuration, event format, performance, and collection. They apply whether you keep an existing Logback deployment or use Log4j 2 for a specific requirement. The goal is a logging contract your team can operate, not a framework switch for its own sake.

1. Separate the logging API from its implementation

SLF4J gives application code a common logging API. Logback and Log4j 2 are common backend choices, while java.util.logging remains a valid option in environments built around the JDK API. It is not inherently unsuitable for production.

A typical SLF4J declaration is:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
}

Spring Boot commonly supplies Logback through its default logging starter. Keep it if it meets your needs. Evaluate Log4j 2 when its layouts, plugins, asynchronous loggers, or allocation controls solve a measured problem.

A facade reduces coupling in logging calls, but changing implementations still requires compatible dependencies, providers, bridges, and configuration. Backend-specific features do not become portable merely because the ordinary calls use SLF4J. Ensure only the intended provider is present, and avoid bridge loops that route events between APIs indefinitely. SLF4J FAQ

2. Define severity in terms of the operation

Level Practical use
TRACE / DEBUG Detailed diagnostic state, usually disabled outside an investigation
INFO Significant normal events such as startup or job completion
WARN An unexpected condition that the application currently handles
ERROR A failed operation, such as a request or dependency call

Apply those meanings consistently. A retry that eventually succeeds is different from an operation that fails after all retries. Treating both as errors can inflate detection counts and hide the failure rate you actually care about.

In the collection pipeline, preserve severity separately from HTTP status. error describes a log event's severity; 500 is a response code. Both can be useful query fields.

3. Defer formatting on disabled log levels

Use parameterized messages instead of eager concatenation:

// Concatenation happens even when DEBUG is disabled.
log.debug("Processing order " + orderId + " amount " + amount);

// Message interpolation can be deferred.
log.debug("Processing order {} amount {}", orderId, amount);

This defers formatting, not argument evaluation. buildDiagnosticState() still runs in log.debug("State: {}", buildDiagnosticState()). For expensive work, check log.isDebugEnabled() or use a supported supplier-based API with the appropriate evaluation behavior.

Keep the message stable and put identifiers in parameters or structured fields. That makes it easier to group the same event across many requests.

4. Choose a JSON layout and verify its schema

JSON is useful when the downstream system needs to query individual fields. It does not remove the need to agree on their names and types.

For an existing Log4j 2 configuration using the legacy JsonLayout, a console appender can emit one JSON object per line:

<Console name="Console" target="SYSTEM_OUT">
  <JsonLayout compact="true" eventEol="true" />
</Console>

This is an appender fragment, not a complete log4j2.xml. Reference it from a logger configuration, and supply the layout's required Jackson dependencies for your Log4j 2 version. Inspect the actual output fields, such as level, loggerName, message, and instant.

For a new configurable JSON schema, evaluate Log4j 2's JSON Template Layout and its separate layout module. A template can express a chosen field structure, including an ECS-compatible schema when that is useful. A schema designed for another backend may still need mapping for Guance. Log4j JSON layouts and JSON Template Layout

Test an ordinary message, an exception, and a record with request context. Confirm that each remains a single physical JSON line and that numbers are emitted as numbers. DataKit still needs the correct parsing and field-mapping configuration; producing JSON does not automatically normalize the event.

5. Scope MDC values to the request or operation

Mapped Diagnostic Context (MDC) can carry values such as a request ID or user ID without repeating them at every call. The layout must actually render those values.

For nested code, restore the previous values rather than clearing all context owned by other instrumentation:

import org.slf4j.MDC;

String previousTraceId = MDC.get("trace_id");
String previousUserId = MDC.get("user_id");
MDC.put("trace_id", traceId);
MDC.put("user_id", userId);
try {
    log.info("Order processed");
} finally {
    if (previousTraceId == null) {
        MDC.remove("trace_id");
    } else {
        MDC.put("trace_id", previousTraceId);
    }
    if (previousUserId == null) {
        MDC.remove("user_id");
    } else {
        MDC.put("user_id", previousUserId);
    }
}

Here traceId and userId come from the current application's context. Do not create a random value and label it as a distributed trace ID. If tracing instrumentation already owns the correlation keys, use its documented integration instead of overwriting them.

MDC is typically thread-local. Thread pools, asynchronous callbacks, and reactive execution need explicit propagation or instrumentation that supports those boundaries. A missing cleanup can leak one request's attributes into the next request on the same thread. Log4j Thread Context

For Guance correlation, verify the Agent's supported MDC injection and the layout's output, then compare a collected log with its actual trace. Installing an Agent alone does not guarantee that every logging backend and custom layout emits the expected field.

6. Let the deployment own log routing

For containers, stdout or stderr is often the simplest application boundary. The application emits events; the runtime and collection layer route and retain them. On hosts, managed file output can also be appropriate.

Configure the container runtime's rotation and the collector's input explicitly. “Writes to stdout” does not establish that logs are collected, retained, or protected against disk exhaustion.

The application still pays for record construction, formatting, and writes. A full pipe can block. DataKit can take on collection and forwarding, but it does not make application logging free or establish end-to-end durability by itself. Test the behavior when the destination is slow or unavailable.

7. Record useful events without exposing sensitive values

Useful events include authentication outcomes, permission decisions, resource changes, job results, and high-risk operations. Record enough stable context to investigate them without dumping the entire request or object.

Keep passwords, API keys, authentication tokens, full payment details, government identifiers, and unnecessary personal data out of logs. Mask the fields that genuinely need partial visibility before constructing the record.

A custom message formatter can help with a defined field schema, but overriding getFormattedMessage() does not automatically sanitize MDC, structured parameters, exception text, or every other layout field. Apply redaction to the actual paths that can carry sensitive values and test representative failures.

Collection-side Pipeline processing or sensitive-data controls can provide another layer. Verify the feature, fields, and execution stage in your Guance deployment before treating it as a guarantee that secrets never reach buffers or storage. Avoid building a policy around an unverified count of built-in detection rules.

8. Measure asynchronous logging and allocation costs

Synchronous appenders perform their output work on the caller's path. Asynchronous logging can move work to another thread, at the cost of buffering, shutdown coordination, and different failure behavior.

Log4j 2 supports a fully asynchronous context using a selector such as:

-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

Configure the required Disruptor dependency for the version you use. With an asynchronous context selector, use ordinary Logger and Root elements. For a mixed configuration, use AsyncLogger or AsyncRoot with the appropriate regular context instead. Do not combine the two approaches accidentally and add unnecessary asynchronous stages. Log4j asynchronous loggers

Test sustained throughput, queue saturation, configured full-buffer behavior, exception reporting, and shutdown flushing. Events still in memory can be lost when the process fails. Cross-thread ordering also should not be treated as a global causal sequence.

Allocation tuning is a separate decision. Settings such as log4j2.enableThreadlocals can enable reuse, but the message, context map, layout, appender, and filters all affect allocations. Log4j's web-application mode also has specific compatibility constraints with thread-local reuse. Do not force a setting into a servlet deployment simply because it helped a standalone benchmark. Garbage-free logging requirements

Use profiling to decide whether these changes matter. A high-throughput service can still be limited by serialization or the destination, and an audit trail may prioritize a delivery guarantee over minimum caller latency.

9. Include the exception object

Pass the throwable to the logger so the formatter can preserve its stack:

try {
    charge(order);
} catch (PaymentException e) {
    log.error("Payment failed orderId={}", order.getId(), e);
    throw e;
}

In this SLF4J call, the final exception argument is available as the throwable rather than another placeholder value. The application-specific charge, order, and PaymentException stand in for your own operation and types.

Logging only e.getMessage() loses the stack and causal chain. Also decide which layer owns the diagnostic record: logging and rethrowing at every layer creates duplicates that can distort error-count alerts.

10. Validate centralized collection before relying on it

A consistent event schema makes multi-instance investigation practical. For Guance:

  1. Configure DataKit for container output or host files, with stable source and service values.
  2. Parse JSON and map severity to status and the event timestamp to the correct time field.
  3. Search for known successful and failed events by service and correlation ID.
  4. Configure a detection rule for an actionable condition, such as more than ten error events in a minute, only after validating the query and workload.
  5. Set retention and index routing according to operational value and access needs. Evaluate historical archive access separately from interactive search.

The threshold above is an example, not a universal default. A service with two requests per minute and one with ten thousand need different detection logic.

Start with the DataKit log collector, then verify the result in the Log Explorer. Use log detection and index settings once the fields and queries are correct.

Frequently asked questions

What should we do about Log4Shell?

Inventory the actual Log4j artifacts and transitive versions in the deployed application, then use Apache's current security advisories and supported release guidance. Do not treat “2.17 or later” as a permanent security guarantee: the original vulnerability had version-specific fixes and subsequent issues required further updates. Dependency-update tools and software composition analysis help maintain that inventory. Apache Logging Services security advisories

Should a Spring Boot service replace Logback with Log4j 2?

Only for a concrete requirement. Keep the default when it works. If you switch, use the supported spring-boot-starter-log4j2 setup for your Boot version, exclude the default logging starter where required, and check the dependency tree for conflicting providers or bridges. Port and test the configuration as well as the dependencies.

Is synchronous logging enough for audit records?

It removes one in-process asynchronous buffer, but a completed write does not necessarily mean data is durably stored or acknowledged by the final backend. Define the required failure tolerance across application, file or pipe, collector, buffer, network, and backend. Validate that path instead of assuming DataKit can collect bytes that never left a failed process.

How should log levels differ between environments?

Keep application calls stable and vary configuration. Spring Boot's logback-spring.xml can use springProfile, and external properties can control levels. For incident-time DEBUG, set a scope and expiry, check the extra sensitive-data and throughput risk, and plan collection capacity. A cheaper retention tier does not remove the ingestion burst or guarantee that every debug event remains immediately searchable.

Related reading

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting