OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack
0 (0)

Автор |  16/05/2026
Click to rate this post!
[Total: 0 Average: 0]

Сьогодні поговоримо про те, як запустити OpenTelemetry в Kubernetes та інтегрувати його з VictoriaMetrics stack – VictoriaMetrics для метрик, VictoriaLogs для логів, та VictoriaTraces для трейсів.

Взагалі-то цей пост не планувався взагалі, а коли все ж з’явилась його чорнетка – то він мав бути третім в серії, але врешті-решт вирішив зробити його першим.

Вже після нього допишу два інших поста – перший про Observability та tracing з VictoriaTraces, другий – по OpenTelemetry instrumentation в Python та запису трейсів до VictoriaTraces, а потім – про LLM Observability та моніторингу.

Власне, саме так OpenTelemetry на моєму поточному проекті і з’явився – захотіли ми подивитись детальніше по тому, що у нас по роботі з різними LLM-провайдерами, а там все “заточено” під OpenTelemetry, бо формат Prometheus metrics для цього не підходить.

Тому першим ділом додав VictoriaTraces та запис трейсів в з нашого Backend API, потім подивився на це всі діло, подумав, що мені замало контексту – і вирішив додати повний OpenTelemetry стек.

Власне, з контексту і почнемо.

OpenTelemetry, Observability та Context

Головна суть observability – в контексті, бо context – це, surprise, не тільки про AI/LLM, а і про моніторинг та Observability.

Про Monitoring vs Observability будемо говорити в наступному пості (який мав бути першим), а сьогодні подивимось як запустити OpenTelemetry в Kubernetes.

Але, якщо коротко, то Observability будується на “three pillars of observability” – Metrics, Logs, Traces.

Але просто мати метрики, логи та трейси мало – бо всі наші three pillars повинні мати якісь загальні ознаки, загальні дані, які дозволять робити “наскрізний observability” – тобто, мати можливість в єдиному контексті дослідити і метрики EC2, і метрики AWS Application Load Balancer, і конкретні Kubernetes Pods самого Backend API та, врешті решт – до конкретних викликів функцій, бізнес-логіки, яка виконується в цьому Pod у відповідь на реквест, який прийшов з AWS ALB від конкретного юзеру – тобто, побудувати observability pipeline.

А для того, аби всі дані мали цей загальний контекст – вони повинні мати якісь загальні риси, ознаки, за якими ми можемо всі отримані – тобто labels.

Проте при використанні “дефолтного” Prometheus-стеку ми маємо купу різних експортерів для метрик, окремі експортери для логів, ще і на додачу трейси в OTel-форматі – і кожен пише лейбли на свій лад. Тому, аби якось це все об’єднати в Grafana dashboards або алертах доводиться гемороїтись зі всякими label_replace.

Живий приклад з одного з мої алертів:

- record: aws:node:cpu_utilization:percent
  expr: |
    100 * (1 - avg by(instance, cluster) (
      label_replace(
        rate(node_cpu_seconds_total{mode="idle"}[5m]),
        "instance",
        "ip-${1}-${2}-${3}-${4}.ec2.internal",
        "instance",
        "(.*)\\.(.*)\\.(.*)\\.(.*):9100"
      )
    ))

Тут з метрики node_cpu_seconds_total береться значення лейбли instance типу 10.0.50.18 і створюється нове значення виду ip-10-0-50-18.ec2.internal, яке потім використовується в Grafana dashboards для фільтрів – бо якась інша метрика віддає ім’я хоста в такому вигляді, а метрика від node_exporter не має дефолтної лейбли у вигляді node_name="ip-10-0-50-18.ec2.internal".

Тому можна піти іншим шляхом – замінити те, як ми ці метрики отримуємо: замість того, щоб мати 10 різних експортерів для метрик – node_exporter для EC2, YACE exporter для AWS CloudWatch, окремого експортеру k8s-event-logger для експорту Kubernetes Events в логи, замість окремого AWS ALB Logs collector із S3 – ми можемо мати єдину систему, яка все це робить сама і, головне – сама додає загальний загальні лейбли до всіх signals – metrics, logs, traces.

Плюси та мінуси OpenTelemetry

Звісно, не все так просто: OpenTelemetry Collector трохи складніший в налаштуванні, споживає більше ресурсів, потребує додаткового моніторингу.

Власне, це цілком очікувано, бо якщо система “з коробки” дає більше можливостей – то і її конфігурація буде трохи складнішою, ніж для якогось одного Prometheus Node Exporter.

Те саме і по ресурсам – коли експортер займається збором і метрик і логів – то він буде споживати більше ресурсів, ніж один експортер, який “заточений” тільки під одну задачу: вже те, що OTel має захист від OOMKiller “з коробки” про щось говорить.

Втім, якщо порахувати споживання CPU/RAM всіма експортерами Prometheus-формату і порівняти з одним Kubernetes Pod для OpenTelemetry Collector – то ще питання, що буде легше.

Крім того – формат OTel для метрик за розміром більший, ніж Prometheus-метрики – бо сам формат в собі містить більше даних.

