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:

  1. Kosteneffectief te zijn — Object storage backend, geen indexering
  2. Simpel te zijn — Geen complexe cluster management
  3. Schaalbaar te zijn — Handelt enorme trace volumes
  4. 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:

  1. Selecteer Tempo data source
  2. Kies “Search” tab
  3. Filter op service name, duration, status
  4. Klik een trace om de waterfall te zien

Trace naar Logs

Met tracesToLogs geconfigureerd, kun je van een span direct naar gerelateerde logs springen:

  1. Open een trace
  2. Klik een span
  3. Klik “Logs for this span”
  4. Zie Loki logs met dezelfde trace ID

Trace naar Metrics

Vergelijkbaar, link traces naar request metrics:

  1. Zie trage traces
  2. Check corresponderende latency histogrammen
  3. 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_total
  • traces_service_graph_request_failed_total
  • traces_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:

  1. Alert vuurt: Hoge latency op checkout service
  2. Check metrics: P99 latency piekte om 14:32
  3. Vind traces: Zoek Tempo voor checkout-service, duration > 1s, tijdsbereik 14:30-14:35
  4. Analyseer trace: Zie dat payment-service call 4.2s duurde
  5. Duik in span: Zie db.statement attribute die trage query toont
  6. Check logs: Spring naar Loki logs voor die span, zie connection pool uitputting
  7. 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.