Перейти до вмісту

Модуль 1.1: Поглиблений розбір OTel API та SDK

Складність: [СКЛАДНИЙ] — основна предметна область, 46% ваги іспиту OTCA

Час на проходження: 90–120 хвилин

Передумови: базове знайомство з розподіленими системами, потоком HTTP-запитів, викликами між сервісами, а також з Python або Go


Результати навчання

Розділ «Результати навчання»

Після завершення цього модуля ви зможете:

  1. Спроєктувати конвеєр OpenTelemetry SDK, який спрямовує трейси, метрики та логи через правильні провайдери, процесори, рідери та експортери для продакшн-сервісу.
  2. Діагностувати порушену неперервність трейсу, аналізуючи види спанів, зв’язки «батько-нащадок», заголовки W3C TraceContext, пропагатори та використання багажу (baggage).
  3. Реалізувати ручне інструментування, яке додає бізнес-релевантні спани, атрибути, події, винятки, метрики та ресурси, не дублюючи того, що вже надає автоматичне інструментування.
  4. Оцінити компроміси між синхронними та асинхронними метричними інструментами, кумулятивною та дельтовою темпоральністю, консольними та OTLP-експортерами, а також простою та пакетною обробкою.
  5. Рефакторити довідкові фрагменти 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Проміжний компонентПриклади призначення експорту
ТрейсиTracerTracerProviderSpanProcessorКонсоль, OTLP, бекенд трейсів
МетрикиMeterMeterProviderMetricReaderКонсоль, OTLP, скрейпінг Prometheus
ЛогиМіст логераLoggerProviderLogRecordProcessorКонсоль, 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 │ │
│ └──────────────┘ └──────────────────┘ └──────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
Рідер метрикМодель зборуНайкраще пасуєНаслідок для проєктування
PeriodicExportingMetricReaderPush з інтерваломOTLP-експортери та конвеєри колекторівЗастосунок ініціює експорт за розкладом
Рідер PrometheusPull через скрейп-ендпоінтСередовища, рідні для PrometheusPrometheus керує таймінгом скрейпінгу
Рідер ручного зборуЯвний тригер зборуТести та спеціалізовані інтеграціїВикликач має пам’ятати про збір

Логи завершують картину з трьох сигналів, але їх легко зрозуміти неправильно. 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=checkouthttp.request.method=POST
Прикладdeployment.environment=prodhttp.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
Спостереження загального часу CPUObservable 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 │
└────────────────────────────────────────────────────────────────────────────┘
Частина пропагаціїЩо вона несеЧому це важливоПеревірка під час діагностики
traceparentTrace 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 можна налаштовувати двома способами: через код і через змінні середовища. Код корисний тоді, коли застосунок має конструювати провайдери, процесори, інструменти чи ресурси напряму, у самому вихідному коді. Змінні середовища корисні тоді, коли платформеним командам потрібна узгоджена поведінка одразу через багато сервісів, без перезбирання кожного окремого застосунку. Практичне правило тут таке: тримати бізнес-інструментування в коді, а специфічні для розгортання рішення про експорт — у конфігурації, щоразу, коли це взагалі можливо.

Сфера конфігураціїПрограмний прикладПриклад зі змінною середовищаВіддавайте перевагу середовищу, коли
Ідентичність сервісуАтрибути ресурсу в налаштуванні SDKOTEL_SERVICE_NAMEКожне розгортання задає власне ім’я
OTLP-ендпоінтАргумент конструктора експортераOTEL_EXPORTER_OTLP_ENDPOINTЕндпоінт різниться за середовищем
Протокол експортуКонфігурація експортераOTEL_EXPORTER_OTLP_PROTOCOLПолітика фаєрвола чи колектора різниться
Семплювання трейсівОб’єкт семплераOTEL_TRACES_SAMPLERПлатформа володіє політикою семплювання
Увімкнення сигналуНалаштування провайдераOTEL_TRACES_EXPORTERСервіс потребує швидкого вимкнення чи консольної діагностики

Конфігурація, специфічна для сигналу, зазвичай перекриває загальну конфігурацію. Наприклад, платформа може задати загальний OTLP-ендпоінт для всіх сигналів, але надсилати трейси до спеціалізованого колектора трейсів під час міграції. Це старшинство дозволяє командам змінювати один сигнал, не зачіпаючи інші. У сценаріях іспиту шукайте конкретнішу змінну, коли два налаштування здаються конфліктними.