І останній нюанс, який зараз приходить в голову – це те, що 95% всяких алертів та Grafana dahsboards заточені саме під метрики в Prometheus-форматі та від Prometheus-експортерів на кшталт node_exporter та cAdvisor.

Тому якщо впроваджувати OTel в якості основної системи для збору даних – то треба мати на увазі, що треба буде оновлювати і всі пов’язані ресурси.

Втім, конкретно в моєму випадку – ми ще маленький стартап, а основні Grafana dashboards я все одно роблю сам руками, тому з LLM задачка це все оновити вирішується відносно швидко.

Отже, спробую, запущу поки що паралельно  з існуючим Prometheus-like стеком експортерів та логів і подивлюсь, що з цього вийде.

VictoriaTraces і трейси у нас теж вже є, але про це будемо говорити окремо.

VictoriaMetrics та мій поточний стек моніторингу

У нас на проекті все працює в AWS на Elastic Kubernetes Service – Backend API та інші сервіси проекту, сам VictoriaMetrics стек моніторингу, плюс різні сервіси самого AWS – RDS, CloudFront, DynamoDB etc.

Що залишиться без змін – це наші “storages”: VictoriaMetrics для метрик, VictoriaLogs – для логів, VictoriaTraces – для трейсів.

Що зміниться – це те, як  ми ці дані отримуємо: замість пачки Prometheus exporters та VMAgent, який до них ходить і збирає метрики – у нас буде окремий сервіс OTel Gateway, який отримує дані від OTel Collector. А OTel Collector замінить весь зоопарк Prometheus Exporters та Log collectors.

Окремо від цієї інфраструктури є багато інтеграцій з AI-провайдерами – Anthropic, OpenAI – але їхній моніторинг це вже зовсім окрема тема, про які буду (сподіваюсь) писати далі.

OpenTelemetry – загальна архітектура та компоненти

Для збору даних – метрик, логів та трейсів – у OpenTelemetry є власний OpenTelemetry Collector, який може відігравати різні ролі.

Власне, це один і той самий binary-файл, поведінка якого залежить від того, що ми йому передаємо в налаштуваннях:

  • роль Kubernetes Collector: збираємо Kubernetes events, метрики Kubernetes WorkerNodes, Kubernetes Pods, контейнерів, логи
  • роль AWS Collector: збирає метрики з CloudWatch та/або логи з AWS ALB через S3 та/або VPC Flow Log
  • роль OpenTelemetry Gateway: агенти (OTel Collectors) пушать свої дані до Gateway, а Gateway вже передає їх до конкретних бекендів – VictoriaMetrics, VictoriaLogs, VictoriaTraces

Схематично це може виглядати якось так:

OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack

Єдиний момент перед тим, як продовжити: я називаю OTel Collectors і як “collector“, і як “agent“, але назва суті не міняє – це просто роль, яку сервіс виконує.

Структура конфігурації OpenTelemetry Collector

В інтернеті багато прикладів файлів, наприклад в офіційному репозиторії k8s/otel-config.yaml, або невелика колекція Cloud-Architect-Emma/opentelemetry-collector-examples.

Але аби ними користуватись або писати власні – треба трохи глянути за загальний синтаксис та компоненти, які в конфігурації описані.

Документація:

В кожному компоненті ми будемо задавати власні параметри – але структура у всіх однакова:

  • receivers: описують звідки отримувати дані – Kubernetes API, AWS API, логи
    • для Kubernetes Collector тут будуть hostmetrics (метрики як від node_exporter), kubeletstats (метрики контейнерів), filelog (логи Pods)
    • у Gateway у receivers буде otlp – приймати дані від Collectors, та k8s_cluster і k8sobjects – він сам буде збирати дані від Kubernetes API та kubelet
  • processors: трансформації даних – додає метадані (атрибути, лейбли), фільтрує або видаляє зайве, групує, виконує трансформації – перейменування полів, нормалізація
  • exporters: куди дані відправляємо
    • у Gateway у exporters будуть otlphttp/vmetrics, otlphttp/vlogs, otlphttp/vtraces.
    • в Agent у exporters буде otlp_grpc (з адресою Gateway)
  • extensions: додаткові capabilities (аутентифікація, health check, encoding extensions тощо)
  • connectors: об’єднує різні pipelines
  • service: об’єднує і активує описані конфіги – recievers, processors, etc

OpenTelemetry Pipelines

Всі отримані сигнали проходять через pipeline: тобто receiver – отримав сигнал, processor його обробив, exporter – кудись відправив.

Для кожного типу сигналів – метрик, логів та трейсів – у нас будуть власні пайплайни – бо дані хоч і пов’язані, але оброблюються по різному.

Кожен пайплайн може мати власний ідентифікатор – просто ім’я, аби простіше було читати конфіг, наприклад:

connectors:
  spanmetrics:
    # config...

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp/vtraces, spanmetrics]  # spanmetrics here is an exporter
    
    metrics/from_traces:
      receivers: [spanmetrics]                    # the same spanmetrics here is an receiver
      exporters: [otlphttp/vmetrics]

Тепер можемо починати писати власні конфіги та запускати колектори.

OpenTelemetry: запуск в Kubernetes

