/docs/opentelemetry-kubernetes
DaemonSet + sidecar + gateway
free plan / 1 gb-mo
telemtra.io
// kubernetes

OpenTelemetry on Kubernetes:
logs, traces, metrics.

Run the OpenTelemetry Collector as a DaemonSet for node-local collection or as a sidecar for application-direct OTLP, and forward everything to Telemtra over OTLP HTTP or gRPC.

Why a Collector in Kubernetes

You can send OTLP straight from a Kubernetes pod to Telemtra, but running the OpenTelemetry Collector inside the cluster gives you four things app-direct export does not: container log file collection, pod and node metric scraping, batching across many pods over a single egress connection, and a stable place to apply attribute filtering before data leaves the cluster.

The Collector also lets you change backends without restarting workloads. Apps export to a stable in-cluster endpoint (otelcol.observability:4318); the Collector handles the connection to Telemtra.

DaemonSet, sidecar, or gateway?

  • DaemonSet. One Collector pod per node. Reads container log files from /var/log/pods, scrapes the kubelet for node and pod metrics, accepts OTLP from local pods on localhost. Best default.
  • Sidecar. One Collector container injected into each application pod. Useful when an app should not even know an OTLP endpoint exists; the app sends to localhost:4318 and the sidecar handles everything. Higher per-pod cost.
  • Gateway. A Deployment behind a Service that receives OTLP from DaemonSets or sidecars and forwards to Telemtra. Useful for cross-cluster aggregation or strict egress policy.

Most teams start with the DaemonSet and add a gateway only when they have multiple clusters or need to centralize batching.

Where these manifests go in your project

You can apply the manifests directly with kubectl, but most teams keep them in the same Git repo as their application manifests so the Collector is versioned alongside the rest of the deploy. A typical layout:

my-platform/
├── apps/
│   └── checkout-api/           # your existing app manifests (unchanged)
│       ├── deployment.yaml
│       └── service.yaml
└── observability/              # NEW: Collector config lives here
    ├── namespace.yaml          # observability namespace
    ├── secret.yaml             # telemtra-api-key Secret
    ├── rbac.yaml               # ServiceAccount + ClusterRole for kubeletstats
    ├── configmap.yaml          # otelcol-config (the YAML from the next section)
    └── daemonset.yaml          # the DaemonSet itself

How application pods are affected:

  • With the DaemonSet topology (recommended): apps need no changes. Container logs are read from the node filesystem; OTLP-emitting apps target the node-local Collector at http://$(NODE_IP):4318. Add this via the downward API in the app pod spec only if the app already emits OTLP.
  • With the sidecar topology: add a second container to the app's pod spec (shown below) and point the app at http://localhost:4318.

Apply in order:

kubectl apply -f observability/namespace.yaml
kubectl apply -f observability/secret.yaml
kubectl apply -f observability/rbac.yaml
kubectl apply -f observability/configmap.yaml
kubectl apply -f observability/daemonset.yaml

Or, with Kustomize/Helm/Argo CD, treat the observability/ directory as an independent application.

Store the Telemtra API key as a Secret

apiVersion: v1
kind: Namespace
metadata:
  name: observability
---
apiVersion: v1
kind: Secret
metadata:
  name: telemtra-api-key
  namespace: observability
type: Opaque
stringData:
  TELEMTRA_API_KEY: "tk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

DaemonSet manifest

apiVersion: v1
kind: ConfigMap
metadata:
  name: otelcol-config
  namespace: observability
data:
  config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
      filelog:
        include: [/var/log/pods/*/*/*.log]
        start_at: end
        include_file_path: true
        operators:
          - type: container
            id: container-parser
      hostmetrics:
        collection_interval: 30s
        scrapers:
          cpu: {}
          memory: {}
          disk: {}
          filesystem: {}
          load: {}
          network: {}
      kubeletstats:
        collection_interval: 30s
        auth_type: serviceAccount
        endpoint: "https://${env:NODE_NAME}:10250"
        insecure_skip_verify: true

    processors:
      memory_limiter:
        check_interval: 1s
        limit_mib: 512
      k8sattributes: {}
      batch:
        timeout: 5s
        send_batch_size: 1024

    exporters:
      otlphttp/telemtra:
        endpoint: https://ingest.telemtra.io
        headers:
          x-api-key: ${env:TELEMTRA_API_KEY}

    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlphttp/telemtra]
        metrics:
          receivers: [otlp, hostmetrics, kubeletstats]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlphttp/telemtra]
        logs:
          receivers: [otlp, filelog]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlphttp/telemtra]
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otelcol
  namespace: observability
