Назад к блогу

OTel-native подход: как строить продукты с экспортом в любой observability-стек

OTel-native подход: как строить продукты с экспортом в любой observability-стек

Продукты, генерирующие телеметрию, часто вынуждены поддерживать отдельные интеграции под каждый observability-бэкенд — что дорого и хрупко. В статье разбирается OTel-native подход, при котором экспорт строится на стандартах OpenTelemetry и OTLP, и описывается, как это реализуется в двух разных контекстах развёртывания. Полезно, если вы проектируете собственный продукт и хотите отдавать логи, трейсы и метрики в любой стек без кастомной разработки под каждого вендора.

Продукт, который генерирует телеметрию, рано или поздно упирается в вопрос: как отдать её туда, куда хочет пользователь, не собирая под каждый бэкенд отдельную интеграцию. Разбираем механику OTel-native экспорта: два контекста развёртывания, свойства, которым обязан удовлетворять каждый сигнал, устройство декларативной конфигурации, поведение OTLP как транспорта и паттерны интеграции pull против push.

Два контекста развёртывания

Способ добавить OTel-экспорт зависит от того, кто владеет системой, производящей телеметрию. Контекстов два.

Первый — self-hosted software: продукт устанавливается и запускается в окружении заказчика (его дата-центр, его облако, его кластер Kubernetes). Здесь вы инструментируете свой продукт OpenTelemetry, и когда заказчик настраивает endpoint, например через переменные окружения или конфигурационный файл, приложение экспортирует телеметрию из процесса, который он запускает.

Второй — cloud platforms: продукт является платформой, где клиенты разворачивают свои приложения или пользуются управляемыми сервисами (PaaS, serverless, API gateway). Нагрузка выполняется на вашей инфраструктуре, и экспорт выполняет ваша инфраструктура, а не бинарник приложения клиента.

Для каждого поддерживаемого сигнала экспорт обязан обладать набором свойств:

  • Vendor-neutral — пользователь указывает любой OTel-совместимый endpoint: экземпляр Collector или один из многих бэкендов, поддерживающих OTLP напрямую, — без кастомных интеграций под каждый.
  • No deep custom development — внешние платформы или инструменты пользователей интегрируются через стандартные OTel SDK и протокол OTLP, а не через проприетарные API.
  • Rich context preserved — экспортируемые данные включают метаданные, временные метки и корреляцию trace/span, где она доступна (например, записи логов, связанные с trace ID), чтобы пользователь мог разбираться и анализировать данные в своём бэкенде без потери контекста.
  • Support for Semantic Conventions — следование Semantic Conventions сохраняет телеметрию стандартизованной и интерпретируемой любым совместимым бэкендом.

Эти свойства сформулированы как общие для всех сигналов, отдельного набора под каждый контекст нет.

Что именно должен уметь пользователь

Требование vendor-neutral на практике означает: пользователь может указать любой OTel-совместимый endpoint без создания кастомных интеграций. Он должен уметь настраивать endpoint для экспорта телеметрии — например, через переменные окружения или конфигурационный файл — и при этом контролировать бинарник и назначение.

Рекомендуется поддерживать все три сигнала — логи, трейсы и метрики, — но начинать с того, что генерирует продукт. Сигналы следует экспортировать через OTLP с помощью OTel SDK или Collector: одна push-архитектура работает для всех трёх. Пользователям стоит разрешить включать или отключать отдельные сигналы, чтобы контролировать объём данных и затраты.

Как это делают другие

Инфраструктурой. В этой модели экспорт выполняет сама платформа, а не бинарник приложения у клиента. Для платформы, которую вы эксплуатируете, акцент делается на настраиваемых назначениях экспорта, куда инфраструктура пересылает данные.

Cloudflare Workers обрабатывает экспорт через платформенную функцию Observability Destinations: пользователь настраивает OTLP-эндпоинт в дашборде, после чего Cloudflare автоматически отправляет трейсы и логи из Workers в это назначение.

Heroku использует Telemetry Drains: пользователь задаёт эндпоинт, транспорт и заголовки и выбирает сигналы, а платформа собирает данные из приложения (через OTel SDK) и своих сервисов (например, Router) и отправляет их в назначение.

