Distributed tracing для агентів: трасування мульти-агентних систем

Distributed tracing дозволяє відстежувати один run через кілька сервісів, черги, tools і LLM-провайдерів, зберігаючи trace context end-to-end.
На цій сторінці
  1. Ідея за 30 секунд
  2. Основна проблема
  3. Як це працює
  4. Як виглядає distributed trace
  5. Коли використовувати
  6. Приклад реалізації
  7. Типові помилки
  8. Новий trace_id у кожному сервісі
  9. Передається лише trace_id, без зв'язку спанів
  10. Контекст губиться в async-чергах
  11. Немає service_name і operation_name
  12. Самоперевірка
  13. FAQ
  14. Пов'язані сторінки

Ідея за 30 секунд

Distributed tracing показує один run не в межах одного сервісу, а по всьому ланцюгу викликів.

У мульти-агентних системах запит часто проходить через gateway, runtime, tools, черги і LLM-провайдерів.

Distributed tracing пов'язує ці кроки через trace_id, span_id і parent_span_id, тому поведінку системи можна бачити end-to-end.

Основна проблема

Коли агент працює в кількох сервісах, логи зазвичай розкидані в різних місцях.

Окремо видно помилки gateway, окремо — tool service, окремо — agent runtime. Але ці події не зв'язані між собою як один run. Але без спільного trace context важко зрозуміти, що це один і той самий run.

У результаті навіть простий інцидент стає довгим розслідуванням:

  • неясно, у якому сервісі з'явилась затримка;
  • незрозуміло, де загубився контекст;
  • складно пов'язати retries між сервісами;
  • важко відтворити повний шлях проблемного run.

Саме тому для мульти-агентних систем потрібен distributed tracing, а не лише локальний трейсинг усередині одного runtime.

Як це працює

Distributed tracing використовує ту саму модель trace і span, але в межах кількох сервісів.

  • trace — увесь шлях одного запиту через всі сервіси
  • span — конкретна операція в одному сервісі

У реальних системах ці поля зазвичай базуються на OpenTelemetry (OTel):

  • trace_id — спільний ідентифікатор для всього шляху
  • span_id — ідентифікатор поточного кроку
  • parent_span_id — зв'язок з батьківським кроком
  • service_name — де саме виконано крок
  • operation_name — що саме робив сервіс

Щоб трейс не обривався, trace context треба передавати між сервісами разом із кожним викликом. Найчастіше це роблять через headers (traceparent) або metadata у повідомленнях черги.

Як виглядає distributed trace

Найпростіше зрозуміти distributed tracing на прикладі одного запиту.

TEXT
trace_id: tr_7a31
user_query: "Find vendor invoices for March"

gateway         span g1   parent=-     18ms   status=ok
agent_runtime   span a1   parent=g1   240ms   status=ok
tool_service    span t1   parent=a1   410ms   status=ok
agent_runtime   span a2   parent=a1   130ms   status=ok
llm_provider    span l1   parent=a2   690ms   status=ok

stop_reason: completed

Такий трейс показує:

  • повний шлях між сервісами;
  • який сервіс дав найбільшу затримку;
  • де саме обірвався контекст (якщо це сталося);
  • які спани були батьківськими, а які дочірніми.

Коли використовувати

Distributed tracing не завжди потрібен.

Якщо система монолітна і весь run живе в одному процесі, часто вистачає локального трейсингу.

Але distributed tracing стає критичним, коли:

  • один запит проходить через кілька сервісів;
  • у workflow є черги або async-воркери;
  • кілька агентів обмінюються подіями;
  • потрібен точний розбір latency і retries між сервісами.

Приклад реалізації

Нижче — спрощений приклад, як передавати trace context між gateway і worker-сервісом. У прикладі нижче використані спрощені заголовки (x-trace-id, x-parent-span-id), щоб показати сам принцип propagation. У production-системах зазвичай використовують стандартний W3C-заголовок traceparent (через OpenTelemetry), який автоматично передає trace context між сервісами.

PYTHON
import contextvars
import logging
import time
import uuid

logger = logging.getLogger("distributed-tracing")
trace_id_ctx = contextvars.ContextVar("trace_id", default=None)
span_id_ctx = contextvars.ContextVar("span_id", default=None)


def start_span(service_name, operation_name, parent_span_id=None):
    span_id = str(uuid.uuid4())
    started_at = time.time()
    logger.info(
        "span_started",
        extra={
            "trace_id": trace_id_ctx.get(),
            "span_id": span_id,
            "parent_span_id": parent_span_id,
            "service_name": service_name,
            "operation_name": operation_name,
        },
    )
    return span_id, started_at


def finish_span(service_name, operation_name, span_id, started_at, status, parent_span_id=None, error=None):
    logger.info(
        "span_finished",
        extra={
            "trace_id": trace_id_ctx.get(),
            "span_id": span_id,
            "parent_span_id": parent_span_id,
            "service_name": service_name,
            "operation_name": operation_name,
            "status": status,
            "latency_ms": int((time.time() - started_at) * 1000),
            "error": error,
        },
    )


def inject_context(headers):
    headers["x-trace-id"] = trace_id_ctx.get() or ""
    headers["x-parent-span-id"] = span_id_ctx.get() or ""


