Imagine you are a detective investigating a crime scene. Without evidence — fingerprints, security camera footage, witness statements — you would have no way to reconstruct what happened. Logging is the evidence trail for your application. It records what your program did, when it did it, and what went wrong.
Logging is the practice of recording messages from your application during runtime. These messages capture events, errors, state changes, and diagnostic information that help you understand your application’s behavior — especially when things go wrong in production at 3 AM and you cannot attach a debugger.
Every Java developer starts with System.out.println() for debugging. It works, but it is the equivalent of using a flashlight when you need a full surveillance system. Here is why it falls short in real applications:
| Feature | System.out.println | Logging Framework |
|---|---|---|
| Severity levels | None — everything looks the same | TRACE, DEBUG, INFO, WARN, ERROR |
| On/off control | Must delete or comment out lines | Change config file, no code changes |
| Output destination | Console only | Console, files, databases, remote servers |
| Timestamps | Must add manually | Automatic |
| Thread info | Must add manually | Automatic |
| Class/method info | Must add manually | Automatic |
| File rotation | Not possible | Automatic (e.g., daily, by size) |
| Performance | Always executes string building | Lazy evaluation, skip if level disabled |
| Production ready | No | Yes |
A simple rule: if you would not want it on a billboard, do not log it.
// BAD: System.out.println for debugging
public class BadDebugging {
public void processOrder(Order order) {
System.out.println("Processing order: " + order.getId()); // No timestamp
System.out.println("Order total: " + order.getTotal()); // No severity
System.out.println("Sending to payment..."); // Cannot turn off
// These println calls will clutter production logs forever
}
}
// GOOD: Proper logging
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GoodLogging {
private static final Logger log = LoggerFactory.getLogger(GoodLogging.class);
public void processOrder(Order order) {
log.info("Processing order id={}, total={}", order.getId(), order.getTotal());
log.debug("Sending order to payment gateway");
// Output: 2026-02-28 10:15:32.451 [main] INFO GoodLogging - Processing order id=12345, total=99.99
// In production, DEBUG messages are automatically suppressed
}
}
Java has multiple logging frameworks, which can be confusing for newcomers. Here is the landscape and how the pieces fit together:
| Framework | Type | Description | Status |
|---|---|---|---|
| java.util.logging (JUL) | Implementation | Built into the JDK since Java 1.4. No external dependencies needed. | Active but rarely used in modern projects |
| Log4j 1.x | Implementation | Was the de facto standard for years. Uses log4j.properties or log4j.xml. | END OF LIFE — Critical security vulnerability CVE-2021-44228. DO NOT USE. |
| Log4j 2 | Implementation | Complete rewrite of Log4j. Async logging, plugin architecture, modern design. | Active, maintained by Apache |
| Logback | Implementation | Created by the founder of Log4j as its successor. Native SLF4J implementation. | Active, default in Spring Boot |
| SLF4J | Facade (API) | Simple Logging Facade for Java. An abstraction layer — you code against SLF4J and swap implementations without changing code. | Active, industry standard |
Think of SLF4J like a universal remote control. You press the same buttons regardless of whether your TV is Samsung, LG, or Sony. Similarly, you write logging code using SLF4J’s API, and the actual logging is handled by whichever implementation (Logback, Log4j2) is on the classpath.
This means:
org.slf4j.Logger — never a specific implementation classFor most Java applications in 2026, use: SLF4J (facade) + Logback (implementation). This is the default in Spring Boot and the most widely adopted combination. This tutorial will focus primarily on this stack, but we will also cover JUL and Log4j2.
Log levels let you categorize messages by severity. You can then configure your application to show only messages at or above a certain level — for example, showing everything in development but only WARN and ERROR in production.
| Level | Purpose | When to Use | Example |
|---|---|---|---|
| TRACE | Extremely detailed diagnostic information | Step-by-step algorithm execution, variable values in loops, entering/exiting methods | log.trace("Entering calculateTax with amount={}", amount) |
| DEBUG | Detailed information useful during development | SQL queries executed, cache hit/miss, intermediate calculation results, request/response payloads | log.debug("Query returned {} rows in {}ms", count, elapsed) |
| INFO | Important business or application events | Application started, user logged in, order processed, scheduled job completed | log.info("Order {} placed successfully by user {}", orderId, userId) |
| WARN | Potentially harmful situations that are recoverable | Retry attempts, deprecated API usage, approaching disk/memory limits, fallback to default | log.warn("Payment gateway timeout, retrying (attempt {}/3)", attempt) |
| ERROR | Serious failures that need attention | Unhandled exceptions, failed database connections, data corruption, business rule violations that halt processing | log.error("Failed to process payment for order {}", orderId, exception) |
Levels form a hierarchy. When you set the log level to a certain value, all messages at that level and above are logged. Messages below that level are suppressed.
| Configured Level | TRACE | DEBUG | INFO | WARN | ERROR |
|---|---|---|---|---|---|
| TRACE | Yes | Yes | Yes | Yes | Yes |
| DEBUG | No | Yes | Yes | Yes | Yes |
| INFO | No | No | Yes | Yes | Yes |
| WARN | No | No | No | Yes | Yes |
| ERROR | No | No | No | No | Yes |
Rule of thumb: Development uses DEBUG or TRACE. Production uses INFO (or WARN for very high-throughput systems). You should be able to understand what your application is doing from INFO logs alone.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogLevelDemo {
private static final Logger log = LoggerFactory.getLogger(LogLevelDemo.class);
public void processPayment(String orderId, double amount) {
log.trace("Entering processPayment(orderId={}, amount={})", orderId, amount);
log.debug("Validating payment amount: {}", amount);
if (amount <= 0) {
log.warn("Invalid payment amount {} for order {}, using minimum $0.01", amount, orderId);
amount = 0.01;
}
try {
log.info("Processing payment of ${} for order {}", amount, orderId);
// ... payment logic ...
log.info("Payment successful for order {}", orderId);
} catch (Exception e) {
log.error("Payment failed for order {} with amount ${}", orderId, amount, e);
// The exception 'e' is passed as the LAST argument -- SLF4J will print the full stack trace
}
log.trace("Exiting processPayment for order {}", orderId);
}
}
// If level is set to INFO, output would be:
// 2026-02-28 10:30:00.123 [main] INFO LogLevelDemo - Processing payment of $49.99 for order ORD-001
// 2026-02-28 10:30:00.456 [main] INFO LogLevelDemo - Payment successful for order ORD-001
// (TRACE and DEBUG messages are suppressed)
Java includes a built-in logging framework in the java.util.logging package. It requires no external dependencies, which makes it a good starting point for learning and for simple applications where you want zero third-party libraries.
JUL uses its own level names, which differ from SLF4J:
| JUL Level | SLF4J Equivalent | Description |
|---|---|---|
| FINEST | TRACE | Highly detailed tracing |
| FINER | TRACE | Fairly detailed tracing |
| FINE | DEBUG | General debugging |
| CONFIG | - | Configuration info |
| INFO | INFO | Informational messages |
| WARNING | WARN | Potential problems |
| SEVERE | ERROR | Serious failures |
import java.util.logging.Level;
import java.util.logging.Logger;
public class JulExample {
// Create a logger named after the class
private static final Logger logger = Logger.getLogger(JulExample.class.getName());
public static void main(String[] args) {
// Basic logging at different levels
logger.info("Application starting");
logger.warning("Configuration file not found, using defaults");
logger.severe("Database connection failed!");
// Parameterized logging (JUL uses {0}, {1} style -- not {} like SLF4J)
String user = "alice";
int loginAttempts = 3;
logger.log(Level.INFO, "User {0} logged in after {1} attempts", new Object[]{user, loginAttempts});
// Logging an exception
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
logger.log(Level.SEVERE, "Division error occurred", e);
}
// Check if level is enabled before expensive operations
if (logger.isLoggable(Level.FINE)) {
logger.fine("Debug data: " + expensiveToString());
}
}
private static String expensiveToString() {
// Imagine this method is costly to call
return "detailed debug information";
}
}
// Output:
// Feb 28, 2026 10:45:00 AM JulExample main
// INFO: Application starting
// Feb 28, 2026 10:45:00 AM JulExample main
// WARNING: Configuration file not found, using defaults
// Feb 28, 2026 10:45:00 AM JulExample main
// SEVERE: Database connection failed!
While JUL works for simple cases, it has significant drawbacks compared to modern frameworks:
logger.log(Level.INFO, "msg {0}", new Object[]{val}) vs. SLF4J's log.info("msg {}", val)logging.properties file that is awkward to customize per-packageVerdict: Use JUL for quick scripts or when you truly cannot add dependencies. For any real application, use SLF4J + Logback.
SLF4J (Simple Logging Facade for Java) + Logback is the most popular logging stack in the Java ecosystem. Spring Boot uses it by default. SLF4J provides the API you code against; Logback provides the engine that does the actual logging.
org.slf4j slf4j-api 2.0.16 ch.qos.logback logback-classic 1.5.15
// Gradle: Add to build.gradle
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.16'
implementation 'ch.qos.logback:logback-classic:1.5.15'
}
The setup follows a consistent two-step pattern in every class:
org.slf4j.Logger and org.slf4j.LoggerFactoryprivate static final Logger field using LoggerFactory.getLogger(YourClass.class)Passing the class to getLogger() means the logger is named after your class, so log output shows exactly which class produced each message.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
// Step 1: Declare the logger -- always private static final
private static final Logger log = LoggerFactory.getLogger(UserService.class);
public User findUserById(long id) {
log.info("Looking up user with id={}", id);
User user = userRepository.findById(id);
if (user == null) {
log.warn("User not found for id={}", id);
return null;
}
log.debug("Found user: name={}, email={}", user.getName(), user.getEmail());
return user;
}
}
// Output with INFO level:
// 2026-02-28 10:30:00.123 [main] INFO c.e.service.UserService - Looking up user with id=42
// 2026-02-28 10:30:00.125 [main] WARN c.e.service.UserService - User not found for id=42
This is one of SLF4J's most important features. Never use string concatenation in log statements. Use {} placeholders instead.
Why? With string concatenation, Java builds the string every time, even if the log level is disabled. With placeholders, SLF4J only builds the string if the message will actually be logged.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ParameterizedLogging {
private static final Logger log = LoggerFactory.getLogger(ParameterizedLogging.class);
public void demonstrate(Order order) {
// BAD: String concatenation -- always builds the string, even if DEBUG is off
log.debug("Processing order " + order.getId() + " for user " + order.getUserId()
+ " with " + order.getItems().size() + " items");
// GOOD: Parameterized logging -- string built ONLY if DEBUG is enabled
log.debug("Processing order {} for user {} with {} items",
order.getId(), order.getUserId(), order.getItems().size());
// Multiple placeholders -- they are filled in order
log.info("User {} placed order {} with total ${}", "alice", "ORD-123", 99.99);
// Output: User alice placed order ORD-123 with total $99.99
// Logging exceptions -- exception is ALWAYS the last argument
try {
processPayment(order);
} catch (Exception e) {
// The exception goes last -- SLF4J recognizes it and prints the full stack trace
log.error("Payment failed for order {}", order.getId(), e);
// Output:
// 2026-02-28 10:30:00.123 [main] ERROR ParameterizedLogging - Payment failed for order ORD-123
// java.lang.RuntimeException: Insufficient funds
// at ParameterizedLogging.processPayment(ParameterizedLogging.java:35)
// at ParameterizedLogging.demonstrate(ParameterizedLogging.java:22)
// ...
}
}
}
When logging exceptions, always pass the exception object as the last argument. SLF4J will automatically print the full stack trace. This is the single most important logging pattern to get right.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExceptionLogging {
private static final Logger log = LoggerFactory.getLogger(ExceptionLogging.class);
public void demonstrateExceptionLogging() {
try {
riskyOperation();
} catch (Exception e) {
// BAD: Loses the stack trace entirely
log.error("Something failed");
// BAD: Only logs the exception message, no stack trace
log.error("Something failed: " + e.getMessage());
// BAD: Converts stack trace to string manually -- ugly and loses structure
log.error("Something failed: " + e.toString());
// GOOD: Pass exception as the last argument -- full stack trace is printed
log.error("Something failed", e);
// GOOD: With context AND exception -- placeholders first, exception last
log.error("Failed to process order {} for user {}", orderId, userId, e);
// SLF4J knows the last argument is an exception because {} count (2) < argument count (3)
}
}
}
Log4j2 is the modern successor to Log4j 1.x, built from the ground up by Apache. It is a completely different codebase from Log4j 1.x.
Critical Warning: Log4j 1.x (versions 1.2.x) reached end of life in 2015 and has the critical Log4Shell vulnerability (CVE-2021-44228), one of the most severe security vulnerabilities in Java history. If you are using Log4j 1.x, you must migrate immediately. Log4j2 (versions 2.x) is the safe, modern version.
org.slf4j slf4j-api 2.0.16 org.apache.logging.log4j log4j-slf4j2-impl 2.24.3 org.apache.logging.log4j log4j-core 2.24.3
Place this file in src/main/resources/log4j2.xml:
Log4j2's standout feature is its async logging capability using the LMAX Disruptor library. This can dramatically improve performance in high-throughput applications by logging on a separate thread.
com.lmax disruptor 4.0.0
| Feature | Logback | Log4j2 |
|---|---|---|
| Spring Boot default | Yes | No (requires exclusion + config) |
| Async performance | Good (AsyncAppender) | Excellent (LMAX Disruptor) |
| Garbage-free logging | No | Yes (reduces GC pauses) |
| Lambda support | No | Yes (lazy message construction) |
| Plugin architecture | Limited | Extensive |
| Community adoption | Higher (Spring ecosystem) | Strong (Apache ecosystem) |
| Configuration reload | Yes | Yes (automatic) |
Bottom line: Use Logback for most applications, especially with Spring Boot. Choose Log4j2 if you need maximum throughput with async logging (e.g., high-frequency trading, real-time data pipelines).
Logback is configured via an XML file named logback.xml (or logback-spring.xml in Spring Boot) placed in src/main/resources/. The configuration has three main components:
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n logs/application.log %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n logs/application.log logs/application.%d{yyyy-MM-dd}.%i.log.gz 10MB 30 1GB %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
The pattern string controls how each log message is formatted. Here are the most common conversion specifiers:
| Specifier | Output | Example |
|---|---|---|
%d{pattern} |
Date/time | %d{yyyy-MM-dd HH:mm:ss.SSS} = 2026-02-28 10:30:00.123 |
%level or %-5level |
Log level (padded to 5 chars) | INFO, DEBUG, ERROR |
%logger{n} |
Logger name (abbreviated to n chars) | %logger{36} = c.e.service.UserService |
%msg |
The log message | User logged in successfully |
%n |
Newline (platform-specific) | \n or \r\n |
%thread |
Thread name | main, http-nio-8080-exec-1 |
%class |
Full class name (slow) | com.example.service.UserService |
%method |
Method name (slow) | findUserById |
%line |
Line number (slow) | 42 |
%X{key} |
MDC value | %X{requestId} = abc-123 |
%highlight() |
ANSI color by level (console only) | ERROR in red, WARN in yellow |
Performance note: %class, %method, and %line are computed by generating a stack trace, which is expensive. Avoid them in production patterns.
// Development pattern (human-readable with colors)
%d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{36}) - %msg%n
// Output: 10:30:00.123 INFO c.e.service.UserService - Order placed
// Production pattern (full detail, no color)
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
// Output: 2026-02-28 10:30:00.123 [http-nio-8080-exec-1] INFO c.e.service.UserService - Order placed
// Production with MDC (request tracking)
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [requestId=%X{requestId}] - %msg%n
// Output: 2026-02-28 10:30:00.123 [http-nio-8080-exec-1] INFO c.e.service.UserService [requestId=abc-123-def] - Order placed
// JSON pattern for ELK/Splunk (see Section 12)
{"timestamp":"%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}","level":"%level","logger":"%logger","thread":"%thread","message":"%msg","requestId":"%X{requestId}"}%n
One of the most powerful configuration features is setting different log levels for different packages. This lets you see detailed logs from your code while keeping framework noise quiet.
%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n
The format of your log messages matters more than you might think. In development, you want human-readable output. In production, you often want structured (JSON) output that can be parsed by log aggregation tools like the ELK stack (Elasticsearch, Logstash, Kibana) or Splunk.
| Environment | Pattern | Why |
|---|---|---|
| Development | %d{HH:mm:ss} %-5level %logger{20} - %msg%n |
Short, readable, fast to scan |
| Staging | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n |
Full detail for debugging issues that match production |
| Production (text) | %d{ISO8601} [%thread] %-5level %logger{36} [%X{requestId}] - %msg%n |
ISO timestamps, MDC context, full logger names |
| Production (JSON) | Use Logstash encoder (see below) | Machine-parseable for log aggregation |
For production environments using ELK stack, Splunk, or Datadog, structured JSON logs are essential. Each log line is a valid JSON object that these tools can parse, index, and search.
net.logstash.logback logstash-logback-encoder 8.0
requestId userId
With JSON logging, each log line looks like this:
{"@timestamp":"2026-02-28T10:30:00.123Z","@version":"1","message":"Order ORD-123 placed successfully","logger_name":"com.myapp.service.OrderService","thread_name":"http-nio-8080-exec-1","level":"INFO","requestId":"abc-123-def","userId":"user-42"}
This structured output means you can search for all logs where userId="user-42" or find all ERROR-level messages for a specific requestId -- something that is extremely difficult with plain text logs.
Imagine you are a doctor in a busy emergency room, treating 20 patients simultaneously. Without patient wristbands (IDs), you would have no way to tell which vitals belong to which patient. MDC is the wristband for your application's requests.
MDC (Mapped Diagnostic Context) lets you attach key-value pairs to the current thread. These values are then automatically included in every log message produced by that thread. This is invaluable in multi-threaded web applications where dozens of requests are processed concurrently.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class MdcExample {
private static final Logger log = LoggerFactory.getLogger(MdcExample.class);
public void handleRequest(String requestId, String userId) {
// Put values into MDC at the start of the request
MDC.put("requestId", requestId);
MDC.put("userId", userId);
try {
log.info("Request received");
processOrder();
sendConfirmation();
log.info("Request completed successfully");
} finally {
// CRITICAL: Always clear MDC when the request is done
// Threads are reused in thread pools -- leftover MDC values leak into other requests!
MDC.clear();
}
}
private void processOrder() {
// This log line automatically includes requestId and userId from MDC
log.info("Processing order");
// Output: 2026-02-28 10:30:00.123 [http-exec-1] INFO MdcExample [requestId=abc-123, userId=user-42] - Processing order
}
private void sendConfirmation() {
log.info("Sending confirmation email");
// Output: 2026-02-28 10:30:00.456 [http-exec-1] INFO MdcExample [requestId=abc-123, userId=user-42] - Sending confirmation email
}
}
In real applications, you set up MDC in a servlet filter or Spring interceptor so that every request automatically gets a unique ID. You never have to manually add MDC in individual controllers or services.
import org.slf4j.MDC;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
public class LoggingFilter implements Filter {
private static final String REQUEST_ID = "requestId";
private static final String USER_ID = "userId";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
// Generate or extract request ID
String requestId = httpRequest.getHeader("X-Request-ID");
if (requestId == null || requestId.isBlank()) {
requestId = UUID.randomUUID().toString().substring(0, 8);
}
// Set MDC values
MDC.put(REQUEST_ID, requestId);
// Extract user from security context (if authenticated)
String userId = extractUserId(httpRequest);
if (userId != null) {
MDC.put(USER_ID, userId);
}
// Continue processing the request
chain.doFilter(request, response);
} finally {
// Always clean up to prevent thread pool contamination
MDC.clear();
}
}
private String extractUserId(HttpServletRequest request) {
// In a real app, extract from security context or JWT token
return request.getRemoteUser();
}
}
To display MDC values in your log output, use the %X{key} specifier in your pattern:
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [req=%X{requestId} user=%X{userId}] - %msg%n
These are the logging practices that separate junior developers from senior developers. Follow these in every Java project.
Always code against the SLF4J API, never a specific implementation. This gives you the freedom to switch between Logback, Log4j2, or any future implementation without touching your code.
// BAD: Coupling to a specific implementation import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; private static final Logger log = LogManager.getLogger(MyClass.class); // BAD: Using java.util.logging directly import java.util.logging.Logger; private static final Logger log = Logger.getLogger(MyClass.class.getName()); // GOOD: SLF4J facade -- works with ANY implementation import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger log = LoggerFactory.getLogger(MyClass.class);
This is the single most common logging mistake in Java code reviews. Never concatenate strings in log statements.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ParameterizedBestPractice {
private static final Logger log = LoggerFactory.getLogger(ParameterizedBestPractice.class);
public void process(Order order) {
// BAD: String concatenation -- always builds the string even if DEBUG is off
log.debug("Order " + order.getId() + " has " + order.getItems().size() + " items totaling $" + order.getTotal());
// This calls order.getId(), order.getItems().size(), and order.getTotal()
// PLUS concatenates 5 strings -- all wasted work if DEBUG is disabled
// GOOD: Parameterized -- only builds string if DEBUG is enabled
log.debug("Order {} has {} items totaling ${}", order.getId(), order.getItems().size(), order.getTotal());
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AppropriateLevel {
private static final Logger log = LoggerFactory.getLogger(AppropriateLevel.class);
public void processOrder(Order order) {
// TRACE: Very fine-grained, method entry/exit
log.trace("Entering processOrder with order={}", order);
// DEBUG: Technical detail helpful during development
log.debug("Validating order items against inventory");
// INFO: Business event -- this is what operations teams monitor
log.info("Order {} placed by user {} for ${}", order.getId(), order.getUserId(), order.getTotal());
// WARN: Something unusual but recoverable
if (order.getTotal() > 10000) {
log.warn("High-value order {} for ${} -- flagged for review", order.getId(), order.getTotal());
}
// ERROR: Something failed -- needs human attention
try {
chargePayment(order);
} catch (PaymentException e) {
log.error("Payment failed for order {} with amount ${}", order.getId(), order.getTotal(), e);
}
}
}
A log message without context is like a clue without a case number. Always include the relevant IDs and values that will help you investigate.
// BAD: No context -- useless for debugging
log.error("Payment failed");
log.info("User logged in");
log.warn("Retry attempt");
// GOOD: Context-rich -- you can trace exactly what happened
log.error("Payment failed for order={} user={} amount=${} gateway={}", orderId, userId, amount, gateway);
log.info("User {} logged in from IP {} using {}", userId, ipAddress, userAgent);
log.warn("Retry attempt {}/{} for order={} after {}ms delay", attempt, maxRetries, orderId, delay);
While parameterized logging avoids string concatenation overhead, it does not avoid the cost of computing the arguments. If computing an argument is expensive, guard the log statement.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExpensiveLogging {
private static final Logger log = LoggerFactory.getLogger(ExpensiveLogging.class);
public void processLargeDataSet(List records) {
// BAD: computeStats() is called EVERY TIME, even when DEBUG is off
log.debug("Dataset statistics: {}", computeStats(records));
// computeStats() might iterate over millions of records
// GOOD: Guard expensive computation
if (log.isDebugEnabled()) {
log.debug("Dataset statistics: {}", computeStats(records));
}
// ALSO GOOD for simple arguments -- no guard needed
log.debug("Processing {} records", records.size());
// records.size() is O(1) and trivially cheap
}
private String computeStats(List records) {
// Imagine this iterates the entire list, computes averages, etc.
return "min=1, max=100, avg=42.5, stddev=12.3";
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SensitiveDataLogging {
private static final Logger log = LoggerFactory.getLogger(SensitiveDataLogging.class);
public void authenticateUser(String username, String password) {
// BAD: NEVER log passwords
log.info("Login attempt: user={}, password={}", username, password);
// GOOD: Log the event without sensitive data
log.info("Login attempt for user={}", username);
}
public void processPayment(String creditCardNumber, double amount) {
// BAD: NEVER log full credit card numbers
log.info("Charging card {} for ${}", creditCardNumber, amount);
// GOOD: Mask the sensitive data
String masked = maskCreditCard(creditCardNumber);
log.info("Charging card {} for ${}", masked, amount);
// Output: Charging card ****-****-****-4242 for $99.99
}
private String maskCreditCard(String number) {
if (number == null || number.length() < 4) return "****";
return "****-****-****-" + number.substring(number.length() - 4);
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoopLogging {
private static final Logger log = LoggerFactory.getLogger(LoopLogging.class);
public void processRecords(List records) {
// BAD: Logging inside a loop with 1 million records = 1 million log lines
for (Record record : records) {
log.debug("Processing record: {}", record.getId());
process(record);
}
// GOOD: Log summary information
log.info("Starting to process {} records", records.size());
int successCount = 0;
int failCount = 0;
for (Record record : records) {
try {
process(record);
successCount++;
} catch (Exception e) {
failCount++;
// Only log individual failures -- these are exceptional
log.warn("Failed to process record {}: {}", record.getId(), e.getMessage());
}
}
log.info("Completed processing: {} succeeded, {} failed out of {} total",
successCount, failCount, records.size());
}
}
| Practice | Do | Do Not |
|---|---|---|
| API | Use SLF4J facade | Use implementation-specific API (JUL, Log4j directly) |
| Parameters | log.info("User {}", userId) |
log.info("User " + userId) |
| Exceptions | log.error("Msg", exception) |
log.error("Msg: " + e.getMessage()) |
| Levels | INFO for business events, DEBUG for technical details | Everything at INFO or everything at DEBUG |
| Context | Include IDs, amounts, counts | Vague messages like "Error occurred" |
| MDC | Set requestId/userId in filter | Manually add IDs to every message |
| Sensitive data | Mask or omit | Log passwords, credit cards, tokens |
| Loops | Log summary before/after | Log every iteration |
| Guards | if (log.isDebugEnabled()) for expensive computation |
Call expensive methods as log arguments |
| Logger declaration | private static final Logger |
Creating new Logger per method call |
Every experienced Java developer has made these mistakes. Recognizing them in code reviews will make you a better developer.
// MISTAKE: System.out.println scattered through production code
public class OrderService {
public void placeOrder(Order order) {
System.out.println("Placing order: " + order); // No level, no timestamp, no thread
System.out.println("Validating..."); // Cannot turn off without deleting
System.out.println("Done!"); // Goes to stdout only
}
}
// FIX: Use a proper logger
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public void placeOrder(Order order) {
log.info("Placing order {}", order.getId());
log.debug("Validating order items");
log.info("Order {} placed successfully", order.getId());
}
}
// MISTAKE: String concatenation is evaluated even when the level is disabled
log.debug("User " + user.getName() + " has " + user.getOrders().size() + " orders"
+ " totaling $" + calculateTotal(user.getOrders()));
// If DEBUG is off, Java still:
// 1. Calls user.getName()
// 2. Calls user.getOrders().size()
// 3. Calls calculateTotal() -- potentially expensive!
// 4. Concatenates 5 strings
// 5. Throws the result away
// FIX: Use parameterized logging
log.debug("User {} has {} orders totaling ${}",
user.getName(), user.getOrders().size(), calculateTotal(user.getOrders()));
// With parameterized logging, if DEBUG is off, SLF4J skips building the string.
// NOTE: The arguments are still evaluated. For expensive arguments, use isDebugEnabled() guard.
try {
connectToDatabase();
} catch (SQLException e) {
// MISTAKE 1: Swallowing the exception entirely
// (empty catch block -- the worst possible thing)
// MISTAKE 2: Only logging the message, losing the stack trace
log.error("Database error: " + e.getMessage());
// Output: Database error: Connection refused
// WHERE did it fail? Which line? What was the root cause? All lost.
// MISTAKE 3: Using printStackTrace() instead of logging
e.printStackTrace();
// This goes to System.err, bypassing the logging framework entirely.
// No timestamp, no level, no file output, no MDC.
// CORRECT: Pass the exception as the last argument
log.error("Failed to connect to database", e);
// Output includes the full stack trace:
// 2026-02-28 10:30:00.123 [main] ERROR DatabaseService - Failed to connect to database
// java.sql.SQLException: Connection refused
// at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:839)
// at com.mysql.cj.jdbc.ConnectionImpl.(ConnectionImpl.java:453)
// at DatabaseService.connectToDatabase(DatabaseService.java:42)
// ...
// Caused by: java.net.ConnectException: Connection refused (Connection refused)
// at java.base/java.net.PlainSocketImpl.socketConnect(Native Method)
// ...
}
// MISTAKE: Logging too much -- "log diarrhea"
public double calculateTax(double amount, String state) {
log.info("calculateTax called"); // Noise
log.info("amount = " + amount); // Noise + concatenation
log.info("state = " + state); // Noise + concatenation
double rate = getTaxRate(state);
log.info("tax rate = " + rate); // Noise
double tax = amount * rate;
log.info("tax = " + tax); // Noise
log.info("returning tax"); // Noise
return tax;
}
// This method generates 6 log lines for a simple calculation.
// Multiply by 1000 requests/second and you have 6000 lines/second of noise.
// MISTAKE: Logging too little
public double calculateTax(double amount, String state) {
return amount * getTaxRate(state);
// No logging at all. If tax calculations are wrong, where do you start?
}
// CORRECT: Log meaningful events at the right level
public double calculateTax(double amount, String state) {
log.debug("Calculating tax for amount={} state={}", amount, state);
double rate = getTaxRate(state);
double tax = amount * rate;
log.debug("Tax calculated: amount={} state={} rate={} tax={}", amount, state, rate, tax);
return tax;
}
// Two DEBUG lines that can be turned off in production but enabled when needed.
// MISTAKE: Logging user data verbatim
public void registerUser(UserRegistration reg) {
log.info("Registering user: {}", reg);
// If UserRegistration.toString() includes password, SSN, or credit card... game over.
// Log files are often stored in plain text, backed up to multiple servers,
// and accessed by many team members.
}
// CORRECT: Log only safe, relevant fields
public void registerUser(UserRegistration reg) {
log.info("Registering user: email={}", reg.getEmail());
// Or override toString() to exclude sensitive fields:
// @Override public String toString() {
// return "UserRegistration{email='" + email + "', name='" + name + "'}";
// // password, ssn, creditCard intentionally excluded
// }
}
// MISTAKE: Still using Log4j 1.x (versions 1.2.x) import org.apache.log4j.Logger; // <-- This is Log4j 1.x -- SECURITY VULNERABILITY! // Log4j 1.x reached End of Life in August 2015. // CVE-2021-44228 (Log4Shell) allows Remote Code Execution -- attackers can take over your server. // This is a CRITICAL vulnerability rated 10.0 out of 10.0 on the CVSS scale. // FIX: Migrate to SLF4J + Logback (or Log4j2) // Step 1: Remove log4j 1.x dependency // Step 2: Add SLF4J + Logback dependencies (see Section 5) // Step 3: Replace imports: import org.slf4j.Logger; // <-- SLF4J facade import org.slf4j.LoggerFactory; // Step 4: Replace logger creation: // OLD: private static final Logger log = Logger.getLogger(MyClass.class); // NEW: private static final Logger log = LoggerFactory.getLogger(MyClass.class); // Step 5: Replace log4j.properties with logback.xml (see Section 7) // Step 6: The logging method calls (log.info, log.error, etc.) are almost identical
Production logging has different requirements than development logging. In production, your logs are the primary tool for understanding what is happening across hundreds of servers processing thousands of requests per second.
In production, logs should be machine-parseable. Plain text logs like 2026-02-28 10:30 INFO OrderService - Order placed are hard for log aggregation tools to parse reliably. JSON format solves this.
With JSON logging, tools like Elasticsearch, Splunk, Datadog, and Grafana Loki can index every field and let you write queries like:
The ELK stack (Elasticsearch, Logstash, Kibana) is the most popular open-source log aggregation platform:
| Component | Role | Description |
|---|---|---|
| Elasticsearch | Store and search | Distributed search engine that indexes log data for fast queries |
| Logstash | Collect and transform | Ingests logs from multiple sources, parses them, and sends to Elasticsearch |
| Kibana | Visualize | Web UI for searching logs, building dashboards, and setting up alerts |
Without log rotation, log files grow until they fill the disk and your application crashes. Always configure rolling policies:
logs/application.log logs/application.%d{yyyy-MM-dd}.%i.log.gz 50MB 90 5GB %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{requestId}] - %msg%n
| Concern | Solution |
|---|---|
| High-throughput logging blocks threads | Use async appenders (Logback's AsyncAppender or Log4j2's AsyncLogger) |
| Disk I/O bottleneck | Write to a local buffer, ship to remote collector (Logstash, Fluentd) |
| Large stack traces | Logback automatically shortens repeated stack frames with ... 42 common frames omitted |
| GC pressure from log string building | Use parameterized logging ({}), consider Log4j2's garbage-free mode |
| Log file size | Use GZIP compression on rolled files (.log.gz) |
| Too many DEBUG/TRACE in production | Set root level to INFO, use DEBUG only for your packages when investigating |
logs/application.log logs/application.%d{yyyy-MM-dd}.log.gz 30 %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 1024 0 false
Let us tie everything together with a realistic, production-quality example. This OrderService demonstrates all the logging concepts we have covered: appropriate log levels, parameterized messages, exception handling, MDC for request tracking, and best practices throughout.
org.slf4j slf4j-api 2.0.16 ch.qos.logback logback-classic 1.5.15
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} [orderId=%X{orderId} user=%X{userId}] - %msg%n logs/orders.log logs/orders.%d{yyyy-MM-dd}.%i.log.gz 10MB 30 1GB %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [orderId=%X{orderId} user=%X{userId}] - %msg%n
package com.example.orders;
import java.util.List;
public class Order {
private final String id;
private final String userId;
private final List items;
private double total;
private OrderStatus status;
public Order(String id, String userId, List items) {
this.id = id;
this.userId = userId;
this.items = items;
this.total = items.stream().mapToDouble(OrderItem::getSubtotal).sum();
this.status = OrderStatus.PENDING;
}
public String getId() { return id; }
public String getUserId() { return userId; }
public List getItems() { return items; }
public double getTotal() { return total; }
public OrderStatus getStatus() { return status; }
public void setStatus(OrderStatus status) { this.status = status; }
public void setTotal(double total) { this.total = total; }
// toString excludes any sensitive user data
@Override
public String toString() {
return "Order{id='" + id + "', items=" + items.size() + ", total=" + total + ", status=" + status + "}";
}
}
enum OrderStatus { PENDING, VALIDATED, PAID, SHIPPED, CANCELLED }
class OrderItem {
private final String productName;
private final int quantity;
private final double price;
public OrderItem(String productName, int quantity, double price) {
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
public String getProductName() { return productName; }
public int getQuantity() { return quantity; }
public double getPrice() { return price; }
public double getSubtotal() { return quantity * price; }
}
package com.example.orders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.List;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
private static final double HIGH_VALUE_THRESHOLD = 1000.0;
private static final double TAX_RATE = 0.08;
private static final double DISCOUNT_THRESHOLD = 500.0;
private static final double DISCOUNT_RATE = 0.10;
/**
* Process an order end-to-end with proper logging at every stage.
*/
public void processOrder(Order order) {
// Set MDC context for this order -- all subsequent log lines include these values
MDC.put("orderId", order.getId());
MDC.put("userId", order.getUserId());
long startTime = System.currentTimeMillis();
try {
// INFO: Business event -- order processing started
log.info("Order processing started: {} items, total=${}",
order.getItems().size(), order.getTotal());
// Step 1: Validate
validateOrder(order);
// Step 2: Apply discounts
applyDiscounts(order);
// Step 3: Calculate tax
calculateTax(order);
// Step 4: Process payment
processPayment(order);
// Step 5: Ship
shipOrder(order);
long elapsed = System.currentTimeMillis() - startTime;
// INFO: Business event -- order completed with timing
log.info("Order processing completed successfully in {}ms, finalTotal=${}",
elapsed, order.getTotal());
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - startTime;
order.setStatus(OrderStatus.CANCELLED);
// ERROR: Something went wrong -- include the exception for stack trace
log.error("Order processing failed after {}ms", elapsed, e);
} finally {
// CRITICAL: Always clear MDC to prevent thread contamination
MDC.clear();
}
}
private void validateOrder(Order order) {
log.debug("Validating order");
if (order.getItems() == null || order.getItems().isEmpty()) {
// ERROR: Invalid input -- this should not happen if upstream validation works
log.error("Order has no items");
throw new IllegalArgumentException("Order must have at least one item");
}
for (OrderItem item : order.getItems()) {
if (item.getQuantity() <= 0) {
log.error("Invalid quantity {} for product '{}'",
item.getQuantity(), item.getProductName());
throw new IllegalArgumentException("Quantity must be positive for: " + item.getProductName());
}
if (item.getPrice() < 0) {
log.error("Negative price ${} for product '{}'",
item.getPrice(), item.getProductName());
throw new IllegalArgumentException("Price cannot be negative for: " + item.getProductName());
}
}
order.setStatus(OrderStatus.VALIDATED);
// DEBUG: Technical detail about validation result
log.debug("Order validated: {} items passed all checks", order.getItems().size());
}
private void applyDiscounts(Order order) {
double originalTotal = order.getTotal();
log.debug("Checking discounts for total=${}", originalTotal);
if (originalTotal >= DISCOUNT_THRESHOLD) {
double discount = originalTotal * DISCOUNT_RATE;
order.setTotal(originalTotal - discount);
// INFO: Business event -- discount applied (operations wants to track this)
log.info("Discount applied: {}% off ${} = -${}, newTotal=${}",
(int)(DISCOUNT_RATE * 100), originalTotal, discount, order.getTotal());
} else {
log.debug("No discount applied: total ${} below threshold ${}",
originalTotal, DISCOUNT_THRESHOLD);
}
}
private void calculateTax(Order order) {
double beforeTax = order.getTotal();
double tax = beforeTax * TAX_RATE;
order.setTotal(beforeTax + tax);
// DEBUG: Technical calculation detail
log.debug("Tax calculated: ${} * {} = ${}, newTotal=${}",
beforeTax, TAX_RATE, tax, order.getTotal());
}
private void processPayment(Order order) {
// INFO: Business event -- payment attempt
log.info("Processing payment of ${}", order.getTotal());
// WARN: Flag high-value orders
if (order.getTotal() > HIGH_VALUE_THRESHOLD) {
log.warn("High-value order detected: ${} exceeds threshold ${}",
order.getTotal(), HIGH_VALUE_THRESHOLD);
}
// Simulate payment processing
try {
simulatePaymentGateway(order);
order.setStatus(OrderStatus.PAID);
log.info("Payment processed successfully for ${}", order.getTotal());
} catch (RuntimeException e) {
// ERROR: Payment failed -- include the exception
log.error("Payment gateway rejected transaction for ${}", order.getTotal(), e);
throw e;
}
}
private void simulatePaymentGateway(Order order) {
// Simulate: orders with total over $5000 fail (for demo purposes)
if (order.getTotal() > 5000) {
throw new RuntimeException("Payment declined: exceeds single transaction limit");
}
log.debug("Payment gateway returned: APPROVED");
}
private void shipOrder(Order order) {
log.info("Initiating shipment");
order.setStatus(OrderStatus.SHIPPED);
log.info("Order shipped to user {}", order.getUserId());
}
}
package com.example.orders;
import java.util.List;
public class OrderApp {
public static void main(String[] args) {
OrderService service = new OrderService();
// Scenario 1: Normal order
System.out.println("=== Scenario 1: Normal Order ===");
Order normalOrder = new Order("ORD-001", "alice",
List.of(new OrderItem("Laptop Stand", 1, 45.99),
new OrderItem("USB-C Cable", 2, 12.99)));
service.processOrder(normalOrder);
System.out.println();
// Scenario 2: High-value order with discount
System.out.println("=== Scenario 2: High-Value Order ===");
Order highValue = new Order("ORD-002", "bob",
List.of(new OrderItem("MacBook Pro", 1, 2499.00),
new OrderItem("AppleCare+", 1, 399.00)));
service.processOrder(highValue);
System.out.println();
// Scenario 3: Order that exceeds payment limit (will fail)
System.out.println("=== Scenario 3: Failed Payment ===");
Order tooExpensive = new Order("ORD-003", "charlie",
List.of(new OrderItem("Server Rack", 3, 2500.00)));
service.processOrder(tooExpensive);
System.out.println();
// Scenario 4: Invalid order (empty items)
System.out.println("=== Scenario 4: Invalid Order ===");
Order emptyOrder = new Order("ORD-004", "dave", List.of());
service.processOrder(emptyOrder);
}
}
=== Scenario 1: Normal Order ===
2026-02-28 10:30:00.001 [main] INFO c.e.orders.OrderService [orderId=ORD-001 user=alice] - Order processing started: 2 items, total=$71.97
2026-02-28 10:30:00.002 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Validating order
2026-02-28 10:30:00.002 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Order validated: 2 items passed all checks
2026-02-28 10:30:00.002 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Checking discounts for total=$71.97
2026-02-28 10:30:00.002 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - No discount applied: total $71.97 below threshold $500.0
2026-02-28 10:30:00.003 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Tax calculated: $71.97 * 0.08 = $5.7576, newTotal=$77.7276
2026-02-28 10:30:00.003 [main] INFO c.e.orders.OrderService [orderId=ORD-001 user=alice] - Processing payment of $77.7276
2026-02-28 10:30:00.003 [main] DEBUG c.e.orders.OrderService [orderId=ORD-001 user=alice] - Payment gateway returned: APPROVED
2026-02-28 10:30:00.003 [main] INFO c.e.orders.OrderService [orderId=ORD-001 user=alice] - Payment processed successfully for $77.7276
2026-02-28 10:30:00.003 [main] INFO c.e.orders.OrderService [orderId=ORD-001 user=alice] - Initiating shipment
2026-02-28 10:30:00.003 [main] INFO c.e.orders.OrderService [orderId=ORD-001 user=alice] - Order shipped to user alice
2026-02-28 10:30:00.004 [main] INFO c.e.orders.OrderService [orderId=ORD-001 user=alice] - Order processing completed successfully in 3ms, finalTotal=$77.7276
=== Scenario 2: High-Value Order ===
2026-02-28 10:30:00.005 [main] INFO c.e.orders.OrderService [orderId=ORD-002 user=bob] - Order processing started: 2 items, total=$2898.0
2026-02-28 10:30:00.005 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Validating order
2026-02-28 10:30:00.005 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Order validated: 2 items passed all checks
2026-02-28 10:30:00.005 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Checking discounts for total=$2898.0
2026-02-28 10:30:00.005 [main] INFO c.e.orders.OrderService [orderId=ORD-002 user=bob] - Discount applied: 10% off $2898.0 = -$289.8, newTotal=$2608.2
2026-02-28 10:30:00.006 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Tax calculated: $2608.2 * 0.08 = $208.656, newTotal=$2816.856
2026-02-28 10:30:00.006 [main] INFO c.e.orders.OrderService [orderId=ORD-002 user=bob] - Processing payment of $2816.856
2026-02-28 10:30:00.006 [main] WARN c.e.orders.OrderService [orderId=ORD-002 user=bob] - High-value order detected: $2816.856 exceeds threshold $1000.0
2026-02-28 10:30:00.006 [main] DEBUG c.e.orders.OrderService [orderId=ORD-002 user=bob] - Payment gateway returned: APPROVED
2026-02-28 10:30:00.006 [main] INFO c.e.orders.OrderService [orderId=ORD-002 user=bob] - Payment processed successfully for $2816.856
2026-02-28 10:30:00.006 [main] INFO c.e.orders.OrderService [orderId=ORD-002 user=bob] - Initiating shipment
2026-02-28 10:30:00.006 [main] INFO c.e.orders.OrderService [orderId=ORD-002 user=bob] - Order shipped to user bob
2026-02-28 10:30:00.007 [main] INFO c.e.orders.OrderService [orderId=ORD-002 user=bob] - Order processing completed successfully in 2ms, finalTotal=$2816.856
=== Scenario 3: Failed Payment ===
2026-02-28 10:30:00.008 [main] INFO c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Order processing started: 3 items, total=$7500.0
2026-02-28 10:30:00.008 [main] DEBUG c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Validating order
2026-02-28 10:30:00.008 [main] DEBUG c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Order validated: 3 items passed all checks
2026-02-28 10:30:00.008 [main] DEBUG c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Checking discounts for total=$7500.0
2026-02-28 10:30:00.008 [main] INFO c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Discount applied: 10% off $7500.0 = -$750.0, newTotal=$6750.0
2026-02-28 10:30:00.009 [main] DEBUG c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Tax calculated: $6750.0 * 0.08 = $540.0, newTotal=$7290.0
2026-02-28 10:30:00.009 [main] INFO c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Processing payment of $7290.0
2026-02-28 10:30:00.009 [main] WARN c.e.orders.OrderService [orderId=ORD-003 user=charlie] - High-value order detected: $7290.0 exceeds threshold $1000.0
2026-02-28 10:30:00.009 [main] ERROR c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Payment gateway rejected transaction for $7290.0
java.lang.RuntimeException: Payment declined: exceeds single transaction limit
at com.example.orders.OrderService.simulatePaymentGateway(OrderService.java:112)
...
2026-02-28 10:30:00.010 [main] ERROR c.e.orders.OrderService [orderId=ORD-003 user=charlie] - Order processing failed after 2ms
=== Scenario 4: Invalid Order ===
2026-02-28 10:30:00.011 [main] INFO c.e.orders.OrderService [orderId=ORD-004 user=dave] - Order processing started: 0 items, total=$0.0
2026-02-28 10:30:00.011 [main] DEBUG c.e.orders.OrderService [orderId=ORD-004 user=dave] - Validating order
2026-02-28 10:30:00.011 [main] ERROR c.e.orders.OrderService [orderId=ORD-004 user=dave] - Order has no items
2026-02-28 10:30:00.011 [main] ERROR c.e.orders.OrderService [orderId=ORD-004 user=dave] - Order processing failed after 0ms
java.lang.IllegalArgumentException: Order must have at least one item
at com.example.orders.OrderService.validateOrder(OrderService.java:70)
...
| # | Concept | Where in Code |
|---|---|---|
| 1 | Logger declaration (private static final) | OrderService class field |
| 2 | MDC for request tracking | processOrder() -- MDC.put/MDC.clear |
| 3 | MDC cleanup in finally block | processOrder() -- prevents thread contamination |
| 4 | INFO for business events | "Order processing started", "Payment processed", "Order shipped" |
| 5 | DEBUG for technical details | "Validating order", "Tax calculated", "Payment gateway returned" |
| 6 | WARN for recoverable issues | "High-value order detected" |
| 7 | ERROR with exception | "Payment gateway rejected" -- exception passed as last argument |
| 8 | Parameterized logging ({}) | Every log statement uses {} instead of string concatenation |
| 9 | Context in messages | Order ID, user ID, amounts, item counts included |
| 10 | Performance tracking | Elapsed time measured and logged on completion/failure |
| 11 | No sensitive data logged | toString() excludes user details; no passwords/tokens |
| 12 | Separate logback.xml configuration | Console + rolling file, package-level filtering, MDC in pattern |
| Topic | Key Point |
|---|---|
| Recommended stack | SLF4J (facade) + Logback (implementation) |
| Logger declaration | private static final Logger log = LoggerFactory.getLogger(MyClass.class) |
| Parameterized logging | log.info("User {} placed order {}", userId, orderId) |
| Exception logging | log.error("Something failed for order {}", orderId, exception) -- exception is always the last argument |
| Log levels | TRACE < DEBUG < INFO < WARN < ERROR. Use INFO for business events, DEBUG for technical details. |
| MDC | MDC.put("requestId", id) in filter/interceptor, %X{requestId} in pattern, MDC.clear() in finally |
| Configuration file | logback.xml in src/main/resources |
| Production format | JSON via logstash-logback-encoder for ELK/Splunk/Datadog |
| Log rotation | SizeAndTimeBasedRollingPolicy with maxFileSize, maxHistory, totalSizeCap |
| Async logging | Logback AsyncAppender or Log4j2 AsyncLogger for high throughput |
| Never log | Passwords, credit cards, SSNs, API keys, session tokens |
| Never use | Log4j 1.x (CVE-2021-44228), System.out.println, string concatenation in log calls |