Бинарником приложения. Если пользователи сами разворачивают ПО в своих окружениях, лучшая практика — поставлять приложение уже инструментированным OpenTelemetry и давать флаги конфигурации для их OTLP-эндпоинтов. Keycloak задаёт конфигурацию при запуске:

bin/kc.sh start --telemetry-endpoint=http://my-otel-endpoint:4317 --telemetry-protocol=grpc

Флаг --telemetry-endpoint задаёт адрес OTLP-эндпоинта, --telemetry-protocol — транспортный протокол; в примере это grpc. Keycloak использует один общий эндпоинт для всех трёх сигналов.

Общий шаблон — push-based экспорт через OTel/OTLP. Одни продукты дают один эндпоинт на все сигналы (Keycloak), другие позволяют выбирать сигналы (Heroku --signals).

Kuma. MeshTrace. Политика MeshTrace с бэкендом OpenTelemetry задаёт адрес коллектора через поле endpoint, например otel-collector:4317. MeshAccessLog использует тот же шаблон с тем же полем endpoint, но дополнительно задаёт тело записи через body.kvlistValue.values, где элементы описываются парой key и value (например, stringValue: '%KUMA_MESH%'), и attributes — список пар key/value (например, key: start_time со stringValue: '%START_TIME%'). Таким образом, endpoint в обеих политиках указывает на OTLP-эндпоинт коллектора, а body и attributes формируют содержимое и метаданные отправляемой телеметрии.

Heroku CLI. Команда heroku telemetry:add принимает первым позиционным аргументом <endpoint> — адрес назначения, куда платформа будет отправлять телеметрию. Флаг --signals задаёт, какие именно сигналы экспортировать (traces,metrics,logs), что даёт гранулярный контроль над объёмом данных и стоимостью ингестии. Флаг --transport задаёт транспортный протокол доставки (http), --headers передаёт заголовки аутентификации в виде JSON, например '{"Authorization": "ingestion key"}'. Дополнительно команда принимает --app <app-name>.

Гранулярность сигналов и её цена

Гранулярный контроль над экспортируемыми сигналами — прагматичный шаблон проектирования, особенно для команд, стремящихся управлять объёмом данных и стоимостью приёма. Пользователь включает или отключает отдельные сигналы, чтобы управлять объёмом и расходами.

Компромисс в том, что внедрение OpenTelemetry не бесплатно: команде нужно вложить время в изучение и реализацию, а также создать документацию, чтобы направлять пользователей и внутренние команды по лучшим практикам. Продукту следует документировать формат эндпоинта (gRPC/HTTP), обязательные заголовки и семантику атрибутов/схемы для каждого сигнала, чтобы пользователи могли уверенно полагаться на данные в своих бэкендах.

Декларативная конфигурация: как она устроена

Декларативный конфиг описывает провайдеры сигналов и их экспортёры отдельными ключами: для логов — logger_provider.processors, для трейсов — tracer_provider.processors, для метрик — meter_provider.readers.

Трейсы. Провайдер строится так: сначала список процессоров спанов, затем лимиты спанов, затем сэмплер, затем генератор идентификаторов — и всё это передаётся в конструктор провайдера. Каждый процессор выбирается по имени: batch накапливает спаны и отправляет их пакетами по таймеру, что снижает накладные расходы при высокой нагрузке; simple отправляет каждый спан сразу при завершении, что удобно для отладки, но дороже по числу запросов. Экспортёр выбирается по имени: otlp_http даёт OTLPHttpTraceExporter для кодировки json или OTLPProtoTraceExporter для protobuf, otlp_grpc — OTLPGrpcTraceExporter, console — ConsoleSpanExporter.

Сэмплер строится по имени: always_off → AlwaysOffSampler, always_on → AlwaysOnSampler, trace_id_ratio_based → TraceIdRatioBasedSampler с ratio по умолчанию 1.0, parent_based → ParentBasedSampler. ParentBasedSampler принимает решение о сэмплировании, опираясь на решение родительского спана: для корневого спана используется вложенный сэмплер root, а для дочерних — один из четырёх вложенных сэмплеров в зависимости от того, был ли родитель удалённым или локальным и было ли его решение сэмплировать (remote_parent_sampled, remote_parent_not_sampled, local_parent_sampled, local_parent_not_sampled). Генератор идентификаторов возвращает undefined, если не задан, иначе для random — RandomIdGenerator.

