Spring Boot
Logging Basics
Spring Boot configures logging automatically through spring-boot-starter-logging, which bundles Logback as the default implementation behind the SLF4J facade. Log levels, output format, and file output are configurable in application.yml without any additional dependencies. This entry covers log levels, configuration in YAML, Lombok's @Slf4j, MDC context, and structured logging fundamentals.
Log Levels and Basic Configuration
Spring Boot uses five log levels in ascending severity: TRACE, DEBUG, INFO, WARN, and ERROR. The root level defaults to INFO — only INFO, WARN, and ERROR messages are emitted. Override levels per package or class in application.yml. Spring Boot and Spring Framework internal packages are set to WARN by default so they do not clutter application logs.
yaml
# ── application.yml ────────────────────────────────────────────────────
logging:
# ── Root level — applies to everything not explicitly configured ──────
level:
root: INFO
# ── Package-level overrides ───────────────────────────────────────
com.myapp: DEBUG # all application classes
com.myapp.service: DEBUG # service layer
com.myapp.repository: WARN # suppress Hibernate SQL details
# ── Framework packages ────────────────────────────────────────────
org.springframework: WARN
org.springframework.web: INFO # HTTP request mapping
org.springframework.security: WARN
org.hibernate.SQL: DEBUG # log SQL statements
org.hibernate.type.descriptor: TRACE # log bind parameters
org.flywaydb: INFO
# ── Console output format ─────────────────────────────────────────────
pattern:
console: >
%d{yyyy-MM-dd HH:mm:ss.SSS} %5p
[%15.15t] %-40.40logger{39} : %m%n%wEx
# ── Log to file ───────────────────────────────────────────────────────
file:
name: logs/application.log
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 30 # keep 30 days of rotated files
total-size-cap: 1GB
file-name-pattern: logs/application-%d{yyyy-MM-dd}.%i.log.gz
# ── application-dev.yml — verbose development logging ────────────────
logging:
level:
root: DEBUG
com.myapp: TRACE
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql: TRACE
# ── application-prod.yml — minimal production logging ────────────────
logging:
level:
root: WARN
com.myapp: INFOUsing @Slf4j and the Logger
Inject a logger into every class that needs one. Lombok's @Slf4j annotation generates a static final Logger field named log, eliminating boilerplate. Always use parameterised logging — log.info("msg {}", value) — never string concatenation — to avoid building the message string when the level is disabled.
Java
// ── @Slf4j — Lombok generates: private static final Logger log ───────
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final OrderRepository orderRepo;
private final InventoryService inventoryService;
public OrderResponse create(CreateOrderRequest request,
Long userId) {
log.debug("Creating order for user {} with {} items",
userId, request.items().size());
try {
inventoryService.reserve(request.items());
Order order = orderRepo.save(
Order.from(request, userId));
log.info("Order {} created for user {}",
order.getId(), userId);
return OrderResponse.from(order);
} catch (InsufficientStockException ex) {
log.warn("Insufficient stock for order — user={} items={}",
userId, request.items(), ex);
throw ex;
} catch (Exception ex) {
log.error("Unexpected error creating order " +
"for user {}", userId, ex);
throw ex;
}
}
// ── Level guard — avoid computing expensive arguments ─────────────
public void processLargeDataset(List<Order> orders) {
if (log.isDebugEnabled()) {
log.debug("Processing {} orders: {}",
orders.size(),
orders.stream()
.map(o -> o.getId().toString())
.collect(Collectors.joining(",")));
}
}
}
// ── WRONG — string concatenation always builds the string ────────────
log.debug("Order id=" + order.getId() + " user=" + userId);
// CORRECT — parameterised — string built only if DEBUG is enabled ──
log.debug("Order id={} user={}", order.getId(), userId);
// ── Log the exception with stack trace ───────────────────────────────
try {
riskyOperation();
} catch (Exception ex) {
// Pass exception as the LAST argument — Logback prints stack trace
log.error("Operation failed for order {}", orderId, ex);
}MDC — Mapped Diagnostic Context
MDC attaches key-value pairs to the current thread's logging context. Every log statement on that thread automatically includes the MDC values without passing them explicitly. Use MDC for correlation IDs, user IDs, tenant IDs, and request paths to make logs traceable across a request lifecycle.
Java
// ── MDC filter — set context on every inbound request ────────────────
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class MdcLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String correlationId = Optional
.ofNullable(request.getHeader("X-Correlation-ID"))
.orElse(UUID.randomUUID().toString());
String requestId = UUID.randomUUID().toString();
try {
MDC.put("correlationId", correlationId);
MDC.put("requestId", requestId);
MDC.put("method", request.getMethod());
MDC.put("path", request.getRequestURI());
MDC.put("remoteIp", getClientIp(request));
// Propagate correlation ID back to caller
response.setHeader("X-Correlation-ID", correlationId);
response.setHeader("X-Request-ID", requestId);
chain.doFilter(request, response);
} finally {
MDC.clear(); // ALWAYS clear — thread pool reuse
}
}
private String getClientIp(HttpServletRequest req) {
String fwd = req.getHeader("X-Forwarded-For");
return fwd != null ? fwd.split(",")[0].trim()
: req.getRemoteAddr();
}
}
# ── Include MDC values in log pattern ─────────────────────────────────
logging:
pattern:
console: >
%d{HH:mm:ss.SSS} %5p
[%X{correlationId:-},%X{requestId:-}]
%-40.40logger{39} : %m%n
// ── Log output with MDC ────────────────────────────────────────────────
// 10:30:00.123 INFO [abc-123,req-456] c.m.s.OrderService : Order 42 created
// 10:30:00.456 DEBUG [abc-123,req-456] c.m.r.OrderRepo : SELECT ...
// ── Set MDC manually in a service ────────────────────────────────────
@Service
@Slf4j
public class PaymentService {
public PaymentResponse charge(Long orderId, BigDecimal amount) {
MDC.put("orderId", String.valueOf(orderId));
MDC.put("paymentAmount", amount.toPlainString());
try {
log.info("Processing payment");
// All log statements below automatically include orderId
PaymentResponse result = gateway.charge(amount);
log.info("Payment processed successfully ref={}",
result.reference());
return result;
} finally {
MDC.remove("orderId");
MDC.remove("paymentAmount");
}
}
}Logging Best Practices
Well-structured logs are searchable, actionable, and noise-free. Apply consistent conventions across the codebase: log at the right level, include context, avoid sensitive data, and use structured formats in production.
Java
// ── Rule 1: Log at the correct level ─────────────────────────────────
log.trace("Entering method with args={}", args); // fine-grained debug
log.debug("Repository query returned {} rows", count);
log.info("Order {} placed for user {}", orderId, userId);
log.warn("Payment retry {} of {} for order {}", attempt, max, orderId);
log.error("Failed to charge payment for order {}", orderId, ex);
// ── Rule 2: Never log sensitive data ─────────────────────────────────
// WRONG:
log.info("User {} authenticated password={}", email, password);
log.debug("JWT token={}", jwtToken);
log.info("Credit card charged last4={} cvv={}", last4, cvv);
// CORRECT — log identifiers and outcomes, not secrets
log.info("User {} authenticated successfully", email);
log.debug("JWT issued for user {}", email);
log.info("Card charged successfully for order {}", orderId);
// ── Rule 3: Include enough context to act on ──────────────────────────
// WRONG — useless without context
log.error("Something went wrong");
log.warn("Retry");
// CORRECT — actionable
log.error("Order creation failed userId={} itemCount={} cause={}",
userId, items.size(), ex.getMessage(), ex);
log.warn("Payment retry attempt {}/{} orderId={} delayMs={}",
attempt, maxAttempts, orderId, delay);
// ── Rule 4: Log at service boundaries ────────────────────────────────
// Log when entering a significant operation (DEBUG)
// Log when an operation completes (INFO for business events)
// Log when an operation fails (WARN for expected, ERROR for unexpected)
// ── Rule 5: Use structured logging in production ──────────────────────
// JSON log enables queries like:
// { level: "ERROR", service: "order-service", userId: "42" }
// Use logstash-logback-encoder or log4j2 JSON layout
// ── Rule 6: Log durations for SLA-sensitive operations ───────────────
@Slf4j
public class ExternalApiClient {
public Response call(Request req) {
long start = System.currentTimeMillis();
try {
Response response = httpClient.send(req);
log.info("External API responded status={} durationMs={}",
response.status(),
System.currentTimeMillis() - start);
return response;
} catch (Exception ex) {
log.error("External API failed durationMs={} error={}",
System.currentTimeMillis() - start,
ex.getMessage(), ex);
throw ex;
}
}
}Structured Logging with JSON
Structured logging emits JSON instead of plain text. Each field is a distinct JSON key — level, message, timestamp, traceId — making logs queryable by any log aggregation platform (Loki, Elasticsearch, Splunk, Datadog). Add the logstash-logback-encoder to produce JSON without changing application code.
XML
<!-- pom.xml -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
// ── logback-spring.xml ─────────────────────────────────────────────────
// <configuration>
//
// <!-- JSON appender for production -->
// <springProfile name="prod">
// <appender name="JSON"
// class="ch.qos.logback.core.ConsoleAppender">
// <encoder
// class="net.logstash.logback.encoder.LogstashEncoder">
// <customFields>
// {"service":"order-service","env":"production"}
// </customFields>
// <fieldNames>
// <timestamp>timestamp</timestamp>
// <message>message</message>
// <logger>logger</logger>
// <thread>thread</thread>
// </fieldNames>
// </encoder>
// </appender>
// <root level="INFO">
// <appender-ref ref="JSON"/>
// </root>
// </springProfile>
//
// <!-- Human-readable appender for development -->
// <springProfile name="dev,default">
// <appender name="CONSOLE"
// class="ch.qos.logback.core.ConsoleAppender">
// <encoder>
// <pattern>
// %d{HH:mm:ss.SSS} %5p [%X{correlationId:-}]
// %-40.40logger{39} : %m%n
// </pattern>
// </encoder>
// </appender>
// <root level="DEBUG">
// <appender-ref ref="CONSOLE"/>
// </root>
// </springProfile>
//
// </configuration>
// ── Sample JSON output ────────────────────────────────────────────────
// {
// "timestamp": "2024-03-15T10:30:00.123Z",
// "level": "INFO",
// "logger": "com.myapp.service.OrderService",
// "message": "Order 42 created for user 1",
// "thread": "http-nio-8080-exec-1",
// "traceId": "abc123def456abc1",
// "spanId": "def456abc123def4",
// "correlationId": "req-uuid-789",
// "service": "order-service",
// "env": "production"
// }
// ── Add custom fields to a specific log event ─────────────────────────
import net.logstash.logback.argument.StructuredArguments.*;
log.info("Order created {}",
kv("orderId", order.getId()),
kv("userId", userId),
kv("total", order.getTotal()),
kv("itemCount", order.getItems().size()));
// ── Output ────────────────────────────────────────────────────────────
// { "message": "Order created", "orderId": 42,
// "userId": 1, "total": 99.99, "itemCount": 3 }