LinkMesh

Search docs, blog and changelog

ENDE

Snippets · Sources

Collect Kubernetes cluster metrics (one replica)

Cluster state - desired versus available replicas, pod phases, container restarts, node conditions, quotas - lives in the Kubernetes API, not on any node. The k8s_cluster receiver watches it from a single-replica Deployment. In our kind cluster the ClusterRole in this manifest was enough for 76 metrics per collection and no permission errors.

Signals
Metrics
Category
CollectionKubernetes
Source
Kubernetes API (cluster state)
Components
batchmemory_limiterotlphttp
Destination
OTLP/HTTP endpoint

OpenTelemetry Collector

OpenTelemetry Collector YAML
receivers:
  k8s_cluster:
    auth_type: serviceAccount
    collection_interval: 30s
    node_conditions_to_report: [Ready, MemoryPressure, DiskPressure]
    allocatable_types_to_report: [cpu, memory]

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25
  batch:
    timeout: 5s

exporters:
  otlphttp:
    endpoint: https://your-backend:4318

service:
  pipelines:
    metrics:
      receivers: [k8s_cluster]
      processors: [memory_limiter, batch]
      exporters: [otlphttp]

The collector needs this to run in a cluster: a ServiceAccount, the RBAC it uses and the workload. Create the config first with kubectl -n observability create configmap k8s-cluster-metrics --from-file=config.yaml.

Kubernetes manifest
apiVersion: v1
kind: ServiceAccount
metadata:
  name: k8s-cluster-metrics
  namespace: observability
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: k8s-cluster-metrics
rules:
  - apiGroups: [""]
    resources: [events, namespaces, namespaces/status, nodes, nodes/spec, pods, pods/status, replicationcontrollers, replicationcontrollers/status, resourcequotas, services]
    verbs: [get, list, watch]
  - apiGroups: [apps]
    resources: [daemonsets, deployments, replicasets, statefulsets]
    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: k8s-cluster-metrics
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: k8s-cluster-metrics
subjects:
  - kind: ServiceAccount
    name: k8s-cluster-metrics
    namespace: observability
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-cluster-metrics
  namespace: observability
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: k8s-cluster-metrics
  template:
    metadata:
      labels:
        app: k8s-cluster-metrics
    spec:
      serviceAccountName: k8s-cluster-metrics
      containers:
        - name: otelcol
          image: otel/opentelemetry-collector-contrib:0.161.0
          args: ["--config=/conf/config.yaml"]
          env:
            - name: K8S_NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          volumeMounts:
            - name: config
              mountPath: /conf
      volumes:
        - name: config
          configMap:
            name: k8s-cluster-metrics

LinkMesh

Library › Sources › Kubernetes Cluster Metrics

Collection Interval
30s
Auth Type
serviceAccount
Node Conditions To Report
["Ready","MemoryPressure","DiskPressure","PIDPressure","NetworkUnavailable"]
Allocatable Types To Report
["cpu","memory","pods","ephemeral-storage"]
Optional Metrics
["k8s.service.endpoint.count","k8s.service.load_balancer.ingress.count","k8s.container.status.reason","k8s.pod.status_reason","k8s.persistentvolumeclaim.status.phase"]

What LinkMesh then pushes to the collector, under receivers:

Pushed to the collector
k8s_cluster/input_src_00000501:
  allocatable_types_to_report:
    - cpu
    - memory
    - pods
    - ephemeral-storage
  auth_type: serviceAccount
  collection_interval: 30s
  metrics:
    k8s.container.status.reason:
      enabled: true
    k8s.persistentvolumeclaim.status.phase:
      enabled: true
    k8s.pod.status_reason:
      enabled: true
    k8s.service.endpoint.count:
      enabled: true
    k8s.service.load_balancer.ingress.count:
      enabled: true
  node_conditions_to_report:
    - Ready
    - MemoryPressure
    - DiskPressure
    - PIDPressure
    - NetworkUnavailable

The same, as an API call (see automation recipes):

API call
curl -X POST "$LINKMESH/sources" \
  -H "Authorization: Bearer $LMSAT" \
  -H "Content-Type: application/json" \
  -d '{
  "defaultConfig": {
    "allocatable_types_to_report": [
      "cpu",
      "memory",
      "pods",
      "ephemeral-storage"
    ],
    "auth_type": "serviceAccount",
    "collection_interval": "30s",
    "enabled_metrics": [
      "k8s.service.endpoint.count",
      "k8s.service.load_balancer.ingress.count",
      "k8s.container.status.reason",
      "k8s.pod.status_reason",
      "k8s.persistentvolumeclaim.status.phase"
    ],
    "node_conditions_to_report": [
      "Ready",
      "MemoryPressure",
      "DiskPressure",
      "PIDPressure",
      "NetworkUnavailable"
    ]
  },
  "name": "Kubernetes Cluster Metrics",
  "receiverType": "k8s_cluster"
}'

The rest of the collector config above (memory_limiter, batch, otlphttp) is not part of this template: in LinkMesh those are separate sources, processors and destinations on the route.

Generated from LinkMesh's template catalogue by linkmesh-server snippet-render 1.464.3, not written by hand.

Download config.yamlValidated with otelcol-contrib v0.161.0 on 2026-10-26

Complete config files, each checked with the collector's own validate command (otelcol-contrib v0.161.0). Replace https://your-backend:4318 with your own OTLP endpoint before you run it.

The gotcha

Run exactly one replica. Every replica watches the whole cluster: in our test, with two replicas, each one reported the same 79 metrics per collection, so every series arrived twice. The Deployment uses replicas: 1 and the Recreate strategy so a rollout never runs two at once.

Manage this across a fleet

LinkMesh manages collector configuration across a fleet from one self-hosted control plane, so a change like this is made once rather than host by host. The first 25 Collectors are free after a no-card registration in the OpenSight Customer Portal (5 without one).

Doing this in LinkMesh: Install LinkMesh on Kubernetes