Метрики. Провайдер строится из readers и views. Каждый reader выбирается по имени: periodic создаёт PeriodicExportingMetricReader с exportIntervalMillis по умолчанию 60 000 и exportTimeoutMillis по умолчанию 30 000, где экспортёр задаётся ключом exporter; pull создаёт reader, где экспортёр задаётся вложенным ключом exporter, имя которого выбирается через mustSingleEntry — вспомогательную функцию, которая требует ровно одну запись в объекте и возвращает её имя и свойства, а при нуле или нескольких записях сообщает об ошибке конфигурации. Для pull-ридера поддерживается имя prometheus/development.

Логи. Провайдер строится из процессоров и лимитов записей. Процессор выбирается по имени batch или simple, в обоих случаях экспортёр берётся из поля exporter. Экспортёр логов выбирается по имени otlp_http (с кодировками json и protobuf), otlp_grpc или console.

Порядок кастомайзеров в Java SDK. Каждый кастомайзер провайдера хранится в отдельном поле. Метод добавления кастомайзера объединяет новую функцию с уже сохранённой через mergeCustomizer, который возвращает функцию, применяющую сначала предыдущую, затем новую. При сборке SDK после настройки билдера провайдера применяется соответствующий кастомайзер, и только затем строится провайдер. Для meter provider кастомайзер применяется после MeterProviderConfiguration.configureMeterProvider и до meterProviderBuilder.build(); для tracer provider — после TracerProviderConfiguration.configureTracerProvider и до tracerProviderBuilder.build().

Node SDK и окружение. Конструктор NodeSDK сохраняет переданные metricReaders, иначе берёт их из окружения. Функция читает список значений из OTEL_METRICS_EXPORTER, убирает дубликаты; если список пуст, добавляется otlp как значение по умолчанию. Если среди значений есть none, метрический провайдер не инициализируется. Для каждого значения создаётся соответствующий reader: otlp — периодический reader из окружения, console — PeriodicExportingMetricReader с ConsoleMetricExporter, prometheus — PrometheusMetricExporter с host и port из OTEL_EXPORTER_PROMETHEUS_HOST и OTEL_EXPORTER_PROMETHEUS_PORT; неизвестные значения игнорируются с предупреждением. Провайдер создаётся только если readers есть и их длина больше нуля, и регистрируется как глобальный.

Python. Выбор экспортёра начинается с чтения имён из переменной окружения по типу сигнала: пустое значение или none даёт пустой список, иначе строка разбивается по запятым. Если имя не входит в набор OTLP-имён, оно возвращается как есть. Для OTLP-имён читается протокол из переменной окружения по типу сигнала, а при её отсутствии — из общей OTEL_EXPORTER_OTLP_PROTOCOL. Если протокол не задан и имя равно _EXPORTER_OTLP, выбирается gRPC; если имя уже конкретное (grpc/http), оно остаётся без изменений. Если протокол задан и имя равно _EXPORTER_OTLP, значение ищется в таблице, и при отсутствии ключа конфигурация отвергается с сообщением Unsupported OTLP protocol '{otlp_protocol}' is configured. Если имя уже конкретное, а протокол из env конфликтует с ним, пишется предупреждение Conflicting values for %s OTLP exporter protocol, using '%s' и используется исходное имя. Далее имена объединяются с именами из env, и для каждого экспортёра создаётся провайдер с процессором или ридером.

Маппинг переменных окружения. Флаг OTEL_SDK_DISABLED напрямую присваивается полю disabled. Для OTEL_LOG_LEVEL строка ищется в таблице по ключу в верхнем регистре; при неизвестном значении пишется предупреждение и выбирается info, при пустой строке уровень не задаётся. Пропагаторы из OTEL_PROPAGATORS сохраняются в composite_list, разбиваются по запятым с обрезкой пробелов, и каждый непустой элемент добавляется в composite как объект с именем в качестве ключа. Сэмплер задаётся по OTEL_TRACES_SAMPLER и OTEL_TRACES_SAMPLER_ARG: аргумент парсится через parseFloat (по умолчанию 1.0), и в зависимости от типа сэмплера записывается соответствующая структура. Лимиты атрибутов задаются через getNumberFromEnv: OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT применяется только если значение >= 0, а OTEL_ATTRIBUTE_COUNT_LIMIT — без такого условия; при отсутствии attribute_limits создаётся объект с attribute_count_limit: 128. Числовые значения читаются так же для лимитов трейс-провайдера и параметров батчевых процессоров с fallback через ?? (например, schedule_delay: getNumberFromEnv('OTEL_BSP_SCHEDULE_DELAY') ?? 5000).