Є кілька варіантів запусту стека – “голими” контейнерами, Helm chart, або OpenTelemetry Operator, див. Install the Collector.

Для VictoriaMetrics я користуюсь Helm chart victoria-metrics-k8s-stack, який встановлює VictoriaMetrics Operator, VMAgent, VMAlert, Alertmanager, Grafana, а всі налаштування виконуються з VictoriaMetrics CRD resources.

Про цей сетап писав у VictoriaMetrics: створення Kubernetes monitoring stack з власним Helm-чартом, а про Kubernetes Operators та CRD – у Kubernetes: що таке Kubernetes Operator та CustomResourceDefinition.

Для OpenTelemetry поки зроблю просто з Helm chart – бо простіше буде розібратись з основними компонентами і не витрачати час на документацію оператора та його CRD.

А вже коли це все діло піде у production – можна буде замінити на OpenTelemetry Operator.

Робити будемо у вигляді трьох окремих компонентів:

  • OTel Gateway: отримує дані від Kubernetes API, Kubernetes та AWS Collectors, оброблює їх, передає до бекендів – VictoriaMetrics, VictoriaLogs, VictoriaTraces
  • Kubernetes Agent: запускається на кожній Kubernetes WorkerNode, збирає дані від kubelet та логи Pods
  • AWS Agent: збирає дані від AWS – метрики, логи

Почнемо саме з OTel Gateway, бо всі інші компоненти будуть слати дані саме через нього, саме він буде виконувати всі операції, і саме він буде відправляти дані до VictoriaMetrics stack.

Додаємо собі Helm-репозиторій:

$ helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
$ helm repo update

Перевіряємо наявність чартів:

$ helm search repo open-telemetry/opentelemetry-collector
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION                                      
open-telemetry/opentelemetry-collector  0.155.0         0.151.0         OpenTelemetry Collector Helm chart for Kubernetes

Всі компоненти – OTel Gateway, Kubernetes Agent, AWS Agent – будуть встановлюватись з нього, але кожний з власними values.

Запуск OpenTelemetry Gateway

Готуємо файл otel-gateway-values.yaml – це будуть values для нашого OTel Gateway:

# OTel Collector - Gateway role (Deployment)
#
# Responsibilities at this phase:
#   - Accept OTLP from future Agents (DaemonSet)
#   - Collect cluster-level metrics via k8s_cluster receiver
#   - Collect K8s events as logs via k8sobjects receiver
#   - Enrich all signals with K8s metadata (k8sattributes processor)
#   - Export metrics to VictoriaMetrics, logs to VictoriaLogs
#
# Traces pipeline is intentionally not enabled yet - that's Phase 2

# docs: https://opentelemetry.io/docs/collector/architecture/

mode: deployment

replicaCount: 2

# contrib image has all the receivers/processors/exporters we need
image:
  repository: otel/opentelemetry-collector-contrib

resources:
  limits:
    cpu: 1000m
    memory: 2Gi
  requests:
    cpu: 200m
    memory: 512Mi

# RBAC for k8sattributes (pod metadata lookup) and k8s_cluster (cluster state).
# Full list of required permissions:
# https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/k8sclusterreceiver
clusterRole:
  create: true
  rules:
    - apiGroups: [""]
      resources:
        - pods
        - namespaces
        - nodes
        - nodes/stats
        - nodes/proxy
        - events
        - services
        - resourcequotas
        - replicationcontrollers
        - replicationcontrollers/status
      verbs: ["get", "list", "watch"]
    - apiGroups: ["apps"]
      resources: ["replicasets", "deployments", "statefulsets", "daemonsets"]
      verbs: ["get", "list", "watch"]
    - apiGroups: ["extensions"]
      resources: ["replicasets"]
      verbs: ["get", "list", "watch"]
    - apiGroups: ["batch"]
      resources: ["jobs", "cronjobs"]
      verbs: ["get", "list", "watch"]
    - apiGroups: ["autoscaling"]
      resources: ["horizontalpodautoscalers"]
      verbs: ["get", "list", "watch"]
    - apiGroups: ["events.k8s.io"]
      resources: ["events"]
      verbs: ["get", "list", "watch"]

# Self-monitoring port
ports:
  metrics:
    enabled: true
    containerPort: 8888
    servicePort: 8888
    protocol: TCP

service:
  type: ClusterIP

