Orchestration

OpenTelemetry Collector: Unified Observability

September 2, 2026 Kubezilla Team 13 min read

Welcome, Kubezilla engineers! In the dynamic world of Kubernetes, observability is not merely a feature; it’s a foundational pillar for maintaining healthy, high-performing applications. Yet, achieving a unified view across logs, metrics, and traces can feel like herding cats. Different agents, varying formats, and a multitude of backend systems often lead to a fragmented, complex, and costly observability stack.

Enter the OpenTelemetry Collector – a vendor-agnostic, powerful, and highly flexible component designed to solve this very problem. It acts as a central processing pipeline for your telemetry data, allowing you to collect, process, and export data in a standardized way, regardless of its origin or destination. By deploying the Collector within your Kubernetes clusters, you gain unparalleled control over your observability data, simplifying your architecture and enhancing your ability to understand and troubleshoot your distributed applications.

This guide will walk you through deploying and configuring the OpenTelemetry Collector on Kubernetes, transforming your fragmented observability landscape into a streamlined, efficient, and unified pipeline. We’ll cover everything from basic setup to advanced processing, ensuring your telemetry data flows seamlessly from your applications to your chosen analytics backends. Let’s dive in and unlock the full potential of your Kubernetes observability!

TL;DR: Unified Observability with OpenTelemetry Collector

The OpenTelemetry Collector standardizes telemetry data (metrics, logs, traces) collection, processing, and export in Kubernetes. Deploy it as a DaemonSet for node-level collection or a Deployment for cluster-wide aggregation. It simplifies your observability stack by acting as a central pipeline, reducing agent sprawl and providing flexible data transformation before sending to various backends.

Key Commands:

  • Deploy the Collector (DaemonSet example):
    kubectl apply -f opentelemetry-collector-daemonset.yaml
  • Deploy the Collector (Deployment example):
    kubectl apply -f opentelemetry-collector-deployment.yaml
  • Verify Deployment:
    kubectl get pods -l app.kubernetes.io/name=opentelemetry-collector -n opentelemetry-collector
    kubectl logs -l app.kubernetes.io/name=opentelemetry-collector -n opentelemetry-collector
  • Port-forward to the Collector (for testing):
    kubectl port-forward svc/opentelemetry-collector-headless 4317:4317 -n opentelemetry-collector

 

Prerequisites

Before we embark on this observability journey, ensure you have the following:

  • A Kubernetes Cluster: A running Kubernetes cluster (e.g., Minikube, Kind, GKE, EKS, AKS). This guide assumes you have kubectl configured to interact with your cluster.
  • kubectl: The Kubernetes command-line tool, installed and configured. Refer to the official Kubernetes documentation for installation instructions.
  • Basic Kubernetes Knowledge: Familiarity with Kubernetes concepts like Pods, Deployments, Services, ConfigMaps, and Namespaces.
  • Helm (Optional but Recommended): For easier management and deployment of complex applications like the OpenTelemetry Collector, Helm is highly recommended. We will primarily use raw Kubernetes manifests for granular control, but mention Helm where appropriate.
  • A Target Observability Backend: While the Collector can send data anywhere, having a target like Jaeger (for traces), Prometheus (for metrics), or an OTLP-compatible backend (e.g., Grafana Cloud, Datadog, New Relic) will allow you to see your data in action. We’ll use a simple local debugging setup for demonstration.

 

Step-by-Step Guide: Deploying the OpenTelemetry Collector on Kubernetes

We’ll deploy the OpenTelemetry Collector in a dedicated namespace, configure it via a ConfigMap, and then deploy it as both a DaemonSet (for node-level collection) and a Deployment (for cluster-level aggregation). This dual approach covers common use cases.

Step 1: Create a Dedicated Namespace

It’s a best practice to isolate infrastructure components like the OpenTelemetry Collector in their own Kubernetes namespace. This helps with organization, resource management, and security. We’ll create a namespace named opentelemetry-collector.

This command creates a new namespace. All our subsequent resources for the Collector will reside within this namespace, making it easy to manage and clean up later. Good namespace hygiene is crucial in any Kubernetes environment, especially when dealing with core infrastructure components.

kubectl create namespace opentelemetry-collector

Verify Namespace Creation

You should see the opentelemetry-collector namespace listed among others.

kubectl get namespaces
NAME                      STATUS   AGE
default                   Active   2d
kube-system               Active   2d
kube-public               Active   2d
kube-node-lease           Active   2d
opentelemetry-collector   Active   5s
...