Валидация таймаута экспорта. Функция принимает значение таймаута и метку ошибки. Если timeout равен null или undefined, значение считается незаданным. Если timeout меньше нуля, конфигурация отвергается с сообщением, что значение должно быть неотрицательным. Если timeout равен нулю, конфигурация отвергается с сообщением, что нулевой (бесконечный) таймаут не поддерживается. В остальных случаях значение принимается как есть.

TLS. loadTlsConfigFile проверяет, что путь абсолютный, и читает файл: если путь не абсолютный, бросается ошибка TLS config file "${absPath}" must be an absolute path, а при ошибке чтения — could not load "tls.${propName}" from config: ${err.message}. Для HTTP-экспортёра опции TLS заполняются из tls.ca_file, tls.cert_file, tls.key_file; если tls не задан — TLS не настраивается. Для gRPC-экспортёра при tls.insecure используются insecure credentials; иначе при наличии любого из ca/key/cert — SSL credentials, иначе TLS не настраивается. В опциях HTTP-экспортёра формируются compression (GZIP при gzip, иначе NONE), url из endpoint, headers, timeoutMillis и httpAgentOptions из TLS; для gRPC — compression, url, timeoutMillis, credentials из TLS и metadata из заголовков. checkConfigUse предупреждает через diag.warn о необработанных свойствах конфигурации.

Ресурс. createResourceFromConfig проверяет поддерживаемые свойства attributes, attributes_list, schema_url, detection/development; для каждого детектора вызывается mustSingleEntry, и по имени выбирается host/os, process или service. Атрибуты include/exclude не поддерживаются — в коде стоит комментарий TODO(6986): support attributes.{include,exclude}; resources package doesn't currently support this. Удаление дубликатов и обработка none выполняются в другой функции через Set и ранний возврат undefined.

Пропагаторы и ресурс. В декларативном конфиге пропагаторы задаются через composite и composite_list, которые объединяются; каждый элемент — объект с единственным ключом-именем, а при нескольких ключах или значении undefined конфигурация отвергается. Имена разрешаются в конкретные реализации (tracecontext, baggage, b3, b3multi), неизвестное имя приводит к ошибке, а при имени none пропагатор не задаётся; результат всегда оборачивается в CompositePropagator. Ресурс настраивается через createResourceFromConfig, который начинает с defaultResource(). Переменные окружения также влияют: setPropagators заполняет composite_list и composite из OTEL_PROPAGATORS, а OTEL_NODE_RESOURCE_DETECTORS добавляет детекторы в detection/development. Пропагаторы сериализуют и десериализуют значения сквозных concern'ов, таких как Span (обычно только SpanContext) и Baggage; TextMapPropagator внедряет и извлекает значения из текстовых носителей. Корреляция между сигналами обеспечивается тем, что Baggage может потребляться и использоваться как дополнительные атрибуты для метрик или дополнительный контекст для логов и трейсов, а Resource описывает сущность, для которой записывается телеметрия.

Python: импорт компонентов. Sampler, id generator и tracer configurator импортируются через общий помощник, который по имени выбирает entry point из заданной группы. Для сэмплера при пустом имени сэмплер не задаётся, иначе он создаётся и проверяется, что результат является Sampler; при исключении логируется предупреждение и используется сэмплер по умолчанию. Для id generator проверяется, что реализация — подкласс IdGenerator, и при несоответствии конфигурация отвергается с сообщением "{id_generator_name} is not an IdGenerator"; ошибки не перехватываются. Для tracer configurator при пустом имени конфигуратор не задаётся, иначе загружается компонент; при исключении логируется предупреждение и используется конфигуратор по умолчанию. Если компонент не найден, общий помощник сообщает об ошибке с текстом Requested component '{selected_component}' not found in entry point '{entry_point_name}'.

OTLP как транспорт: протокол и надёжность