config:
  receivers:
    # PUSH receiver
    # Accepts data from Agents and from apps
    # OTel TracerProvider() for the Backend API will send traces to this receiver
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
          # Agent batches of logs may exceed default 4 MiB gRPC limit
          max_recv_msg_size_mib: 16
        http:
          endpoint: 0.0.0.0:4318

    # PULL receiver
    # Will go to the Kubernetes API to get the cluster-level state
    # Runs only on Gateway (one place per cluster)
    # uses GET /api/v1/nodes, GET /apis/apps/v1/deployments etc.
    # converts responses to metircs like k8s.deployment.available, k8s.node.condition_ready, k8s.hpa.current_replicas
    # returns them to a corresponding pipeline
    k8s_cluster:
      collection_interval: 30s
      node_conditions_to_report: [Ready, MemoryPressure, DiskPressure, PIDPressure]
      allocatable_types_to_report: [cpu, memory, ephemeral-storage]

    # PULL receiver
    # Will go to the Kubernetes API, but uses `watch` mode
    # uses the 'events.k8s.io/v1/events' endpoint to receive event stream in real time
    # converts Kubernetes Events to Log records
    # returns them to the logs pipeline
    k8sobjects:
      objects:
        - name: events
          mode: watch
          group: events.k8s.io

  processors:
    # Memory protection against traffic spikes to avoid OOM kills
    memory_limiter:
      check_interval: 1s
      limit_percentage: 80
      spike_limit_percentage: 25

    # Enrich every signal with K8s pod metadata - this is what unifies labels
    # across metrics, logs and traces
    # docs: https://opentelemetry.io/docs/platforms/kubernetes/collector/components/#kubernetes-attributes-processor
    k8sattributes:
      auth_type: serviceAccount
      passthrough: false
      extract:
        # data taken from the Kubernetes API - fields from the Pod object to be added as attributes
        # i.e. a Kubernetes Namespace 'dev-backend-api-ns' for a Pod will be set as k8s.namespace.name="dev-backend-api-ns"
        # https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/k8sattributesprocessor#configuration
        metadata:
          - k8s.namespace.name
          - k8s.pod.name
          - k8s.pod.uid
          - k8s.pod.start_time
          - k8s.deployment.name
          - k8s.statefulset.name
          - k8s.daemonset.name
          - k8s.cronjob.name
          - k8s.job.name
          - k8s.node.name
        # add custom labels from the Pod object
        # i.e. a Pod with label 'app.kubernetes.io/component=backend' will be set as app.label.component="backend"
        labels:
          - tag_name: app.label.component
            key: app.kubernetes.io/component
            from: pod
          - tag_name: app.label.name
            key: app.kubernetes.io/name
            from: pod
      # pod_association processor is used to associate signals (metrics, logs, traces) with the correct Pod
      # e.g. when the Gateway receive a metric from a Pod, it need to know how to find that Pod in the Kubernetes API
      # for example, our Kubernetes Agent will send a metric from 'kubeletstats' for a container
      # but this metrics will not have a corresponding 'k8s.deployment.name'
      # so here, k8sattributes proecessor will ask the Kubernetes API to get additional metadata and set it as attributes
      pod_association:
        - sources:
            - from: resource_attribute
              name: k8s.pod.ip
        - sources:
            - from: resource_attribute
              name: k8s.pod.uid
        - sources:
            - from: connection

    # similar to the k8sattributes.extract.labels above, but for the resource attributes to all signals
    # sets hard-coded values
    resource:
      attributes:
        # action may be set as:
        # - insert: add only if not exists
        # - update: update if exists
        # - upsert: insert if not exists, update if exists
        # - delete: delete if exists
        - key: k8s.cluster.name
          value: eks-ops-1-33
          action: upsert
        - key: cloud.provider
          value: aws
          action: upsert

    # Batch records for efficient export
    # collects data to its buffer and sends it to the exporter in batches
    # docs: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/batchprocessor
    batch:
      send_batch_size: 8192
      timeout: 10s

  # Where to send the data to - in our case, to VictoriaMetrics and VictoriaLogs
  # docs: https://docs.victoriametrics.com/opentelemetry/
  exporters:
    # VictoriaMetrics - OTLP endpoint
    # docs: https://docs.victoriametrics.com/victoriametrics/data-ingestion/opentelemetry-collector/
    # the '/v1/metrics' part will be added by the exporter itself
    otlphttp/vmetrics:
      endpoint: http://vmsingle-vm-k8s-stack.ops-monitoring-ns.svc.cluster.local:8428/opentelemetry
      tls:
        insecure: true

    # VictoriaLogs - OTLP endpoint
    # docs: https://docs.victoriametrics.com/victorialogs/data-ingestion/opentelemetry/
    # the '/v1/logs' part will be added by the exporter itself
    otlphttp/vlogs:
      endpoint: http://atlas-victoriametrics-victoria-logs-single-server.ops-monitoring-ns.svc.cluster.local:9428/insert/opentelemetry
      tls:
        insecure: true

    # Debug exporter - for troubleshooting, can be added to any pipeline temporarily
    debug:
      verbosity: basic

  # Combine everything into a single service definition
  service:
    # Pipelines operate on three telemetry data types: traces, metrics, and logs.
    # Each pipeline has its own set of receivers, processors and exporters.
    # docs: https://opentelemetry.io/docs/collector/architecture/#pipelines
    pipelines:
      metrics:
        # Reference receivers by their names from the config.receivers section above
        receivers: [otlp, k8s_cluster]
        # Reference processors by their names from the config.processors section above
        # IMPORTANT NOTE: order matters - processors run in the order listed here
        processors: [memory_limiter, k8sattributes, resource, batch]
        # Reference exporters by their names from the config.exporters section above
        exporters: [otlphttp/vmetrics]

      logs:
        receivers: [otlp, k8sobjects]
        processors: [memory_limiter, k8sattributes, resource, batch]
        exporters: [otlphttp/vlogs, debug]

    telemetry:
      metrics:
        readers:
          - pull:
              exporter:
                prometheus:
                  host: 0.0.0.0
                  port: 8888

