Logging in Java
BeginnerEffective logging uses SLF4J as the facade with Logback or Log4j2 as the implementation — structured, levelled, and never blocking.
Overview
Logging is critical for diagnosing production issues. The standard approach: SLF4J (Simple Logging Facade for Java) as the API — your code only depends on the facade, not the implementation. Logback or Log4j2 as the implementation configured at deployment time. Key concepts: log levels (TRACE/DEBUG/INFO/WARN/ERROR), parameterised logging to avoid string concatenation costs, MDC (Mapped Diagnostic Context) for correlating logs in concurrent systems, and structured/JSON logging for log aggregation tools.
SLF4J and Logback Basics
Always declare loggers as private static final. Use parameterised logging ({}) instead of string concatenation — the concatenation is skipped if the log level is not enabled.
Log levels: TRACE (very detailed debug), DEBUG (developer info), INFO (notable events), WARN (potential issues), ERROR (failures requiring attention). In production, typically INFO or WARN.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
// One logger per class — static final
private static final Logger log =
LoggerFactory.getLogger(OrderService.class);
public Order placeOrder(OrderRequest req) {
log.debug("Placing order for user={} item={}", req.getUserId(), req.getItemId());
try {
Order order = processOrder(req);
log.info("Order placed orderId={} userId={} amount={}",
order.getId(), req.getUserId(), order.getAmount());
return order;
} catch (InsufficientStockException e) {
log.warn("Stock insufficient for item={} requested={}",
req.getItemId(), req.getQuantity());
throw e;
} catch (Exception e) {
// Always log exception with message — not just e.getMessage()
log.error("Failed to place order for user={}", req.getUserId(), e);
throw new OrderException("Order processing failed", e);
}
}
}MDC for Request Tracing
MDC (Mapped Diagnostic Context) attaches key-value pairs to the current thread's log context. These are automatically included in every log statement from that thread — perfect for correlating all logs for a single HTTP request.
Always clear MDC after the request to prevent ThreadLocal leaks in thread pool environments.
import org.slf4j.MDC;
// In a servlet filter or Spring interceptor
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
String traceId = UUID.randomUUID().toString();
String userId = extractUserId((HttpServletRequest) req);
MDC.put("traceId", traceId);
MDC.put("userId", userId);
MDC.put("path", ((HttpServletRequest) req).getRequestURI());
try {
chain.doFilter(req, res);
} finally {
MDC.clear(); // MUST clear — thread pool reuses threads!
}
}
}
// logback.xml pattern — includes MDC fields automatically
// <pattern>%d{ISO8601} [%thread] [%X{traceId}] [%X{userId}] %-5level %logger - %msg%n</pattern>
// Every log statement in the request now includes traceId + userId:
// 2025-06-15T10:30:00 [http-1] [a1b2c3d4] [user-42] INFO OrderService - Order placed orderId=...Logback Configuration and Structured Logging
Logback is configured via logback.xml (classpath). Structured logging (JSON) makes logs parseable by tools like Elasticsearch, Splunk, and Loki.
Key practices: never log sensitive data (passwords, tokens, PII), use async appenders for high-throughput services, set separate log levels per package.
<!-- logback.xml — JSON structured logging -->
<configuration>
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- Includes MDC fields, stack traces, message, level, etc. -->
</encoder>
</appender>
<!-- Async wrapper — log calls return immediately (non-blocking) -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="JSON_CONSOLE" />
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
<!-- Per-package levels -->
<logger name="com.example" level="DEBUG"/>
<logger name="org.hibernate.SQL" level="DEBUG"/>
<logger name="org.springframework" level="WARN"/>
<root level="INFO">
<appender-ref ref="ASYNC" />
</root>
</configuration>
// Java output (JSON — parseable by log aggregators)
// {"@timestamp":"2025-06-15T10:30:00","level":"INFO",
// "logger":"OrderService","traceId":"a1b2c3","message":"Order placed",
// "orderId":"order-42","userId":"user-7","amount":99.99}Key Points to Remember
- Use SLF4J as the logging facade — never import Logback/Log4j2 classes in application code.
- Parameterised logging ({}) avoids string concatenation when the level is disabled.
- Always log exceptions as the last argument to include the full stack trace.
- MDC adds request-scoped context (traceId, userId) to every log line — clear it in finally.
- Use async appenders in production to prevent slow I/O from blocking application threads.
Practice Logging in Java in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhy should you use SLF4J instead of logging directly to Logback?
What is the difference between log.info("value: " + x) and log.info("value: {}", x)?
What is MDC and how does it help in a multi-threaded server?
Why must you clear MDC in a finally block?
What is structured logging and why is it preferred in cloud environments?
Ask Aria about Logging in Java
Your personal AI tutor — ask anything about this concept