Сьогодні поговоримо про те, як запустити 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
Схематично це може виглядати якось так:
Єдиний момент перед тим, як продовжити: я називаю OTel Collectors і як “collector“, і як “agent“, але назва суті не міняє – це просто роль, яку сервіс виконує.
Структура конфігурації OpenTelemetry Collector
В інтернеті багато прикладів файлів, наприклад в офіційному репозиторії k8s/otel-config.yaml, або невелика колекція Cloud-Architect-Emma/opentelemetry-collector-examples.
Але аби ними користуватись або писати власні – треба трохи глянути за загальний синтаксис та компоненти, які в конфігурації описані.
Документація:
- Configuration reference
- Agent pattern
- Gateway pattern
- Agent-to-Gateway pattern
- Components registry (всі receivers/processors/exporters з пошуком)
- Contrib repo
В кожному компоненті ми будемо задавати власні параметри – але структура у всіх однакова:
- receivers: описують звідки отримувати дані – Kubernetes API, AWS API, логи
- для Kubernetes Collector тут будуть
hostmetrics(метрики як відnode_exporter),kubeletstats(метрики контейнерів),filelog(логи Pods) - у Gateway у
receiversбудеotlp– приймати дані від Collectors, таk8s_clusterіk8sobjects– він сам буде збирати дані від Kubernetes API таkubelet
- для Kubernetes Collector тут будуть
- processors: трансформації даних – додає метадані (атрибути, лейбли), фільтрує або видаляє зайве, групує, виконує трансформації – перейменування полів, нормалізація
- exporters: куди дані відправляємо
- у Gateway у exporters будуть
otlphttp/vmetrics,otlphttp/vlogs,otlphttp/vtraces. - в Agent у exporters буде
otlp_grpc(з адресою Gateway)
- у Gateway у exporters будуть
- 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 Agentsk8s_cluster: звертається до Kubernetes API, отримує інформацію по Nodes, Pods, Eventsk8sobjects.objects="events": від Kubernetes API постійно отримує Kubernetes Events, записує у вигляді логів
processors:k8sattributes: додає атрибути до кожної метрики чи лога (namespace, deployment name, etc)resource.attributes: додає “глобальні” атрибути до кожного отриманого сигналу (див. OpenTelemetry Resource Attributes Explained Practically)
exporters: куди дані пишуть – бекенди, в нашому випадку передаємо до VictoriaMetrics, VictoriaLogs та VictoriaTracesservice: об’єднуємо все описане вище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"}:
Де маємо метрику 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– щобresourceprocessor вже встиг проставити свої лейбли - перед
batch– щобbatchгрупував вже очищені дані
Апдейтимо деплой, перевіряємо:
І лейблів з .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:
Тут є дві невеликі – але проблеми:
- поле
_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:
Запуск 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"}:
Та логи, наприклад з {k8s.namespace.name="dev-backend-api-ns"}:
Тут не дуже ОК, що 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:
Для VictoriaMetrics можна задати opentelemetry.usePrometheusNaming (див. List of command-line flags) – тоді метрики будуть створюватись в форматі Prometheus з “_” замість “.“.
Але для VictoriaLogs та VictoriaTraces такої опції не бачу – спитаю девелоперів, чи є там якісь адекватні варіанти це вирішити.
![]()