В принципі, все описав в коментарях – але давайте коротко про те, що ми тут маємо:

  • mode="deployment": Gateway створюємо у вигляді Kubernetes Deployment з двома Pods
    • для Kubernetes Agent будемо робити DaemonSet, бо він має працювати на кожній WorkerNode
  • receivers: описуємо вхідні дані – можуть бути PULL (самі звертаються до зовнішніх API), або PUSH (в них пушать агенти/колектори)
    • otlp: ендпоінти для Kubernetes та AWS Agents
    • k8s_cluster: звертається до Kubernetes API, отримує інформацію по Nodes, Pods, Events
    • k8sobjects.objects="events": від Kubernetes API постійно отримує Kubernetes Events, записує у вигляді логів
  • processors:
    • k8sattributes: додає атрибути до кожної метрики чи лога (namespace, deployment name, etc)
    • resource.attributes: додає “глобальні” атрибути до кожного отриманого сигналу (див. OpenTelemetry Resource Attributes Explained Practically)
  • exporters: куди дані пишуть – бекенди, в нашому випадку передаємо до VictoriaMetrics, VictoriaLogs та VictoriaTraces
  • service: об’єднуємо все описане вище
    • pipelines:
      • metrics: в якому порядку і що робити з метриками
      • logs: те саме – але для логів
      • пізніше тут буде пайплайн для traces
    • telemetry: включаємо self monitoring – можемо подивитись на метрики самого OTel

Деплоїмо:

$ helm -n ops-monitoring-ns upgrade --install otel-gateway open-telemetry/opentelemetry-collector -f otel-gateway-values.yaml

Перевіряємо поди:

$ kubectl -n ops-monitoring-ns get pod -l app.kubernetes.io/instance=otel-gateway
NAME                                                    READY   STATUS    RESTARTS   AGE
otel-gateway-opentelemetry-collector-57b74ffd98-4pqhw   1/1     Running   0          68s
otel-gateway-opentelemetry-collector-57b74ffd98-td6hr   1/1     Running   0          68s

Kubernetes Service – його будуть використовувати Agents:

$ kubectl -n ops-monitoring-ns get svc -l app.kubernetes.io/instance=otel-gateway
NAME                                   TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                                            AGE
otel-gateway-opentelemetry-collector   ClusterIP   172.20.204.222   <none>        6831/UDP,14250/TCP,14268/TCP,8888/TCP,4317/TCP,4318/TCP,9411/TCP   90s

Перевірка Metrics

І за хвилину можемо вже перевірити метрики по {k8s.cluster.name="eks-ops-1-33"}:

OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack

Де маємо метрику k8s.container.cpu_limit – це від k8s_cluster receiver, який сходив до /api/v1/pods в Kubernetes APIs і прочитав spec.containers[].resources.limits.cpu.

The Cardinality Issue

Тепер важливий момент – в лейблах бачимо багато різних ID, наприклад:

k8s.container.cpu_limit {..., container.id="a6a73186104e064e406330620b09bc367418ad4ce3564a1ef21d48de3597dad7", ..., k8s.pod.name="otel-gateway-opentelemetry-collector-57b74ffd98-td6hr",k8s.pod.start_time="2026-05-15T10:36:54Z",k8s.pod.uid="55b9990a-49e7-4913-be53-40d0d640cf72", ...}

Кожен раз, коли Kubernetes Pod перестворюється – для нього генерується нове значення в k8s.pod.uid.

Детально чому і як це впливає на VictoriaMetrics storage та навантаження описував в пості VictoriaMetrics: Churn Rate, High cardinality, метрики та IndexDB, але якщо коротко – кожне унікальне значення кожної лейбли збільшує і зайняте місце на диску, і розмір індексної бази VictoriaMetrics, та, відповідно, впливає на споживання CPU/RAM і швидкість пошуку.

Аби запобігти цьому – можемо додати ще один processor, який буде видаляти такі лейбли.

Порядок додавання в config.processors неважливий – він важливий в пайплайні, але логічно додати біля блоку resource:

...
  processors:
    ...
    resource:
      attributes:
        - key: k8s.cluster.name
          value: eks-ops-1-33
          action: upsert
        - key: cloud.provider
          value: aws
          action: upsert

    # Drop high-cardinality resource attributes from metrics only
    # These change on every pod recreation and cause series explosion in VictoriaMetrics.
    # Logs and traces keep them - useful for debugging specific pod instances.
    resource/drop_volatile_labels:
      attributes:
        - key: k8s.pod.uid
          action: delete
        - key: container.id
          action: delete
        - key: k8s.pod.start_time
          action: delete
...

Інший варіант – видаляти з -search.maxStalenessInterval=4h самої VictoriaMetrics, див. List of command-line flags.

При цьому пам’ятаємо, що у нас є два різних типи атрибутів, і, відповідно, це будуть різні processors:

  • record-level attributes: атрибути конкретного запису (i.e. container CPU usage)
  • resource-level attributes: атрибути джерела – додаються до всіх signals, які передаються до бекендів