Retryable и non-retryable на gRPC. Retryable и non-retryable различаются по коду gRPC. Для неретраибельных ошибок серверу рекомендуется использовать код InvalidArgument и MAY передавать дополнительные детали через status с помощью BadRequest. Для сигнализации backpressure сервер SHOULD вернуть ошибку с кодом Unavailable и MAY передать дополнительные детали через status с помощью RetryInfo. Клиент SHOULD интерпретировать коды gRPC как retryable или non-retryable: UNAVAILABLE — Yes, INVALID_ARGUMENT — No. Сервер передаёт retry_delay, создавая статус с деталью RetryInfo, содержащей RetryDelay как duration.Duration с секундами. Клиент должен ждать retry_delay времени с момента получения ответа об ошибке перед повтором, а при повторных неудачах использовать экспоненциальный backoff, увеличивая задержку на основе retry_delay до достижения максимума попыток или максимального предела задержки.

Partial success. При получении ответа с заполненным полем partial_success клиент обязан не повторять запрос. Это правило действует и для OTLP/HTTP, где сервер при частичном принятии данных отвечает HTTP 200 OK и инициализирует partial_success, устанавливая число отклонённых элементов в поле rejected_spans, rejected_data_points, rejected_log_records или rejected_profiles. Повторная отправка запрещена, потому что частичный успех означает, что сервер уже принял часть данных и отклонил остаток; повторная отправка того же запроса не исправит отклонённую часть. Отдельно, при постоянной ошибке декодирования или невалидных данных сервер отвечает HTTP 400 Bad Request, и клиент также не должен повторять запрос.

Размеры сообщений. Для запроса клиент SHOULD ограничивать размер тела, включая до сжатия, рекомендуемый предел — 64 MiB; при превышении клиент MUST NOT выполнять запрос. Для ответа клиент MUST ограничивать размер при получении, включая после распаковки, рекомендуемый предел — 4 MiB; при превышении ответ считается non-retryable ошибкой. Сервер также MUST ограничивать размер ответа, включая до сжатия, рекомендуемый предел — 4 MiB, а при невозможности уложиться — завершать запрос ошибкой (RESOURCE_EXHAUSTED для gRPC, HTTP 500 для HTTP). Для gRPC клиент должен ограничивать размер входящего ответа, включая после распаковки, типичный предел 4 MiB, при превышении — non-retryable ошибка с кодом RESOURCE_EXHAUSTED.

Формула максимальной пропускной способности выводится из того, что за один цикл «запрос-ответ» передаётся не более max_request_size данных, а длительность цикла складывается из сетевой задержки и времени ответа сервера:

max_concurrent_requests * max_request_size / (network_latency + server_response_time)

OTLP/HTTP: успех и ошибки. При успехе сервер обязан ответить статусом HTTP 200 OK, а тело ответа должно быть Protobuf-сообщением Export<signal>ServiceResponse, при этом поле partial_success должно оставаться незаданным. При частичном принятии сервер тоже отвечает HTTP 200 OK с тем же типом сообщения, но обязан инициализировать partial_success и указать в нём число отклонённых элементов. При ошибке обработки сервер отвечает подходящим кодом HTTP 4xx или HTTP 5xx, и тело ответа обязано быть Protobuf-сообщением Status. Повторять запрос следует только при кодах 429 Too Many Requests, 502 Bad Gateway, 503 Service Unavailable и 504 Gateway Timeout; все прочие коды 4xx и 5xx повторять нельзя. В частности, при постоянной ошибке из-за невалидных или не декодируемых данных сервер обязан ответить HTTP 400 Bad Request, и клиент не должен повторять запрос.

Совместимость версий. OTLP/JSON-приёмник обязан игнорировать поля сообщения с неизвестными именами и разбирать сообщение так, как если бы неизвестного поля в полезной нагрузке не было. Это поведение совпадает с работой двоичного Protobuf-демунициализатора и гарантирует, что добавление новых полей в сообщения OTLP не сломает уже существующие приёмники. Будущие версии OTLP должны проектироваться так, чтобы клиенты и серверы разных версий могли обмениваться телеметрией. Для небольших изменений протокола будущие версии и расширения OTLP поощряются использовать способность Protobuf развивать схему обратно совместимым образом, а новые поля будут игнорироваться теми, кто их не понимает. При этом ключами JSON-объектов являются имена полей, преобразованные в lowerCamelCase; исходные имена недопустимы в качестве ключей.

