Observability Patterns for Spring Boot
Structured Logging
Logback JSON (Production)
<springProfile name="!local">
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"service":"order-service","env":"${SPRING_PROFILES_ACTIVE}"}</customFields>
</encoder>
</appender>
</springProfile>
Log Levels
| Level |
Use For |
| ERROR |
Failures requiring attention (exceptions, data loss) |
| WARN |
Recoverable issues (fallback triggered, retry) |
| INFO |
Business events (order created, payment processed) |
| DEBUG |
Technical detail (query params, cache hits) — local only |
Rules
- SLF4J placeholders:
log.info("Order created orderId={}", order.id()) — never concatenation.
- MDC for context:
MDC.put("orderId", id). Clear in finally.
- Never log PII. Log identifiers only.
Reactive Context Propagation
public Mono<Order> createReactive(CreateOrderCommand cmd) {
return Mono.deferContextual(ctx -> {
MDC.put("traceId", ctx.getOrDefault("traceId", "none"));
MDC.put("orderId", cmd.orderId());
return orderRepository.save(Order.from(cmd))
.doOnNext(o -> log.info("Order created orderId={}", o.id()))
.doOnError(e -> log.error("Order creation failed", e))
.doFinally(sig -> MDC.clear());
});
}
Distributed Tracing
management:
tracing:
sampling:
probability: 1.0 # 100% dev; 0.1 (10%) prod
otlp:
tracing:
endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318}/v1/traces
Custom spans: tracer.nextSpan().name("order.enrich").tag(...). Use WebClient.builder().observationRegistry(registry) for automatic trace propagation.
Custom Metrics
- Counter:
Counter.builder("orders.created.total").register(meterRegistry) — totals
- Timer:
Timer.builder("orders.creation.duration").register(meterRegistry).record(() -> work()) — latency
- Gauge:
Gauge.builder("orders.pending.count", this, s -> s.count()).register(meterRegistry) — current value
- @Timed on controllers, @Observed on business methods
Prometheus Config
management:
endpoints.web.exposure.include: health,metrics,prometheus
endpoint.health.probes.enabled: true
metrics.distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 50ms,200ms,500ms,1s
Health Indicators & Kubernetes Probes
@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
public Health health() {
boolean ok = client.ping();
return ok ? Health.up().build() : Health.down().withDetail("reason", "ping failed").build();
}
}
# Kubernetes probes
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
initialDelaySeconds: 30
failureThreshold: 3
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
initialDelaySeconds: 10
failureThreshold: 3
Liveness = app alive (restart if fails). Readiness = can accept traffic (db, redis).
Alerting Thresholds
| Metric |
Condition |
Severity |
| P99 latency > 500ms |
5 min sustained |
WARNING |
| P99 latency > 2s |
2 min sustained |
CRITICAL |
| Error rate > 1% |
5 min sustained |
WARNING |
| Error rate > 5% |
1 min sustained |
CRITICAL |
| JVM heap > 85% |
10 min sustained |
WARNING |
| DB pool saturation > 90% |
Instant |
CRITICAL |
| Health endpoint DOWN |
Instant |
CRITICAL |
Rules
- Never log PII — mask emails, never log passwords/tokens/credentials
- SLF4J placeholders (
log.info("x={}", x)) — never string concatenation
- Clear MDC in reactive
doFinally blocks — prevent context leakage
- Set timeouts on health check external calls (2-5s) — prevent probe hangs
References
- references/logging.md — Full Logback config (local + JSON), reactive MDC propagation, WebFilter correlation, sensitive data rules
- references/tracing-metrics.md — Dependencies, custom spans, Observation API, Micrometer auto-instrumentation, Prometheus alerts, reactive health indicators
- references/detailed-patterns.md — Legacy combined reference (all patterns in one file)
Related Skills
- spring-webflux-patterns — WebFilter setup, production actuator defaults
- pentest — PII logging detection (OWASP A09)
- testing-workflow — Verification pipeline includes observability checks
1---2name: observability-patterns3description: Observability patterns for Spring Boot — structured JSON logging (Logstash encoder), distributed tracing (Micrometer Tracing), custom Micrometer metrics, Prometheus alerting, and Grafana dashboards. Use when configuring logback-spring.xml, adding MDC correlation, creating @Timed/@Counted metrics, writing PromQL alert rules, setting up ELK/Loki log pipelines, or building Grafana dashboards for Spring Boot services.4---56# Observability Patterns for Spring Boot78## Structured Logging910### Logback JSON (Production)1112```xml13<springProfile name="!local">14 <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">15 <encoder class="net.logstash.logback.encoder.LogstashEncoder">16 <customFields>{"service":"order-service","env":"${SPRING_PROFILES_ACTIVE}"}</customFields>17 </encoder>18 </appender>19</springProfile>20```2122### Log Levels2324| Level | Use For |25|-------|---------|26| ERROR | Failures requiring attention (exceptions, data loss) |27| WARN | Recoverable issues (fallback triggered, retry) |28| INFO | Business events (order created, payment processed) |29| DEBUG | Technical detail (query params, cache hits) — local only |3031### Rules3233- SLF4J placeholders: `log.info("Order created orderId={}", order.id())` — never concatenation.34- MDC for context: `MDC.put("orderId", id)`. Clear in `finally`.35- Never log PII. Log identifiers only.3637## Reactive Context Propagation3839```java40public Mono<Order> createReactive(CreateOrderCommand cmd) {41 return Mono.deferContextual(ctx -> {42 MDC.put("traceId", ctx.getOrDefault("traceId", "none"));43 MDC.put("orderId", cmd.orderId());44 return orderRepository.save(Order.from(cmd))45 .doOnNext(o -> log.info("Order created orderId={}", o.id()))46 .doOnError(e -> log.error("Order creation failed", e))47 .doFinally(sig -> MDC.clear());48 });49}50```5152## Distributed Tracing5354```yaml55management:56 tracing:57 sampling:58 probability: 1.0 # 100% dev; 0.1 (10%) prod59 otlp:60 tracing:61 endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318}/v1/traces62```6364Custom spans: `tracer.nextSpan().name("order.enrich").tag(...)`. Use `WebClient.builder().observationRegistry(registry)` for automatic trace propagation.6566## Custom Metrics6768- **Counter**: `Counter.builder("orders.created.total").register(meterRegistry)` — totals69- **Timer**: `Timer.builder("orders.creation.duration").register(meterRegistry).record(() -> work())` — latency70- **Gauge**: `Gauge.builder("orders.pending.count", this, s -> s.count()).register(meterRegistry)` — current value71- **@Timed** on controllers, **@Observed** on business methods7273### Prometheus Config7475```yaml76management:77 endpoints.web.exposure.include: health,metrics,prometheus78 endpoint.health.probes.enabled: true79 metrics.distribution:80 percentiles-histogram:81 http.server.requests: true82 slo:83 http.server.requests: 50ms,200ms,500ms,1s84```8586## Health Indicators & Kubernetes Probes8788```java89@Component90public class ExternalServiceHealthIndicator implements HealthIndicator {91 public Health health() {92 boolean ok = client.ping();93 return ok ? Health.up().build() : Health.down().withDetail("reason", "ping failed").build();94 }95}96```9798```yaml99# Kubernetes probes100livenessProbe:101 httpGet: { path: /actuator/health/liveness, port: 8080 }102 initialDelaySeconds: 30103 failureThreshold: 3104readinessProbe:105 httpGet: { path: /actuator/health/readiness, port: 8080 }106 initialDelaySeconds: 10107 failureThreshold: 3108```109110Liveness = app alive (restart if fails). Readiness = can accept traffic (db, redis).111112## Alerting Thresholds113114| Metric | Condition | Severity |115|--------|----------|----------|116| P99 latency > 500ms | 5 min sustained | WARNING |117| P99 latency > 2s | 2 min sustained | CRITICAL |118| Error rate > 1% | 5 min sustained | WARNING |119| Error rate > 5% | 1 min sustained | CRITICAL |120| JVM heap > 85% | 10 min sustained | WARNING |121| DB pool saturation > 90% | Instant | CRITICAL |122| Health endpoint DOWN | Instant | CRITICAL |123124## Rules125126- Never log PII — mask emails, never log passwords/tokens/credentials127- SLF4J placeholders (`log.info("x={}", x)`) — never string concatenation128- Clear MDC in reactive `doFinally` blocks — prevent context leakage129- Set timeouts on health check external calls (2-5s) — prevent probe hangs130131## References132133- **[references/logging.md](references/logging.md)** — Full Logback config (local + JSON), reactive MDC propagation, WebFilter correlation, sensitive data rules134- **[references/tracing-metrics.md](references/tracing-metrics.md)** — Dependencies, custom spans, Observation API, Micrometer auto-instrumentation, Prometheus alerts, reactive health indicators135- **[references/detailed-patterns.md](references/detailed-patterns.md)** — Legacy combined reference (all patterns in one file)136137## Related Skills138139- **spring-webflux-patterns** — WebFilter setup, production actuator defaults140- **pentest** — PII logging detection (OWASP A09)141- **testing-workflow** — Verification pipeline includes observability checks