Перевірити які саме атрибути треба модифікувати можна в документації конкретного processor, наприклад для k8sattributes processor:

The processor automatically discovers k8s resources (pods), extracts metadata from them and adds the extracted metadata to the relevant spans, metrics and logs as resource attributes.

Або в OTel specification, наприклад для Pod документація має URI /resource/k8s/#pod.

Додаємо новий processors в pipeline для merics – після resource, але перед batch:

...
  service:
    pipelines:
      metrics:
        receivers: [otlp, k8s_cluster]
        processors: [memory_limiter, k8sattributes, resource, resource/drop_volatile_labels, batch]
...

Чому саме така позиція в pipeline – тому всі ресурси в pipeline виконується в тому порядку, в якому вони описані, а обробка resource/drop_volatile_labels має йти:

  • після k8sattributes – бо саме він додає k8s.pod.uid, треба його викидати після того, як він з’явився
  • після resource – щоб resource processor вже встиг проставити свої лейбли
  • перед batch – щоб batch групував вже очищені дані

Апдейтимо деплой, перевіряємо:

OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack

І лейблів з .id більше нема

Тепер у нас є робочий OTel Gateway, в якому ми:

  • готові приймати дані майбутніх Agents та наших сервісів типу Backend API (порти 4317/4318)
  • збираємо cluster-level метрики (k8s_cluster)
  • збираємо K8s events як логи (k8sobjects)
  • доповнюємо k8s-метаданими (k8sattributes)
  • додаємо до всіх даних  власні лейбли (k8s.cluster.name, cloud.provider)
  • контролюємо cardinality (resource/drop_volatile_labels)
  • маємо захист від OOM Killer (memory_limiter)
  • налаштували batch-експорт до VictoriaMetrics і VictoriaLogs

Що залишилось – AWS Collector для метрик з AWS CloudWatch та логів AWS ALB, і налаштувати отримання та передачу traces.

Перевірка Logs

Перевіряємо логи – запит {k8s.cluster.name="eks-ops-1-33"}.

Поки тут тільки логи по Kubernetes Events – логи Pods додамо пізніше з filelog в Kubernetes Agent:

OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack

Тут є дві невеликі – але проблеми:

  • поле _msg не сформоване
  • мусор в object.metadata.managedFields

Додавання transform для логів

Перевизначити що саме і як буде записано в лог можемо через processors.transform:

...
config:
  ...
  processors:

    ...
    # Normalize k8sobjects events: set readable body, drop noisy fields
    transform/k8s_events:
      #error_mode: ignore
      error_mode: propagate
      log_statements:
        - context: log
          statements:
            # k8sobjects stores the Event as a map in body.
            # VictoriaLogs flattens it into object.* fields automatically.
            # Build readable "REASON: note" message from body fields.
            - >-
              set(body, Concat([body["object"]["reason"], ": ", body["object"]["note"]], ""))
              where attributes["event.domain"] == "k8s" and attributes["k8s.resource.name"] == "events"
...

Тут ми самі формуємо поле body, яке VictoriaLogs використає для свого поля _msg.

Побачити як взагалі формується event object можна включивши debug exporter в detailed verbosity:

...
debug:
      verbosity: detailed
...

А потім додати його в logs pipeline:

...
logs:
        receivers: [otlp, k8sobjects]
        processors: [memory_limiter, k8sattributes, resource, batch]
        exporters: [otlphttp/vlogs, debug]
...

І потім просто подивитись логи подів з Gateway.

Додаємо transform/k8s_events до logs pipeline перед batch:

...
  service:
    pipelines:
      metrics:
        ...

      logs:
        receivers: [otlp, k8sobjects]
        processors: [memory_limiter, k8sattributes, resource, transform/k8s_events, batch]
        exporters: [otlphttp/vlogs, debug]
...

І тепер маємо красиве поле _msg:

OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack

Запуск Kubernetes Agent

Наступний крок – додати експортер, який вже буде збирати Pod level дані – метрики та логи.

Створюємо файл otel-k8s-agent-values.yaml:

# OTel Collector - Agent role (DaemonSet)
#
# Runs on every node, collects local data only:
#   - System metrics from host /proc, /sys (hostmetrics receiver)
#   - Pod/container metrics from local kubelet (kubeletstats receiver)
#   - Container logs from /var/log/pods (filelog receiver)
#
# Forwards everything to Gateway via OTLP gRPC.
# Gateway adds k8s metadata and exports to Victoria-* backends.

mode: daemonset

# contrib image has hostmetrics, kubeletstats, filelog receivers
image:
  repository: otel/opentelemetry-collector-contrib

# Mount host filesystem paths needed by hostmetrics and filelog
extraVolumes:
  - name: varlogpods
    hostPath:
      path: /var/log/pods
  - name: varlibdockercontainers
    hostPath:
      path: /var/lib/docker/containers
  - name: hostfs
    hostPath:
      path: /

