Companion to
../MICROSERVICES_TEACHING_GUIDE.md§12.Distributed systems are only as good as your ability to understand them. This document covers logs, metrics, traces, dashboards, and alerts.
1. The three pillars (and why traces are king)
| Pillar | Question it answers | Source |
|---|---|---|
| Logs | What exactly happened in service X at time T? | application + Kafka + DB |
| Metrics | What is the rate, error rate, and duration (RED) per endpoint? | Prometheus / CloudWatch |
| Traces | What was the path of one request across services? | OpenTelemetry → Jaeger / X-Ray |
In a microservices system, when something is wrong you usually start with a trace, then drill down to logs of the specific span, and use metrics for SLOs and dashboards.
2. Logs
2.1 Standards
- JSON format, one log per line.
- Required fields:
@timestamp,level,service.name,trace.id,span.id,merchant.id,payment.id(when applicable). - Forbidden: PAN, CVV, full magstripe, JWTs, passwords. Apply masking in a logback layout.
2.2 Logback config (sample)
services/payment-service/src/main/resources/logback-spring.xml:
<configuration>
<springProperty name="appName" source="spring.application.name"/>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"service.name":"${appName}"}</customFields>
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<includeMdcKeyName>merchantId</includeMdcKeyName>
<includeMdcKeyName>paymentId</includeMdcKeyName>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON"/>
</root>
</configuration>
2.3 Aggregation
| Stack | Notes |
|---|---|
| ELK | Self-hosted, full control. Heavier to operate. |
| Loki + Grafana | Cheaper for high cardinality. |
| CloudWatch Logs | Default in our AWS deployment. Use Log Insights for queries. |
CloudWatch Insights query for finding errors on a specific payment:
fields @timestamp, level, service.name, message, paymentId
| filter paymentId = "fa8b…"
| sort @timestamp asc
3. Metrics
3.1 Spring Boot Actuator + Micrometer
Each service exposes:
/actuator/health/liveness— for ECS health checks./actuator/health/readiness— for ALB target group / Service Connect./actuator/prometheus— Prometheus-format metrics.
application.yml (excerpt):
management:
endpoints.web.exposure.include: health,info,metrics,prometheus,refresh
endpoint.health.probes.enabled: true
metrics:
tags:
service.name: ${spring.application.name}
env: ${ENV:dev}
distribution.percentiles-histogram.http.server.requests: true
3.2 Standard service-level (RED) metrics
Already provided by Micrometer:
http_server_requests_seconds_count{uri,status}— rate.http_server_requests_seconds_count{status=~"5.."}— error rate.http_server_requests_seconds_bucket{uri,le}— histogram for percentiles.
3.3 Domain metrics (write them yourself)
Inside payment-service:
private final Counter authorised;
private final Counter declined;
private final Timer bankCallTimer;
public PaymentMetrics(MeterRegistry registry) {
this.authorised = Counter.builder("payments.authorised").register(registry);
this.declined = Counter.builder("payments.declined").register(registry);
this.bankCallTimer = Timer.builder("payments.bank.call")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
}
3.4 Resource metrics
In AWS, ECS Container Insights exposes:
ECS/ContainerInsights:CpuUtilized,MemoryUtilized,NetworkRxBytes, etc.AWS/RDS:CPUUtilization,DatabaseConnections,ReadLatency,WriteLatency.AWS/ElastiCache:Engine CPU,Evictions.AWS/Kafka:UnderReplicatedPartitions,BytesInPerSec.
4. Tracing
4.1 Setup
Add to each service:
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
implementation 'net.ttddyy.observation:datasource-micrometer-spring-boot:1.0.5' // DB spans
application.yml:
management:
tracing:
sampling.probability: 1.0 # 100% in dev, sample down in prod (e.g. 0.1)
otel:
exporter.otlp.endpoint: ${OTLP_ENDPOINT:http://otel-collector:4317}
resource.attributes: service.name=${spring.application.name},env=${ENV:dev}
4.2 Propagation
W3C traceparent is propagated automatically over HTTP. For Kafka, configure the producer/consumer interceptors:
@Bean
public ProducerFactory<String, Object> producerFactory(KafkaProperties props) {
Map<String, Object> cfg = new HashMap<>(props.buildProducerProperties());
cfg.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
List.of(io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingProducerInterceptor.class.getName()));
return new DefaultKafkaProducerFactory<>(cfg);
}
4.3 Backends
- Local dev: Jaeger (in
docker-compose.yml). - AWS: AWS Distro for OpenTelemetry (ADOT) sidecar exporting to AWS X-Ray or a self-managed Tempo cluster.
5. Dashboards (Grafana)
Each service gets a dashboard with:
- RED: rate (req/s), error rate (%), p50/p95/p99 latency.
- Resource saturation: CPU, memory, GC pauses, DB connection pool.
- Domain: payment outcomes per minute by status; Kafka lag (consumer side).
- Dependencies: bank-adapter circuit breaker state; fraud-check decision distribution.
Cross-service "platform" dashboard:
- Kafka topic lag (per consumer group).
- ALB request count and 5xx by target group.
- RDS active connections, CPU, replica lag.
- Top 10 slowest endpoints across all services.
6. Alerts
Alert rules should be:
- Symptom-based, not cause-based ("error rate > 1%" not "DB pool full").
- Tied to an SLO where possible.
- Linked to a runbook in the alert annotation.
Example (Prometheus / Grafana):
- alert: PaymentService5xxRate
expr: |
sum(rate(http_server_requests_seconds_count{service_name="payment-service",status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{service_name="payment-service"}[5m]))
> 0.01
for: 5m
labels: { severity: page }
annotations:
summary: "payment-service 5xx > 1%"
runbook: "https://runbooks/paymentgw/payment-service-5xx"
- alert: BankAdapterCircuitOpen
expr: resilience4j_circuitbreaker_state{name="bankAdapter",state="open"} == 1
for: 2m
labels: { severity: ticket }
Alert routing (Alertmanager / PagerDuty):
severity=page→ on-call PagerDuty.severity=ticket→ Jira queue.
7. SLOs
For each user-facing service, define an SLO and an error budget. Example:
| Service | SLI | SLO target | Window |
|---|---|---|---|
| api-gateway | 2xx/3xx rate on /api/payments |
≥ 99.9 % | 30 d |
| payment-service | p95 latency POST /api/payments |
≤ 800 ms | 30 d |
| payment-query-service | p95 latency GET /api/payments/{id} |
≤ 200 ms | 30 d |
| payment events delivery | time from authorise → query visible | p99 ≤ 1 s | 30 d |
Error budget burn-rate alerts (multi-window, multi-burn):
- 14.4× burn over 1 h → page (means 2 % of monthly budget burned in 1 h).
- 6× burn over 6 h → ticket.
- 3× burn over 24 h → ticket.
See Google's SRE Workbook, ch. 5 for the math.
8. The "golden signals" cheatsheet
For every service, in a meeting, anyone should be able to answer in 30 seconds:
- Latency — p95 of the main endpoint right now?
- Traffic — req/s right now? trend vs same time yesterday?
- Errors — % 5xx right now? top error message?
- Saturation — CPU, memory, DB connection pool, Kafka consumer lag?
If your dashboards make those four things hard to find, the dashboards need work.