Настройка gRPC-экспортёра метрик (Java). Конструктор по умолчанию создаёт билдер с типом OTLP_GRPC_METRIC_EXPORTER, DEFAULT_TIMEOUT, DEFAULT_ENDPOINT и GRPC_FULL_METHOD_NAME, а также задаёт селектор агрегационной темпоральности, DefaultAggregationSelector и DEFAULT_MEMORY_MODE. Канал задаётся методом setChannel и имеет приоритет над setEndpoint, если вызваны оба; метод помечен @Deprecated. Endpoint по умолчанию — DEFAULT_ENDPOINT_URL, и endpoint должен начинаться с http:// или https://. Таймаут ожидания обработки пакета по умолчанию — DEFAULT_TIMEOUT. Таймаут установления новых соединений по умолчанию — DEFAULT_CONNECT_TIMEOUT_SECS секунд. Максимальный размер сообщения по умолчанию — 64 MiB; метод проверяет положительность значения. Retry policy по умолчанию — RetryPolicy#getDefault(), а null отключает retry. Aggregation temporality по умолчанию — alwaysCumulative(), а deltaPreferred() — типичная конфигурация для delta-бэкендов. Memory mode по умолчанию — DEFAULT_MEMORY_MODE, а MemoryMode#REUSABLE_DATA оптимизирует сериализацию для снижения выделения памяти.

Паттерны интеграции: pull против push

Polling с состоянием. На каждой итерации опроса состояние last_end_time используется как начало интервала: start_time = last_end_time, а конец интервала задаётся текущим моментом end_time = now(). Перед циклом выборки next_token сбрасывается в null, после чего выполняется вызов FilterLogEvents(log_groups, start_time, end_time, next_token), возвращающий response. Полученные события передаются дальше через emit(response.events), а next_token обновляется значением response.nextToken. Цикл повторяется, пока next_token != null. Поскольку start_time на следующем опросе берётся из last_end_time, интервалы опросов стыкуются, что и предотвращает потерю записей между опросами.

Компромисс. OTLP рекомендуется для большинства современных developer-focused платформ, поскольку единый стандартный протокол сохраняет контекст, доставляет данные почти в реальном времени и позволяет пользователям менять бэкенды без перестройки пайплайнов. Кастомные polling API предлагается использовать только тогда, когда стандартизация невозможна. Для self-hosted продуктов (например, Keycloak, Kuma) рекомендуется инструментировать сам продукт через OpenTelemetry, чтобы клиент сам задавал endpoint и экспортировал телеметрию из своего процесса. Для cloud-платформ (например, Cloudflare, Heroku) рекомендуется платформенный подход: платформа собирает данные из workload пользователя и пересылает их на OTLP-endpoint клиента. OTLP требует вложений в обучение и документацию, но эти затраты окупаются по мере роста adoption OpenTelemetry. Кастомный polling API требует от пользователей писать код для преобразования ответов API в стандартные форматы.

Pull MetricReader в декларативной конфигурации. Pull reader моделируется как объект с единственным ключом-экспортёром, например pull: { exporter: { prometheus/development: ... } }. Функция проверяет допустимые свойства exporter и producers и строит список metricProducers. Затем через mustSingleEntry извлекается имя экспортёра; для prometheus/development создаётся PrometheusExporter с host по умолчанию localhost и port по умолчанию 9464. Сам экспортёр является MetricReader и принимает конфигурацию из полей exporter и producers. Отличие от periodic reader по семантике сбора: periodic reader создаётся с параметрами exportIntervalMillis (по умолчанию 60 000) и exportTimeoutMillis (по умолчанию 30 000), то есть сбор инициируется по таймеру; pull reader же не имеет interval/timeout и представлен самим PrometheusExporter, который по своей природе отдаёт метрики по запросу, а не по расписанию.

Сквозная корреляция и маршрутизация

Статья описывает end-to-end связность как требование, чтобы при попадании телеметрии в бэкенд пользователя всё связывалось end-to-end и рассказывало полную историю. Конкретный перечень идентификаторов (trace_id, span_id, resource attributes), которые должны сохраняться, в статье не приводится.