extraVolumeMounts:
  - name: varlogpods
    mountPath: /var/log/pods
    readOnly: true
  - name: varlibdockercontainers
    mountPath: /var/lib/docker/containers
    readOnly: true
  - name: hostfs
    mountPath: /hostfs
    readOnly: true
    mountPropagation: HostToContainer

# Root is required to read /proc, /sys from the host
securityContext:
  runAsUser: 0
  runAsGroup: 0

resources:
  limits:
    cpu: 500m
    memory: 1Gi
  requests:
    cpu: 100m
    memory: 256Mi

# Agent must run on every node, including tainted ones
tolerations:
  - effect: NoSchedule
    operator: Exists
  - key: CriticalAddonsOnly
    operator: Exists
    effect: NoSchedule
  - key: CriticalAddonsOnly
    operator: Exists
    effect: NoExecute
  - key: BackendOnly
    operator: Exists
  - key: BackendDevOnly
    operator: Exists
  - key: BackendProdOnly
    operator: Exists
  - key: GitHubOnly
    operator: Exists
  - key: GitHubControllerOnly
    operator: Exists
  - key: GitHubRunnersOnly
    operator: Exists

# Inject node identity and host paths into the collector container
extraEnvs:
  - name: K8S_NODE_NAME
    valueFrom:
      fieldRef:
        fieldPath: spec.nodeName
  - name: K8S_POD_IP
    valueFrom:
      fieldRef:
        fieldPath: status.podIP
  # hostmetrics uses these env vars to read host /proc, /sys instead of container's
  - name: HOST_PROC
    value: /hostfs/proc
  - name: HOST_SYS
    value: /hostfs/sys
  - name: HOST_ETC
    value: /hostfs/etc
  - name: HOST_VAR
    value: /hostfs/var
  - name: HOST_RUN
    value: /hostfs/run
  - name: HOST_DEV
    value: /hostfs/dev

# Need read access to kubelet stats endpoint
clusterRole:
  create: true
  rules:
    - apiGroups: [""]
      resources: ["nodes/stats", "nodes/proxy", "nodes/metrics"]
      verbs: ["get"]
    - apiGroups: [""]
      resources: ["pods", "namespaces", "nodes"]
      verbs: ["get", "list", "watch"]

# Self-monitoring port
ports:
  metrics:
    enabled: true
    containerPort: 8888
    servicePort: 8888
    protocol: TCP

