Why this matters
- Grepping unstructured log files across twelve pods during an outage is slow; JSON logs with a correlation ID let you filter one checkout journey in seconds.
- Micrometer meters are vendor-neutral — the same
Timerfeeds Prometheus, Datadog, or CloudWatch depending on which registry you add at deploy time. - Business metrics like
orders.placedandinventory.reservations.failedbelong alongside JVM metrics; they surface revenue-impacting problems before users complain.
Logs
Logback + MDCStructured JSON
Metrics
MicrometerCounters, timers
Traces
OpenTelemetryTrace propagation
Logging and metrics toolkit
- MDC (Mapped Diagnostic Context) — thread-local map of key-value pairs appended to every log line in that thread.
- Logback
logback-spring.xml— configure JSON appenders, log levels per package, and profile-specific overrides. MeterRegistry— central registry for counters, timers, gauges, and distribution summaries.@Timed/@Counted— declarative instrumentation via Micrometer's aspect (requires@EnableAspectJAutoProxy).ObservationAPI — unified abstraction for metrics, tracing, and logging context in Spring Boot 3.x.
Correlation IDs with MDC
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorrelationIdFilter extends OncePerRequestFilter {
public static final String HEADER = "X-Correlation-Id";
public 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(HEADER))
.filter(id -> !id.isBlank())
.orElse(UUID.randomUUID().toString());
MDC.put(MDC_KEY, correlationId);
response.setHeader(HEADER, correlationId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove(MDC_KEY);
}
}
}
<!-- logback-spring.xml -->
<configuration>
<springProfile name="prod">
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="JSON"/>
</root>
</springProfile>
</configuration>
Custom business metrics
@Service
public class OrderService {
private final Counter ordersPlaced;
private final Timer checkoutTimer;
private final OrderRepository orderRepository;
public OrderService(MeterRegistry registry, OrderRepository orderRepository) {
this.orderRepository = orderRepository;
this.ordersPlaced = registry.counter("bookstore.orders.placed");
this.checkoutTimer = registry.timer("bookstore.checkout.duration");
}
public OrderResponse placeOrder(PlaceOrderRequest request) {
return checkoutTimer.record(() -> {
Order order = orderRepository.save(buildOrder(request));
ordersPlaced.increment();
log.info("Order {} placed for customer {}", order.getId(), request.customerId());
return OrderResponse.from(order);
});
}
}
@Configuration
public class MetricsConfig {
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
@Service
public class BookService {
@Timed(value = "bookstore.catalog.lookup", description = "ISBN lookup duration")
public BookDetail getByIsbn(String isbn) {
return bookRepository.findByIsbn(isbn)
.map(BookDetail::from)
.orElseThrow(() -> new BookNotFoundException(isbn));
}
}
management:
metrics:
tags:
application: bookstore-api
environment: ${spring.profiles.active}
distribution:
percentiles-histogram:
bookstore.checkout.duration: true
Quick recall
Everything you need if you only revisit this box.
- Put a correlation ID in MDC at the filter layer; include it in every log line via Logback pattern or JSON encoder.
- Use
MeterRegistry.counter()andregistry.timer()for business metrics alongside auto-configured JVM meters. @Timedon service methods gives free latency histograms whenTimedAspectis registered.- Tag meters with
applicationandenvironmentso dashboards filter correctly in multi-tenant setups. - Propagate MDC across async boundaries — it does not travel automatically.
Test yourself
Answer these before moving on — recall is what makes it stick.