Docker Observability with OpenTelemetry: Replace Your Scattered Monitoring Stack
OpenTelemetry for Docker observability — OTel Collector 0.156.0+, OTel eBPF Instrumentation (OBI) for zero-code tracing, Collector-to-Prometheus with prometheusremotewrite, OTel vs Prometheus decision guide, structured logging, and trace propagation across containers..
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓Docker and Docker Compose basics
- ✓Understanding of metrics, logs, and traces (observability signals)
- ✓Basic familiarity with Prometheus and Grafana
- ✓OpenTelemetry concepts (traces, spans, metrics, logs)
Deploy the OTel Collector in Docker: docker run -v $(pwd)/otel-config.yaml:/etc/otel/config.yaml otel/opentelemetry-collector-contrib:0.156.0. Configure receivers (OTLP, Prometheus, filelog), processors (batch, filter, attributes), and exporters (prometheusremotewrite, debug). For zero-code tracing, run the OTel eBPF Instrumentation agent: docker run --privileged --pid=host -v /sys/kernel/debug:/sys/kernel/debug ghcr.io/otel-ebpf/instrumentation:latest. This automatically instruments all HTTP/gRPC traffic without changing application code. Connect the eBPF agent to the OTel Collector via OTLP to see distributed traces across your Docker compose services.
Imagine your application is a busy restaurant kitchen with multiple stations (containers). Before OpenTelemetry, each station shouted its status in its own language — one used Prometheus for metrics, another used Jaeger for traces, a third used a custom log format. The head chef (you) had three different notebooks to understand what was happening. OpenTelemetry is like giving everyone the same notepad and pen: the OTel Collector is the head waiter who collects all notepads, checks for consistency, and delivers them to the right places. The eBPF Instrumentation is like installing cameras that automatically record every order and delivery without any chef having to write anything down.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Your microservices are running in Docker containers. You have Prometheus scraping metrics, Jaeger for traces, and the ELK stack for logs — three separate systems, each with its own configuration, storage, and alerting. A slow database query shows up as a high P99 latency in Prometheus, but connecting that to the specific trace in Jaeger and the error log in Kibana requires manual correlation. This fragmented approach is the legacy of the 2010s observability landscape.
OpenTelemetry changes this by unifying all three signals — traces, metrics, and logs — into a single data model and pipeline. The OTel Collector becomes the single point of configuration, processing, and routing. You define receivers (where data comes from), processors (what to do with it — sample, filter, enrich, batch), and exporters (where to send it). The same Collector instance can forward traces to Jaeger, metrics to Prometheus via prometheusremotewrite, and logs to any OTLP-compatible backend.
The latest evolution: OTel eBPF Instrumentation (OBI), which uses eBPF to automatically instrument network calls, database queries, and messaging without any code changes. This is game-changing for legacy applications that cannot be modified to add OTel SDKs. The eBPF agent attaches to network syscalls and reconstructs HTTP/gRPC spans, then exports them via OTLP to the Collector.
By the end of this guide, you'll have a complete OTel observability stack running in Docker — Collector, eBPF instrumentation, Prometheus storage, and trace/visualization — configured end-to-end with examples you can adapt to any application stack.
OTel Collector Architecture: Receivers, Processors, and Exporters
The OTel Collector is a pipeline — data flows from receivers through processors to exporters. Understanding this pipeline is the key to configuring OTel for any Docker environment.
Receivers are the data entry points. otlp receiver accepts OTel Protocol (gRPC on 4317, HTTP on 4318). prometheus receiver scrapes Prometheus endpoints. filelog receiver tails log files. hostmetrics receiver collects host-level metrics (CPU, memory, disk, network). Each receiver produces one or more of the three signals: traces, metrics, or logs.
Processors transform the data. batch aggregates for efficient export. filter drops data based on conditions. attributes adds or modifies attributes. memory_limiter prevents OOM by dropping data when memory exceeds threshold. tail_sampling implements trace sampling. transform applies OTTL for complex transformations.
Exporters are the data destinations. otlp exports to another Collector or backend. prometheusremotewrite exports metrics to Prometheus-compatible storage. jaeger exports traces to Jaeger. debug prints to stdout.
Define pipelines under service.pipelines. A typical traces pipeline: receivers: [otlp] → processors: [batch, memory_limiter] → exporters: [otlp, debug].
batch processor had send_batch_max_size: 100000 (default is 1000). The Collector held 100K spans in memory per batch. Fix: reduce to 1000, add memory_limiter. No more OOMs.OTel eBPF Instrumentation (OBI): Zero-Code Tracing for Docker Containers
Traditional OTel instrumentation requires adding SDKs to application code. For legacy applications, vendor-modified frameworks, or polyglot environments, this is impractical. OTel eBPF Instrumentation (OBI) solves this by automatically instrumenting network-level operations from outside the application.
OBI works by attaching eBPF programs to the kernel's network stack. When a container makes an HTTP request, the eBPF program intercepts the syscall, reads the HTTP headers, and reconstructs a span. It captures: method, URL path, status code, latency, and body size. It also propagates trace context via the traceparent header.
OBI supports: HTTP/1.1 and HTTP/2, gRPC, TLS connections (via uprobe attachment to OpenSSL/BoringSSL), MySQL and PostgreSQL queries, Redis commands, and Kafka produce/consume operations.
Running OBI: deploy as a privileged container sharing the host PID namespace and mounting debugfs. Configure with OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_SERVICE_NAME.
cat /sys/kernel/security/lsm to verify. If the output is 'none' or doesn't include 'bpf', OBI fails silently.Metrics Pipeline: OTel Collector to Prometheus via prometheusremotewrite
The OTel Collector can replace Prometheus server's scraping function entirely. Instead of having Prometheus scrape metrics from each container, the Collector receives metrics via OTLP and exports them using the prometheusremotewrite exporter.
This architecture ('OTel as the metrics pipeline frontend') is the recommended pattern since Collector 0.156.0+. Benefits: single configuration for all metrics, ability to process metrics before storage (filter, aggregate, enrich), and reduced load on Prometheus.
The prometheusremotewrite exporter sends metrics to Prometheus's remote write endpoint (/api/v1/write). Configure with endpoint, namespace, and resource_to_telemetry_conversion. The exporter uses a queue with configurable consumers and retry settings.
For long-term storage, use Prometheus in remote-write-only mode with Thanos or Grafana Mimir as the backend. The Collector sends metrics to the remote-write endpoint, and Thanos/Mimir handles storage and querying.
Metric naming: OTel uses dots (http.server.duration), Prometheus uses underscores (http_server_duration). The Collector automatically converts dots to underscores.
endpoint: http://victoriametrics:8428/api/v1/write. It's 10x more storage-efficient for high-cardinality metric sets.Structured Logging and Trace Correlation with OpenTelemetry
OTel's log signal unifies logs with traces and metrics. The key benefit: log entries can be correlated with traces using trace_id and span_id attributes, enabling clicking from a log line to the trace that generated it.
Structured logging: applications emit logs in JSON format with trace context embedded. Use OTel SDK logging (auto-injecting trace_id/span_id) and export via OTLP to the Collector.
For existing applications that log to files, use the filelog receiver. It tails log files, parses them (JSON, regex), and converts entries to OTel log records.
Log enrichment: use the attributes processor to add service.name, container.id, environment to all log entries.
Trace-log correlation: when the application emits logs with trace_id and span_id, the Collector correlates them. In Grafana, click from a log entry to the trace span.
Trace Propagation Across Containers: Making Distributed Tracing Actually Work
Distributed tracing across containers fails when trace context isn't propagated between services. This is the most common OTel implementation issue. If Service A calls Service B and the trace_id doesn't continue, you get two disconnected traces.
Trace context propagation uses the traceparent header (W3C Trace Context). When Service A makes an HTTP request, the OTel SDK injects the traceparent header. Service B's SDK extracts it and continues the trace. Format: 00-<trace_id_32_hex>-<span_id_16_hex>-01.
For SDK-instrumented services: set OTEL_PROPAGATORS=tracecontext,baggage. This enables W3C Trace Context plus baggage.
For eBPF-instrumented services: OBI automatically propagates trace context by capturing the traceparent header from outgoing requests and injecting it into downstream requests.
Common failure: proxies and load balancers strip unknown headers. Ensure your reverse proxy (nginx, Envoy, HAProxy) is configured to pass through the traceparent header.
For non-HTTP protocols (gRPC, messaging), use the OTel propagator for that protocol. gRPC uses binary propagation in metadata. Kafka messages propagate via message headers.
traceparent. This is the #1 cause of broken distributed tracing. Configure your reverse proxy to pass through traceparent, tracestate, and baggage headers. nginx: underscores_in_headers on; proxy_pass_request_headers on;.traceparent header. The fix: use the OTel AWS Lambda extension which re-injects the header after the ALB removes it. The traces appeared immediately.The 8-Minute P99 Latency Spike Nobody Could Explain
- Without unified observability, connecting latency symptoms to I/O root causes requires cross-referencing three tools. OTel's single pipeline eliminates this.
- Per-container CPU/memory metrics miss I/O contention. OTel eBPF Instrumentation captured the actual trace showing the database wait event.
- eBPF instrumentation discovered the root cause without any code changes — the checkout service was a legacy Java app without OTel SDKs.
docker logs <collector-container>. Look for 'Failed to export' or 'receiver is closed'. 2. Verify receiver configuration matches your app's protocol (OTLP gRPC on 4317, OTLP HTTP on 4318). 3. Test with Collector's health check: curl http://localhost:13133. 4. Use the debug exporter to see data in Collector stdout.cat /sys/kernel/security/lsm should not be 'none'. 2. Check eBPF agent logs for 'failed to attach BPF program'. 3. Ensure agent runs with --privileged and --pid=host. 4. Test with a known HTTP request: curl http://localhost:8080/api/health. 5. Verify OTLP endpoint in eBPF agent config points to the Collector.curl -I http://prometheus:9090/api/v1/write. 2. Verify WAL disk space: df -h /var/lib/prometheus. 3. Add batch processor and queue settings to the exporter. 4. Check for metric name conflicts: duplicate names cause 409.| File | Command / Code | Purpose |
|---|---|---|
| otel-collector-config.yaml | receivers: | OTel Collector Architecture |
| docker-compose-obi.yaml | version: '3.8' | OTel eBPF Instrumentation (OBI) |
| metrics-pipeline.yaml | receivers: | Metrics Pipeline |
| otel-structured-logging.py | from opentelemetry import trace | Structured Logging and Trace Correlation with OpenTelemetry |
| trace-propagation-check.sh | curl -v http://service-b:8080/api/endpoint 2>&1 | grep -i traceparent | Trace Propagation Across Containers |
Key takeaways
Common mistakes to avoid
4 patternsNot configuring memory_limiter processor, causing Collector OOM during traffic spikes
Forgetting to set OTEL_PROPAGATORS=tracecontext on all services
Using Prometheus receiver in Collector to scrape, then also having Prometheus scrape the same targets
Not filtering high-cardinality metrics before they reach Prometheus storage
Interview Questions on This Topic
Compare and contrast OpenTelemetry and Prometheus for metrics collection. When would you use each?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Docker. Mark it forged?
3 min read · try the examples if you haven't