config:
  receivers:
    # PULL receiver
    # Reads node-level system metrics from host /proc and /sys
    # Replaces node_exporter functionality
    # Produces: system.cpu.*, system.memory.*, system.disk.*, system.network.*,
    #           system.filesystem.*, system.load.*, system.paging.*, system.processes.*
    hostmetrics:
      collection_interval: 30s
      root_path: /hostfs
      scrapers:
        cpu:
          metrics:
            system.cpu.utilization:
              enabled: true
        memory:
          metrics:
            system.memory.utilization:
              enabled: true
        disk:
        filesystem:
          exclude_mount_points:
            mount_points: ["/var/lib/kubelet/*", "/var/lib/docker/*", "/proc/*", "/sys/*"]
            match_type: regexp
          exclude_fs_types:
            fs_types: [tmpfs, devtmpfs, overlay, squashfs]
            match_type: strict
        network:
        load:
        paging:
        processes:

    # PULL receiver
    # Queries local kubelet (port 10250) for per-pod and per-container metrics
    # Replaces cadvisor functionality (which is built into kubelet)
    # Produces: k8s.node.*, k8s.pod.*, container.* (cpu/memory/network/filesystem)
    kubeletstats:
      collection_interval: 30s
      auth_type: serviceAccount
      endpoint: "https://${env:K8S_NODE_NAME}:10250"
      insecure_skip_verify: true
      metric_groups:
        - node
        - pod
        - container
        - volume

    # PULL receiver
    # Reads container logs from disk - standard CRI/containerd path
    # Replaces promtail / fluent-bit functionality
    # Container operator parses CRI log format and extracts k8s.* attributes from file path
    filelog:
      include:
        - /var/log/pods/*/*/*.log
      exclude:
        # Don't collect our own logs to avoid feedback loops
        - /var/log/pods/ops-monitoring-ns_otel-*/*/*.log
      start_at: end
      include_file_path: true
      include_file_name: false
      operators:
        - type: container
          id: container-parser

  processors:
    # Memory protection against traffic spikes
    memory_limiter:
      check_interval: 1s
      limit_percentage: 80
      spike_limit_percentage: 25

    # Tag everything with the node we're running on
    # Cluster-level attributes (k8s.cluster.name etc.) are added by Gateway
    resource:
      attributes:
        - key: k8s.node.name
          value: ${env:K8S_NODE_NAME}
          action: upsert

    # Batch records before sending to Gateway
    batch:
      send_batch_size: 8192
      timeout: 10s

  exporters:
    # Forward everything to Gateway via OTLP gRPC
    # Gateway will add k8s metadata and route to the right Victoria backend
    otlp:
      endpoint: otel-gateway-opentelemetry-collector.ops-monitoring-ns.svc.cluster.local:4317
      tls:
        insecure: true
      sending_queue:
        enabled: true
        num_consumers: 4
        queue_size: 1000
      retry_on_failure:
        enabled: true
        initial_interval: 5s
        max_interval: 30s

  service:
    pipelines:
      metrics:
        receivers: [hostmetrics, kubeletstats]
        processors: [memory_limiter, resource, batch]
        exporters: [otlp]

      logs:
        receivers: [filelog]
        processors: [memory_limiter, resource, batch]
        exporters: [otlp]

    telemetry:
      metrics:
        readers:
          - pull:
              exporter:
                prometheus:
                  host: 0.0.0.0
                  port: 8888

Тут маємо аналогічну до Gateway структуру – теж receivers, processors, exporters та pipelines.

Різниця в тому, як деплоїмо Pods, які receivers описуємо та куди виконуємо export:

  • mode="daemonset": Collector має бути запущеним на кожній WorkerNode кластеру
  • receivers:
    • hostmetrics: node-level – CPP, RAM, диски, нетворк (аналог Prometheus Node Exporter)
    • kubeletstats: метрики контейнерів (аналог cAdvisor_exporter)
    • filelog: збираємо логи контейнерів (аналог Promtail/Filebeat/etc)
  • exporters: зібрані агентом дані передаємо до OTel Gateway – він їх обробить та передасть до VictoriaMetrics/Logs/Traces

Деплоїмо:

$ helm -n ops-monitoring-ns upgrade --install otel-k8s-agent open-telemetry/opentelemetry-collector -f otel-k8s-agent-values.yaml

Перевіряємо поди:

$ kubectl -n ops-monitoring-ns get pods -l app.kubernetes.io/instance=otel-k8s-agent
NAME                                                 READY   STATUS    RESTARTS   AGE
otel-k8s-agent-opentelemetry-collector-agent-2ft7s   1/1     Running   0          35s
otel-k8s-agent-opentelemetry-collector-agent-79gs2   1/1     Running   0          35s
otel-k8s-agent-opentelemetry-collector-agent-bdhsd   0/1     Pending   0          35s
...

За хвилину перевіряємо метрики в VictoriaMetrics – {__name__=~"k8s\\.pod\\.cpu\\..*", k8s.cluster.name="eks-ops-1-33"}:

OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack

Та логи, наприклад з {k8s.namespace.name="dev-backend-api-ns"}:

OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack

Тут не дуже ОК, що log streams створюються з таким великим набором labels:

_stream	{cloud.provider="aws",k8s.cluster.name="eks-ops-1-33",k8s.container.name="backend-celery-workers-container",k8s.container.restart_count="1",k8s.deployment.name="backend-celery-workers-deployment",k8s.namespace.name="dev-backend-api-ns",k8s.node.name="ip-10-0-37-96.ec2.internal",k8s.pod.name="backend-celery-workers-deployment-669c8bb67-vspzn",k8s.pod.start_time="2026-05-15T11:10:26Z",k8s.pod.uid="6c6c12e6-cade-41e4-aa80-20cb4e08a54a"}

Це теж можна вирішити з processor, який робили для metrics, або створити новий, наприклад:

resource/drop_log_labels:
      attributes:
        - key: k8s.pod.uid
          action: delete
        - key: k8s.container.restart_count
          action: delete

І потім підключити в logs pipeline:

...
      logs:
        receivers: [otlp, k8sobjects]
        processors: [memory_limiter, k8sattributes, resource, resource/drop_log_labels, transform/k8s_events, batch]
        exporters: [otlphttp/vlogs]
...

Але деякі лейбли можуть бути корисним – як-от k8s.container.restart_count.

Тому інший варіант – на самій VictoriaLogs передати collector.streamFields або collector.ignoreFields, можна зробити прямо в OTel Gateway через header VL-Stream-Fields:

...

    otlphttp/vlogs:
      endpoint: http://atlas-victoriametrics-victoria-logs-single-server.ops-monitoring-ns.svc.cluster.local:9428/insert/opentelemetry
      tls:
        insecure: true
      headers:
        VL-Stream-Fields: "k8s.cluster.name,k8s.namespace.name,k8s.deployment.name,k8s.container.name,k8s.pod.name"

...

Grafana і запити Prometheus vs OpenTelemetry

І трохи про те, що зміниться в Grafana та алертах.

Наприклад, є такий запит в Prometheus-форматі:

sum(container_memory_working_set_bytes{namespace="$namespace", pod="$pod", image!="", container!="POD", container!=""}) by (pod)

В OpenTelemetry форматі він буде виглядати так:

sum({__name__="container.memory.working_set", k8s.namespace.name="$namespace", k8s.pod.name="$pod"}) by (k8s.pod.name)

Результат на графіках – зверху старий, Prometheus, внизу – новий, OpenTelemetry:

OpenTelemetry: OTel Collectors в Kubernetes та інтеграція з VictoriaMetrics stack

Для VictoriaMetrics можна задати opentelemetry.usePrometheusNaming (див. List of command-line flags) – тоді метрики будуть створюватись в форматі Prometheus з “_” замість “.“.

Але для VictoriaLogs та VictoriaTraces такої опції не бачу – спитаю девелоперів, чи є там якісь адекватні варіанти це вирішити.

Loading