OpenTelemetry: OTel Collectors в Kubernetes и интеграция со стеком VictoriaMetrics
Это перевод оригинальной статьи OpenTelemetry: OTel Collectors in Kubernetes and VictoriaMetrics Stack integration.
Перевод сделан специально для телеграм-канала Мониторим ИТ. Подписывайтесь! Там еще больше полезных постов о мониторинге.
Сегодня поговорим о том, как запустить OpenTelemetry в Kubernetes и интегрировать его со стеком VictoriaMetrics — VictoriaMetrics для метрик, VictoriaLogs для логов и VictoriaTraces для трейсов.
Содержание
- OpenTelemetry, наблюдаемость и контекст
- Плюсы и минусы OpenTelemetry
- VictoriaMetrics и мой текущий стек мониторинга
- OpenTelemetry — общая архитектура и компоненты
- Структура конфигурации OpenTelemetry Collector
- Пайплайны OpenTelemetry
- OpenTelemetry: запуск в Kubernetes
- Запуск OpenTelemetry Gateway
- Проверка метрик
- Проблема кардинальности
- Проверка логов
- Добавление transform для логов
- Запуск Kubernetes Agent
- Grafana и запросы Prometheus vs OpenTelemetry
OpenTelemetry, наблюдаемость и контекст
Основная идея наблюдаемости заключается в контексте, потому что контекст, как ни странно, касается не только ИИ/LLM, но и мониторинга и наблюдаемости.
Вкратце, наблюдаемость строится на «трех столпах наблюдаемости» — метриках, логах и трейсах.
Но одних только метрик, логов и трейсов недостаточно — потому что все три наших столпа должны иметь общие атрибуты, общие данные, которые позволили бы обеспечить «сквозную наблюдаемость» — то есть, возможность в рамках одного контекста проверять метрики EC2, метрики AWS Application Load Balancer, конкретные поды Kubernetes самого бэкенд-API и, в конечном итоге, — конкретные вызовы функций, бизнес-логику, которая выполняется внутри этого пода в ответ на запрос, пришедший от AWS ALB от конкретного пользователя — то есть, построить конвейер наблюдаемости.
А для того, чтобы все данные имели общий контекст, им необходимы общие характеристики, атрибуты, по которым мы можем группировать все получаемые данные — другими словами, метки.
Но при использовании «стека Prometheus по умолчанию» у нас есть куча разных экспортёров для метрик, отдельные экспортёры для логов, а вдобавок трейсы в формате OTel — и каждый из них записывает labels по-своему. Поэтому, чтобы как-то унифицировать всё это в дашбордах Grafana или алертах, приходится разбираться со всевозможными проблемами 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 мы берём значение label instance, например 10.0.50.18, и строим новое значение вида ip-10-0-50-18.ec2.internal, которое затем используется в дашбордах Grafana для фильтров — потому что какая-то другая метрика возвращает имя хоста в таком формате, в то время как метрика node_exporter по умолчанию не имеет label вроде node_name="ip-10-0-50-18.ec2.internal".
Таким образом, мы можем пойти другим путем — заменить способ, которым мы вообще собираем эти метрики: вместо 10 разных экспортёров для метрик — node_exporter для EC2, YACE exporter для AWS CloudWatch, отдельный k8s-event-logger exporter для отправки Kubernetes Events как логов, отдельный AWS ALB Logs collector, читающий из S3 — мы можем иметь единую систему, которая делает всё это самостоятельно и, что самое главное, сама добавляет общие labels ко всем signals — метрикам, логам, трейсам.
Плюсы и минусы OpenTelemetry
Конечно, не всё так радужно: OpenTelemetry Collector немного сложнее в настройке, потребляет больше ресурсов и требует дополнительного мониторинга.
Это вполне ожидаемо, потому что если система предоставляет больше возможностей «из коробки», то её конфигурация будет несколько сложнее, чем у какого-либо отдельного Prometheus Node Exporter.
То же самое касается ресурсов — когда экспортёр одновременно занимается сбором метрик и логов — он будет потреблять больше ресурсов, чем отдельный экспортёр, «сфокусированный» только на одной задаче: тот факт, что OTel имеет защиту OOMKiller «из коробки», о многом говорит.
Тем не менее, если сложить потребление CPU/RAM всех экспортёров в формате Prometheus и сравнить его с одним подом Kubernetes для OpenTelemetry Collector — всё ещё вопрос, какой из них окажется легче.
Кроме того, формат OTel для метрик больше по размеру, чем метрики Prometheus — потому что сам формат содержит больше данных.
И последний нюанс, который сейчас приходит в голову, заключается в том, что 95% всех алертов и дашбордов Grafana написаны специально для метрик в формате Prometheus и из экспортёров Prometheus, таких как node_exporter и cAdvisor.
Поэтому, если вы разворачиваете OTel в качестве основной системы сбора данных, имейте в виду, что вам также потребуется обновить все связанные с ней ресурсы.
При этом в моём конкретном случае мы всё ещё небольшая стартап-компания, а основные дашборды Grafana я в любом случае делаю вручную, поэтому с LLM задача обновления всего этого выполняется относительно быстро.
Поэтому я попробую, пока запущу его параллельно с существующим стеком экспортёров и логов, похожим на Prometheus, и посмотрю, что из этого получится.
VictoriaTraces и трейсы тоже уже работают, но об этом поговорим отдельно.
VictoriaMetrics и мой текущий набор инструментов мониторинга
В нашем проекте всё работает на AWS Elastic Kubernetes Service — бэкенд API и другие сервисы проекта, сам стек мониторинга VictoriaMetrics, а также различные сервисы AWS — RDS, CloudFront, DynamoDB и т. д.
Что остаётся без изменений — наши «хранилища»: VictoriaMetrics для метрик, VictoriaLogs для логов, VictoriaTraces для трейсов.
Изменения коснутся способа сбора данных: вместо набора экспортёров Prometheus и VMAgent, который их собирает, у нас будет отдельный сервис OTel Gateway, принимающий данные от OTel Collector. А OTel Collector заменит весь зоопарк Prometheus Exporters и Log collectors.
Помимо этой инфраструктуры, существует множество интеграций с поставщиками ИИ — Anthropic, OpenAI, — но их мониторинг — это совершенно отдельная тема, о которой я (надеюсь) напишу позже.
OpenTelemetry — общая архитектура и компоненты
Для сбора данных — метрик, логов и трейсов — у OpenTelemetry есть собственный OpenTelemetry Collector, который может выполнять различные функции.
На самом деле это один и тот же бинарный файл, поведение которого зависит от того, что мы передаём ему в конфигурации:
- Роль 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: описывают, откуда получать данные — API Kubernetes, API AWS, логи.
- Для Kubernetes Collector здесь у нас будут
hostmetrics(метрики вроде тех, что выдаётnode_exporter),kubeletstats(метрики контейнеров),filelog(логи подов). - На Gateway в
receiversбудетotlp— для получения данных от Collectors, а такжеk8s_clusterиk8sobjects.Он будет собирать данные из Kubernetes API и непосредственно отkubelet - Processors: преобразования данных — добавляют метаданные (атрибуты, labels), фильтруют или удаляют ненужное, группируют, выполняют преобразования — переименование полей, нормализацию.
- exporters: куда мы отправляем данные.
- На Gateway exporters будут
otlphttp/vmetrics,otlphttp/vlogs,otlphttp/vtraces. - На Agent exporters будут
otlp_grpc(с адресом Gateway) - extensions: дополнительные возможности (аутентификация, health check, расширения кодирования и т. д.)/
- Connectors: соединяют разные пайплайны между собой.
- Service: связывает и активирует описанные конфигурации —
recievers,processorsи т. д.
Пайплайны OpenTelemetry
Все полученные сигналы проходят через pipeline: то есть receiver получает сигнал, processor обрабатывает его, exporter отправляет его куда-либо.
Для каждого типа сигналов — метрик, логов и трейсов — у нас будут собственные пайплайны, потому что данные связаны между собой, но обрабатываются по-разному.
Каждый pipeline может иметь свой идентификатор — просто имя, чтобы конфигурацию было проще читать, например:
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
Есть несколько способов запустить стек: «bare»-контейнеры, Helm chart или OpenTelemetry Operator (см. Install the Collector).
Для VictoriaMetrics я использую Helm chart victoria-metrics-k8s-stack, который устанавливает VictoriaMetrics Operator, VMAgent, VMAlert, Alertmanager, Grafana, а все настройки выполняются через ресурсы VictoriaMetrics CRD.
Об этой настройке я писал в VictoriaMetrics: building a Kubernetes monitoring stack with a custom Helm chart, а о Kubernetes Operators и CRDs — в Kubernetes: what is a Kubernetes Operator and CustomResourceDefinition.
Для OpenTelemetry я пока просто остановлюсь на Helm chart — так будет проще разобраться с основными компонентами, не тратя время на документацию оператора и его CRD.
И когда всё это будет отправлено в production — мы сможем перейти на OpenTelemetry Operator.
Мы настроим его как три отдельных компонента:
- OTel Gateway: получает данные от Kubernetes API, Kubernetes и AWS Collectors, обрабатывает их, отправляет их в backend-системы — VictoriaMetrics, VictoriaLogs, VictoriaTraces
- Kubernetes Agent: запускается на каждом Kubernetes WorkerNode, собирает данные из
kubeletи логи подов - AWS Agent: собирает данные из AWS — метрики, логи.
Начнём с OTel Gateway, потому что все остальные компоненты будут отправлять данные через него: именно он выполняет всю обработку и отправляет данные в стек VictoriaMetrics.
$ 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 — будут установлены из него, но каждый со своими значениями.
Запуск 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 с двумя подами- Для Kubernetes Agent мы сделаем DaemonSet, потому что ему нужно запускаться на каждом WorkerNode
receivers: описывает входные данные — может быть PULL (они сами обращаются к внешним API) или PUSH (agents/collectors отправляют данные в них)otlp: endpoints для агентов Kubernetes и AWSk8s_cluster: обращается к Kubernetes API, получает информацию о Nodes, Pods, Events.k8sobjects.objects="events": непрерывно получает события Kubernetes из API Kubernetes и записывает их в логи.processors:k8sattributes: добавляет атрибуты к каждой метрике или логу (namespace, имя deployment и т. д.)resource.attributes: добавляет «глобальные» атрибуты к каждому полученному сигналу (см. OpenTelemetry Resource Attributes Explained Practically).exporters: куда записываются данные — бэкенды, в нашем случае мы отправляем их в VictoriaMetrics, VictoriaLogs и VictoriaTraces.service: связывает всё описанное выше.pipelines:metrics: в каком порядке и что делать с метриками.logs: то же самое — но для логов.- Позже здесь будет pipeline для трейсов.
telemetry: включает сам мониторинг — чтобы мы могли смотреть собственные метрики 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 — тот, который будут использовать агенты:
$ 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
Проверка метрик
И уже через минуту мы можем проверить метрики с помощью {k8s.cluster.name="eks-ops-1-33"}:
Мы видим метрику k8s.container.cpu_limit — она поступает от k8s_cluster receiver, который обратился к /api/v1/pods в Kubernetes API и прочитал spec.containers[].resources.limits.cpu.
Проблема кардинальности
И вот важный момент — в labels мы видим множество разных идентификаторов, например:
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, для его k8s.pod.uid генерируется новое значение.
Я подробно разбирал, почему и как это влияет на хранилище и нагрузку VictoriaMetrics в статье «VictoriaMetrics: Churn Rate, High cardinality, metrics and IndexDB», но вкратце — каждое уникальное значение каждого label увеличивает как использование диска, так и размер IndexDB VictoriaMetrics и, соответственно, влияет на потребление CPU/RAM и скорость поиска.
Чтобы предотвратить это — мы можем добавить ещё один processor, который будет удалять такие labels.
Порядок объявления в config.processors не имеет значения — он важен в pipeline, но логично разместить его рядом с блоком 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:
- Атрибуты уровня записи: атрибуты конкретной записи (например, использование CPU контейнером).
- Атрибуты уровня ресурса: атрибуты источника — добавляются ко всем сигналам, отправляемым на бэкенды.
Чтобы проверить, какие именно атрибуты нужно изменить, смотрите документацию конкретного processor, например для процессора k8sattributes:
Процессор автоматически обнаруживает ресурсы Kubernetes (поды), извлекает из них метаданные и добавляет извлеченные метаданные к соответствующим трейсам, метрикам и логам в качестве атрибутов ресурсов.
Или в спецификации OTel, например для Pod, в документации есть URI /resource/k8s/#pod.
Добавим новый processor в metrics pipeline — после 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— чтобы processorresourceуспел установить свои собственные labels. - перед
batch— чтобыbatchгруппировал уже очищенные данные
Обновите развертывание, проверьте:
Теперь у нас есть работающий OTel Gateway, где мы:
- готовы принимать данные от будущих Agents и наших сервисов, таких как Backend API (порты 4317/4318)
- собираем метрики на уровне кластера (
k8s_cluster) - собираем события Kubernetes в виде логов (
k8sobjects) - обогащаем их метаданными k8s (
k8sattributes) - добавляем собственные labels ко всем данным (
k8s.cluster.name,cloud.provider) - контролируем кардинальность (
resource/drop_volatile_labels) - имеем защиту от OOM Killer (
memory_limiter) - Настроили пакетный экспорт в VictoriaMetrics и VictoriaLogs.
Что осталось — AWS Collector для метрик из AWS CloudWatch и логов AWS ALB, а также настройка приёма и отправки трейсов.
Проверка логов
Проверяем логи — запрос {k8s.cluster.name="eks-ops-1-33"}.
На данный момент у нас есть только логи из Kubernetes Events — логи подов мы добавим позже через filelog в Kubernetes Agent:
Здесь есть две небольшие проблемы:
Добавление 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.
Чтобы посмотреть, как именно изначально формируется объект события, включим debug exporter с уровнем детализации detailed:
...
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
Следующий шаг — добавить exporter, который будет собирать данные на уровне подов — метрики и логи.
Создаём файл 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.
Разница заключается в том, как мы разворачиваем поды, какие receivers описываем и куда выполняем export:
mode="daemonset": Collector должен запускаться на каждом WorkerNode в кластереreceivers:hostmetrics: на уровне ноды — CPU, RAM, диски, сеть (эквивалент Prometheus Node Exporter)kubeletstats: метрики контейнеров (эквивалент cAdvisor_exporter)filelog: сбор логов контейнеров (эквивалент Promtail/Filebeat/и т. д.)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 создаются с таким огромным набором меток:
_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.
Поэтому другой вариант — передать collector.streamFields или collector.ignoreFields непосредственно в VictoriaLogs или сделать это прямо в OTel Gateway через заголовок 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 против запросов 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) и Label sanitization — тогда метрики будут создаваться в формате Prometheus с "_" вместо ".".
Но для VictoriaLogs и VictoriaTraces я не вижу такой опции — спрошу разработчиков, есть ли какие-нибудь разумные способы решить это.
Подписывайтесь на телеграм-канал Мониторим ИТ, там еще больше полезной информации о мониторинге!