def extract_context(headers):
    incoming_trace_id = headers.get("x-trace-id") or str(uuid.uuid4())
    incoming_parent_span_id = headers.get("x-parent-span-id")
    trace_token = trace_id_ctx.set(incoming_trace_id)
    return incoming_parent_span_id, trace_token


def gateway_handle_request():
    trace_id = str(uuid.uuid4())
    trace_token = trace_id_ctx.set(trace_id)

    root_span_id, root_started_at = start_span("gateway", "handle_request", parent_span_id=None)
    span_token = span_id_ctx.set(root_span_id)

    try:
        headers = {}
        inject_context(headers)
        call_worker_service(headers)  # умовний HTTP/gRPC виклик до worker_handle_request на іншому сервісі
        finish_span(
            "gateway",
            "handle_request",
            root_span_id,
            root_started_at,
            status="ok",
            parent_span_id=None,
        )
    except Exception as error:
        finish_span(
            "gateway",
            "handle_request",
            root_span_id,
            root_started_at,
            status="error",
            parent_span_id=None,
            error=str(error),
        )
        raise
    finally:
        span_id_ctx.reset(span_token)
        trace_id_ctx.reset(trace_token)


def worker_handle_request(headers):
    parent_span_id, trace_token = extract_context(headers)
    span_id, started_at = start_span("worker", "process_task", parent_span_id=parent_span_id)
    span_token = span_id_ctx.set(span_id)

    try:
        # ... робота агента, tool calls, LLM кроки ...
        finish_span("worker", "process_task", span_id, started_at, status="ok", parent_span_id=parent_span_id)
    except Exception as error:
        finish_span(
            "worker",
            "process_task",
            span_id,
            started_at,
            status="error",
            parent_span_id=parent_span_id,
            error=str(error),
        )
        raise
    finally:
        span_id_ctx.reset(span_token)
        trace_id_ctx.reset(trace_token)

Навіть ручний підхід вище допомагає зрозуміти базову механіку distributed tracing.

У реальному workflow кожен сервіс зазвичай створює свій span і далі прокидає вже його span_id як parent_span_id для наступного hop. Якщо цей крок пропустити, наступний сервіс почне новий trace і end-to-end картина зламається.

Наприклад, один span-event у JSON-логу може виглядати так:

JSON
{
  "timestamp": "2026-03-21T15:17:00Z",
  "event": "span_finished",
  "trace_id": "tr_7a31",
  "span_id": "sp_worker_02",
  "parent_span_id": "sp_gateway_01",
  "service_name": "worker",
  "operation_name": "process_task",
  "latency_ms": 410,
  "status": "ok"
}

Типові помилки

Навіть якщо distributed tracing уже додано, у production часто залишаються типові проблеми.

Новий trace_id у кожному сервісі

Якщо кожен сервіс генерує свій trace_id, end-to-end трейс розпадається на шматки. У такому режимі складно локалізувати причину інциденту між сервісами.

Передається лише trace_id, без зв'язку спанів

trace_id без span_id і parent_span_id дає лише плоский список подій. Без дерева спанів важко зрозуміти, які кроки були вкладеними.

Контекст губиться в async-чергах

Якщо metadata черги не містить trace context, частина workflow випадає з трейсу. Такі обриви часто маскують ранню фазу часткового збою або каскадних збоїв.

Немає service_name і operation_name

Без цих полів видно, що помилка є, але незрозуміло, в якому саме сервісі й операції вона виникла. Через це дебаг займає значно більше часу.

Самоперевірка

Нижче — короткий checklist базового distributed tracing перед релізом.

Прогрес: 0/9

⚠ Бракує базової observability

Систему буде складно дебажити в production. Почніть з run_id, structured logs і tracing tool calls.

FAQ

Q: Чим distributed tracing відрізняється від звичайного agent tracing?
A: Agent tracing показує кроки в межах одного runtime. Distributed tracing з'єднує кроки між кількома сервісами в один end-to-end трейс.

Q: Що ламається, якщо між сервісами прокидається лише trace_id, без span_id і parent_span_id?
A: Тоді трейс стає плоским: видно, що події належать до одного шляху, але вже важко зрозуміти, які кроки були вкладеними, який сервіс кого викликав і де саме виникла затримка або помилка. trace_id з'єднує загальний шлях, а span_id і parent_span_id будують дерево кроків.

Q: Як передавати trace context через черги?
A: Додавай trace_id, span_id і parent_span_id у metadata повідомлення. Інакше async-кроки випадуть з трейсу.

Q: Чи обов'язково одразу впроваджувати OpenTelemetry?
A: Ні. Можна почати з ручної propagation і структурованих логів, а потім перейти на OTel SDK, коли система зросте.

Пов'язані сторінки

Далі за темою:

⏱️ 6 хв читанняОновлено 21 березня 2026 р.Складність: ★★★
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.

Автор

Микола — інженер, який будує інфраструктуру для продакшн AI-агентів.

Фокус: патерни агентів, режими відмов, контроль рантайму та надійність систем.

🔗 GitHub: https://github.com/mykolademyanov


Редакційна примітка

Ця документація підготовлена з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Контент базується на реальних відмовах, постмортемах та операційних інцидентах у розгорнутих AI-агентних системах.