Step 2: Define the Collector Configuration with a ConfigMap

The OpenTelemetry Collector’s behavior is entirely driven by its configuration file. We’ll store this configuration in a Kubernetes ConfigMap, allowing us to easily update it without redeploying the Collector Pods. This ConfigMap will define receivers (how data is ingested), processors (how data is transformed), and exporters (where data is sent).

For this example, we’ll set up a basic configuration:

  • Receivers:
    • otlp: Listens for OpenTelemetry Protocol (OTLP) data over gRPC (port 4317) and HTTP (port 4318). This is the standard way applications instrumented with OpenTelemetry send their telemetry.
    • prometheus: Scrapes metrics from a local Prometheus endpoint (port 8889). This is useful if the collector itself exposes metrics.
  • Processors:
    • batch: Batches data before sending, improving efficiency.
    • memory_limiter: Prevents the collector from consuming too much memory, crucial for stability.
  • Exporters:
    • logging: Prints all received telemetry data to the Collector’s standard output (useful for debugging and verification).
    • otlp: Exports data to another OTLP endpoint. In a real-world scenario, this would be your observability backend. For now, we’ll configure it to point to a non-existent endpoint or another collector.
    • prometheus: Exposes metrics in Prometheus format at /metrics (port 8889).
  • Service Pipelines: These connect the receivers, processors, and exporters for traces, metrics, and logs.

Create a file named otel-collector-config.yaml:

# otel-collector-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: opentelemetry-collector-config
  namespace: opentelemetry-collector
  labels:
    app.kubernetes.io/name: opentelemetry-collector
data:
  # The OpenTelemetry Collector configuration.
  # For more details, see: https://opentelemetry.io/docs/collector/configuration/
  collector.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
          http:
      prometheus:
        config:
          scrape_configs:
            - job_name: 'otel-collector'
              scrape_interval: 10s
              static_configs:
                - targets: ['0.0.0.0:8889'] # Collector's own Prometheus exporter

    processors:
      memory_limiter:
        # 75% of maximum memory up to 1GB
        limit_mib: 1024
        spike_limit_mib: 256
        check_interval: 5s
      batch:
        send_batch_size: 10000
        timeout: 10s

    exporters:
      logging:
        loglevel: debug # Useful for debugging, shows all telemetry data
      otlp:
        endpoint: "otel-backend:4317" # Replace with your actual OTLP backend endpoint
        tls:
          insecure: true # Use this for local testing, disable for production
      prometheus:
        endpoint: "0.0.0.0:8889" # Exposes metrics on this port

    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [logging, otlp]
        metrics:
          receivers: [otlp, prometheus]
          processors: [memory_limiter, batch]
          exporters: [logging, otlp, prometheus]
        logs:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [logging, otlp]

Apply this ConfigMap to your cluster:

kubectl apply -f otel-collector-config.yaml

Verify ConfigMap Creation

Check if the ConfigMap was created successfully and inspect its contents.

kubectl get configmap opentelemetry-collector-config -n opentelemetry-collector -o yaml
apiVersion: v1
data:
  collector.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
          http:
      prometheus:
        config:
          scrape_configs:
            - job_name: 'otel-collector'
              scrape_interval: 10s
              static_configs:
                - targets: ['0.0.0.0:8889'] # Collector's own Prometheus exporter

    processors:
      memory_limiter:
        # 75% of maximum memory up to 1GB
        limit_mib: 1024
        spike_limit_mib: 256
        check_interval: 5s
      batch:
        send_batch_size: 10000
        timeout: 10s

    exporters:
      logging:
        loglevel: debug # Useful for debugging, shows all telemetry data
      otlp:
        endpoint: "otel-backend:4317" # Replace with your actual OTLP backend endpoint
        tls:
          insecure: true # Use this for local testing, disable for production
      prometheus:
        endpoint: "0.0.0.0:8889" # Exposes metrics on this port

    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [logging, otlp]
        metrics:
          receivers: [otlp, prometheus]
          processors: [memory_limiter, batch]
          exporters: [logging, otlp, prometheus]
        logs:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [logging, otlp]
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"collector.yaml":"..."},"kind":"ConfigMap","metadata":{"labels":{"app.kubernetes.io/name":"opentelemetry-collector"},"name":"opentelemetry-collector-config","namespace":"opentelemetry-collector"}}
  creationTimestamp: "2023-10-27T10:00:00Z"
  labels:
    app.kubernetes.io/name: opentelemetry-collector
  name: opentelemetry-collector-config
  namespace: opentelemetry-collector
  resourceVersion: "12345"
  uid: abcdef-1234-5678-90ab-cdef12345678

