Je hebt metrics die je vertellen dat iets traag is. Je hebt logs die vertellen dat er errors waren. Maar welke request faalde? Waar kwam de latency vandaan? Welke service in de keten veroorzaakte de timeout?
Dit is waar distributed tracing om de hoek komt kijken. Het volgt individuele requests terwijl ze door je microservices stromen, en toont je precies wat er gebeurde en waar.
De Observability Driehoek
flowchart TD
subgraph observability["Complete Observability"]
M["Metrics<br/>(Prometheus/Thanos)<br/>WAT gebeurt er"]
L["Logs<br/>(Loki)<br/>WAAROM gebeurde het"]
T["Traces<br/>(Tempo)<br/>WAAR gebeurde het"]
end
M <--> L
L <--> T
T <--> M
G["Grafana"] --> M
G --> L
G --> T
- Metrics beantwoorden: “Wat is de error rate? Wat is de latency?”
- Logs beantwoorden: “Welke error message? Wat was de context?”
- Traces beantwoorden: “Welke service? Welke call? Wat was het pad?”
Samen geven ze je volledig begrip.
Wat is een Trace?
Een trace is een boom van spans die werk representeren voor één request:
flowchart LR
subgraph trace["Trace: order-12345"]
A["API Gateway<br/>250ms"] --> B["Order Service<br/>180ms"]
B --> C["Inventory Check<br/>45ms"]
B --> D["Payment Service<br/>120ms"]
D --> E["Bank API<br/>95ms"]
B --> F["Notification<br/>15ms"]
end
Elk blok is een span. Spans hebben:
- Name: Welke operatie (bijv. “HTTP GET /orders”)
- Duration: Hoe lang het duurde
- Parent: Welke span initieerde deze
- Attributes: Key-value metadata (user_id, order_id, etc.)
- Status: Success/error
De trace ID linkt alle spans van dezelfde request over alle services.
Waarom Tempo?
Grafana Tempo is ontworpen om:
- Kosteneffectief te zijn — Object storage backend, geen indexering
- Simpel te zijn — Geen complexe cluster management
- Schaalbaar te zijn — Handelt enorme trace volumes
- Geïntegreerd te zijn — Native Grafana support, links naar metrics/logs
Net als Loki voor logs, indexeert Tempo alleen trace IDs. Het indexeert geen spans of attributes. Dit houdt kosten laag maar betekent dat je trace IDs nodig hebt om te queryen — je kunt niet zoeken naar “alle traces met user_id=123”.
De oplossing: gebruik metrics en logs om trace IDs te vinden, duik dan diep in Tempo.
Architectuur
flowchart TD
subgraph apps["Applicaties"]
A1["Service A<br/>(instrumented)"]
A2["Service B<br/>(instrumented)"]
A3["Service C<br/>(instrumented)"]
end
subgraph collector["OpenTelemetry"]
OC["OTel Collector"]
end
A1 -->|"OTLP"| OC
A2 -->|"OTLP"| OC
A3 -->|"OTLP"| OC
OC -->|"traces"| T["Tempo"]
OC -->|"metrics"| P["Prometheus"]
OC -->|"logs"| L["Loki"]
T --> OS["Object Storage"]
T --> G["Grafana"]
P --> G
L --> G
Applicaties zijn geïnstrumenteerd met OpenTelemetry SDKs. OTel Collector ontvangt telemetry, verwerkt en exporteert. Tempo slaat traces op in object storage. Grafana visualiseert en correleert alles.
Tempo Installeren
Met Helm:
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm install tempo grafana/tempo \
--namespace monitoring \
--values tempo-values.yaml
Basis single-binary deployment:
# tempo-values.yaml
tempo:
storage:
trace:
backend: local
local:
path: /var/tempo/traces
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
persistence:
enabled: true
size: 50Gi
Productie met object storage:
# tempo-values.yaml
tempo:
storage:
trace:
backend: s3
s3:
bucket: tempo-traces
endpoint: minio.storage:9000
access_key: ${MINIO_ACCESS_KEY}
secret_key: ${MINIO_SECRET_KEY}
insecure: true
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Retentie
compactor:
compaction:
block_retention: 48h
# Distributed mode voor schaal
distributor:
replicas: 2
ingester:
replicas: 3
querier:
replicas: 2
compactor:
replicas: 1
OpenTelemetry Collector Installeren
De OTel Collector fungeert als pipeline voor alle telemetry:
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm install otel-collector open-telemetry/opentelemetry-collector \
--namespace monitoring \
--values otel-collector-values.yaml
Collector configuratie:
# otel-collector-values.yaml
mode: deployment
replicaCount: 2
config:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
# Voeg Kubernetes metadata toe
k8sattributes:
auth_type: serviceAccount
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.deployment.name
# Sample om volume te reduceren
probabilistic_sampler:
sampling_percentage: 10
exporters:
otlp/tempo:
endpoint: tempo.monitoring:4317
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
namespace: otel
service:
pipelines:
traces:
receivers: [otlp]
processors: [k8sattributes, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
Applicaties Instrumenteren
Auto-Instrumentatie (Easy Mode)
Voor veel talen kan OpenTelemetry automatisch instrumenteren zonder code wijzigingen.
Java:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
image: my-java-app:latest
env:
- name: JAVA_TOOL_OPTIONS
value: "-javaagent:/otel/opentelemetry-javaagent.jar"
- name: OTEL_SERVICE_NAME
value: "order-service"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector.monitoring:4317"
volumeMounts:
- name: otel-agent
mountPath: /otel
initContainers:
- name: otel-agent
image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
command: [cp, /javaagent.jar, /otel/opentelemetry-javaagent.jar]
volumeMounts:
- name: otel-agent
mountPath: /otel
volumes:
- name: otel-agent
emptyDir: {}
Python:
FROM python:3.11
RUN pip install opentelemetry-distro opentelemetry-exporter-otlp
RUN opentelemetry-bootstrap -a install
CMD ["opentelemetry-instrument", "python", "app.py"]
Node.js:
// tracing.js - require dit eerst
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://otel-collector:4317',
}),
instrumentations: [getNodeAutoInstrumentations()],
serviceName: process.env.OTEL_SERVICE_NAME || 'my-service',
});
sdk.start();
Handmatige Instrumentatie (Meer Controle)
Voor custom spans en attributes:
// Go voorbeeld
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
)
func ProcessOrder(ctx context.Context, orderID string) error {
tracer := otel.Tracer("order-service")
ctx, span := tracer.Start(ctx, "process-order")
defer span.End()
// Voeg attributes toe
span.SetAttributes(
attribute.String("order.id", orderID),
attribute.String("order.type", "standard"),
)
// Maak child span voor sub-operatie
ctx, childSpan := tracer.Start(ctx, "validate-inventory")
err := validateInventory(ctx, orderID)
childSpan.End()
if err != nil {
span.RecordError(err)
return err
}
return nil
}
Context Propagation
Voor traces om te werken over services, moet context meegegeven worden met requests.
HTTP headers (automatisch met instrumentatie):
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: vendor=value
gRPC metadata (automatisch met instrumentatie)
Bij handmatige HTTP calls:
// Inject context in uitgaande request
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
// Extract context uit inkomende request
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
Grafana Integratie
Voeg Tempo toe als data source:
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-datasources
data:
tempo.yaml: |
apiVersion: 1
datasources:
- name: Tempo
type: tempo
url: http://tempo.monitoring:3100
access: proxy
jsonData:
tracesToLogs:
datasourceUid: loki
tags: ['app', 'namespace']
tracesToMetrics:
datasourceUid: prometheus
tags: ['service.name']
serviceMap:
datasourceUid: prometheus
nodeGraph:
enabled: true
search:
hide: false
lokiSearch:
datasourceUid: loki
Traces Vinden
In Grafana Explore:
- Selecteer Tempo data source
- Kies “Search” tab
- Filter op service name, duration, status
- Klik een trace om de waterfall te zien
Trace naar Logs
Met tracesToLogs geconfigureerd, kun je van een span direct naar gerelateerde logs springen:
- Open een trace
- Klik een span
- Klik “Logs for this span”
- Zie Loki logs met dezelfde trace ID
Trace naar Metrics
Vergelijkbaar, link traces naar request metrics:
- Zie trage traces
- Check corresponderende latency histogrammen
- Correleer met error rates
Service Graph
Tempo kan een service dependency graph genereren uit traces:
# Enable metrics generator in Tempo
tempo:
metricsGenerator:
enabled: true
remoteWriteUrl: http://prometheus.monitoring:9090/api/v1/write
Dit creëert metrics zoals:
traces_service_graph_request_totaltraces_service_graph_request_failed_totaltraces_service_graph_request_server_seconds
Grafana toont dit als interactieve service map die traffic flow en error rates tussen services laat zien.
Sampling Strategieën
Op schaal kun je niet elke trace opslaan. Sampling strategieën:
Head Sampling (Bij Collectie)
# OTel Collector
processors:
probabilistic_sampler:
sampling_percentage: 10 # Houd 10% van traces
Simpel maar je mist mogelijk interessante traces.
Tail Sampling (Na Collectie)
processors:
tail_sampling:
decision_wait: 10s
policies:
# Altijd errors bewaren
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
# Altijd trage traces bewaren
- name: slow
type: latency
latency:
threshold_ms: 1000
# Sample 5% van de rest
- name: probabilistic
type: probabilistic
probabilistic:
sampling_percentage: 5
Beter: bewaart alle errors en trage traces, sampled normale.
Mijn Productie Setup
# Tempo met object storage
tempo:
storage:
trace:
backend: s3
s3:
bucket: tempo-traces
endpoint: minio.storage:9000
compactor:
compaction:
block_retention: 72h # 3 dagen traces
metricsGenerator:
enabled: true
remoteWriteUrl: http://prometheus:9090/api/v1/write
# OTel Collector met tail sampling
otel-collector:
config:
processors:
tail_sampling:
policies:
- name: errors
type: status_code
status_codes: [ERROR]
- name: slow
type: latency
threshold_ms: 500
- name: sample-rest
type: probabilistic
sampling_percentage: 5
Belangrijke keuzes:
- 72u retentie — Genoeg om recente issues te debuggen
- Tail sampling — Bewaar alle errors en trage traces
- 5% general sampling — Beheerbaar volume
- Service graph — Visuele dependency map
Debuggen met Traces
Echte debugging workflow:
- Alert vuurt: Hoge latency op checkout service
- Check metrics: P99 latency piekte om 14:32
- Vind traces: Zoek Tempo voor checkout-service, duration > 1s, tijdsbereik 14:30-14:35
- Analyseer trace: Zie dat payment-service call 4.2s duurde
- Duik in span: Zie
db.statementattribute die trage query toont - Check logs: Spring naar Loki logs voor die span, zie connection pool uitputting
- Fix: Vergroot connection pool size
Zonder tracing zou je raden welke service de latency veroorzaakte.
Waarom Dit Ertoe Doet
Microservices zijn geweldig voor teams maar verschrikkelijk voor debugging. Een enkele user request raakt mogelijk 10 services. Als iets faalt:
- Logs tonen errors maar geen causaliteit
- Metrics tonen symptomen maar geen root cause
- Alleen traces tonen het complete plaatje
Met Prometheus/Thanos voor metrics, Loki voor logs, en Tempo voor traces, heb je complete observability. Alles in Grafana. Alles gecorreleerd. Alles zelf-gehost.
Geen “works on my machine” meer. Geen “ik denk dat het de payment service is” meer. Alleen data.
Metrics vertellen je de score. Logs vertellen je het verslag. Traces vertellen je wie de bal naar wie speelde. Je hebt alle drie nodig om de wedstrijd te begrijpen.