Змінна середовищаПризначенняПриклад значенняОпераційний сенс
OTEL_SERVICE_NAMEЗадає ресурсний атрибут service.namecheckoutГрупує телеметрію за сервісом
OTEL_RESOURCE_ATTRIBUTESДодає ресурсні атрибутиdeployment.environment=prod,service.version=2.4.1Збагачує кожен сигнал від процесу
OTEL_EXPORTER_OTLP_ENDPOINTЗагальний OTLP-ендпоінтhttp://collector:4317Призначення за замовчуванням для OTLP-сигналів
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTOTLP-ендпоінт, специфічний для трейсівhttp://trace-collector:4317Перекриває загальний ендпоінт для трейсів
OTEL_EXPORTER_OTLP_PROTOCOLКодування транспорту OTLPgrpc чи http/protobufМає збігатися з конфігурацією приймача колектора
OTEL_TRACES_SAMPLERСтратегія семплювання трейсівalways_on чи traceidratioКонтролює, які трейси семплюються
OTEL_TRACES_SAMPLER_ARGАргумент семплера0.10Десятивідсоткове семплювання для семплера за співвідношенням

Семплери трейсів і головне семплювання

Розділ «Семплери трейсів і головне семплювання»

Семплювання вирішує, чи трейс записується, перш ніж накопичиться вартість експорту. SDK застосовує семплер під час створення спану для кореневих спанів і для нащадків згідно з правилами семплера. Змінні середовища відображаються на вбудовані типи семплерів; ви також можете налаштувати семплери програмно на TracerProvider.

СемплерЗначення OTEL_TRACES_SAMPLERПоведінкаТипове застосування
AlwaysOnalways_onКожен кореневий трейс семплюєтьсяРозробка, низьконавантажені сервіси, діагностика
AlwaysOffalways_offЖоден новий трейс не семплюєтьсяТести, навмисне вимкнення
TraceIdRatioBasedtraceidratioІмовірнісне головне семплювання за trace ID (напр., OTEL_TRACES_SAMPLER_ARG=0.10 → ~10%)Контроль вартості, поки спани ще незалежні
ParentBased(декоратор, не окреме ім’я змінної)Обгортає кореневий семплер: дочірній спан успадковує рішення про семплювання батька через межі сервісівСтандартний продакшн-патерн

ParentBased — це декоратор, який слід уявляти на межах сервісів. Якщо сервіс вище за течією не семплював трейс, робота нижче за течією не має раптом створювати повне дерево трейсу, якщо ви навмисно не перекриваєте цю політику. Стандарт SDK фактично є ParentBased(TraceIdRatioBased) з аргументом співвідношення, тож виклики між сервісами залишаються узгодженими з прапорцем семплювання батьківського traceparent. TraceIdRatioBased окремо застосовується лише на коренях; ParentBased гарантує, що нащадки поважають рішення вище за течією.

OTLP — це стандартний протокол експорту, який ви маєте очікувати в сучасних дизайнах OTel. Експортер може відправляти напряму до бекенду, але багато продакшн-архітектур спершу відправляють до OpenTelemetry Collector. Колектор може приймати телеметрію, пакувати її, фільтрувати її, збагачувати її та розгалужувати її до одного чи кількох бекендів. Цей модуль зосереджується на стороні SDK, але вибір експорту вже має змусити вас думати про архітектуру колектора в наступному модулі.

Вибір експортераХороше пасуванняКомпромісПитання старшого рівня
Консольний експортерЛокальне навчання та діагностикаНе підходить для продакшн-пропускної здатностіЧи не залишили ми це випадково на гарячому шляху?
OTLP-експортерСтандартний продакшн-шляхПотребує ендпоінта колектора чи бекендуЧи узгоджено налаштовано протокол і ендпоінт?
Шлях рідера/експорту PrometheusСкрейпінг метрик, рідний для PrometheusPull-модель відрізняється від експорту трейсівЧи безпечно цей сервіс відкриває скрейп-ендпоінт?
Застарілий експортер Jaeger чи ZipkinСтаріші середовища під час міграційМенш портативний за OTLPЧи може колектор натомість транслювати OTLP?
No-op експортерТести чи тимчасове вимкненняТелеметрія зникаєЦе навмисно вимкнено чи неправильно налаштовано?