Step 3: Deploy the Collector as a DaemonSet (Node-Level Collection)

A DaemonSet ensures that a copy of the OpenTelemetry Collector runs on every (or selected) node in your cluster. This is ideal for collecting host-level metrics, logs from system components, or acting as an agent for applications running on that specific node. It often acts as the first hop in a multi-stage collection pipeline.

In this DaemonSet, we mount the ConfigMap we created earlier. The Collector Pod will read its configuration from /conf/collector.yaml. We also expose the OTLP gRPC port (4317) and HTTP port (4318) for applications to send data to.

Create a file named otel-collector-daemonset.yaml:

# otel-collector-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: opentelemetry-collector-daemonset
  namespace: opentelemetry-collector
  labels:
    app.kubernetes.io/name: opentelemetry-collector-daemonset
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: opentelemetry-collector-daemonset
  template:
    metadata:
      labels:
        app.kubernetes.io/name: opentelemetry-collector-daemonset
    spec:
      serviceAccountName: opentelemetry-collector # We'll create this in the next step
      containers:
        - name: opentelemetry-collector
          image: otel/opentelemetry-collector-contrib:0.87.0 # Using contrib for more receivers/exporters
          command:
            - "/otelcol-contrib"
            - "--config=/conf/collector.yaml"
          ports:
            - name: otlp-grpc
              containerPort: 4317
              protocol: TCP
            - name: otlp-http
              containerPort: 4318
              protocol: TCP
            - name: prometheus
              containerPort: 8889
              protocol: TCP
          volumeMounts:
            - name: opentelemetry-collector-config
              mountPath: /conf
          livenessProbe:
            httpGet:
              path: /health
              port: 13133 # Health Check Extension
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 13133 # Health Check Extension
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            limits:
              cpu: 200m
              memory: 256Mi
            requests:
              cpu: 100m
              memory: 128Mi
      volumes:
        - name: opentelemetry-collector-config
          configMap:
            name: opentelemetry-collector-config
      # Optional: Node affinity for specific nodes
      # nodeSelector:
      #   kubernetes.io/os: linux

Before applying the DaemonSet, we need a ServiceAccount for the Collector to interact with the Kubernetes API (e.g., to discover Prometheus targets or collect Kubernetes events). Create otel-serviceaccount.yaml:

