Skip to content
Incident LabOpenTelemetry

Collector 0.162.0

The telemetry pipeline

Every service sends everything to one place over OTLP. The Collector decides what happens next: it receives, processes and exports, with one pipeline per signal. Nothing here is hidden; this is the exact configuration the lab runs.

Follow the data

Processors

What it does

Run in order on everything that passes. memory_limiter refuses data before the Collector runs out of memory, attributes/scrub deletes attributes that must never be stored, batch groups items into fewer, larger exports.

Why it exists

Policy lives in one place instead of in every service: protection, redaction, sampling, enrichment, batching.

What flows through

The same signals, minus scrubbed attributes, grouped into batches of up to 1024 items or 2 s.

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
  attributes/scrub:
    actions:
      - key: user.email
        action: delete
  batch:
    send_batch_size: 1024
    timeout: 2s

Three pipelines

PipelineReceiverProcessors, in orderExportersEnds up in
tracesotlpmemory_limiter → attributes/scrub → batchotlp_grpc/tempo, debugTempo
metricsotlpmemory_limiter → batchotlp_http/prometheus, debugPrometheus (OTLP endpoint)
logsotlpmemory_limiter → attributes/scrub → batchotlp_http/loki, debugLoki (OTLP endpoint)

Order matters: memory_limiter goes first so overload is refused before work is done, batch goes last so sampling and filtering happen before batching. Tempo also derives service-graph and span metrics from the traces it receives and writes them to Prometheus.

infra/otel/collector.yaml
# OpenTelemetry Collector configuration (otelcol-contrib 0.162.0).
#
# Every service sends traces, metrics and logs here over OTLP. The Collector is the only thing
# that knows where telemetry ends up: swap a backend by editing this file, not the services.
#
#   receivers   how telemetry gets in          (OTLP over gRPC :4317 and HTTP :4318)
#   processors  what happens to it on the way  (protect memory, scrub, batch)
#   exporters   where it goes                  (Tempo, Prometheus, Loki, and the console)
#   pipelines   which receivers, processors and exporters each signal uses
#
# Check it without starting anything:
#   docker compose run --rm otel-collector validate --config=/etc/otelcol-contrib/config.yaml

extensions:
  # GET :13133/ answers 200 while the Collector is running. Docker uses it as a health check.
  health_check:
    endpoint: 0.0.0.0:13133

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  # First in every pipeline: refuse data before the Collector runs out of memory.
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25

  # Defence in depth: drop attributes that must never be stored, even if a service sends them.
  # The services do not set these; this is the safety net for when someone eventually does.
  attributes/scrub:
    actions:
      - key: http.request.header.authorization
        action: delete
      - key: http.request.header.cookie
        action: delete
      - key: user.email
        action: delete
      - key: user.password
        action: delete

  # Last before export: group items into batches. Fewer, larger requests to each backend.
  batch:
    send_batch_size: 1024
    timeout: 2s

exporters:
  # Traces to Tempo over OTLP/gRPC.
  otlp_grpc/tempo:
    endpoint: tempo:4317
    tls:
      insecure: true

  # Metrics to Prometheus' native OTLP endpoint (Prometheus runs with --web.enable-otlp-receiver).
  otlp_http/prometheus:
    endpoint: http://prometheus:9090/api/v1/otlp
    tls:
      insecure: true

  # Logs to Loki's native OTLP endpoint.
  otlp_http/loki:
    endpoint: http://loki:3100/otlp
    tls:
      insecure: true

  # A one-line summary per batch in `docker compose logs otel-collector`. Set verbosity to
  # `detailed` to print every span, metric and log record while you learn.
  debug:
    verbosity: basic

service:
  extensions: [health_check]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, attributes/scrub, batch]
      exporters: [otlp_grpc/tempo, debug]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp_http/prometheus, debug]
    logs:
      receivers: [otlp]
      processors: [memory_limiter, attributes/scrub, batch]
      exporters: [otlp_http/loki, debug]
  telemetry:
    # The Collector's own metrics (accepted, refused, exported items), scraped by Prometheus.
    metrics:
      readers:
        - pull:
            exporter:
              prometheus:
                host: 0.0.0.0
                port: 8888

Validate it without starting anything: docker compose run --rm otel-collector validate --config=/etc/otelcol-contrib/config.yaml. Chapters 06 and 07 walk through it and change it.

Batching

Exporting 1000 spans

Educational simulation, not live data
Export requests
10
Overhead (model)
20 ms
Fewer requests than unbatched
99%

A toy cost model: 2 ms of fixed cost per request. One span per request pays the fixed cost 1000 times. Batching pays it 10 times, compresses better and makes fewer connections. The trade-off is delivery delay: an item waits until the batch fills or the timeout (2 s here) fires, and a crash loses what was still buffered.

Sampling

1000 requests, one sampling policy

Educational simulation, not live data
Sampling policy

normal, keptslow (> 1 s), kepterror, keptdropped

Traces stored
100 / 1000
Errors and slow traces kept
5 / 45
Storage vs keeping all
10%

Head sampling decides when a trace starts, before anyone knows whether it will fail. It is cheap and simple, and it throws away most of the traces you will want during an incident.

The lab keeps every trace by default. Chapter 12 switches the real Collector to the tail-sampling configuration (keep errors, keep traces over 1 s, 10% of the rest) so you can measure the effect on live traffic instead of trusting this simulation.