Java observability patterns: SLF4J structured logging with MDC correlation IDs, Micrometer metrics (counters, timers), Spring Actuator health and info endpoints. Use when writing or reviewing logging, metrics, health checks, or any observability concern in Java or Spring Boot code.
Always use SLF4J. Never use System.out.println, System.err.println, java.util.logging, or Log4j directly.
// Forbidden
System.out.println("Creating order for customer: " + customerId);
// Correct
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public Order createOrder(CreateOrderRequest request) {
log.info("Creating order for customer={} itemCount={}", request.customerId(), request.items().size());
// ...
}
}
Use parameterised placeholders ({}), never string concatenation — concatenation allocates memory even when the log level is disabled.
Log levels:
DEBUG — internal flow, variable values during developmentINFO — significant business events (order created, payment processed)WARN — recoverable anomalies (retry attempted, degraded mode)ERROR// Correct: always pass the exception as the last argument so the stack trace is captured
try {
paymentGateway.charge(order);
} catch (PaymentGatewayException e) {
log.error("Payment failed for orderId={} customerId={}", order.id(), order.customerId(), e);
throw new OrderProcessingException("Payment failed", e);
}
Use MDC (Mapped Diagnostic Context) to attach a correlation ID to every log line in a request. Without it, log lines from concurrent requests are indistinguishable in production.
// Servlet filter / Spring OncePerRequestFilter
import org.slf4j.MDC;
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final String CORRELATION_ID_HEADER = "X-Correlation-Id";
private static final String MDC_KEY = "correlationId";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String correlationId = Optional.ofNullable(request.getHeader(CORRELATION_ID_HEADER))
.orElse(UUID.randomUUID().toString());
MDC.put(MDC_KEY, correlationId);
response.setHeader(CORRELATION_ID_HEADER, correlationId);
try {
chain.doFilter(request, response);
} finally {
MDC.clear(); // always clear — threads are reused from a pool
}
}
}
Register %X{correlationId} in logback-spring.xml:
<pattern>%d{ISO8601} [%X{correlationId}] %-5level %logger{36} - %msg%n</pattern>
Now every log line in a request shares the same correlation ID, making distributed tracing trivial.
Log key-value pairs with consistent field names. Avoid free-form sentences that are hard to query in log aggregators (Loki, Datadog, Splunk).
// Hard to query: field names and positions vary
log.info("Order {} was created for customer {} with {} items totalling {}",
orderId, customerId, itemCount, total);
// Queryable: consistent field names
log.info("order.created orderId={} customerId={} itemCount={} totalAmount={}",
orderId, customerId, itemCount, total);
For structured JSON output (production), add logstash-logback-encoder and configure a JsonEncoder in logback-spring.xml.
Use Micrometer for application-level metrics. Spring Boot auto-configures it when spring-boot-starter-actuator is on the classpath.
Counter — count events:
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
@Service
public class OrderService {
private final Counter ordersCreated;
private final Counter ordersFailed;
public OrderService(MeterRegistry registry) {
this.ordersCreated = Counter.builder("orders.created")
.description("Total orders successfully created")
.register(registry);
this.ordersFailed = Counter.builder("orders.failed")
.description("Total orders that failed to create")
.register(registry);
}
public Order createOrder(CreateOrderRequest request) {
try {
Order order = buildAndSave(request);
ordersCreated.increment();
return order;
} catch (Exception e) {
ordersFailed.increment();
throw e;
}
}
}
Timer — measure latency:
import io.micrometer.core.instrument.Timer;
private final Timer orderCreationTimer;
public OrderService(MeterRegistry registry) {
this.orderCreationTimer = Timer.builder("orders.creation.duration")
.description("Time taken to create an order")
.register(registry);
}
public Order createOrder(CreateOrderRequest request) {
return orderCreationTimer.record(() -> buildAndSave(request));
}
Use tags to slice metrics by dimension:
Counter.builder("orders.created")
.tag("channel", request.channel().name())
.register(registry)
.increment();
Include spring-boot-starter-actuator and expose health and info endpoints. Do not expose all endpoints publicly — restrict sensitive ones.
# application.yml