Модуль 1.1: Поглиблений розбір OTel API та SDK
Складність:
[СКЛАДНИЙ]— основна предметна область, 46% ваги іспиту OTCAЧас на проходження: 90–120 хвилин
Передумови: базове знайомство з розподіленими системами, потоком HTTP-запитів, викликами між сервісами, а також з Python або Go
Результати навчання
Розділ «Результати навчання»Після завершення цього модуля ви зможете:
- Спроєктувати конвеєр OpenTelemetry SDK, який спрямовує трейси, метрики та логи через правильні провайдери, процесори, рідери та експортери для продакшн-сервісу.
- Діагностувати порушену неперервність трейсу, аналізуючи види спанів, зв’язки «батько-нащадок», заголовки W3C TraceContext, пропагатори та використання багажу (baggage).
- Реалізувати ручне інструментування, яке додає бізнес-релевантні спани, атрибути, події, винятки, метрики та ресурси, не дублюючи того, що вже надає автоматичне інструментування.
- Оцінити компроміси між синхронними та асинхронними метричними інструментами, кумулятивною та дельтовою темпоральністю, консольними та OTLP-експортерами, а також простою та пакетною обробкою.
- Рефакторити довідкові фрагменти SDK у придатні для запуску патерни інструментування «від вхідних даних до розв’язку», які ви зможете адаптувати для реальних сервісів і сценаріїв іспиту OTCA.
Чому цей модуль важливий
Розділ «Чому цей модуль важливий»Гіпотетичний сценарій: платформена команда у платіжній компанії має знайому проблему зі спостережуваністю: кожен сервіс генерує телеметрію, але жодні два сервіси не описують одну й ту саму операцію однаково. Java-сервіс оформлення замовлення використовував специфічний для вендора трейсер, Python-сервіс виявлення шахрайства вручну записував ідентифікатори запитів, а Go-шлюз платежів експортував метрики Prometheus із мітками, що не збігалися з іменами трейсів. Коли продакшн-інцидент перетнув усі три сервіси, черговий інженер витратив більше часу на переклад телеметрії, ніж на діагностику збою, який бачив клієнт.
Першою спробою виправлення була стандартизація на єдиному бекенді. Це допомогло зробити дашборди узгодженими, але не розв’язало глибшої проблеми, тому що інструментування досі було прив’язане до бібліотеки одного вендора та одного формату експорту. Коли пізніше компанія перейшла з одного бекенду трейсів на інший, командам довелося змінювати код застосунку, перерозгортати сервіси та повторно перевіряти кожну власну інтеграцію. Ризик міграції виник через помилку проєктування: конвеєр телеметрії був вбудований у логіку застосунку замість того, щоб трактуватися як налаштовувана межа SDK.
OpenTelemetry змінює цю межу. Код застосунку створює спани, метрики, логи, атрибути та контекст за допомогою стабільного API, тоді як SDK вирішує, як телеметрію семплювати, пакувати, агрегувати та експортувати. Саме це розділення є причиною того, що команда може зберігати бізнес-інструментування у вихідному коді, водночас змінюючи експортери, колектори, політики семплювання чи бекенди через конфігурацію. Для OTCA це не дрібниця; це механізм, що стоїть за майже кожним сценарним питанням у предметній області API та SDK.
Цей модуль навчає SDK зсередини назовні, шар за шаром. Спершу ви побудуєте ментальну модель конвеєрів провайдерів, потім дослідите спани та метрики як конкретні структури даних, а вже далі з’єднаєте ці дані через межі сервісів за допомогою механізму пропагації. Після цього ви перетворите довідкові фрагменти на повноцінні проопрацьовані приклади, де неінструментований сервіс крок за кроком стає трасованим і вимірюваним сервісом. Мета тут — не запам’ятати імена класів напам’ять; мета — навчитися впевнено розпізнавати, де саме телеметрія створюється, збагачується, буферизується, перетворюється та зрештою відправляється далі.
Досвідчений практик запитує не лише: «Чи можу я згенерувати спан?» Він запитує: «Чи допоможе цей спан наступному інженерові локалізувати збій, не читаючи вихідний код?» Він також запитує, чи метрика агрегуватиметься коректно, чи ресурсний атрибут має бути на кожному сигналі, чи багаж не витікає чутливих даних, і чи можна змінити конфігурацію експортера без перезбирання сервісу. Саме на цей рівень націлений цей модуль. Хоча іспит OTCA не є тестом з експлуатації Kubernetes, більшість продакшн-розгортань OpenTelemetry, які ви зустрінете в цій програмі, працюватимуть поруч із робочими навантаженнями, колекторами, сайдкарами, шлюзами та просторами імен з контролем допуску в Kubernetes 1.35+. Це має значення, бо рішення щодо SDK, прийняті всередині коду застосунку, стають поведінкою платформи, щойно сотні Подів успадковують ті самі змінні середовища й експортують до того самого флоту колекторів. Якщо один сервіс обирає нестабільні атрибути метрик або витікає багаж, шкода не обмежується локальним вибором бібліотеки; вона стає загальнокластерною проблемою вартості, приватності та реагування на інциденти. Тому цей модуль трактує SDK як першу точку контролю в більшій системі спостережуваності.
Основний матеріал
Розділ «Основний матеріал»Частина 1: Ментальна модель конвеєра SDK
Розділ «Частина 1: Ментальна модель конвеєра SDK»OpenTelemetry має багато специфічних для конкретної мови класів, але сама модель навмисно зроблена повторюваною. Для кожного сигналу код застосунку звертається до об’єкта API, провайдер SDK володіє конфігурацією, процесор або рідер готує дані, а експортер відправляє ці дані кудись далі. Щойно ви навчитеся розпізнавати цю форму, новий мовний SDK стає значно легше читати, бо самі імена змінюються менше, ніж розподіл відповідальностей між компонентами. Провайдер — це і є та межа, що пролягає між «мій код створює телеметрію» та «SDK керує телеметрією».
┌────────────────────────────────────────────────────────────────────────────┐│ One OpenTelemetry SDK Boundary ││ ││ Application code ││ creates telemetry ││ │ ││ ▼ ││ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────┐ ││ │ API object │─────▶│ SDK provider │─────▶│ Processor or reader │ ││ │ tracer/meter │ │ owns resource │ │ batches or collects │ ││ └──────────────┘ └──────────────────┘ └──────────┬───────────┘ ││ │ ││ ▼ ││ ┌──────────────────────┐ ││ │ Exporter sends data │ ││ │ to console, OTLP, │ ││ │ Prometheus, backend │ ││ └──────────────────────┘ │└────────────────────────────────────────────────────────────────────────────┘API навмисно невеликий, бо код інструментування не повинен знати багато про внутрішню роботу бекенду. Обробник має вміти стартувати спан, встановити атрибут, записати метрику й продовжити виконувати бізнес-роботу. Провайдер SDK потім застосовує ідентичність ресурсу, семплювання, агрегацію, пакування та політику експорту. Саме цей дизайн дозволяє тому самому інструментованому коду працювати в демонстрації на ноутбуці, у просторі імен стейджингу чи у продакшн-кластері з різними призначеннями експорту.
| Сигнал | Об’єкт API, який використовує код | Власник у SDK | Проміжний компонент | Приклади призначення експорту |
|---|---|---|---|---|
| Трейси | Tracer | TracerProvider | SpanProcessor | Консоль, OTLP, бекенд трейсів |
| Метрики | Meter | MeterProvider | MetricReader | Консоль, OTLP, скрейпінг Prometheus |
| Логи | Міст логера | LoggerProvider | LogRecordProcessor | Консоль, OTLP, бекенд логів |
Конвеєр трейсів зазвичай перший, який осягають ті, хто навчається, бо спан відчувається конкретним. Ваш застосунок стартує та завершує спани, провайдер прикріплює ресурс і поведінку семплювання, процесор вирішує, коли завершені спани експортуються, а експортер серіалізує їх у призначення. Найважливіший продакшн-вибір майже завжди — це процесор, бо експорт на шляху запиту може перетворити спостережуваність на затримку. Саме тому пакетна обробка є нормальним продакшн-стандартом.
┌────────────────────────────────────────────────────────────────────────────┐│ TracerProvider ││ ││ Resource: service.name="checkout", deployment.environment="prod" ││ ││ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────┐ ││ │ Tracer │─────▶│ SpanProcessor │─────▶│ SpanExporter │ ││ │ │ │ │ │ │ ││ │ start span │ │ Simple: sync │ │ Console: local debug │ ││ │ set attrs │ │ Batch: async │ │ OTLP: collector │ ││ │ add events │ │ │ │ Backend: vendor API │ ││ │ end span │ │ │ │ │ ││ └──────────────┘ └──────────────────┘ └──────────────────────┘ │└────────────────────────────────────────────────────────────────────────────┘| Процесор спанів | Як він поводиться | Практичне застосування | Ризик за неправильного використання |
|---|---|---|---|
SimpleSpanProcessor | Експортує кожен завершений спан негайно на шляху виклику | Локальна діагностика та маленькі демо | Додає затримку експортера до запитів застосунку |
BatchSpanProcessor | Ставить завершені спани в чергу та експортує їх асинхронно групами | Продакшн-сервіси та навантажувальні тести | Втрачає спани, якщо процес аварійно завершується до скидання |
| Власний фільтрувальний процесор | Відкидає або змінює спани перед експортом | Зниження вартості або вилучення небезпечних атрибутів | Може приховати збої, якщо фільтрація надто широка |
Пакетний процесор — це не чарівна черга без втрат. Він має розмір черги, інтервал скидання та максимальний розмір пакету експорту, тож питання налаштування насправді стосується компромісів. Більша черга поглинає сплески, але використовує більше пам’яті; коротша затримка дає свіжіші трейси, але збільшує накладні витрати на експорт; а більший пакет покращує пропускну здатність, але може створювати більші сплески експорту. Для OTCA зосередьтеся спершу на поведінці: batch означає асинхронну буферизацію; simple означає синхронний експорт.
| Налаштування пакування | Поширена змінна середовища | Що ви налаштовуєте | Режим збою за помилки |
|---|---|---|---|
| Розмір черги | OTEL_BSP_MAX_QUEUE_SIZE | Скільки завершених спанів можуть чекати на експорт | Сплески високої кардинальності відкидають спани |
| Затримка скидання | OTEL_BSP_SCHEDULE_DELAY | Як часто процесор намагається експортувати | Інциденти з’являються у бекенді із запізненням |
| Розмір пакету | OTEL_BSP_MAX_EXPORT_BATCH_SIZE | Скільки спанів іде в один виклик експорту | Виклики експорту стають надто малими або надто сплесковими |
Метрики використовують ту саму ідею провайдера, але інший проміжний компонент.
Meter створює інструменти, інструменти записують вимірювання, а рідер вирішує, коли відбувається збір; налаштування експортера preferred_temporality потім повідомляє рідеру, чи експортувати кумулятивні, чи дельтові значення.
Ця відмінність має значення, бо метрики не відправляються по одному вимірюванню за раз; SDK агрегує багато записів у суми, гістограми, останні значення чи інші форми.
Коли метрика виглядає неправильно у бекенді, помилка часто полягає у виборі інструмента, агрегації, кардинальності міток чи темпоральності, а не в експортері.
┌────────────────────────────────────────────────────────────────────────────┐│ MeterProvider ││ ││ Resource: service.name="checkout", service.version="2.4.1" ││ ││ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────┐ ││ │ Meter │─────▶│ MetricReader │─────▶│ MetricExporter │ ││ │ │ │ │ │ │ ││ │ counter │ │ Periodic: push │ │ OTLP: collector │ ││ │ histogram │ │ Prometheus: pull │ │ Console: local debug │ ││ │ observable │ │ │ │ Prometheus endpoint │ ││ └──────────────┘ └──────────────────┘ └──────────────────────┘ │└────────────────────────────────────────────────────────────────────────────┘| Рідер метрик | Модель збору | Найкраще пасує | Наслідок для проєктування |
|---|---|---|---|
PeriodicExportingMetricReader | Push з інтервалом | OTLP-експортери та конвеєри колекторів | Застосунок ініціює експорт за розкладом |
| Рідер Prometheus | Pull через скрейп-ендпоінт | Середовища, рідні для Prometheus | Prometheus керує таймінгом скрейпінгу |
| Рідер ручного збору | Явний тригер збору | Тести та спеціалізовані інтеграції | Викликач має пам’ятати про збір |
Логи завершують картину з трьох сигналів, але їх легко зрозуміти неправильно. OpenTelemetry зазвичай не замінює API логування, який команди застосунків уже використовують; натомість міст під’єднує наявні записи логів до конвеєра логів OTel. Цей міст може прикріпити контекст трейсу, щоб рядок логу, записаний усередині спану запиту, ніс ті самі ідентифікатори трейсу та спану, що й дані трейсу. Це дозволяє логам, метрикам і трейсам вказувати на той самий інцидент, не змушуючи кожну команду відмовлятися від звичних бібліотек логування.
┌────────────────────────────────────────────────────────────────────────────┐│ LoggerProvider ││ ││ ┌──────────────────┐ ┌────────────────────┐ ┌────────────────────┐ ││ │ Existing logger │───▶│ OTel log bridge │───▶│ LogRecordProcessor │ ││ │ │ │ │ │ │ ││ │ Python logging │ │ adds trace context │ │ simple or batch │ ││ │ Java Log4j │ │ maps severity │ │ filtering possible │ ││ │ .NET ILogger │ │ preserves message │ │ │ ││ └──────────────────┘ └────────────────────┘ └─────────┬──────────┘ ││ │ ││ ▼ ││ ┌────────────────────┐ ││ │ LogExporter │ ││ │ console or OTLP │ ││ └────────────────────┘ │└────────────────────────────────────────────────────────────────────────────┘| Сигнал | Зріла концепція, яку треба знати | Що зазвичай іде не так | Питання для діагностики |
|---|---|---|---|
| Трейси | Зв’язки «батько-нащадок» між спанами та види спанів | Порушена пропагація або відсутні серверні спани | Чи спільний trace ID у спанів нижче за течією? |
| Метрики | Вибір інструмента, агрегація, темпоральність, кардинальність | Лічильники несподівано скидаються або гістограми не мають корисних бакетів | Чи бекенд правильно інтерпретує темпоральність? |
| Логи | Поведінка моста та кореляція з трейсами | Логи прибувають, але їх не вдається з’єднати з трейсами | Чи був лог згенерований, поки спан був активним? |
| Багаж | Бізнес-контекст, пропагований як заголовки | Чутливі дані витікають нижче за течією | Чи безпечно було б передавати це значення по мережі? |
Зупиніться й подумайте: ваша команда каже «OpenTelemetry повільний» після додавання консольного експорту до кожного запиту у високонавантаженому сервісі. Перш ніж звинувачувати саме трасування, визначте, який компонент у конвеєрі трейсів перебуває на шляху запиту і який вибір процесора усунув би більшу частину цих накладних витрат.
Відповідь має вказати на поєднання простого процесора та консольного експортера. API трасування справді додають певну роботу, але синхронний експорт є значно більшою проблемою, бо змушує кожен запит чекати на серіалізацію та виведення. Пакетний процесор переносить експорт із гарячого шляху, а OTLP-експортер до колектора зазвичай поводиться більше схоже на продакшн-архітектуру. Це різниця між діагностикою симптому та діагностикою дизайну конвеєра.
Частина 2: Анатомія спану та проєктування трейсу
Розділ «Частина 2: Анатомія спану та проєктування трейсу»Спан — це структурований запис виконаної роботи, а не просто таймер з якимось ім’ям. Він має поля ідентичності, що з’єднують його з трейсом, поля таймінгу, що визначають тривалість, вид, що описує комунікаційну роль, атрибути, що описують саму операцію, події, що позначають важливі моменти, і статус, що підсумовує підсумковий результат. Якщо будь-яке з цих полів виявиться неправильним, трейс усе одно може з’явитися у бекенді, але натомість ввести в оману того інженера, який його читатиме. Саме тому якісне інструментування — це насамперед завдання проєктування, а не завдання простого декорування.
┌────────────────────────────────────────────────────────────────────────────┐│ Span ││ ││ Identity: trace_id, span_id, parent_span_id ││ Operation: name, kind, start_time, end_time ││ Outcome: status code and optional description ││ Detail: attributes and timestamped events ││ Context: links to other traces when parent-child is not correct ││ Resource: service, host, process, deployment, runtime identity │└────────────────────────────────────────────────────────────────────────────┘Trace ID групує спани, що належать одній розподіленій операції. Span ID ідентифікує одну одиницю роботи всередині цієї операції, а parent span ID описує дерево. Коли Сервіс A отримує запит і викликає Сервіс B, Сервіс B зазвичай має створити серверний спан із тим самим trace ID і клієнтським спаном Сервісу A як батьком. Коли ця неперервність порушується, бекенд показує два окремі трейси, і реконструювати часову шкалу інциденту стає важче.
| Поле | Область | Приклад значення | Питання проєктування |
|---|---|---|---|
trace_id | Уся розподілена операція | Один запит оформлення замовлення через сервіси | Чи згруповані пов’язані спани разом? |
span_id | Один спан | Спан запиту до бази даних | Чи може інший спан назвати цей батьком? |
parent_span_id | Дерево «батько-нащадок» | HTTP-клієнтський спан як батько серверного спану | Чи показує часова шкала причину та наслідок? |
name | Мітка операції | POST /checkout або charge-card | Чи групувало б це ім’я подібну роботу? |
kind | Комунікаційна роль | SERVER, CLIENT, PRODUCER, CONSUMER, INTERNAL | Чи правильно спан описує перетин межі? |
Вид спану — одна з найкорисніших дрібниць у трейсі, бо повідомляє читачам, яку роль відіграв сервіс. Серверний спан означає, що сервіс отримав роботу, клієнтський спан означає, що він викликав інший сервіс чи залежність, спан-продюсер означає, що він поставив роботу в чергу, а спан-консумер означає, що він обробив роботу з черги. Внутрішні спани все ще цінні, але вони мають представляти внутрішньопроцесну роботу, а не вихідну комунікацію. Помилкове маркування вихідних викликів як внутрішніх робить карти залежностей і розбивку затримок менш корисними.
| Вид спану | Використовуйте, коли | Приклад | Поширене хибне маркування |
|---|---|---|---|
SERVER | Сервіс отримує запит | HTTP-обробник або метод gRPC | Маркування вхідного обробника як INTERNAL |
CLIENT | Сервіс викликає іншу залежність | HTTP-клієнт, запит до бази даних, виклик кешу | Маркування виклику бази даних як INTERNAL |
PRODUCER | Сервіс ставить у чергу або публікує роботу | Публікація в Kafka або надсилання в чергу | Трактування публікації як звичайного клієнтського виклику |
CONSUMER | Сервіс отримує роботу з месиджингу | Воркер обробляє повідомлення з черги | Втрата зв’язку з продюсером |
INTERNAL | Робота залишається всередині процесу | Валідація, обчислення ціни, рендеринг шаблону | Використання для кожного власного спану |
Атрибути та ресурси різняться, бо відповідають на різні питання.
Ресурс описує сутність, що генерує телеметрію, наприклад сервіс, середовище розгортання, версію, хост, процес чи Под Kubernetes.
Атрибут описує одну операцію чи одну точку даних метрики, наприклад HTTP-метод, маршрут, код статусу, систему бази даних, тип замовлення чи ім’я черги.
Розміщення service.name в атрибуті кожного спану замість на ресурсі створює шумну телеметрію та порушує групування на основі ресурсу.
| Питання | Використовуйте ресурс, коли | Використовуйте атрибут, коли |
|---|---|---|
| Чи описує це процес або сервіс? | Так, встановіть це на ресурсі провайдера | Ні, уникайте повторення на кожному спані |
| Чи змінюється це з кожним запитом або операцією? | Зазвичай ні | Зазвичай так |
| Чи спільне це для кожного сигналу від цього SDK? | Так | Ні |
| Приклад | service.name=checkout | http.request.method=POST |
| Приклад | deployment.environment=prod | http.response.status_code=200 |
Події корисні, коли спану потрібна часова шкала всередині часової шкали.
Наприклад, спан оформлення замовлення може додати події cart.validated, payment.authorized та inventory.reserved, особливо коли ці кроки надто малі чи надто численні, щоб заслуговувати на окремі спани.
Запис винятків — це особливий випадок подій: SDK записує тип винятку, повідомлення та стек виклику як подію.
Однак запис події винятку не доводить автоматично, що спан зазнав збою, у кожному SDK та конфігурації; встановлення статусу помилки робить результат видимим для читачів трейсів та правил оповіщення.
| Деталь спану | Найкраще застосування | Погане застосування | Краща альтернатива |
|---|---|---|---|
| Атрибут | Стабільні виміри для фільтрації та групування | Унікальні ID з необмеженою кардинальністю всюди | Використовуйте вибіркові атрибути та логи для значень високої кардинальності |
| Подія | Момент усередині часової шкали спану | Заміна кожного дочірнього спану подіями | Використовуйте дочірні спани для значущої вкладеної роботи |
| Статус | Кінцевий результат операції | Встановлення помилки для кожної обробленої повторної спроби | Встановлюйте помилку, коли операція спану зазнала збою |
| Зв’язок (link) | Відношення між трейсами | Заміна звичайної пропагації «батько-нащадок» | Використовуйте «батько-нащадок», коли одна операція безпосередньо спричинила іншу |
Зв’язки (links) — це правильний інструмент, коли дерево «батько-нащадок» брехало б. Пакетний воркер може обробляти повідомлення, що надійшли з кількох незалежних запитів, тож один спан-консумер не може мати всі ці спани-продюсери як батьків. Зв’язок спану дозволяє воркеру сказати: «ця обробка пов’язана з тими ранішими спанами», зберігаючи власну структуру трейсу. Зв’язки особливо важливі у дизайнах із збиранням (fan-in), пакетуванням і повторними спробами, де причинні відношення реальні, але не деревоподібні.
Зробіть паузу й передбачте: сервіс оформлення замовлення отримує HTTP-запит, створює спан
SERVER, а потім викликає PostgreSQL і Redis. Якщо обидва виклики залежностей позначені якINTERNAL, що, ймовірно, приховає чи спотворить переглядач трейсів, коли хтось досліджуватиме повільні запити оформлення замовлення?
Переглядач трейсів може не показати ці операції як вихідні залежності, а карти сервісів можуть применшити, наскільки оформлення замовлення залежить від PostgreSQL і Redis. Спани все одно можуть показувати тривалість, але комунікаційна роль неправильна, тож читачі втрачають сигнал про перетин межі. Правильний дизайн — серверний спан для вхідного запиту та клієнтські спани для вихідних викликів бази даних і кешу. Внутрішні спани слід резервувати для значущої внутрішньопроцесної роботи, як-от обчислення ціни чи валідація.
Частина 3: Метрики, що зберігають сенс
Розділ «Частина 3: Метрики, що зберігають сенс»Метрики легко генерувати, але так само легко й зіпсувати. Лічильник з неправильно обраними атрибутами може створити забагато часових рядів, ґейдж там, де насправді належить гістограма, може приховати справжній розподіл затримок, а невідповідність темпоральності може зробити цілком здоровий сервіс хворим на вигляд. SDK допомагає, надаючи типи інструментів з конкретною, чітко визначеною семантикою, але він не може вирішити за вас, що саме ви хочете виміряти. Тому перше питання проєктування тут завжди одне й те саме: «Яке саме рішення ця метрика має допомогти комусь ухвалити?»
Синхронні інструменти викликаються, коли ваш код знає, що щось сталося. Лічильник збільшується, коли запит завершується, гістограма записує тривалість, коли обробник завершує роботу, а up-down-лічильник змінюється, коли з’єднання відкривається чи закривається. Асинхронні інструменти — це зворотні виклики, що спостерігають значення, яке вже десь існує, наприклад глибину черги, використання пам’яті чи розмір пулу з’єднань. Використання асинхронного зворотного виклику для опитування віддаленого сервісу на кожному інтервалі збору — це ознака проблеми з продуктивністю та надійністю.
| Інструмент | Синхронний чи асинхронний | Може зменшуватися | Хороший приклад | Поганий приклад |
|---|---|---|---|---|
| Counter | Синхронний | Ні | Загальна кількість успішних оформлень | Поточна глибина черги |
| UpDownCounter | Синхронний | Так | Активні WebSocket-з’єднання | Загальна кількість запитів від старту |
| Histogram | Синхронний | Не застосовується | Тривалість запиту чи розмір відповіді | Поточний відсоток CPU |
| Observable Counter | Асинхронний | Ні | Час CPU, зчитаний з ОС | Лічильник бізнес-подій у коді запиту |
| Observable UpDownCounter | Асинхронний | Так | Поточний розмір пулу воркерів | Розподіл затримок |
| Observable Gauge | Асинхронний | Так | Глибина черги чи температура | Загальна кількість завершених замовлень |
Гістограми заслуговують на додаткову увагу, бо відповідають на питання, на які середні значення відповісти не можуть.
Якщо середня тривалість оформлення замовлення прийнятна, але невелика група клієнтів бачить екстремальну затримку, гістограма може зберегти цей розподіл, а простий ґейдж — ні.
Добре названа гістограма, як-от http.server.request.duration чи orders.processing.duration, дозволяє дашбордам показувати перцентилі, кількості в бакетах та зразки (exemplars).
Для діагностики продуктивності гістограми часто є містком від «щось стало повільнішим» до «які трейси мені варто дослідити?».
| Потреба у вимірюванні | Рекомендований інструмент | Чому пасує | Приклад атрибута |
|---|---|---|---|
| Підрахунок бізнес-подій | Counter | Події лише рухаються вперед | order.type=standard |
| Відстеження активної роботи | UpDownCounter | Значення зростає і зменшується | worker.pool=checkout |
| Вимірювання затримки | Histogram | Розподіл має значення | http.route=/checkout |
| Спостереження глибини черги | Observable Gauge | Значення існує поза потоком запитів | queue.name=orders |
| Спостереження загального часу CPU | Observable Counter | Лічильник ОС зростає з часом | cpu.state=user |
Темпоральність визначає, що значення метрики означає з часом. Кумулятивна темпоральність повідомляє суму від початкової точки, тоді як дельтова темпоральність повідомляє зміну від останнього збору. Якщо лічильник записує прирости десять, двадцять і п’ять у трьох вікнах збору, кумулятивна повідомляє десять, тридцять і тридцять п’ять, а дельтова — десять, двадцять і п’ять. Жодна не є універсально кращою; бекенд і рідер мають узгодити інтерпретацію.
┌────────────────────────────────────────────────────────────────────────────┐│ Counter Temporality Example ││ ││ Raw increments: +10 +20 +5 ││ ││ Cumulative export: 10 30 35 ││ Delta export: 10 20 5 ││ ││ Reader and backend must agree on which row the exported values represent. │└────────────────────────────────────────────────────────────────────────────┘| Темпоральність | Що означає експортоване значення | Приклад переваги бекенду | Симптом діагностики |
|---|---|---|---|
| Кумулятивна | Сума від старту чи скидання | Інтерпретація лічильника у стилі Prometheus | Обробка скидання має значення під час перезапусків |
| Дельтова | Зміна від попереднього збору | Деякі конвеєри у стилі StatsD чи вендорські | Значення виглядають надто малими, якщо читати як суми |
| Конвертація на бекенді | Колектор чи бекенд конвертує сенс | Змішане середовище під час міграцій | Графіки швидкості розходяться між інструментами |
Агрегація — це спосіб SDK перетворювати багато сирих вимірювань на експортовані дані. Лічильники зазвичай агрегуються як суми, ґейджі — як останні значення, а гістограми — як розподіли за бакетами. Саме тому метричний інструмент — це не просто ім’я методу; він визначає, яка математика буде можливою згодом. Якщо ви записуєте тривалість запиту в лічильник, жоден бекенд нижче за течією не зможе відновити корисний розподіл затримок, бо сира форма була втрачена під час збору.
| Інструмент | Типова агрегація | Корисний вигляд на дашборді | Ризик, за яким стежити |
|---|---|---|---|
| Counter | Сума | Швидкість запитів, частота помилок, пропускна здатність | Атрибути високої кардинальності розривають кількість рядів |
| UpDownCounter | Сума з додатними та від’ємними змінами | Активні сесії чи робота в процесі | Пропущене зменшення залишає значення завищеним |
| Histogram | Явні бакети чи експоненційні бакети | Перцентилі та теплові карти затримок | Погані бакети приховують важливі діапазони |
| Observable Gauge | Останнє значення | Глибина черги чи поточна утилізація | Затримка зворотного виклику впливає на збір |
Зразки (exemplars) з’єднують метрики назад із трейсами. Коли бакет гістограми містить сплеск затримки, зразок може прикріпити семпльований trace ID і span ID до одного вимірювання в цьому бакеті. Це дозволяє інженерові перейти від «затримка оформлення замовлення на рівні p99 стрибнула» до «ось один трейс, що спричинив цей сплеск». Зразки не є заміною трейсів чи метрик; вони є містком між агрегованою поведінкою та доказами окремих запитів.
┌────────────────────────────────────────────────────────────────────────────┐│ Histogram With Exemplar ││ ││ orders.duration bucket 250ms-500ms ││ count=18 ││ exemplar: value=392ms, trace_id=0af7651916cd43dd8448eb211c80319c ││ ││ Dashboard question: "Which trace explains this slow bucket?" │└────────────────────────────────────────────────────────────────────────────┘Рішення про іменування метрики має пережити операційне використання.
Імена мають описувати те, що вимірюється, одиниці мають бути явними, а атрибути мають бути достатньо обмеженими, щоб агрегуватися.
Наприклад, orders.duration з одиницею ms та атрибутом order.type є корисним, тоді як додавання user.id до кожного вимірювання тривалості може створити один ряд на користувача.
Висока кардинальність не просто дорога; вона може зробити дашборди повільними, оповіщення шумними, а бекенди — змусити відкидати дані.
Частина 4: Пропагація контексту та багаж
Розділ «Частина 4: Пропагація контексту та багаж»Розподілений трейс працює лише тоді, коли контекст успішно перетинає межі окремих процесів. Сервіс вище за течією має інжектувати контекст у носій, як-от HTTP-заголовки чи атрибути повідомлення, а сервіс нижче за течією, своєю чергою, має витягнути цей контекст перед стартом власного спану. Коли будь-яка зі сторін забуває виконати свою половину роботи, усе дерево трейсу ламається, навіть якщо обидва сервіси інструментовані цілком коректно поодинці. Саме тому помилки пропагації так часто проявляються у вигляді «у нас начебто є спани, але всі вони опиняються в окремих, не пов’язаних трейсах».
┌──────────────────────────┐ HTTP request ┌────────────────────┐│ Service A: checkout │──────────────────────────▶│ Service B: payment ││ │ traceparent header │ ││ client span: call-payment│ │ server span: POST ││ trace_id: same value │ │ trace_id: same ││ span_id: parent candidate│ │ parent: A client │└──────────────────────────┘ └────────────────────┘Формат W3C TraceContext — це стандарт пропагації за замовчуванням у сучасних конфігураціях OpenTelemetry.
Заголовок traceparent несе версію, trace ID, parent span ID та прапорці трейсу, тоді як tracestate несе специфічний для вендора стан.
У сценарії діагностики вам рідко потрібно декламувати граматику заголовка; вам потрібно розпізнати, чи той самий trace ID пережив межу і чи сервіс нижче за течією використав витягнутого батька.
Ця практична інтерпретація важливіша за запам’ятовування ширини полів.
┌────────────────────────────────────────────────────────────────────────────┐│ traceparent Header Shape ││ ││ traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 ││ │ │ │ │ ││ │ │ │ └ flags ││ │ │ └ parent span ID ││ │ └ trace ID shared across the distributed operation ││ └ version │└────────────────────────────────────────────────────────────────────────────┘| Частина пропагації | Що вона несе | Чому це важливо | Перевірка під час діагностики |
|---|---|---|---|
traceparent | Trace ID, parent span ID, прапорці | Встановлює неперервність розподіленого трейсу | Той самий trace ID між сервісами |
tracestate | Специфічний для вендора чи платформи стан | Зберігає рішення, специфічні для бекенду | Присутній, коли бекенд цього вимагає |
| Пропагатор | Інжектує та витягує контекст | З’єднує SDK з форматом носія | Той самий пропагатор налаштований з обох боків |
| Носій | HTTP-заголовки чи метадані повідомлення | Переносить контекст через межу | Заголовок чи метадані видимі по мережі |
Багаж відрізняється від ідентичності трейсу. Він несе контекст застосунку як пари «ключ-значення», і цей контекст подорожує нижче за течією з запитами. Це може бути корисним для маршрутизації, когорти експерименту, рівня тенанта чи інших нечутливих операційних підказок. Небезпека в тому, що багаж передається в заголовках чи метаданих, тож його треба трактувати як дані, видимі для систем і інфраструктури нижче за течією.
| Кандидат на багаж | Добре чи погано | Причина | Безпечніша альтернатива |
|---|---|---|---|
tenant.tier=enterprise | Добре, якщо нечутливе | Корисне для маршрутизації чи політики семплювання | Тримайте значення низької кардинальності |
user.email=person@example.com | Погано | Персональні дані подорожують нижче за течією | Натомість використовуйте внутрішню категорію користувача |
auth.token=secret | Погано | Ризик витоку облікових даних | Ніколи не пропагуйте секрети як багаж |
experiment.group=B | Добре, якщо обмежене | Корисне для аналізу та діагностики | Документуйте дозволені значення |
cart.value=123.45 | Залежить | Може бути чутливим чи високої кардинальності | Записуйте як атрибут спану вибірково |
Пропагація також поводиться по-різному через синхронні та асинхронні межі. HTTP-виклик має очевидні запит і відповідь, тож клієнтський спан і серверний спан нижче за течією зазвичай утворюють чіткий зв’язок «батько-нащадок». Черга повідомлень може розділяти продюсера й консумера в часі, може пакувати повідомлення або повторювати доставку, тож зв’язки можуть бути кращим представленням, ніж прямий батько, у деяких дизайнах. Сильна відповідь на OTCA пояснює відношення, а не лише механізм.
Зупиніться й подумайте: консумер черги обробляє один пакет, що містить повідомлення, створені кількома різними запитами користувачів. Чи точно один батьківський спан представляв би цей пакет, чи зв’язки спанів зберегли б відношення чесніше?
Зв’язки спанів зазвичай зберігають відношення чесніше, бо робота пакета пов’язана з кількома ранішими трейсами. Вибір одного батька означав би єдиний причинний ланцюг і приховав би природу збирання (fan-in) цього навантаження. Консумер усе одно може створити спан для обробки пакета, але зв’язки дозволяють йому посилатися на контексти продюсерів, прикріплені до кожного повідомлення. Це хороший приклад того, як проєктування трейсу служить читачеві, а не втискає кожен робочий процес у дерево.
Частина 5: Конфігурація SDK та вибір експорту
Розділ «Частина 5: Конфігурація SDK та вибір експорту»SDK OpenTelemetry можна налаштовувати двома способами: через код і через змінні середовища. Код корисний тоді, коли застосунок має конструювати провайдери, процесори, інструменти чи ресурси напряму, у самому вихідному коді. Змінні середовища корисні тоді, коли платформеним командам потрібна узгоджена поведінка одразу через багато сервісів, без перезбирання кожного окремого застосунку. Практичне правило тут таке: тримати бізнес-інструментування в коді, а специфічні для розгортання рішення про експорт — у конфігурації, щоразу, коли це взагалі можливо.
| Сфера конфігурації | Програмний приклад | Приклад зі змінною середовища | Віддавайте перевагу середовищу, коли |
|---|---|---|---|
| Ідентичність сервісу | Атрибути ресурсу в налаштуванні SDK | OTEL_SERVICE_NAME | Кожне розгортання задає власне ім’я |
| OTLP-ендпоінт | Аргумент конструктора експортера | OTEL_EXPORTER_OTLP_ENDPOINT | Ендпоінт різниться за середовищем |
| Протокол експорту | Конфігурація експортера | OTEL_EXPORTER_OTLP_PROTOCOL | Політика фаєрвола чи колектора різниться |
| Семплювання трейсів | Об’єкт семплера | OTEL_TRACES_SAMPLER | Платформа володіє політикою семплювання |
| Увімкнення сигналу | Налаштування провайдера | OTEL_TRACES_EXPORTER | Сервіс потребує швидкого вимкнення чи консольної діагностики |
Конфігурація, специфічна для сигналу, зазвичай перекриває загальну конфігурацію. Наприклад, платформа може задати загальний OTLP-ендпоінт для всіх сигналів, але надсилати трейси до спеціалізованого колектора трейсів під час міграції. Це старшинство дозволяє командам змінювати один сигнал, не зачіпаючи інші. У сценаріях іспиту шукайте конкретнішу змінну, коли два налаштування здаються конфліктними.
| Змінна середовища | Призначення | Приклад значення | Операційний сенс |
|---|---|---|---|
OTEL_SERVICE_NAME | Задає ресурсний атрибут service.name | checkout | Групує телеметрію за сервісом |
OTEL_RESOURCE_ATTRIBUTES | Додає ресурсні атрибути | deployment.environment=prod,service.version=2.4.1 | Збагачує кожен сигнал від процесу |
OTEL_EXPORTER_OTLP_ENDPOINT | Загальний OTLP-ендпоінт | http://collector:4317 | Призначення за замовчуванням для OTLP-сигналів |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | OTLP-ендпоінт, специфічний для трейсів | http://trace-collector:4317 | Перекриває загальний ендпоінт для трейсів |
OTEL_EXPORTER_OTLP_PROTOCOL | Кодування транспорту OTLP | grpc чи http/protobuf | Має збігатися з конфігурацією приймача колектора |
OTEL_TRACES_SAMPLER | Стратегія семплювання трейсів | always_on чи traceidratio | Контролює, які трейси семплюються |
OTEL_TRACES_SAMPLER_ARG | Аргумент семплера | 0.10 | Десятивідсоткове семплювання для семплера за співвідношенням |
Семплери трейсів і головне семплювання
Розділ «Семплери трейсів і головне семплювання»Семплювання вирішує, чи трейс записується, перш ніж накопичиться вартість експорту.
SDK застосовує семплер під час створення спану для кореневих спанів і для нащадків згідно з правилами семплера.
Змінні середовища відображаються на вбудовані типи семплерів; ви також можете налаштувати семплери програмно на TracerProvider.
| Семплер | Значення OTEL_TRACES_SAMPLER | Поведінка | Типове застосування |
|---|---|---|---|
| AlwaysOn | always_on | Кожен кореневий трейс семплюється | Розробка, низьконавантажені сервіси, діагностика |
| AlwaysOff | always_off | Жоден новий трейс не семплюється | Тести, навмисне вимкнення |
| TraceIdRatioBased | traceidratio | Імовірнісне головне семплювання за trace ID (напр., OTEL_TRACES_SAMPLER_ARG=0.10 → ~10%) | Контроль вартості, поки спани ще незалежні |
| ParentBased | (декоратор, не окреме ім’я змінної) | Обгортає кореневий семплер: дочірній спан успадковує рішення про семплювання батька через межі сервісів | Стандартний продакшн-патерн |
ParentBased — це декоратор, який слід уявляти на межах сервісів.
Якщо сервіс вище за течією не семплював трейс, робота нижче за течією не має раптом створювати повне дерево трейсу, якщо ви навмисно не перекриваєте цю політику.
Стандарт SDK фактично є ParentBased(TraceIdRatioBased) з аргументом співвідношення, тож виклики між сервісами залишаються узгодженими з прапорцем семплювання батьківського traceparent.
TraceIdRatioBased окремо застосовується лише на коренях; ParentBased гарантує, що нащадки поважають рішення вище за течією.
OTLP — це стандартний протокол експорту, який ви маєте очікувати в сучасних дизайнах OTel. Експортер може відправляти напряму до бекенду, але багато продакшн-архітектур спершу відправляють до OpenTelemetry Collector. Колектор може приймати телеметрію, пакувати її, фільтрувати її, збагачувати її та розгалужувати її до одного чи кількох бекендів. Цей модуль зосереджується на стороні SDK, але вибір експорту вже має змусити вас думати про архітектуру колектора в наступному модулі.
| Вибір експортера | Хороше пасування | Компроміс | Питання старшого рівня |
|---|---|---|---|
| Консольний експортер | Локальне навчання та діагностика | Не підходить для продакшн-пропускної здатності | Чи не залишили ми це випадково на гарячому шляху? |
| OTLP-експортер | Стандартний продакшн-шлях | Потребує ендпоінта колектора чи бекенду | Чи узгоджено налаштовано протокол і ендпоінт? |
| Шлях рідера/експорту Prometheus | Скрейпінг метрик, рідний для Prometheus | Pull-модель відрізняється від експорту трейсів | Чи безпечно цей сервіс відкриває скрейп-ендпоінт? |
| Застарілий експортер Jaeger чи Zipkin | Старіші середовища під час міграцій | Менш портативний за OTLP | Чи може колектор натомість транслювати OTLP? |
| No-op експортер | Тести чи тимчасове вимкнення | Телеметрія зникає | Це навмисно вимкнено чи неправильно налаштовано? |
Семантичні домовленості (semantic conventions) — це стандарти іменування, що роблять телеметрію портативною.
Вони допомагають дашбордам, правилам оповіщення та запитам працювати між бібліотеками й мовами.
Для HTTP-спанів поточні стабільні домовленості використовують імена на кшталт http.request.method та http.response.status_code.
Старі імена все ще можуть з’являтися на старіших дашбордах чи прикладах, але сценарії іспиту очікують, що ви розумієте: семантичні домовленості еволюціонують, а узгоджене іменування є частиною портативності.
| Домен | Рекомендований атрибут | Приклад значення | Чим допомагає |
|---|---|---|---|
| HTTP-запит | http.request.method | POST | Групує запити за методом |
| HTTP-відповідь | http.response.status_code | 201 | Підтримує аналіз частоти помилок |
| URL | url.full | https://api.example.test/checkout | Зберігає повну ціль запиту, коли це безпечно |
| Сервіс | service.name | checkout | Ідентифікує сервіс-генератор |
| Сервіс | service.version | 2.4.1 | Корелює телеметрію з релізами |
| База даних | db.system | postgresql | Ідентифікує технологію залежності |
| Месиджинг | messaging.system | kafka | Ідентифікує бекенд месиджингу |
Найважливіша звичка щодо семантичних домовленостей — це узгодженість.
Якщо половина флоту використовує http.method, а інша половина — http.request.method, фільтр дашборда може мовчки пропускати дані.
Якщо одна команда записує service.name як атрибут спану, а інша задає його як ресурс, вигляди сервісів на основі ресурсу стають неповними.
Інструментування — це не лише про створення телеметрії; це про створення телеметрії, яку можна запитувати з упевненістю.
Частина 6: Автоматичне інструментування, ручне інструментування та межа між ними
Розділ «Частина 6: Автоматичне інструментування, ручне інструментування та межа між ними»Автоматичне інструментування обгортає підтримувані бібліотеки без жодної потреби для розробників застосунків вручну редагувати кожне окреме місце виклику в коді. У Java агент може модифікувати байт-код під час завантаження класу; у Python інструментування зазвичай патчить бібліотеки під час імпорту; у Node.js хуки require можуть обгортати модулі; у .NET профілювання та стартові хуки можуть приєднувати інструментування. Це потужно, бо швидко дає командам базові трейси для HTTP-фреймворків, клієнтів, баз даних, бібліотек месиджингу та gRPC. Цього недостатньо для бізнес-спостережуваності, бо бібліотеки не знають, що означає ваше оформлення замовлення, поновлення, повернення коштів чи рішення про шахрайство.
| Мова | Поширений механізм автоінструментування | Типова команда чи налаштування | Що це зазвичай захоплює |
|---|---|---|---|
| Java | Java-агент і байт-код інструментування | java -javaagent:opentelemetry-javaagent.jar -jar app.jar | HTTP, JDBC, gRPC, бібліотеки месиджингу |
| Python | Monkey-патчинг під час імпорту | opentelemetry-instrument .venv/bin/python app.py | Flask, FastAPI, requests, клієнти баз даних |
| .NET | CLR-профайлер і стартові хуки | Змінні середовища та хуки виконання | ASP.NET, HTTP-клієнти, бібліотеки баз даних |
| Node.js | Хуки require та пакети інструментування | node --require @opentelemetry/auto-instrumentations-node/register app.js | Express, HTTP, клієнти баз даних |
Ручне інструментування має заповнювати семантичні прогалини, які бібліотеки не бачать.
Бібліотека може сказати, що відбувся HTTP-запит, але не може знати, що reserve-inventory — це критичний бізнес-крок або що order.type=subscription — це вимір, потрібний експлуатації.
Бібліотека бази даних може записати спан запиту, але не може вирішити, чи невдала авторизація платежу має позначити батьківський спан оформлення замовлення як помилку.
Тому ручні спани, метрики, атрибути та події мають описувати домен, а не дублювати те, що вже створює автоінструментування.
| Ситуація | Автоінструментування достатньо? | Додати ручне інструментування? | Причина |
|---|---|---|---|
| Базова затримка HTTP-сервера | Часто так | Можливо, додати іменування маршруту, якщо відсутнє | Інструментування фреймворку знає межі запитів |
| Бізнес-крок авторизації платежу | Ні | Так | Бібліотека не може вивести бізнес-сенс |
| Таймінг запиту до бази даних | Часто так | Обережно додати атрибути за потреби | Інструментування драйвера захоплює виклик залежності |
| Глибина черги з API брокера | Ні | Так, зазвичай асинхронна метрика | Значення існує поза потоком запитів |
| Обрана гілка фічофлагу | Ні | Так, подія чи атрибут | Бізнес-контекст важливий під час інцидентів |
Хороший огляд інструментування запитує, чи зможе наступний інженер діагностувати реальний інцидент за телеметрією. Якщо автоінструментування створює десять спанів, але жоден з них не показує, яке бізнес-рішення зазнало збою, потрібне ручне інструментування. Якщо ручне інструментування створює десятки вкладених спанів навколо кожної допоміжної функції, трейс стає шумним і дорогим. Найкращий результат — багатошаровий трейс: автоматичні спани показують технічні межі, а ручні спани та метрики показують бізнес-намір.
| Ознака проблеми в дизайні | Як це виглядає | Напрям рефакторингу |
|---|---|---|
| Дублювання спанів | Ручний HTTP-спан обгортає автогенерований серверний HTTP-спан з тим самим ім’ям | Залиште автоспан і додайте атрибути чи події |
| Відсутній бізнес-контекст | Трейс показує виклики бази даних і HTTP, але не точки рішень оформлення замовлення | Додайте ручні внутрішні спани для доменних кроків |
| Перевантаження атрибутами | Кожен спан несе ID користувача, email, ID кошика та тіло запиту | Винесіть чутливі чи високої кардинальності дані з атрибутів спану |
| Порушена кореляція | Логи мають ID запитів, але не ID трейсів | Налаштуйте міст логів і контекст поточного спану |
| Прив’язка до бекенду | Застосунок імпортує вендорські API трасування напряму | Використовуйте API OTel та експортери SDK |
Частина 7: Проопрацьовані приклади від вхідних даних до розв’язку
Розділ «Частина 7: Проопрацьовані приклади від вхідних даних до розв’язку»Довідкові фрагменти, що просто лежать на сторінці, самі по собі не вчать тому, як саме перейти від конкретної проблеми до робочого розв’язку, тож цей розділ натомість послідовно використовує патерн «від вхідних даних до розв’язку». Кожен приклад починається з реальної операційної проблеми, показує неінструментований чи неповний початковий вхід, формулює бажану телеметрію, а вже потім крок за кроком проводить вас через готовий розв’язок. А вже після кожного проопрацьованого прикладу ви отримаєте подібне завдання у форматі «тепер ваша черга» — у практичній вправі наприкінці модуля.
7.1 Проопрацьований приклад A: перетворіть неспостережувану функцію оформлення замовлення на трейс
Розділ «7.1 Проопрацьований приклад A: перетворіть неспостережувану функцію оформлення замовлення на трейс»Початкова проблема навмисно мала. Функція оформлення замовлення валідує кошик, викликає функцію платежу, записує замовлення та повертає ID замовлення. Коли вона зазнає збою в продакшні, логи можуть показати виняток, але немає структури трейсу, що показувала б, який крок зазнав збою чи скільки часу зайняв кожен крок. Мета — створити один батьківський спан для операції оформлення замовлення та дочірні спани для значущих бізнес-кроків, не інструментуючи кожен допоміжний рядок.
Вхідний файл: checkout_plain.py
import randomimport time
def validate_cart(cart_id: str) -> None: time.sleep(0.03) if not cart_id: raise ValueError("cart_id is required")
def charge_payment(cart_id: str) -> str: time.sleep(0.05) if random.random() < 0.20: raise RuntimeError("payment gateway timeout") return "PAY-1001"
def record_order(cart_id: str, payment_id: str) -> str: time.sleep(0.02) return f"ORD-{cart_id}-{payment_id}"
def checkout(cart_id: str) -> str: validate_cart(cart_id) payment_id = charge_payment(cart_id) return record_order(cart_id, payment_id)
if __name__ == "__main__": print(checkout("CART-123"))Перше рішення проєктування — де має бути кореневий спан.
Функція оформлення замовлення є операцією, про яку дбає бізнес, тож вона має бути батьківським спаном.
Кроки валідації та платежу достатньо значущі, щоб стати дочірніми спанами, бо збої там ведуть до різних операційних дій.
Допоміжна функція record_order коротка в цьому прикладі, але в багатьох реальних сервісах вона все одно перетинає межу збереження, тож дочірній спан є розумним, якщо він представляє запис у базу даних.
| Вибір проєктування | Рішення | Причина |
|---|---|---|
| Ім’я батьківського спану | checkout | Описує бізнес-операцію |
| Вид спану валідації | INTERNAL | Робота залишається всередині процесу |
| Вид спану платежу | CLIENT | Представляє вихідну залежність |
| Вид спану запису замовлення | CLIENT у реальному шляху збереження | Записи в базу даних є викликами залежностей |
| Обробка ID кошика | Атрибут лише в демо | У продакшні оцініть чутливість і кардинальність |
Розв’язок починається зі створення одного TracerProvider під час старту процесу.
Ресурс іде на провайдер, бо кожен спан від цього процесу належить тому самому сервісу.
Пакетний процесор використовується, навіть попри те, що експортер — консольний, бо ті, хто навчається, мають практикувати продакшн-форму рано.
Нарешті, обробка винятків записує подію винятку, встановлює статус помилки та повторно викидає виняток, щоб інструментування не проковтувало реальні збої.
Файл розв’язку: checkout_traced.py
import randomimport time
from opentelemetry import tracefrom opentelemetry.sdk.resources import Resourcefrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporterfrom opentelemetry.trace import SpanKind, Status, StatusCode
resource = Resource.create( { "service.name": "checkout-service", "service.version": "1.0.0", "deployment.environment": "local", })
trace_provider = TracerProvider(resource=resource)trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))trace.set_tracer_provider(trace_provider)
tracer = trace.get_tracer("kubedojo.checkout", "0.1.0")
def validate_cart(cart_id: str) -> None: with tracer.start_as_current_span("validate-cart", kind=SpanKind.INTERNAL) as span: span.set_attribute("cart.present", bool(cart_id)) time.sleep(0.03) if not cart_id: error = ValueError("cart_id is required") span.record_exception(error) span.set_status(Status(StatusCode.ERROR, str(error))) raise error
def charge_payment(cart_id: str) -> str: with tracer.start_as_current_span("charge-payment", kind=SpanKind.CLIENT) as span: span.set_attribute("payment.provider", "demo-gateway") span.set_attribute("cart.id", cart_id) time.sleep(0.05) if random.random() < 0.20: error = RuntimeError("payment gateway timeout") span.record_exception(error) span.set_status(Status(StatusCode.ERROR, str(error))) raise error span.set_status(Status(StatusCode.OK)) return "PAY-1001"
def record_order(cart_id: str, payment_id: str) -> str: with tracer.start_as_current_span("record-order", kind=SpanKind.CLIENT) as span: span.set_attribute("db.system", "postgresql") span.set_attribute("payment.id", payment_id) time.sleep(0.02) order_id = f"ORD-{cart_id}-{payment_id}" span.add_event("order.recorded", {"order.id": order_id}) return order_id
def checkout(cart_id: str) -> str: with tracer.start_as_current_span("checkout", kind=SpanKind.INTERNAL) as span: span.set_attribute("cart.id", cart_id) try: validate_cart(cart_id) payment_id = charge_payment(cart_id) order_id = record_order(cart_id, payment_id) span.add_event("checkout.completed", {"order.id": order_id}) return order_id except Exception as error: span.record_exception(error) span.set_status(Status(StatusCode.ERROR, str(error))) raise
if __name__ == "__main__": try: print(checkout("CART-123")) finally: trace_provider.shutdown()Запустіть розв’язок у чистому віртуальному середовищі, щоб команда відповідала локальній конвенції Python цього репозиторію.
Консольний експортер друкує записи спанів після завершення роботи (shutdown), тож блок finally є частиною прикладу, а не необов’язковою деталлю прибирання.
У довготривалому сервісі shutdown відбувся б під час завершення процесу, а не після одного виклику функції.
Для короткого скрипта неспроможність скинути дані перед виходом може змусити тих, хто навчається, подумати, що інструментування зазнало збою.
.venv/bin/python -m pip install opentelemetry-api opentelemetry-sdk.venv/bin/python checkout_traced.pyКоли ви інспектуєте вивід, шукайте той самий trace ID у батьківському та дочірніх спанах.
Спан checkout має бути батьком, а спани валідації, платежу та запису замовлення мають з’явитися як нащадки.
Якщо станеться симульований збій платежу, і спан платежу, і батьківський спан оформлення замовлення мають показати статус помилки.
Ця пропагація статусу збою — це вибір дизайну: залежність зазнала збою, а отже, операція оформлення замовлення зазнала збою.
7.2 Проопрацьований приклад B: додайте метрики, не перетворюючи атрибути на проблему вартості
Розділ «7.2 Проопрацьований приклад B: додайте метрики, не перетворюючи атрибути на проблему вартості»Трасована функція повідомляє, який запит зазнав збою, але не відповідає на агреговані питання на кшталт «скільки спроб оформлення замовлення зазнають збою?» чи «скільки часу зазвичай триває оформлення замовлення?». Саме тут належать метрики. Мета — додати лічильник для спроб, лічильник для збоїв, гістограму для тривалості та спостережуваний ґейдж для глибини черги. Важливе обмеження — обрати обмежені атрибути, бо атрибути метрик визначають кардинальність часових рядів.
Вхідні дані для проєктування метрик
| Операційне питання | Потрібна метрика | Інструмент | План атрибутів |
|---|---|---|---|
| Скільки спроб оформлення замовлення відбулося? | checkout.attempts | Counter | Лише checkout.channel |
| Скільки зазнало збою? | checkout.failures | Counter | error.type з обмеженими іменами винятків |
| Скільки тривало оформлення замовлення? | checkout.duration | Histogram | Лише checkout.channel |
| Наскільки глибока зараз черга? | checkout.queue.depth | Observable Gauge | Лише queue.name |
Спокусливий, але поганий дизайн метрик прикріплював би cart.id чи user.id до кожної метрики.
Це може відчуватися корисним під час одного розслідування, але створює багато часових рядів і погіршує агреговані дашборди.
Атрибути трейсів іноді можуть нести ідентифікатор рівня запиту, коли це виправдано, а логи можуть зберігати докладніші дані запиту.
Метрики зазвичай мають використовувати атрибути, які обмежені, стабільні та корисні для групування.
Файл розв’язку: checkout_traces_metrics.py
import randomimport timefrom typing import Iterable
from opentelemetry import metrics, tracefrom opentelemetry.metrics import Observationfrom opentelemetry.sdk.metrics import MeterProviderfrom opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReaderfrom opentelemetry.sdk.resources import Resourcefrom opentelemetry.sdk.trace import TracerProviderfrom opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporterfrom opentelemetry.trace import SpanKind, Status, StatusCode
resource = Resource.create( { "service.name": "checkout-service", "service.version": "1.0.0", "deployment.environment": "local", })
trace_provider = TracerProvider(resource=resource)trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))trace.set_tracer_provider(trace_provider)
metric_reader = PeriodicExportingMetricReader( ConsoleMetricExporter(), export_interval_millis=1000,)meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])metrics.set_meter_provider(meter_provider)
tracer = trace.get_tracer("kubedojo.checkout", "0.1.0")meter = metrics.get_meter("kubedojo.checkout", "0.1.0")
checkout_attempts = meter.create_counter( "checkout.attempts", unit="1", description="Total checkout attempts",)checkout_failures = meter.create_counter( "checkout.failures", unit="1", description="Total failed checkout attempts",)checkout_duration = meter.create_histogram( "checkout.duration", unit="ms", description="Checkout processing duration",)
def current_queue_depth() -> int: return 3
def observe_queue_depth(options) -> Iterable[Observation]: return [Observation(current_queue_depth(), {"queue.name": "checkout"})]
meter.create_observable_gauge( "checkout.queue.depth", callbacks=[observe_queue_depth], unit="1", description="Current checkout queue depth",)
def charge_payment() -> str: with tracer.start_as_current_span("charge-payment", kind=SpanKind.CLIENT) as span: span.set_attribute("payment.provider", "demo-gateway") time.sleep(0.05) if random.random() < 0.20: error = RuntimeError("payment gateway timeout") span.record_exception(error) span.set_status(Status(StatusCode.ERROR, str(error))) raise error return "PAY-1001"
def checkout(cart_id: str, channel: str) -> str: start = time.perf_counter() checkout_attempts.add(1, {"checkout.channel": channel})
with tracer.start_as_current_span("checkout", kind=SpanKind.INTERNAL) as span: span.set_attribute("cart.id", cart_id) span.set_attribute("checkout.channel", channel) try: payment_id = charge_payment() order_id = f"ORD-{cart_id}-{payment_id}" span.add_event("checkout.completed", {"order.id": order_id}) return order_id except Exception as error: checkout_failures.add(1, {"error.type": type(error).__name__}) span.record_exception(error) span.set_status(Status(StatusCode.ERROR, str(error))) raise finally: elapsed_ms = (time.perf_counter() - start) * 1000 checkout_duration.record(elapsed_ms, {"checkout.channel": channel})
if __name__ == "__main__": try: for index in range(5): try: print(checkout(f"CART-{index}", "web")) except RuntimeError as error: print(f"checkout failed: {error}") time.sleep(0.20) time.sleep(1.50) finally: meter_provider.shutdown() trace_provider.shutdown()Приклад з метриками додає ще одну ментальну модель: метрики записують агреговані факти, навіть коли трейси семплюються. Якщо продакшн-семплер зберігає лише частку трейсів, лічильники та гістограми все одно можуть представляти все навантаження. Саме тому трейси та метрики є взаємодоповнювальними, а не надлишковими. Під час інциденту метрики зазвичай повідомляють вам, що щось не так, а трейси допомагають інспектувати репрезентативні приклади.
.venv/bin/python checkout_traces_metrics.pyПісля запуску файлу інспектуйте консольний вивід на наявність імен метрик і ресурсних атрибутів.
Ви маєте побачити лічильники та гістограми, пов’язані з service.name=checkout-service.
Ви не маєте побачити cart.id, прикріплений до потоків метрик, бо приклад навмисно тримає цю специфічну для запиту деталь на трейсі.
Ця межа — звичка інструментування старшого рівня: розміщуйте докази запиту високої кардинальності там, де вони допомагають діагностиці, а метрики тримайте придатними до агрегації.
7.3 Проопрацьований приклад C: продовжіть трейс через HTTP-межу
Розділ «7.3 Проопрацьований приклад C: продовжіть трейс через HTTP-межу»Третій проопрацьований приклад зосереджується на пропагації, а не на локальних спанах. Уявіть, що сервіс-шлюз отримує вхідний запит і викликає сервіс платежу. Шлюз може створити клієнтський спан, але сервіс платежу не приєднається до трейсу, якщо шлюз не інжектує контекст, а сервіс платежу його не витягне. Порушена конфігурація пропагатора створює два правдоподібні на вигляд трейси, які не вдається з’єднати.
Проєктування пропагації
| Межа | Відповідальність відправника | Відповідальність отримувача | Симптом збою |
|---|---|---|---|
| Шлюз до платежу через HTTP | Інжектувати контекст у заголовки запиту | Витягнути контекст перед стартом серверного спану | Трейс платежу з’являється окремо |
| Продюсер до черги | Зберегти контекст у метаданих повідомлення | Витягнути чи зв’язати контекст під час споживання | Робота консумера втрачає причину вище за течією |
| Сервіс до фонового потоку | Перенести контекст у виконання завдання | Стартувати спан із перенесеним контекстом | Дочірня робота з’являється як новий корінь |
Наступний приклад на Go достатньо повний, щоб показати потрібні імпорти та налаштування SDK, але навмисно тримає HTTP-сервер маленьким. Ключові рядки — це конфігурація пропагатора, витягнення з вхідних заголовків та інжекція у вихідні заголовки. Без цих рядків спани все одно можуть існувати, але неперервність трейсу між сервісами буде порушена. Це різниця між локальним інструментуванням і розподіленим трасуванням.
package main
import ( "context" "fmt" "log" "net/http" "time"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" stdouttrace "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/trace")
func initTracer() (*sdktrace.TracerProvider, error) { exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) if err != nil { return nil, err }
res, err := resource.New( context.Background(), resource.WithAttributes(attribute.String("service.name", "gateway-service")), ) if err != nil { return nil, err }
provider := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), )
otel.SetTracerProvider(provider) otel.SetTextMapPropagator( propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, ), )
return provider, nil}
func gatewayHandler(w http.ResponseWriter, r *http.Request) { ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) tracer := otel.Tracer("kubedojo.gateway")
ctx, span := tracer.Start(ctx, "POST /checkout", trace.WithSpanKind(trace.SpanKindServer)) defer span.End()
// Client span FIRST — its context is what must be propagated downstream. ctx, clientSpan := tracer.Start(ctx, "POST payment-service", trace.WithSpanKind(trace.SpanKindClient)) defer clientSpan.End()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://127.0.0.1:8081/pay", nil) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) // resp, _ := http.DefaultClient.Do(req) // would actually send time.Sleep(25 * time.Millisecond) fmt.Fprintln(w, "checkout accepted")}
func main() { provider, err := initTracer() if err != nil { log.Fatal(err) } defer func() { _ = provider.Shutdown(context.Background()) }()
http.HandleFunc("/checkout", gatewayHandler) log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))}Цей приклад також показує, чому вид спану має значення під час пропагації. Спан обробника шлюзу є серверним спаном, бо він отримує запит. Вихідний спан платежу є клієнтським спаном, бо він викликає інший сервіс. Сервіс платежу, якщо реалізований окремо, має витягнути заголовки та створити власний серверний спан із витягнутим контекстом, що змусить дерево трейсу показати обидві сторони межі.
Патерни та антипатерни
Розділ «Патерни та антипатерни»Найбезпечніші дизайни OpenTelemetry SDK чітко розділяють три аспекти, які так часто бувають переплутані в ранніх, поспішних зусиллях з інструментування: код застосунку описує лише значущу бізнес-роботу, конвеєр SDK контролює поведінку збору даних, а конфігурація розгортання вже обирає кінцеві призначення для експорту. Коли ці три аспекти залишаються по-справжньому окремими, сервіс може спокійно зберігати стабільні бізнес-спани, тоді як платформа вільно змінює колектори, співвідношення семплювання чи навіть вендорів бекенду — і все це суто через конфігурацію середовища. Коли ж ці аспекти змішані докупи, кожна міграція бекенду перетворюється на повноцінний реліз застосунку, кожен локальний вибір, зроблений заради діагностики, ризикує непомітно витекти в продакшн, а кожен окремий сервіс зрештою винаходить трохи інший, несумісний діалект телеметрії.
| Патерн | Коли його використовувати | Чому він працює | Міркування щодо масштабування |
|---|---|---|---|
| Ідентичність ресурсу, що належить провайдеру | Кожен сервіс, джоб, воркер і CLI, що генерує телеметрію | Один ресурс прикріплює спільну ідентичність до трейсів, метрик і логів | Стандартизуйте service.name, service.version та deployment.environment через навантаження Kubernetes 1.35+ |
| Пакетні трейси, обмежені метрики | Продакшн-шляхи з реальним обсягом запитів | Робота з експорту переходить із шляху запиту, а метрики залишаються придатними до агрегації | Налаштуйте розмір черги, інтервал експорту та атрибути метрик перед додаванням трафіку |
| Шарування авто плюс ручне | Сервіси з підтримуваними фреймворками та важливими бізнес-кроками | Бібліотеки захоплюють технічні межі, а ручні спани захоплюють доменний намір | Перевіряйте трейси на дублювання спанів після увімкнення автоінструментування |
| Політика експорту, що належить середовищу | Кілька середовищ, колекторів чи бекендів | Застосунки уникають жорстко закодованих ендпоінтів і протоколів | Документуйте старшинство між загальними та специфічними для сигналу OTLP-змінними |
Патерн ідентичності ресурсу, що належить провайдеру, виглядає буденним, але усуває напрочуд багато операційного тертя.
Якщо кожен сервіс задає service.name по-різному, дашборди фрагментуються, навіть коли спани валідні.
Якщо розгортання записує інформацію про версію як атрибут спану замість ресурсу, метрики й логи можуть не нести ту саму ідентичність релізу, що й трейси.
Хороший огляд запитує, чи зміг би хтось відфільтрувати кожен сигнал від того самого Поду, викочування й сервісу, не знаючи, який мовний SDK його згенерував.
Патерн пакетних трейсів і обмежених метрик не дає спостережуваності конкурувати з навантаженням, яке вона має пояснювати. Експорт трейсів може терпіти асинхронну буферизацію, бо запит уже завершився, коли спан закінчується, тоді як метрики потребують дисциплінованих атрибутів, бо кожна нова комбінація міток стає новим часовим рядом. Саме тому пакетний процесор і огляд атрибутів часто належать до того самого пул-реквесту. Один захищає затримку, а інший захищає бекенд від проблеми кардинальності, що з’являється лише після прибуття реального трафіку.
Патерн «авто плюс ручне» — це той, до якого більшість команд зрештою сходиться.
Автоінструментування чудово показує вхідні HTTP-запити, вихідні HTTP-виклики, запити до баз даних і операції месиджингу, але не може назвати бізнес-рішення, що має значення під час інциденту.
Ручне інструментування має додавати відсутній бізнес-шар, не замінюючи шар бібліотеки.
Якщо трейс уже містить серверний спан фреймворку з ім’ям POST /checkout, ручний спан із тим самим ім’ям — це шум; ручний спан з ім’ям fraud-decision чи reserve-inventory — це доказ.
| Антипатерн | Що йде не так | Чому команди потрапляють у це | Краща альтернатива |
|---|---|---|---|
| Експортер у бізнес-логіці | Зміни бекенду потребують редагування коду та перерозгортань | Перше демо жорстко кодує консольні чи вендорські експортери | Тримайте вибір експортера в налаштуванні SDK та змінних середовища |
| Атрибути метрик, скопійовані з логів | Кількість рядів зростає з користувачами, кошиками, замовленнями чи ID запитів | Інженери хочуть діагностики на рівні запиту з агрегованих даних | Використовуйте трейси й логи для доказів запиту, метрики — для обмеженого групування |
| Багаж як прихований мішок контексту | Заголовки несуть чутливі чи високої кардинальності значення нижче за течією | Багаж відчувається зручним розподіленим сховищем | Дозволяйте лише документовані, нечутливі, низької кардинальності ключі багажу |
| Ручні спани навколо кожного помічника | Трейси стають довгими, дорогими й важкими для читання | Більше спанів відчувається як більше спостережуваності | Інструментуйте значущі операції та межі залежностей |
Ці антипатерни поділяють спільну помилку: вони оптимізують для першої людини, яка пише інструментування, замість наступної людини, яка з ним діагностує. Перша людина може хотіти швидкий експортер, кожну локальну змінну як атрибут чи спан навколо кожної функції, щоб довести, що SDK працює. Наступній людині потрібен трейс, що розповідає зв’язну історію, метрика, що агрегується через тисячі запитів, і лог, що корелюється без розкриття секретів. Тому хороший дизайн SDK — це дисципліна, орієнтована на читача.
Фреймворк ухвалення рішень
Розділ «Фреймворк ухвалення рішень»Використовуйте цей фреймворк щоразу, коли переглядаєте чергову зміну інструментування, відповідаєте на сценарне питання OTCA чи рефакторите довідковий фрагмент у придатний для реального запуску патерн сервісу. Завжди починайте з питання про сам сигнал, а вже потім поступово рухайтеся назовні — до поведінки конвеєра та до контролю на рівні розгортання. Саме такий порядок дій запобігає дуже поширеній помилці, коли команда починає сперечатися про вибір експортерів ще перед тим, як вирішити, що взагалі має означати ця телеметрія.
┌────────────────────────────────────────────────────────────────────────────┐│ SDK Design Decision Flow ││ ││ 1. What operational question must be answered? ││ │ ││ ▼ ││ 2. Is the evidence per operation, aggregate, or log-like narrative? ││ │ ││ ├── Per operation ─────▶ trace span, event, status, or link ││ ├── Aggregate ────────▶ counter, histogram, observable instrument ││ └── Narrative ────────▶ log bridge with current trace context ││ │ ││ ▼ ││ 3. Which attributes are safe, bounded, and semantically consistent? ││ │ ││ ▼ ││ 4. Which SDK component controls delivery: processor, reader, exporter? ││ │ ││ ▼ ││ 5. Which settings belong in code, and which belong in environment config? │└────────────────────────────────────────────────────────────────────────────┘Перша гілка розділяє трейси, метрики та логи за роботою, яку вони виконують. Якщо питання — «який крок зазнав збою для цього запиту оформлення замовлення?», то спан, подія, статус чи зв’язаний контекст трейсу є правильною формою, бо доказ належить одній операції. Якщо питання — «чи зростають збої для навантаження оформлення замовлення?», то лічильник чи гістограма є правильною формою, бо доказ має агрегуватися через багато операцій. Якщо питання — «яке повідомлення записав застосунок, поки цей спан був активним?», то міст логів із кореляцією трейсу є правильною формою, бо доказ є наративним і з мітками часу.
| Точка рішення | Оберіть це | Коли сценарій каже | За чим стежити |
|---|---|---|---|
| Процесор спанів | BatchSpanProcessor | Продакшн-сервіс, чутливий до затримки шлях, OTLP-експорт | Скидання при завершенні та поведінка переповнення черги |
| Процесор спанів | SimpleSpanProcessor | Локальне демо, юніт-тест, одноразовий скрипт із консольним виводом | Не переносьте це у високонавантажені шляхи запитів |
| Метричний інструмент | Counter | Кількість лише зростає, як-от спроби чи помилки | Уникайте значень поточного стану, як-от глибина черги |
| Метричний інструмент | Histogram | Розподіл має значення, особливо затримка чи розмір | Оберіть одиниці та атрибути, що зберігають сенс |
| Проєктування пропагації | Батько-нащадок | Одна операція безпосередньо спричиняє наступну операцію | Той самий trace ID і правильний вид спану через межу |
| Проєктування пропагації | Зв’язки | Пакет, збирання, повторна спроба чи асинхронна робота пов’язана з кількома причинами | Не нав’язуйте одного довільного батька |
| Розташування конфігурації | Змінні середовища | Ендпоінт, протокол, семплер, ім’я сервісу різняться за розгортанням | Специфічні для сигналу змінні можуть перекривати загальні |
| Розташування конфігурації | Код | Бізнес-спани, імена метрик і стандартні значення ресурсу є частиною сервісу | Уникайте жорсткого кодування ендпоінтів бекенду |
Зробіть паузу й передбачте: якщо ви перенесете OTEL_EXPORTER_OTLP_ENDPOINT із маніфесту розгортання в код застосунку, що станеться, коли колектор стейджингу змінить імена хостів, але образ сервісу вже зібрано?
Сервіс тепер потребує нового збирання чи специфічного для коду перекриття, навіть попри те, що призначення телеметрії є питанням розгортання.
Це уникна прив’язка між вихідним кодом і маршрутизацією платформи.
Тримання конфігурації експорту поза бізнес-логікою дозволяє викочуванню Kubernetes змінювати топологію колектора, не змінюючи інструментування, що описує поведінку оформлення замовлення.
Той самий фреймворк допомагає вам рефакторити довідкові фрагменти. Фрагмент, який лише створює трейсер і друкує спан, ще не є продакшн-патерном, бо йому бракує ідентичності ресурсу, поведінки завершення, статусу помилки та шляху експорту, що належить розгортанню. Щоб перетворити його на придатний для повторного використання патерн, запитайте, який провайдер володіє сигналом, який процесор чи рідер контролює доставку, які атрибути безпечно запитувати та яка фінальна перевірка доводить, що сигнал відповідає на початкове операційне питання. Ця звичка рефакторингу — те, що робить знання SDK корисним поза екзаменаційними картками.
Чи знали ви?
Розділ «Чи знали ви?»- OpenTelemetry навмисно розділяє API та SDK. Бібліотеки можуть залежати від API для створення телеметрії, не змушуючи застосунки використовувати конкретний експортер, процесор, семплер чи бекенд.
- Колектор необов’язковий з погляду SDK. SDK може експортувати напряму до бекенду, але продакшн-команди часто використовують Колектор для маршрутизації, пакування, фільтрації та гнучкості міграції бекенду.
- Семантичні домовленості — це операційні контракти. Невелика невідповідність іменування, як-от використання старого HTTP-атрибута, може зламати дашборди, навіть коли спани технічно експортуються.
- Автоінструментування — це відправна точка, а не повна стратегія спостережуваності. Воно захоплює поширені межі бібліотек, тоді як ручне інструментування захоплює бізнес-сенс і докази, специфічні для інциденту.
Типові помилки
Розділ «Типові помилки»| Помилка | Чому це стається | Як це виправити |
|---|---|---|
Використання SimpleSpanProcessor у продакшн-сервісах | Експорт відбувається синхронно й може додати затримку бекенду чи консолі до обробки запитів | Використовуйте BatchSpanProcessor і скидайте дані під час завершення |
Задання service.name як атрибута спану | Ідентичність сервісу належить ресурсу й має застосовуватися до всієї згенерованої телеметрії | Задайте service.name на ресурсі провайдера чи через OTEL_SERVICE_NAME |
Маркування вихідних викликів бази даних чи HTTP як INTERNAL | Карти залежностей і читачі трейсів втрачають сигнал комунікаційної ролі | Використовуйте CLIENT для вихідних викликів залежностей і SERVER для вхідних обробників |
| Запис винятків без задання статусу помилки спану | Подія винятку існує, але спан може не підсумовувати операцію як невдалу | Записуйте виняток і задавайте StatusCode.ERROR, коли операція зазнає збою |
| Розміщення ідентифікаторів користувачів, токенів чи email у багажі | Багаж пропагується нижче за течією через заголовки чи метадані й може витекти чутливі дані | Використовуйте обмежений, нечутливий контекст чи тримайте деталі в захищених логах |
| Додавання ID запитів високої кардинальності до атрибутів метрик | Кожне унікальне значення може створити окремий часовий ряд і перевантажити дашборди чи бекенди | Тримайте метрики придатними до агрегації, а специфічні для запиту докази — у трейсах чи логах |
| Плутання кумулятивної та дельтової темпоральності | Графіки бекенду можуть показувати оманливі швидкості, скидання чи суми | Узгодьте темпоральність рідера/експортера з очікуваннями бекенду |
| Ручне дублювання автоінструментованих спанів | Трейси стають шумними, вартість зростає, а читачі бачать два спани для тієї самої операції | Тримайте автоспани для технічних меж і додавайте ручні спани для бізнес-кроків |
Тест
Розділ «Тест»Перевірте себе сценарними питаннями у стилі OTCA. Кожне питання просить вас застосувати модель SDK до реалістичної ситуації, а не процитувати визначення.
Q1: Ваша команда додала OpenTelemetry до високонавантаженого API, і затримка p95 зросла після увімкнення консольного експорту трейсів із простим процесором. Що ви зміните першим і чому?
Змініть конвеєр трейсів на використання BatchSpanProcessor і перестаньте трактувати консольний експорт як продакшн-призначення. Простий процесор експортує завершені спани синхронно на шляху запиту, тож затримка експортера стає затримкою застосунку. Пакетний процесор буферизує спани й експортує асинхронно, що зберігає генерацію трейсів, водночас усуваючи більшу частину роботи з експорту з обробки запитів.
Q2: Трейс оформлення замовлення показує вхідний HTTP-спан, але спани сервісу платежу з'являються як окремі кореневі трейси. Обидва сервіси мають встановлений OTel. Що ви інспектуєте далі?
Інспектуйте пропагацію на межі сервісів. Шлюз має інжектувати контекст у вихідні HTTP-заголовки, а сервіс платежу має витягнути контекст перед стартом свого серверного спану. Перевірте, що обидві сторони використовують сумісні пропагатори, як-от W3C TraceContext, а потім переконайтеся, що той самий trace ID з’являється з обох боків запиту.
Q3: Дашборд затримки оформлення замовлення марний, бо кожен ряд метрики включає `cart.id`. Яку зміну інструментування ви порекомендуєте?
Вилучіть cart.id з атрибутів метрик і тримайте виміри метрики обмеженими, як-от checkout.channel чи order.type, якщо ці значення мають контрольовані набори. Специфічні для запиту ідентифікатори можуть належати атрибутам трейсів чи захищеним логам, коли це виправдано. Метрики мають зберігати агреговану поведінку, а ідентифікатори високої кардинальності створюють забагато часових рядів.
Q4: Python-сервіс записує винятки з `span.record_exception(error)`, але пошук трейсів не показує невдалі операції надійно. Чого бракує?
Код також має задавати статус спану як помилку, коли операція зазнає збою, наприклад з Status(StatusCode.ERROR, str(error)). Запис винятку додає подію з міткою часу, але статус підсумовує результат спану. Читач трейсів чи оповіщення часто покладаються на поле статусу, щоб швидко ідентифікувати невдалі спани.
Q5: Пакетний воркер обробляє повідомлення, що надійшли з кількох різних запитів оформлення замовлення. Той, хто навчається, хоче обрати одне вхідне повідомлення як батьківський спан. Як ви оцінили б цей дизайн?
Вибір одного повідомлення як батька спотворює відношення збирання (fan-in), бо робота пакета пов’язана з кількома ранішими трейсами. Кращий дизайн — створити спан консумера чи обробки пакета й додати зв’язки до контекстів, що несуть повідомлення. Зв’язки «батько-нащадок» найкращі для прямих причинних ланцюгів, тоді як зв’язки (links) зберігають відношення через пакетування та збирання.
Q6: Автоінструментування дає сервісу HTTP- та спани бази даних, але ті, хто реагує на інциденти, досі не можуть сказати, чи збої стаються під час перевірок шахрайства чи резервування запасів. Що команда має додати?
Вони мають додати ручне інструментування навколо значущих бізнес-кроків, як-от fraud-check та reserve-inventory, із ретельно обраними атрибутами та подіями. Автоінструментування захоплює межі бібліотек, але не може вивести доменний намір. Ручні спани мають доповнювати автоматичні спани, а не дублювати HTTP- чи спани бази даних, уже створені бібліотекою інструментування.
Q7: Сервіс надсилає кумулятивні лічильники до бекенду, що очікує дельтові значення, і графіки швидкості запитів виглядають неправильно після кожного перезапуску. Яку частину SDK/шляху експорту слід дослідити?
Дослідіть рідер метрик, експортер, конвертацію колектора та очікування темпоральності бекенду. Кумулятивні значення представляють суми від старту чи скидання, тоді як дельтові значення представляють зміни від попереднього збору. Якщо бекенд інтерпретує одне як інше, швидкості та скидання можуть виглядати оманливо, навіть попри те, що застосунок збільшує лічильник коректно.
Q8: Колега скопіював довідковий фрагмент SDK у сервіс: він створює один спан, жорстко кодує OTLP-ендпоінт, пропускає ресурсні атрибути й ніколи не завершує провайдер. Розгортання також задає `OTEL_EXPORTER_OTLP_ENDPOINT` та `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. Як би ви рефакторили фрагмент і обробили старшинство ендпоінтів?
Почніть із запису операційного входу: який запит, залежність чи бізнес-крок має пояснювати телеметрія. Потім створіть провайдер із ресурсними атрибутами, використайте процесор чи рідер продакшн-форми, перенесіть вибір ендпоінта й протоколу в конфігурацію середовища та додайте поведінку завершення чи скидання, щоб короткоживучі процеси експортували дані. Для OTLP-трейсів специфічний для трейсів ендпоінт має перекривати загальний OTLP-ендпоінт, тоді як інші сигнали можуть тримати загальний ендпоінт, якщо вони також не мають специфічних для сигналу перекриттів. Нарешті, додайте крок валідації, що інспектує ідентичність трейсу, види спанів, імена метрик, обмежені атрибути та розв’язане призначення експортера, бо рефакторинг не є завершеним, доки вивід не доведе, що відповідає на початкове питання діагностики.
Практична вправа: побудуйте та діагностуйте інструментований скрипт оформлення замовлення
Розділ «Практична вправа: побудуйте та діагностуйте інструментований скрипт оформлення замовлення»Мета: перетворити невеликий скрипт оформлення замовлення на повний приклад OpenTelemetry SDK, що генерує корисні трейси та метрики, а потім діагностувати вивід так, ніби ви переглядаєте інструментування колеги.
Сценарій
Розділ «Сценарій»У вашої команди є джоб оформлення замовлення, що інколи зазнає збою, коли платіжний шлюз вичерпує час очікування. Поточний скрипт друкує успіх чи невдачу, але не показує, де витрачається час, який крок зазнав збою чи чи зростають збої. Вам потрібно додати трасування та метрики так, щоб це досі мало сенс, якби скрипт став довготривалим сервісом. Використовуйте консольні експортери для вправи, щоб ви могли інспектувати вивід локально.
Налаштування
Розділ «Налаштування»Створіть чи повторно використайте віртуальне середовище репозиторію, потім встановіть пакети, потрібні для консольного експорту трейсів і метрик.
.venv/bin/python -m pip install opentelemetry-api opentelemetry-sdkСтартовий файл
Розділ «Стартовий файл»Створіть otel_checkout_exercise.py із цим неінструментованим входом.
import randomimport time
def validate_cart(cart_id: str) -> None: time.sleep(0.02) if not cart_id: raise ValueError("cart_id is required")
def charge_payment() -> str: time.sleep(0.04) if random.random() < 0.25: raise RuntimeError("payment gateway timeout") return "PAY-1001"
def reserve_inventory() -> None: time.sleep(0.03)
def checkout(cart_id: str, channel: str) -> str: validate_cart(cart_id) payment_id = charge_payment() reserve_inventory() return f"ORD-{cart_id}-{payment_id}"
if __name__ == "__main__": for index in range(6): try: print(checkout(f"CART-{index}", "web")) except RuntimeError as error: print(f"checkout failed: {error}")Завдання
Розділ «Завдання»- Додайте
Resourceзservice.name=checkout-exercise,service.version=0.1.0таdeployment.environment=local. - Налаштуйте один
TracerProviderпід час старту зBatchSpanProcessorтаConsoleSpanExporter. - Налаштуйте один
MeterProviderпід час старту зPeriodicExportingMetricReaderтаConsoleMetricExporter. - Створіть батьківський спан з ім’ям
checkoutдля бізнес-операції та дочірні спани з іменамиvalidate-cart,charge-paymentтаreserve-inventory. - Використовуйте
INTERNALдля валідації та резервування запасів, якщо ваша реалізація не симулює реальний виклик залежності. - Використовуйте
CLIENTдля платежу, бо платіжний шлюз представляє вихідну залежність. - Додайте лічильник з ім’ям
checkout.attemptsз обмеженим атрибутом, як-отcheckout.channel. - Додайте лічильник з ім’ям
checkout.failuresз обмеженим атрибутом, як-отerror.type. - Додайте гістограму з ім’ям
checkout.durationз одиницеюmsі запишіть час оформлення замовлення, що минув. - Коли стається виняток, запишіть його на активному спані, задайте статус помилки, оновіть лічильник збоїв і повторно викиньте чи обробіть його навмисно.
- Скиньте обидва провайдери перед виходом зі скрипта, щоб консольний вивід був видимим.
- Перегляньте вивід і запишіть, яке поле доводить неперервність трейсу «батько-нащадок».
Критерії успіху
Розділ «Критерії успіху»- Скрипт запускається з
.venv/bin/python otel_checkout_exercise.pyі завершується без помилок імпорту. - Консольний вивід трейсів показує батьківський спан
checkoutі дочірні спани для валідації, платежу та резервування запасів. - Збої платежу показують подію винятку та статус помилки на спані платежу.
- Батьківський спан оформлення замовлення також відображає збій, коли оформлення замовлення не може завершитися.
- Усі спани включають ресурсний атрибут
service.name=checkout-exercise. - Вивід метрик включає
checkout.attempts,checkout.failuresтаcheckout.duration. - Атрибути метрик не включають
cart.id,user.id, адреси email, токени чи інші значення високої кардинальності або чутливі значення. - Ви можете пояснити, чи описує кожен вид спану внутрішньопроцесну роботу, вхідну роботу, вихідну роботу, роботу продюсера чи роботу консумера.
Підказки для перевірки
Розділ «Підказки для перевірки»Після запуску скрипта інспектуйте один успішний трейс і один невдалий трейс.
Переконайтеся, що дочірні спани поділяють trace ID батька і що їхній parent span ID вказує назад на спан checkout.
Якщо спан з’являється як окремий корінь, перегляньте, де спан було стартовано і чи був збережений поточний контекст.
Для цієї однопроцесної вправи поточний контекст має текти автоматично через вкладені блоки start_as_current_span.
Тепер інспектуйте метрики.
Лічильник спроб має зростати для кожної спроби оформлення замовлення, лічильник збоїв має зростати лише тоді, коли оформлення замовлення зазнає збою, а гістограма тривалості має записувати і успішні, і невдалі спроби.
Якщо гістограма записує лише успіхи, перенесіть запис тривалості в блок finally, щоб невдалі запити були включені.
Це реалістична продакшн-турбота, бо виключення збоїв може змусити найповільніші чи найважливіші запити зникнути з даних про затримку.
Нарешті, перегляньте свої атрибути.
Якщо ви додали cart.id до метрик, вилучіть його та поясніть, чому він належить доказам трейсу, а не агрегованим вимірам метрик.
Якщо ви розмістили service.name на кожному спані вручну, перенесіть його на ресурс і поясніть, чому ідентичність на рівні провайдера є правильною областю.
Якщо ви використали SimpleSpanProcessor, перейдіть на пакетну обробку та поясніть, як експорт на шляху запиту змінює поведінку затримки.
Sources
Розділ «Sources»- OpenTelemetry traces concepts
- OpenTelemetry metrics concepts
- OpenTelemetry logs concepts
- OpenTelemetry context propagation concepts
- OpenTelemetry baggage concepts
- OpenTelemetry SDK environment variable configuration
- OpenTelemetry Protocol exporter specification
- OpenTelemetry semantic conventions
- OpenTelemetry Python instrumentation
- OpenTelemetry Go instrumentation
- W3C Trace Context Recommendation
- OpenTelemetry Kubernetes getting started
- cncf.io: otca — Офіційна сторінка іспиту CNCF OTCA вказує вагу предметної області OpenTelemetry API та SDK на рівні 46%.
- opentelemetry.io: overview — Специфікація огляду OpenTelemetry явно розрізняє пакети API та реалізацію SDK.
- opentelemetry.io: data model — Специфікація моделі даних метрик визначає дельтову та кумулятивну темпоральність і зразки (exemplars) з trace_id та span_id.
- opentelemetry.io: exporters — Офіційна документація з експортерів протиставляє просту та пакетну обробку й явно рекомендує пакування.
- opentelemetry.io: sdk environment variables — Специфікація змінних середовища SDK визначає саме ці змінні пакетного процесора спанів.
- opentelemetry.io: sdk — Специфікація SDK метрик визначає періодичний експортувальний MetricReader як push-орієнтовану реалізацію рідера.
- opentelemetry.io: prometheus — Специфікація експортера Prometheus явно визначає його як pull-метричний експортер, що відповідає на HTTP-запити.
- opentelemetry.io: exceptions — Специфікація винятків показує винятки, записані як події, та парне задання статусу спану ERROR.
- opentelemetry.io: resources — Сторінка концепцій ресурсів прямо стверджує, що ресурси прикріплюються під час створення провайдера, і документує service.name плюс резервне значення unknown_service.
- opentelemetry.io: general — Сторінка загальної конфігурації SDK документує саме ці змінні та правило старшинства OTEL_SERVICE_NAME.
- opentelemetry.io: otlp exporter — Сторінка конфігурації OTLP-експортера документує загальні та специфічні для сигналу змінні ендпоінта/протоколу й типові стандартні значення 4317/4318.
- opentelemetry.io: collector — Вступ до Колектора явно описує його як вендоронезалежний компонент, що може обробляти та експортувати до одного чи кількох бекендів.
- opentelemetry.io: http migration — Посібник з міграції HTTP семантичних домовленостей прямо відображає старіші імена HTTP-атрибутів на поточні стабільні форми.
- opentelemetry.io: agent — Сторінка Java-агента явно описує безкодове Java-інструментування як Java-агент, що динамічно інжектує байт-код.
- opentelemetry.io: python — Безкодова документація Python показує конфігурацію та запуск через команду opentelemetry-instrument.
- opentelemetry.io: configuration — Документація з конфігурації автоматичного інструментування .NET явно документує змінні CLR-профайлера та DOTNET_STARTUP_HOOKS.
- opentelemetry.io: js — Безкодова документація JavaScript дає саме цей патерн хука require на основі NODE_OPTIONS.
Перевірка для тих, хто навчається
Розділ «Перевірка для тих, хто навчається»Спершу клієнтський спан — саме його контекст має передаватися далі за течією. Стартуйте клієнтський спан перед побудовою вихідного запиту, потім робіть Inject з того ctx, щоб батьком сервісу нижче за течією був клієнтський спан, а не серверний.
ParentBased обгортає кореневий семплер, тож дочірній спан успадковує рішення про семплювання батька через межі сервісів; TraceIdRatioBased з OTEL_TRACES_SAMPLER_ARG=0.10 застосовує імовірнісне головне семплювання на коренях.
Наступний модуль
Розділ «Наступний модуль»Наступний модуль: Модуль 2: Архітектура OTel Collector — як приймати, обробляти, маршрутизувати та експортувати телеметрію у масштабі.