Expert OpenTelemetry guidance for collector configuration, pipeline design, and production telemetry instrumentation across Kubernetes, ECS, serverless, and standalone deployments. Use when configuring collectors, designing pipelines, instrumenting applications, implementing sampling, managing cardinality, securing telemetry, writing OTTL transformations, or setting up AI coding agent observability (Claude Code, Codex, Gemini CLI, GitHub Copilot).
94
98%
Does it follow best practices?
Impact
94%
1.34xAverage score across 18 eval scenarios
Passed
No known issues
Kubernetes is the preferred platform for deploying the OpenTelemetry Collector at scale. This guide covers three core patterns: Agent (DaemonSet), Gateway (Deployment), and Sidecar, along with platform-specific considerations for EKS, GKE, AKS, and OpenShift.
The Kubernetes ecosystem provides native abstractions for resilience, scaling, and RBAC that simplify collector deployments compared to VMs.
┌─────────────────────────────────────────────────┐
│ Application Pods (any namespace) │
│ ┌────────────────────────────────────────────┐ │
│ │ App Container │ Sidecar Collector │ │
│ │ (instrumented) │ (optional) │ │
│ └────────────────────────────────────────────┘ │
└──────────┬──────────────────────────┬───────────┘
│ (localhost:4317) │
┌──────┴──────────────────────────┴───────┐
│ Gateway Collector (Deployment) │
│ • Tail sampling │
│ • Attribute enrichment │
│ • Batching & compression │
│ • 2-5 replicas (sticky routing) │
└──────────────┬──────────────────────────┘
│
┌──────────────┴──────────────────────────┐
│ Agent Collector (DaemonSet) │
│ • Node metrics (CPU, memory, etc.) │
│ • kubelet stats │
│ • Pod logs via volume mounts │
│ • Kubernetes events │
│ One per cluster node │
└──────────────┬──────────────────────────┘
│
┌──────────────┴──────────────────────────┐
│ Backend Exporters (OTLP, etc.) │
│ • Traces, Metrics, Logs │
│ • Managed backends or on-prem │
└──────────────────────────────────────────┘| Platform | Notes | Networking | RBAC | DaemonSet Support |
|---|---|---|---|---|
| EKS | AWS managed, fully compatible | VPC CNI, Security Groups | IAM via IRSA | Yes |
| GKE | Google managed, fully compatible | VPC, Cloud Armor | IAM Service Accounts | Yes |
| AKS | Azure managed, fully compatible | Virtual Networks, NSGs | RBAC with Entra ID | Yes |
| OpenShift | Red Hat distribution, additional security | OVN/Calico | RBAC + SCCs (Security Context Constraints) | Yes, with SCC exceptions |
| Self-managed (kubeadm, kops, etc.) | Custom networking and RBAC setup | Your choice (Calico, Weave, etc.) | Your RBAC policy | Yes |
When to use:
/var/log/podsSingle-node collection breakdown:
DaemonSet Replica on each node:
│
├─ hostNetwork: true (access to host metrics)
├─ hostPID: true (optional, for host processes)
├─ Prometheus receiver → kubelet stats (:10250)
├─ Filelog receiver → /var/log/pods/* (mounted)
├─ K8s events receiver → Kubernetes API
└─ Processors (batch, memory limiter, etc.)
└─ Exporters (OTLP to gateway or backend)Example YAML:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: otel-agent
namespace: observability
spec:
selector:
matchLabels:
app: otel-agent
tier: agent
template:
metadata:
labels:
app: otel-agent
tier: agent
spec:
serviceAccountName: otel-agent
# Tolerations for node taints (control plane, workload-specific)
tolerations:
- effect: NoSchedule
operator: Exists
- effect: NoExecute
operator: Exists
hostNetwork: true # Required for kubelet stats
hostPID: false # Set to true only if monitoring host processes
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:latest
imagePullPolicy: IfNotPresent
ports:
- name: otlp-grpc
containerPort: 4317
protocol: TCP
hostPort: 4317 # Required for pod-to-agent discovery
- name: otlp-http
containerPort: 4318
protocol: TCP
hostPort: 4318
- name: metrics
containerPort: 8888
protocol: TCP
env:
- name: GOGC
value: "80"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-gateway.observability:4317"
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
securityContext:
runAsUser: 0
runAsGroup: 0
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containersWhen to use:
YAML example (Deployment + Service):
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-gateway
namespace: observability
spec:
replicas: 3 # Odd number for quorum-based decisions
selector:
matchLabels:
app: otel-gateway
template:
metadata:
labels:
app: otel-gateway
spec:
serviceAccountName: otel-gateway
containers:
- name: otel-collector
image: otel/opentelemetry-collector-contrib:latest
ports:
- name: otlp-grpc
containerPort: 4317
- name: otlp-http
containerPort: 4318
- name: metrics
containerPort: 8888
env:
- name: GOGC
value: "80"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "https://backend.example.com:4317"
- name: OTEL_EXPORTER_OTLP_HEADERS
valueFrom:
secretKeyRef:
name: backend-credentials
key: headers
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
livenessProbe:
httpGet:
path: /
port: 13133
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 13133
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: otel-gateway
namespace: observability
spec:
type: ClusterIP
clusterIP: None
selector:
app: otel-gateway
ports:
- name: otlp-grpc
port: 4317
targetPort: 4317
protocol: TCP
- name: otlp-http
port: 4318
targetPort: 4318
protocol: TCPKey gotchas:
sessionAffinity: ClientIP is still a valid option for ordinary gateway scale-out and for topologies where all spans for a trace arrive from the same client source. It is not trace-aware, so traces that include spans from multiple pods or services can still be split across gateway replicas. For tail sampling in multi-source traces, send traces through upstream agents that use the loadbalancing exporter with routing_key: traceID and resolve this headless service.memory_limiter in config to prevent OOM kills during traffic spikesClientIP stickiness option for non-tail-sampling gateway scale-out:
apiVersion: v1
kind: Service
metadata:
name: otel-gateway
namespace: observability
spec:
type: ClusterIP
selector:
app: otel-gateway
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIPConfig:
timeoutSeconds: 3600
ports:
- name: otlp-grpc
port: 4317
targetPort: 4317
protocol: TCPAgent exporter for tail-sampling stickiness:
exporters:
loadbalancing:
routing_key: traceID
protocol:
otlp:
endpoint: placeholder
tls:
insecure: true
resolver:
k8s:
service: otel-gateway
ports: [4317]When to use:
Example: Init container pattern or Helm subcharts
Sidecar collectors are typically injected via:
Example init container pattern:
apiVersion: v1
kind: Pod
metadata:
name: app-with-collector
spec:
initContainers:
- name: init-collector-config
image: otel/opentelemetry-collector-contrib:latest
command: ["cp", "/etc/collector/config.yaml", "/var/shared/config.yaml"]
volumeMounts:
- name: collector-config
mountPath: /etc/collector
readOnly: true
- name: shared-config
mountPath: /var/shared
containers:
- name: app
image: myapp:latest
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://localhost:4317"
- name: otel-collector
image: otel/opentelemetry-collector-contrib:latest
ports:
- containerPort: 4317
volumeMounts:
- name: shared-config
mountPath: /etc/otel
volumes:
- name: collector-config
configMap:
name: otel-collector-config
- name: shared-config
emptyDir: {}Symptom: DaemonSet collector fails to bind to port 4317
Cause: hostNetwork: true with conflicts from other processes
Solution:
# Change the receiver and exposed host port to an explicit unused port.
spec:
template:
spec:
hostNetwork: true
containers:
- name: otel-collector
ports:
- containerPort: 14317
hostPort: 14317
name: otlp-grpc-altUpdate the collector OTLP receiver to bind to the same port, for example 0.0.0.0:14317.
Alternative: use hostNetwork: false and expose the DaemonSet behind a Service if your network layer allows pod-to-pod discovery.
Symptom: DaemonSet replicas missing on certain nodes
Cause: Missing tolerations for node taints
Solution:
spec:
template:
spec:
tolerations:
# Tolerate NoSchedule taint (common on control planes)
- key: node-role.kubernetes.io/control-plane
operator: Equal
effect: NoSchedule
# Tolerate NoExecute with grace period
- key: any-key
operator: Exists
effect: NoExecute
tolerationSeconds: 300
# For GPU nodes, workload-specific taints, etc.
- key: workload-type
operator: Equal
value: gpu
effect: NoScheduleSymptom: Collector can't query Kubernetes API for events or metadata
Cause: Missing RBAC bindings
Solution:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: otel-agent
namespace: observability
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: otel-agent
rules:
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods", "namespaces", "nodes"]
verbs: ["get", "list"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: otel-agent
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: otel-agent
subjects:
- kind: ServiceAccount
name: otel-agent
namespace: observabilitySymptom: Collector killed with OOM or CPU throttling
Cause: Insufficient requests/limits
Solution: Start conservative, monitor, and adjust:
resources:
requests:
cpu: 100m # Agent: 100m, Gateway: 500m
memory: 256Mi # Agent: 256Mi, Gateway: 1Gi
limits:
cpu: 500m # Agent: 500m, Gateway: 2000m
memory: 512Mi # Agent: 512Mi, Gateway: 2GiMonitor actual usage: kubectl top pods -n observability
Symptom: Data loss when collector restarts; tail sampling state lost
Cause: File storage not mounted; using in-memory queues
Solution: Mount a PVC for queue persistence:
spec:
template:
spec:
volumes:
- name: collector-queue
persistentVolumeClaim:
claimName: otel-queue
containers:
- name: otel-collector
volumeMounts:
- name: collector-queue
mountPath: /var/lib/otelIn collector config (config.yaml):
exporters:
otlp:
endpoint: backend:4317
sending_queue:
storage: file_storage # Persistent queue
extensions:
file_storage:
directory: /var/lib/otel
timeout: 10s
compaction:
directory: /var/lib/otel
renotify_on_update: true
renotify_on_start: true
interval: 5m
on_start: trueDiagnostic steps:
# Check pod status
kubectl describe pod -n observability otel-agent-xxxxx
# View collector logs
kubectl logs -n observability -l app=otel-agent --tail=100
# Check image availability
kubectl describe pods -n observability otel-agent-xxxxx | grep -A 5 "Containers:"
# Verify RBAC permissions
kubectl auth can-i get events --as=system:serviceaccount:observability:otel-agentCommon causes:
imagePullSecrets and registry credentialskubectl describe nodesDiagnostic steps:
# Verify hostNetwork connectivity
kubectl exec -it otel-agent-xxxxx -n observability -- netstat -tlnp | grep 4317
# Test OTLP endpoint from pod
kubectl exec -it otel-agent-xxxxx -n observability -- curl -s http://localhost:4317/healthz
# Check collector config in ConfigMap
kubectl get configmap -n observability otel-collector-config -o yaml
# Verify environment variables in pod
kubectl exec -it otel-agent-xxxxx -n observability -- env | grep OTELCommon causes:
hostNetwork: false prevents pod-local collection: set hostNetwork: true for DaemonSet agentsotlp: receiver in collectors: sectionDiagnostic steps:
# Check memory limits vs actual usage
kubectl top pods -n observability -l app=otel-gateway
# View OOM kills in events
kubectl get events -n observability --sort-by='.lastTimestamp' | grep -i oom
# Inspect memory_limiter config
kubectl exec -it otel-gateway-xxxxx -n observability -- cat /etc/otel/config.yaml | grep -A 5 memory_limiterCommon causes:
memory_limiter processor before exportersattributes processor to drop high-cardinality dimensionssend_batch_size in batch processor.claude-plugin
.codex-plugin
.cursor-plugin
.github
scripts
docs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
references