В спецификации определены TraceId и SpanId как часть SpanContext, которая должна распространяться на дочерние спаны и через границы процессов. TraceId — идентификатор трейса, используемый для группировки всех спанов одного трейса; SpanId — идентификатор спана, который при передаче дочернему спану становится parent span ID. Resource описывает сущность, для которой записывается телеметрия. Однако статья не связывает эти определения с требованием к бэкенду пользователя и не перечисляет, какие именно идентификаторы должны сохраняться при экспорте.

Практические рекомендации

Статья предлагает последовательность шагов при проектировании экспорта:

  1. Решить, какие сигналы поддерживать из логов, трейсов и метрик. Идеально — все три, но начинать с того, что генерирует продукт.
  2. Использовать OTLP для экспорта этих сигналов через OTel SDK или Collector: одна push-архитектура работает для всех трёх сигналов.
  3. Дать пользователям настроить конечную точку и необязательные заголовки аутентификации, рассмотрев конечные точки для отдельных сигналов для команд со сложными конфигурациями.
  4. Разрешить включать и отключать отдельные сигналы для контроля объёма и стоимости.
  5. Документировать формат конечной точки (gRPC/HTTP), требуемые заголовки и семантику атрибутов/схемы для каждого сигнала.
  6. Выбрать топологию Collector: один Collector на арендатора для сильной изоляции или общий Collector с конвейерами на арендатора для меньших операционных затрат.

Порядок применения свойств и кастомайзеров (Java). Поставщики свойств объединяются так, что более поздние перезаписывают дублирующиеся ключи в более ранних. Кастомайзеры свойств накапливаются в списке и применяются по очереди, и результат каждого накладывается через withOverrides. Кастомайзеры провайдеров и экспортёров объединяются так, что при вызове сначала применяется первый, затем второй к его результату; многократные вызовы выполняются по порядку.

Встраивание в продукт. По умолчанию глобальный экземпляр OpenTelemetry не устанавливается; вызов setResultAsGlobal приводит к установке глобального OpenTelemetry с ленивым построением SDK. По умолчанию хук завершения регистрируется; его отключение оставляет управление жизненным циклом SDK потребителю, которому нужно самому обеспечить завершение SDK.

Что из этого следует на практике

  • Контекст развёртывания определяет, кто инициирует экспорт: в self-hosted это процесс клиента, в cloud-платформе — ваша инфраструктура. От этого зависит, где живёт конфигурация endpoint: в переменных окружения и конфиг-файлах у клиента или в дашборде платформы.
  • Vendor-neutral — это не лозунг, а требование к конфигурации: пользователь должен уметь сменить endpoint без пересборки продукта, а интеграция должна идти через стандартные SDK и OTLP, а не через проприетарные API.
  • Гранулярность сигналов — прагматичный компромисс: она даёт пользователю контроль над объёмом и стоимостью, но требует от команды вложений в обучение и документацию формата endpoint, заголовков и семантики атрибутов.
  • Декларативная конфигурация строит провайдеры сигналов по единому шаблону: процессоры/ридеры выбираются по имени, экспортёры — по имени и кодировке, а параметры батчей и лимитов читаются из окружения с fallback-значениями. Ошибки в конфигурации (неизвестный протокол, нулевой таймаут, неабсолютный путь TLS) приводят к явным исключениям, а не к тихому игнорированию.
  • OTLP различает retryable и non-retryable ошибки: повторять можно только определённые коды (429, 502, 503, 504 для HTTP; UNAVAILABLE и другие из таблицы для gRPC), а partial success и HTTP 400 повторять нельзя. Размеры сообщений ограничены (64 MiB на запрос, 4 MiB на ответ), и это напрямую ограничивает пропускную способность.
  • Совместимость версий OTLP обеспечивается игнорированием неизвестных полей: новые поля не ломают старые приёмники, а ключи JSON-объектов — это имена полей в lowerCamelCase.
  • Pull-модель (Prometheus) и push-модель (OTLP) различаются семантикой сбора: pull отдаёт метрики по запросу и не имеет interval/timeout, push инициируется по таймеру. Для большинства developer-focused платформ рекомендуется push через OTLP, а кастомные polling API — только когда стандартизация невозможна.

Где смотреть в коде

Источники

Похожее