Observability with OpenTelemetry›02 · OpenTelemetry Collector deep dive
Learning Hub / Observability & Reliability / Observability with OpenTelemetry

Lesson 02 of 7 · Modules

OpenTelemetry Collector deep dive

Build telemetry pipelines with the OpenTelemetry Collector: receivers, processors, exporters and connectors, the processors every pipeline needs, agent and gateway deployment patterns on Kubernetes, the OpenTelemetry Operator, and how to see what the Collector itself is doing.

Practitioner
Key wordsOTel Collectorreceiversprocessorsexportersconnectorspipelinesmemory_limiterbatchk8sattributesagent vs gatewayOpenTelemetry Operatorcontrib distribution
apps SDK / auto-instrumented nodes & logs hostmetrics, filelog Prometheus targets scrape OpenTelemetry Collector receivers otlp prometheus filelog processors memory_limiter k8sattributes batch exporters otlp remote write otlphttp Traces Tempo / Jaeger Metrics Prometheus / Mimir Logs Loki / Elasticsearch
Receivers → processors → exporters: one Collector for traces, metrics and logs.

The pipeline model

receivers ──► processors ──► exporters
 (otlp,        (memory_limiter,  (otlp → Tempo,
  prometheus,   k8sattributes,    prometheusremotewrite → Prometheus/Mimir,
  filelog…)     filter, batch…)   otlphttp → Loki…)

Each pipeline handles one signal (traces, metrics or logs). A component can be used in several pipelines; connectors act as an exporter for one pipeline and a receiver for another.

The Collector is a sorting office for letters. Receivers are the doors where letters arrive. Processors are the desks where letters are stamped with an address label, checked, and bundled. Exporters are the vans that take bundles to the right city. If the office gets too full, the doorman stops accepting letters for a while (memory_limiter) instead of letting the building collapse.

A working config

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317      # recent versions default to localhost; bind explicitly in pods
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25
  k8sattributes:
    extract:
      metadata: [ k8s.namespace.name, k8s.pod.name, k8s.deployment.name, k8s.node.name ]
  resource:
    attributes:
      - key: k8s.cluster.name
        value: prod-eu-1
        action: upsert
  batch: {}

exporters:
  otlp/tempo:
    endpoint: tempo.observability.svc:4317
    tls:
      insecure: true                 # lab only; use TLS in production
  prometheusremotewrite:
    endpoint: http://prometheus.monitoring.svc:9090/api/v1/write
  otlphttp/loki:
    endpoint: http://loki-gateway.observability.svc/otlp
  debug: {}

service:
  pipelines:
    traces:
      receivers: [ otlp ]
      processors: [ memory_limiter, k8sattributes, resource, batch ]
      exporters: [ otlp/tempo ]
    metrics:
      receivers: [ otlp ]
      processors: [ memory_limiter, k8sattributes, resource, batch ]
      exporters: [ prometheusremotewrite ]
    logs:
      receivers: [ otlp ]
      processors: [ memory_limiter, k8sattributes, resource, batch ]
      exporters: [ otlphttp/loki ]

Notes:

  • Prometheus accepts remote write only with --web.enable-remote-write-receiver; recent Prometheus versions can also ingest OTLP directly (--web.enable-otlp-receiver).
  • Loki 3.x ingests OTLP natively at /otlp; the older Loki-specific exporter is deprecated.
  • k8sattributes needs RBAC to read pods, namespaces and ReplicaSets.
  • Components live in different distributions: the core otelcol has few; otelcol-contrib has most (k8sattributes, tail_sampling, filelog…); you can also build your own with the OpenTelemetry Collector Builder.

Processors you'll use

Processor Job
memory_limiter First: refuse data before OOM
k8sattributes Add pod/namespace/deployment metadata by source IP or pod UID
resource / attributes Add, change or delete attributes (cluster name, remove PII)
filter Drop telemetry (health checks, noisy spans) with OTTL conditions
transform Rewrite data with OTTL statements
tail_sampling Keep interesting traces (lesson 03; gateway only)
batch Group data before export for efficiency (recent versions also offer batching in exporters)

Agents, gateways and the Operator

  • Agent (DaemonSet): apps send OTLP to the node-local Collector ($(NODE_IP):4317 via the downward API); it also scrapes host and kubelet metrics and tails log files.
  • Gateway (Deployment + HPA): central processing, tail sampling, routing to tenants/backends, credentials for backends in one place.
  • The OpenTelemetry Operator manages Collectors as OpenTelemetryCollector resources (modes: deployment, daemonset, statefulset, sidecar) and auto-instrumentation through Instrumentation resources plus pod annotations such as instrumentation.opentelemetry.io/inject-java: "true".

Watching the Collector

  • Collector self-metrics (port 8888 by default): accepted, refused and failed-to-send items per component, queue sizes. Alert on refused and failed sends.
  • debug exporter prints data flowing through: invaluable for "why is this attribute missing?", but remove it afterwards (it's noisy).
  • otelcol-contrib validate --config catches config errors before a rollout.

Try it: a Collector pipeline on kind

  1. Install the OpenTelemetry Operator (it needs cert-manager; see its install docs) and create an OpenTelemetryCollector in deployment mode with the config above, pointing exporters at debug only.
  2. Send test traces with telemetrygen (from the collector-contrib project): telemetrygen traces --otlp-insecure --otlp-endpoint <collector>:4317 --traces 10.
  3. Read the Collector logs and see the spans printed by the debug exporter.
  4. Add k8sattributes and send from a pod; confirm the pod metadata appears.
  5. Add a filter processor that drops spans with http.route == "/healthz" and verify they disappear.

Going deeper: Collector at scale

  • Enable persistent queues (file storage extension + sending_queue) on exporters that must survive restarts and backend outages.
  • For tail sampling across several gateway replicas, put a loadbalancing exporter in front that routes by trace ID, so all spans of a trace reach the same replica.
  • Keep configs small and layered (agent: collect + enrich; gateway: policy), and version them in Git.
  • Pin Collector versions: component names and defaults evolve between releases; read the changelog before upgrading.

Recap

  • Receivers → processors → exporters, per signal pipeline; connectors link pipelines.
  • Always memory_limiter first, k8sattributes for context, batch for efficiency.
  • Agent (DaemonSet) + gateway (Deployment) patterns; the Operator manages Collectors and auto-instrumentation.
  • Watch the Collector's own metrics; debug with the debug exporter; validate configs before rollout.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.