# otel-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: opentelemetry-collector
  namespace: opentelemetry-collector
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: opentelemetry-collector-clusterrole
rules:
- apiGroups: [""]
  resources: ["nodes", "nodes/metrics", "nodes/stats", "nodes/proxy", "pods", "services", "endpoints"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["extensions"]
  resources: ["ingresses"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["replicasets", "deployments"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
  resources: ["jobs", "cronjobs"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["autoscaling"]
  resources: ["horizontalpodautoscalers"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: opentelemetry-collector-clusterrolebinding
subjects:
- kind: ServiceAccount
  name: opentelemetry-collector
  namespace: opentelemetry-collector
roleRef:
  kind: ClusterRole
  name: opentelemetry-collector-clusterrole
  apiGroup: rbac.authorization.k8s.io

Apply the ServiceAccount and then the DaemonSet:

kubectl apply -f otel-serviceaccount.yaml
kubectl apply -f otel-collector-daemonset.yaml

Verify DaemonSet Deployment

Check if the DaemonSet Pods are running on each node.

kubectl get daemonset opentelemetry-collector-daemonset -n opentelemetry-collector
kubectl get pods -l app.kubernetes.io/name=opentelemetry-collector-daemonset -n opentelemetry-collector
NAME                                DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
opentelemetry-collector-daemonset   3         3         3       3            3           <none>          10s

NAME                                          READY   STATUS    RESTARTS   AGE
opentelemetry-collector-daemonset-abcde       1/1     Running   0          10s
opentelemetry-collector-daemonset-fghij       1/1     Running   0          10s
opentelemetry-collector-daemonset-klmno       1/1     Running   0          10s

You can also check the logs to see the collector starting up and printing debug info (due to logging exporter).

kubectl logs -l app.kubernetes.io/name=opentelemetry-collector-daemonset -n opentelemetry-collector
2023-10-27T10:05:00.123Z INFO  service/collector.go:283  "Starting Otelcol...
2023-10-27T10:05:00.124Z INFO  extensions/extensions.go:42 "Loading extensions...
2023-10-27T10:05:00.125Z INFO  service/collector.go:175  "Setting up own Prometheus scrape targets"
...
2023-10-27T10:05:05.123Z DEBUG loggingexporter@v0.87.0/logging_exporter.go:72 "TracesExporter"  {"#spans": 1, "resource spans #0": 1, "scope spans #0": 1}
...

Step 4: Deploy the Collector as a Deployment (Cluster-Level Aggregation)

While DaemonSets are great for node-level collection, a Deployment is more suitable for cluster-level aggregation or as an intermediary hop for data before it leaves the cluster. This allows for scaling the Collector horizontally based on demand and simplifies management compared to managing individual agents on each node.

This Deployment will use the same ConfigMap, but it’s deployed as a standard Deployment resource. We’ll also create a Service to expose the Collector’s OTLP ports, making it easily discoverable by other applications or DaemonSet Collectors sending data to it.

Create a file named otel-collector-deployment.yaml:

# otel-collector-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: opentelemetry-collector
  namespace: opentelemetry-collector
  labels:
    app.kubernetes.io/name: opentelemetry-collector
spec:
  replicas: 1 # Start with 1 replica, scale as needed
  selector:
    matchLabels:
      app.kubernetes.io/name: opentelemetry-collector
  template:
    metadata:
      labels:
        app.kubernetes.io/name: opentelemetry-collector
    spec:
      serviceAccountName: opentelemetry-collector
      containers:
        - name: opentelemetry-collector
          image: otel/opentelemetry-collector-contrib:0.87.0
          command:
            - "/otelcol-contrib"
            - "--config=/conf/collector.yaml"
          ports:
            - name: otlp-grpc
              containerPort: 4317
              protocol: TCP
            - name: otlp-http
              containerPort: 4318
              protocol: TCP
            - name: prometheus
              containerPort: 8889
              protocol: TCP
          volumeMounts:
            - name: opentelemetry-collector-config
              mountPath: /conf
          livenessProbe:
            httpGet:
              path: /health
              port: 13133
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 13133
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            limits:
              cpu: 500m
              memory: 512Mi
            requests:
              cpu: 250m
              memory: 256Mi
      volumes:
        - name: opentelemetry-collector-config
          configMap:
            name: opentelemetry-collector-config
---
apiVersion: v1
kind: Service
metadata:
  name: opentelemetry-collector
  namespace: opentelemetry-collector
  labels:
    app.kubernetes.io/name: opentelemetry-collector
spec:
  selector:
    app.kubernetes.io/name: opentelemetry-collector
  ports:
    - name: otlp-grpc
      protocol: TCP
      port: 4317
      targetPort: 4317
    - name: otlp-http
      protocol: TCP
      port: 4318
      targetPort: 4318
    - name: prometheus
      protocol: TCP
      port: 8889
      targetPort: 8889

Apply this Deployment and Service:

kubectl apply -f otel-collector-deployment.yaml

Verify Deployment and Service Creation

Check if the Deployment and Service are running correctly.

kubectl get deployment opentelemetry-collector -n opentelemetry-collector
kubectl get service opentelemetry-collector -n opentelemetry-collector
kubectl get pods -l app.kubernetes.io/name=opentelemetry-collector -n opentelemetry-collector
NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
opentelemetry-collector   1/1     1            1           10s

NAME                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                               AGE
opentelemetry-collector   ClusterIP   10.96.10.123     <none>        4317/TCP,4318/TCP,8889/TCP            10s

NAME                                       READY   STATUS    RESTARTS   AGE
opentelemetry-collector-789abcd-efghj      1/1     Running   0          10s

Again, you can check the logs:

kubectl logs -l app.kubernetes.io/name=opentelemetry-collector -n opentelemetry-collector
2023-10-27T10:10:00.123Z INFO  service/collector.go:283  "Starting Otelcol...
...

Step 5: Test the Collector with a Sample Application

To see the Collector in action, we need an application that sends OpenTelemetry data. We’ll use a simple Go application that generates traces and metrics and is configured to send them to our Collector.

First, let’s create a Service for our DaemonSet Collectors to expose their OTLP ports to the cluster. This is crucial for applications to send data to the DaemonSet. Note that this is a ClusterIP service, as we want internal cluster communication.

Create otel-daemonset-service.yaml:

# otel-daemonset-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: opentelemetry-collector-daemonset-headless
  namespace: opentelemetry-collector
  labels:
    app.kubernetes.io/name: opentelemetry-collector-daemonset
spec:
  clusterIP: None # Headless Service for DaemonSet, so each pod gets its own DNS entry
  selector:
    app.kubernetes.io/name: opentelemetry-collector-daemonset
  ports:
    - name: otlp-grpc
      protocol: TCP
      port: 4317
      targetPort: 4317
    - name: otlp-http
      protocol: TCP
      port: 4318
      targetPort: 4318

Apply the Service:

kubectl apply -f otel-daemonset-service.yaml

Now, let’s deploy a sample application. This application will send OTLP data to the DaemonSet Collector running on its node. We’ll configure the OTEL_EXPORTER_OTLP_ENDPOINT environment variable to point to the DaemonSet’s service.

Create a file named sample-app.yaml:

# sample-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-otel-app
  namespace: default # Deploy in default namespace for demonstration
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sample-otel-app
  template:
    metadata:
      labels:
        app: sample-otel-app
    spec:
      containers:
        - name: sample-app
          image: otel/otel-go-demo:latest # A simple Go demo application
          env:
            - name: OTEL_SERVICE_NAME
              value: "my-sample-go-app"
            - name: OTEL_EXPORTER_OTLP_ENDPOINT
              value: "opentelemetry-collector-daemonset-headless.opentelemetry-collector.svc.cluster.local:4317" # Point to the DaemonSet Collector
            - name: OTEL_EXPORTER_OTLP_PROTOCOL
              value: "grpc"
            - name: OTEL_RESOURCE_ATTRIBUTES
              value: "deployment.environment=production,service.version=1.0.0"
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: sample-otel-app
  namespace: default
spec:
  selector:
    app: sample-otel-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP

Apply the sample application:

kubectl apply -f sample-app.yaml

Verify Sample Application and Data Flow

Check if the sample application Pods are running.

kubectl get pods -l app=sample-otel-app -n default
NAME                              READY   STATUS    RESTARTS   AGE
sample-otel-app-789abcd-efghj     1/1     Running   0          20s
sample-otel-app-uvwxy-ijklm       1/1     Running   0          20s

Now, the critical part: check the logs of your OpenTelemetry Collector DaemonSet Pods. You should see the traces and metrics generated by the sample application being received and processed by the Collector, thanks to the logging exporter.

kubectl logs -l app.kubernetes.io/name=opentelemetry-collector-daemonset -n opentelemetry-collector
...
2023-10-27T10:15:00.123Z DEBUG loggingexporter@v0.87.0/logging_exporter.go:72 "TracesExporter" {"#spans": 1, "resource spans #0": 1, "scope spans #0": 1}
2023-10-27T10:15:00.124Z DEBUG loggingexporter@v0.87.0/logging_exporter.go:72 "ResourceSpans #0" {"resource": {"attributes": [{"key": "deployment.environment", "value": {"stringValue": "production"}}, {"key": "host.arch", "value": {"stringValue": "amd64"}}, {"key": "service.name", "value": {"stringValue": "my-sample-go-app"}}, {"key": "service.version", "value": {"stringValue": "1.0.0"}}]}, "scope_spans": [{"scope": {}, "spans": [{"trace_id": "...", "span_id": "...", "parent_span_id": "...", "name": "doWork", "kind": "SPAN_KIND_INTERNAL", "start_time": "...", "end_time": "...", "attributes": [{"key": "work.amount", "value": {"intValue": 100}}]}]}]}
2023-10-27T10:15:05.123Z DEBUG loggingexporter@v0.87.0/logging_exporter.go:72 "MetricsExporter" {"#metrics": 2, "resource metrics #0": 1, "scope metrics #0": 2}
2023-10-27T10:15:05.124Z DEBUG loggingexporter@v0.87.0/logging_exporter.go:72 "ResourceMetrics #0" {"resource": {"attributes": [{"key": "deployment.environment", "value": {"stringValue": "production"}}, {"key": "host.arch", "value": {"stringValue": "amd64"}}, {"key": "service.name", "value": {"stringValue": "my-sample-go-app"}}, {"key": "service.version", "value": {"stringValue": "1.0.0"}}]}, "scope_metrics": [{"scope": {}, "metrics": [{"name": "app.request.count", "description": "Measures the number of incoming requests.", "unit": "{request}", "sum": {"data_points": [{"start_time": "...", "time": "...", "value": 1.0, "attributes": []}], "aggregation_temporality": "AGG

Leave a comment