SLF4J, Logback, Log Levels
Logging is an operational interface
Production logging is not about printing text. It is about leaving high-signal breadcrumbs that help you answer these questions quickly:
- What happened?
- Which request/user/job was affected?
- Where did it fail (dependency, database, network, code path)?
- Is it getting worse (rate, latency, error spikes)?
Bad logging increases incident time and cost. Good logging turns a 2-hour guessing game into a 5-minute diagnosis.
Use SLF4J as the facade (why it matters)
SLF4J is a facade: your application code logs against SLF4J APIs, while the actual implementation can be Logback (most common), Log4j2, or another backend. This gives you:
- Portability: swap the backend without rewriting code.
- Consistent style: one logging API across modules and libraries.
- Better dependency hygiene: fewer conflicting logging jars in production.
Basic SLF4J usage (do it the right way)
Prefer parameterized logging. Do not build strings eagerly; it wastes CPU and allocations when the log level is disabled.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PaymentService {
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
public void charge(String orderId, long amountCents) {
log.info("Charging orderId={} amountCents={}", orderId, amountCents);
}
}
Anti-pattern: string concatenation
// Avoid: eager string building even if INFO is disabled
log.info("Charging orderId=" + orderId + " amountCents=" + amountCents);
Log levels: a production discipline
Log levels are not decoration. They define what you can afford to log at scale and what operators rely on during incidents.
Recommended interpretation
- ERROR: Something failed that likely needs action (or impacts users). Include context and the exception.
- WARN: Unexpected situation or degraded behavior. Might not page, but should be visible in dashboards.
- INFO: High-level lifecycle and business-relevant events (startup, shutdown, job finished, important state transitions).
- DEBUG: Diagnostic details for development or targeted troubleshooting.
- TRACE: Very verbose; almost never enabled in production.
Production rule
Keep production at INFO by default. Enable DEBUG temporarily for specific packages or a single instance when troubleshooting.
What to log (high signal patterns)
Log events, not noise. Examples of high-signal logs:
- service startup banner (version, git SHA, Java version, active profile)
- external dependency failures (HTTP status, timeout, retry count, upstream name)
- business events that explain user-visible behavior (order cancelled due to stock, payment declined)
- security-relevant events (auth failures, rate limits triggered) without leaking secrets
What not to log
- Secrets: passwords, API keys, tokens, session IDs, private keys.
- PII leakage: full emails/phones/addresses unless explicitly required and approved. Prefer hashing or partial masking.
- Unbounded payloads: full request/response bodies, huge JSON blobs (costly, noisy, often sensitive).
Logback configuration: a production baseline
Logback is a common SLF4J backend. A production baseline should provide:
- consistent formatting
- safe rotation (size/time based)
- bounded retention (do not fill disks)
Example logback.xml (console + rolling file)
This is a baseline example; tune paths and retention based on your environment.
<configuration>
<property name="LOG_DIR" value="/var/log/myservice"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{ISO8601} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_DIR}/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR}/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>14</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{ISO8601} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
Why rotation settings matter
Without rotation and retention, logs fill disks and cause outages. The most common operational log failure is:
- disk fills up
- application fails to write logs (or the whole node starts failing)
- databases, queues, or other services on the same host also degrade
Always bound log growth with maxHistory and totalSizeCap, or delegate to the platform (e.g., container stdout with centralized logging).
Structured logging: start with stable key=value
Even if you are not outputting JSON yet, write logs as stable key=value pairs. This makes search and aggregation reliable.
log.info("payment_failed orderId={} reason={} provider={}", orderId, reason, provider);
MDC: request correlation without passing IDs everywhere
MDC (Mapped Diagnostic Context) lets you attach contextual fields (like requestId) to every log line on the current thread. Operators love this because you can grep one ID and see the entire story.
MDC usage example
import org.slf4j.MDC;
public void handleRequest(String requestId) {
MDC.put("requestId", requestId);
try {
log.info("request_start path={}", "/charge");
// handle...
log.info("request_end status={}", 200);
} finally {
MDC.remove("requestId");
}
}
To print MDC fields, include them in your log pattern:
<pattern>%d{ISO8601} %-5level requestId=%X{requestId} %logger{36} - %msg%n</pattern>
Important pitfall: async execution and MDC
MDC is thread-local. If you use thread pools or async execution, you must propagate MDC manually or use framework support. Otherwise, correlation breaks and logs become fragmented. We will revisit this when we cover concurrency and web frameworks.
Logging exceptions correctly
Always log the exception object so you keep the stack trace. Avoid logging only e.getMessage().
try {
// ...
} catch (Exception e) {
log.error("db_query_failed queryName={} timeoutMs={}", "findUserById", 200, e);
}
Anti-pattern: double-logging exceptions
Do not log the same exception at multiple layers with full stack traces. It inflates log volume and hides the real signal. A practical approach:
- log stack traces at the boundary (e.g., HTTP handler, job runner),
- inside the domain layer, rethrow with context but do not spam stack traces.
Security: redaction and safe logging
Assume logs are widely accessible inside an organization and sometimes exported to third-party systems. Treat logs as sensitive.
- Mask emails/phones if you must log them (e.g., userId instead of email).
- Never log authorization headers, bearer tokens, cookies, or passwords.
- Be careful with exception messages from libraries; they may include raw payloads.
Operational checklist
- Use SLF4J in application code; pick one backend (commonly Logback).
- Use parameterized logging (no string concatenation).
- Keep production default at INFO; enable DEBUG selectively when troubleshooting.
- Configure rotation + retention to avoid disk fill outages.
- Adopt stable key=value fields; prepare for structured logging.
- Use MDC for correlation IDs; clear MDC in finally blocks.
- Never log secrets or unbounded payloads.