Семантичні домовленості (semantic conventions) — це стандарти іменування, що роблять телеметрію портативною. Вони допомагають дашбордам, правилам оповіщення та запитам працювати між бібліотеками й мовами. Для HTTP-спанів поточні стабільні домовленості використовують імена на кшталт http.request.method та http.response.status_code. Старі імена все ще можуть з’являтися на старіших дашбордах чи прикладах, але сценарії іспиту очікують, що ви розумієте: семантичні домовленості еволюціонують, а узгоджене іменування є частиною портативності.

ДоменРекомендований атрибутПриклад значенняЧим допомагає
HTTP-запитhttp.request.methodPOSTГрупує запити за методом
HTTP-відповідьhttp.response.status_code201Підтримує аналіз частоти помилок
URLurl.fullhttps://api.example.test/checkoutЗберігає повну ціль запиту, коли це безпечно
Сервісservice.namecheckoutІдентифікує сервіс-генератор
Сервісservice.version2.4.1Корелює телеметрію з релізами
База данихdb.systempostgresqlІдентифікує технологію залежності
Месиджингmessaging.systemkafkaІдентифікує бекенд месиджингу

Найважливіша звичка щодо семантичних домовленостей — це узгодженість. Якщо половина флоту використовує http.method, а інша половина — http.request.method, фільтр дашборда може мовчки пропускати дані. Якщо одна команда записує service.name як атрибут спану, а інша задає його як ресурс, вигляди сервісів на основі ресурсу стають неповними. Інструментування — це не лише про створення телеметрії; це про створення телеметрії, яку можна запитувати з упевненістю.

Частина 6: Автоматичне інструментування, ручне інструментування та межа між ними

Розділ «Частина 6: Автоматичне інструментування, ручне інструментування та межа між ними»

Автоматичне інструментування обгортає підтримувані бібліотеки без жодної потреби для розробників застосунків вручну редагувати кожне окреме місце виклику в коді. У Java агент може модифікувати байт-код під час завантаження класу; у Python інструментування зазвичай патчить бібліотеки під час імпорту; у Node.js хуки require можуть обгортати модулі; у .NET профілювання та стартові хуки можуть приєднувати інструментування. Це потужно, бо швидко дає командам базові трейси для HTTP-фреймворків, клієнтів, баз даних, бібліотек месиджингу та gRPC. Цього недостатньо для бізнес-спостережуваності, бо бібліотеки не знають, що означає ваше оформлення замовлення, поновлення, повернення коштів чи рішення про шахрайство.

МоваПоширений механізм автоінструментуванняТипова команда чи налаштуванняЩо це зазвичай захоплює
JavaJava-агент і байт-код інструментуванняjava -javaagent:opentelemetry-javaagent.jar -jar app.jarHTTP, JDBC, gRPC, бібліотеки месиджингу
PythonMonkey-патчинг під час імпортуopentelemetry-instrument .venv/bin/python app.pyFlask, FastAPI, requests, клієнти баз даних
.NETCLR-профайлер і стартові хукиЗмінні середовища та хуки виконанняASP.NET, HTTP-клієнти, бібліотеки баз даних
Node.jsХуки require та пакети інструментуванняnode --require @opentelemetry/auto-instrumentations-node/register app.jsExpress, 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 random
import 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 random
import time
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from 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 відбувся б під час завершення процесу, а не після одного виклику функції. Для короткого скрипта неспроможність скинути дані перед виходом може змусити тих, хто навчається, подумати, що інструментування зазнало збою.

Terminal window
.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.attemptsCounterЛише checkout.channel
Скільки зазнало збою?checkout.failuresCountererror.type з обмеженими іменами винятків
Скільки тривало оформлення замовлення?checkout.durationHistogramЛише checkout.channel
Наскільки глибока зараз черга?checkout.queue.depthObservable GaugeЛише queue.name