spec:
  selector:
    matchLabels: { app: otelcol }
  template:
    metadata:
      labels: { app: otelcol }
    spec:
      serviceAccountName: otelcol
      hostNetwork: true
      containers:
        - name: otelcol
          image: otel/opentelemetry-collector-contrib:latest
          args: ["--config=/conf/config.yaml"]
          env:
            - name: TELEMTRA_API_KEY
              valueFrom:
                secretKeyRef:
                  name: telemtra-api-key
                  key: TELEMTRA_API_KEY
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          volumeMounts:
            - { name: config, mountPath: /conf }
            - { name: varlogpods, mountPath: /var/log/pods, readOnly: true }
            - { name: varlibdockercontainers, mountPath: /var/lib/docker/containers, readOnly: true }
      volumes:
        - { name: config, configMap: { name: otelcol-config } }
        - { name: varlogpods, hostPath: { path: /var/log/pods } }
        - { name: varlibdockercontainers, hostPath: { path: /var/lib/docker/containers } }

That single manifest covers pod log collection, node metrics, kubelet pod metrics, and OTLP-from-app ingest, all forwarded to Telemtra with k8s.pod.name, k8s.namespace.name, and k8s.deployment.name attributes attached automatically.

Sidecar pattern

For pods that should not depend on an in-cluster Service or for high-volume apps that need their own buffer, run the Collector as a sidecar:

spec:
  containers:
    - name: app
      image: my-app:1.0
      env:
        - { name: OTEL_EXPORTER_OTLP_ENDPOINT, value: "http://localhost:4318" }
        - { name: OTEL_SERVICE_NAME, value: "checkout-api" }
    - name: otelcol
      image: otel/opentelemetry-collector-contrib:latest
      args: ["--config=/conf/config.yaml"]
      volumeMounts:
        - { name: otel-config, mountPath: /conf }

Gateway deployment

If you operate many clusters or want strict egress policy (one identity, one egress rule, one quota), run a gateway Collector behind a Service and point DaemonSets at it. The gateway holds the API key; node DaemonSets do not need it.

Container log collection

The filelog receiver with the container operator parses the container runtime's log format (CRI-O or containerd) and extracts k8s.pod.name, k8s.namespace.name, and k8s.container.name. Combined with k8sattributes, every log line lands in Telemtra fully attributed.

Pod and node metrics

hostmetrics covers the node (CPU, memory, disk, filesystem, network). kubeletstats covers per-pod metrics from the kubelet (container CPU and memory usage). For cluster-level metrics (deployment status, HPA state), add the k8s_cluster receiver to a single gateway Collector, not the DaemonSet.

OpenTelemetry Operator

For larger environments, install the OpenTelemetry Operator via Helm. It manages Collector deployments via OpenTelemetryCollector CRDs and supports auto-instrumentation injection: annotate a pod with instrumentation.opentelemetry.io/inject-python: "true" and the operator attaches the language SDK as an init container.

Helm chart

helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm install otelcol open-telemetry/opentelemetry-collector \
  --namespace observability --create-namespace \
  --set mode=daemonset \
  --set image.repository=otel/opentelemetry-collector-contrib \
  --set-string config.exporters."otlphttp/telemtra".endpoint=https://ingest.telemtra.io \
  --set-string config.exporters."otlphttp/telemtra".headers.x-api-key=${TELEMTRA_API_KEY}

Troubleshooting

  • No logs from filelog. Confirm /var/log/pods is mounted into the Collector container and the path matches your container runtime (CRI-O uses a different layout from containerd).
  • kubeletstats 401. The Collector ServiceAccount needs nodes/stats and nodes/metrics in its ClusterRole.
  • k8sattributes empty. The processor needs RBAC to list pods. Use the chart-provided ClusterRole or copy from the Operator docs.
  • High egress cost. Increase batch.timeout to 10s and enable gzip on the exporter to reduce bytes.

Pair this with Data Shaper to drop noisy logs at ingest. For protocol details see What is OTLP? New to Telemtra? The quickstart includes a Kubernetes Secret + env block you can paste straight into a Deployment. Create a Telemtra workspace for an API key.

Further reading