Observability — the ability to understand what is happening inside an application through its external manifestations (metrics, tracing, logs).
Simple analogy: Observability is like an airplane's instrument panel: altitude (latency), speed (throughput), fuel (memory), pressure (CPU). The pilot doesn't look inside the engine — they look at the instruments. Without observability, you are flying blind. When something goes wrong at 3 AM, you need to know WHERE (tracing), HOW MUCH (metrics), and WHY (logs).
Why it matters in production: In a microservices architecture with 50 services, a single user request may flow through 10 services. Without observability, debugging "why is the user seeing a timeout?" requires logging into each server, grepping log files, and guessing. With observability, you trace the exact request across all services, see which one added 5 seconds of latency, and find the error log with full context.
Three pillars:
1. Metrics (quantitative measurements over time):
1management:2 metrics:3 export:4 prometheus:5 enabled: true6 distribution:7 percentiles-histogram:8 http.server.requests: true9 percentiles:10 http.server.requests: 0.5, 0.95, 0.99
1// Custom metrics2@Service3public class OrderService {4 private final Counter orderCounter;5 private final Timer orderTimer;6 private final DistributionSummary orderSize;78 public OrderService(MeterRegistry registry) {9 this.orderCounter = Counter.builder("orders.created")10 .description("Total orders created")11 .tag("service", "order")12 .register(registry);13 this.orderTimer = Timer.builder("orders.processing")14 .publishPercentiles(0.5, 0.95, 0.99)15 .register(registry);16 this.orderSize = DistributionSummary.builder("orders.items")17 .register(registry);18 }1920 public Order create(Order order) {21 return orderTimer.record(() -> {22 Order result = process(order);23 orderCounter.increment();24 orderSize.record(order.getItems().size());25 return result;26 });27 }28}
2. Tracing (following a single request across services):
1management:2 tracing:3 sampling:4 probability: 1.0 # 100% in dev, 0.1 in prod to reduce overhead5 zipkin:6 tracing:7 endpoint: http://localhost:9411/api/v2/spans
1// Custom spans2@Service3public class PaymentService {4 private final ObservationRegistry observationRegistry;56 public PaymentResult process(Payment payment) {7 return Observation.createNotStarted("payment.process", observationRegistry)8 .lowCardinalityKeyValue("method", payment.getMethod())9 .observe(() -> {10 // Your payment logic here11 return gateway.charge(payment);12 });13 }14}
3. Logging (structured, contextual):
1logging:2 structure:3 json:4 enabled: true # Spring Boot 3.2+5 pattern:6 level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
1@Slf4j2@Service3public class OrderService {4 public Order create(Order order) {5 // traceId and spanId are automatically included via MDC6 log.info("Creating order for user {}", order.getUserId());7 log.debug("Order items: {}", order.getItems());8 try {9 return repository.save(order);10 } catch (Exception e) {11 log.error("Failed to create order for user {}", order.getUserId(), e);12 throw e;13 }14 }15}
Production stack:
Spring Boot Actuator → Micrometer → Prometheus → Grafana dashboards
Spring Boot Actuator → Micrometer Tracing → Zipkin / Jaeger for distributed tracing
SLF4J → Logback → ELK Stack (Elasticsearch + Logstash + Kibana) for log aggregation
Alerting: Grafana Alerts → PagerDuty / Slack notifications
Common pitfalls:
DEBUG level creates gigabytes of logs. Use INFO for production.tag("userId", userId) creates millions of time series. Use low-cardinality labels like tag("status", "success")./actuator/health for load balancers and Kubernetes probes.