Спокусливий, але поганий дизайн метрик прикріплював би cart.id чи user.id до кожної метрики. Це може відчуватися корисним під час одного розслідування, але створює багато часових рядів і погіршує агреговані дашборди. Атрибути трейсів іноді можуть нести ідентифікатор рівня запиту, коли це виправдано, а логи можуть зберігати докладніші дані запиту. Метрики зазвичай мають використовувати атрибути, які обмежені, стабільні та корисні для групування.

Файл розв’язку: checkout_traces_metrics.py

import random
import time
from typing import Iterable
from opentelemetry import metrics, trace
from opentelemetry.metrics import Observation
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from 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()

Приклад з метриками додає ще одну ментальну модель: метрики записують агреговані факти, навіть коли трейси семплюються. Якщо продакшн-семплер зберігає лише частку трейсів, лічильники та гістограми все одно можуть представляти все навантаження. Саме тому трейси та метрики є взаємодоповнювальними, а не надлишковими. Під час інциденту метрики зазвичай повідомляють вам, що щось не так, а трейси допомагають інспектувати репрезентативні приклади.

Terminal window
.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, що генерує корисні трейси та метрики, а потім діагностувати вивід так, ніби ви переглядаєте інструментування колеги.

У вашої команди є джоб оформлення замовлення, що інколи зазнає збою, коли платіжний шлюз вичерпує час очікування. Поточний скрипт друкує успіх чи невдачу, але не показує, де витрачається час, який крок зазнав збою чи чи зростають збої. Вам потрібно додати трасування та метрики так, щоб це досі мало сенс, якби скрипт став довготривалим сервісом. Використовуйте консольні експортери для вправи, щоб ви могли інспектувати вивід локально.

Створіть чи повторно використайте віртуальне середовище репозиторію, потім встановіть пакети, потрібні для консольного експорту трейсів і метрик.

Terminal window
.venv/bin/python -m pip install opentelemetry-api opentelemetry-sdk

Створіть otel_checkout_exercise.py із цим неінструментованим входом.

import random
import 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}")
  1. Додайте Resource з service.name=checkout-exercise, service.version=0.1.0 та deployment.environment=local.
  2. Налаштуйте один TracerProvider під час старту з BatchSpanProcessor та ConsoleSpanExporter.
  3. Налаштуйте один MeterProvider під час старту з PeriodicExportingMetricReader та ConsoleMetricExporter.
  4. Створіть батьківський спан з ім’ям checkout для бізнес-операції та дочірні спани з іменами validate-cart, charge-payment та reserve-inventory.
  5. Використовуйте INTERNAL для валідації та резервування запасів, якщо ваша реалізація не симулює реальний виклик залежності.
  6. Використовуйте CLIENT для платежу, бо платіжний шлюз представляє вихідну залежність.
  7. Додайте лічильник з ім’ям checkout.attempts з обмеженим атрибутом, як-от checkout.channel.
  8. Додайте лічильник з ім’ям checkout.failures з обмеженим атрибутом, як-от error.type.
  9. Додайте гістограму з ім’ям checkout.duration з одиницею ms і запишіть час оформлення замовлення, що минув.
  10. Коли стається виняток, запишіть його на активному спані, задайте статус помилки, оновіть лічильник збоїв і повторно викиньте чи обробіть його навмисно.
  11. Скиньте обидва провайдери перед виходом зі скрипта, щоб консольний вивід був видимим.
  12. Перегляньте вивід і запишіть, яке поле доводить неперервність трейсу «батько-нащадок».
  • Скрипт запускається з .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, перейдіть на пакетну обробку та поясніть, як експорт на шляху запиту змінює поведінку затримки.


Перевірка для тих, хто навчається

Розділ «Перевірка для тих, хто навчається»

Спершу клієнтський спан — саме його контекст має передаватися далі за течією. Стартуйте клієнтський спан перед побудовою вихідного запиту, потім робіть Inject з того ctx, щоб батьком сервісу нижче за течією був клієнтський спан, а не серверний.

ParentBased обгортає кореневий семплер, тож дочірній спан успадковує рішення про семплювання батька через межі сервісів; TraceIdRatioBased з OTEL_TRACES_SAMPLER_ARG=0.10 застосовує імовірнісне головне семплювання на коренях.

Наступний модуль: Модуль 2: Архітектура OTel Collector — як приймати, обробляти, маршрутизувати та експортувати телеметрію у масштабі.