Обычный HTTP trace отвечает, где потрачено время. Для агента этого мало: нужно восстановить, какие данные он увидел, почему выбрал инструмент, что инструмент изменил и какой именно prompt/model/tool contract участвовал. При этом складывать весь prompt в telemetry нельзя: там PII, секреты и документы клиента.
Инцидент, который не разбирается по access log
Агент поддержки дважды возвращает деньги по одному заказу. В access log оба запроса успешны, у провайдера — два ответа 200, итоговая latency в норме. Неизвестно:
- второй запуск был retry всего workflow или новой пользовательской командой;
- модель повторно вызвала
issue_refundили приложение повторило уже выбранный tool call; - какой результат первого вызова попал обратно в контекст;
- действовали ли новая версия prompt и новая схема инструмента;
- был ли у обоих действий один idempotency key.
Observability здесь — не «больше логов», а возможность связать вход, решения, побочные эффекты и версии в одну причинную цепочку.
Три сигнала и журнал действий
Разделите данные по назначению:
- trace хранит причинную структуру и длительности: HTTP-запрос → agent run → model call → tool execution → verification;
- metrics агрегируют поток: latency, ошибки, токены, стоимость, число шагов и исходы. В labels нельзя помещать
user_id, prompt,run_idи текст ошибки: это неограниченная cardinality; - structured logs несут редкие диагностические события и связываются полями
trace_id,span_id,run_id; - audit log фиксирует значимые действия и предназначен не для debugging, а для доказуемого ответа «кто, на каком основании, что изменил». Это отдельное хранилище и схема; подробнее — в главе 36.
Идентификаторы имеют разные области:
request_id один входящий HTTP-запрос
trace_id распределённая причинная цепочка
run_id один логический запуск агента, сохраняется через retry/resume
step_id устойчивый идентификатор шага workflow
attempt номер физической попытки шага
operation_id идемпотентность внешнего побочного эффектаЕсли retry создаёт новый run_id, расследование раскалывается. Если две попытки tool call используют разные operation_id, telemetry лишь аккуратно запишет двойной ущерб.
Модель trace: стандарт сначала, свои spans отдельно
OpenTelemetry публикует GenAI semantic conventions. Они развиваются и могут иметь статус Development, поэтому зафиксируйте версию конвенций и SDK в lock-файле. Для model call используйте стандартные имена и атрибуты, которые поддерживает ваша версия instrumentation: gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, идентификатор ответа и finish reasons.
Не изобретайте параллельные llm.model и tokens_in: иначе dashboard привязывается к локальному диалекту. Но стандарт не описывает ваш бизнес-workflow. Для него нужны custom spans с собственным namespace:
POST /support/refund SERVER
└─ agent.run INTERNAL, custom
├─ chat {gen_ai.operation.name=chat} CLIENT, GenAI conventions
├─ support.tool.execute INTERNAL, custom orchestration
│ └─ POST payments/refunds CLIENT, HTTP conventions
├─ agent.checkpoint.save INTERNAL, custom
└─ agent.verify INTERNAL, customagent.run, agent.checkpoint.save и agent.verify — не «GenAI standard spans». Назовите внутреннюю схему, например com.acme.agent.*, документируйте её версию и не выдавайте за OpenTelemetry convention.
Минимальный custom contract:
com.acme.agent.run:
attributes:
agent.name: string
agent.version: string
workflow.name: string
workflow.version: string
run.id: string
run.outcome: enum[completed, failed, suspended, cancelled]
run.step_count: int
com.acme.agent.tool:
attributes:
tool.name: string
tool.version: string
tool.call_id: string
tool.operation_id: string
tool.outcome: enum[ok, rejected, timeout, error, unknown]
policy.decision: enum[allow, deny, require_approval]Версии prompt и tool schema — атрибуты низкой cardinality, если это immutable digest или release ID. Сам текст — нет.
from opentelemetry import trace
tracer = trace.get_tracer("support-agent", "2.4.0")
async def execute_tool(run, call):
with tracer.start_as_current_span("support.tool.execute") as span:
span.set_attributes({
"run.id": run.id,
"tool.name": call.name,
"tool.version": call.contract_version,
"tool.call_id": call.id,
"tool.operation_id": call.operation_id,
"prompt.template.version": run.prompt_version,
"policy.decision": call.policy_decision,
})
try:
result = await tools.invoke(call)
except Exception as exc:
span.record_exception(exc) # тип и stack trace; проверьте sanitization
span.set_status(trace.Status(trace.StatusCode.ERROR))
raise
span.set_attribute("tool.outcome", result.outcome)
return resultSpan status обозначает техническую ошибку операции. Бизнес-отказ (policy.deny, «заказ уже возвращён») может завершиться без exception и требует отдельного outcome.
Содержимое prompt: ссылка и digest вместо утечки
Telemetry pipeline должен считать payload недоверенным. Безопасная запись model call содержит:
{
"prompt_template_id": "refund-decision",
"prompt_template_version": "git:8d04…",
"rendered_prompt_sha256": "sha256:17ac…",
"input_bytes": 18421,
"retrieval_snapshot_id": "rs_01J…",
"model_config_id": "mc_2026_08_17",
"content_capture": "disabled"
}Digest помогает доказать равенство входов, но не восстановить их. Для воспроизведения исходные артефакты кладут в защищённое хранилище с ACL, retention и шифрованием, а trace хранит opaque reference. Захват содержимого включается отдельной политикой для тестового tenant или конкретного инцидента после redaction. Нельзя маскировать только известные поля JSON: секрет может появиться в tool output, exception message или URL query.
Логи выпускайте как события, а не как литературные строки:
{
"event": "agent.tool.finished",
"severity": "INFO",
"trace_id": "…",
"span_id": "…",
"run_id": "…",
"step_id": "refund",
"attempt": 2,
"tool": "issue_refund",
"operation_id": "refund:order-481",
"outcome": "already_applied",
"duration_ms": 83
}Метрики: считать границы системы, а не мысли модели
Полезны histogram/counter с ограниченным набором labels:
agent.run.duration{workflow,outcome,version}иagent.run.count{workflow,outcome};agent.step.count{workflow,step_kind,outcome}— обнаруживает циклы и рост числа шагов;genai.client.operation.duration{provider,model,operation,status};- input/output tokens по provider/model и оценённая стоимость по versioned price catalog;
tool.duration{tool,outcome}иtool.calls{tool,policy_decision,outcome};agent.resume.count{workflow,outcome}и возраст checkpoint при resume;- доля trace/log export failures и заполнение очереди Collector: слепота telemetry тоже является отказом.
Токены и стоимость нельзя выводить только из sampled traces: получится систематически неполная сумма. Счётчики usage обновляются на каждом ответе либо считаются из биллингового event stream. Trace нужен для объяснения выброса.
Sampling без потери редких отказов
Head sampling принимает решение до результата и дёшев, но пропускает редкие ошибки. Tail sampling в OpenTelemetry Collector видит завершённый trace и может сохранять:
- все traces с
ERROR,run.outcome=failedилиpolicy.decision=deny; - все runs с превышением продуктового latency budget или лимита шагов;
- небольшую вероятностную выборку успешных runs;
- traces конкретного tenant только по временной incident-политике.
processors:
tail_sampling:
decision_wait: 30s
policies:
- name: errors
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-agent-runs
type: latency
latency: {threshold_ms: 15000}
- name: baseline
type: probabilistic
probabilistic: {sampling_percentage: 2}Числа здесь — пример синтаксиса, не норматив. decision_wait должен покрывать реальные длительности trace, а порог latency — SLO use case. Tail sampling требует памяти: Collector буферизует незавершённые traces. Нужны лимиты, метрики dropped spans и нагрузочный тест. Для корректного решения все spans одного trace должны попасть в совместимый collector/shard; иначе trace будет неполным.
Failure modes
- Span на каждый streaming token. Взрыв объёма. Токены агрегируются; события нужны только для значимых фаз, например time-to-first-token.
- Prompt и tool result в attributes. Утечка, ограничения backend-а и дорогая индексация. Содержимое — в защищённом artifact store.
- Высокая cardinality в metrics.
run_idиuser_idпревращают метрики в базу событий. Они принадлежат traces/logs. - Асинхронная задача потеряла context. Передавайте W3C Trace Context через headers/message metadata; для позднего продолжения создавайте link на исходный span, если parent-child уже вводит в заблуждение.
- Retry выглядит двумя независимыми шагами. Сохраняйте
run_id,step_id, увеличивайтеattempt; каждую попытку делайте отдельным span. - Exporter тормозит запрос. Batch processor и bounded queue; при отказе telemetry продукт продолжает работать, но dropped telemetry измеряется.
- Смена prompt без версии. Записывайте immutable template/version, параметры модели, tool schema digest и retrieval index/snapshot.
Практический сценарий: доказать причину двойного refund
- Найдите audit event по заказу и возьмите
operation_id,run_id,trace_id. - В trace проверьте две физические попытки одного
step_idи точку timeout. - Сопоставьте tool call с HTTP span платежного сервиса и его idempotency response.
- По checkpoint span проверьте, был ли результат первого вызова durable записан до падения.
- По prompt/tool versions воспроизведите decision на зафиксированном retrieval snapshot.
Если цепочка обрывается между успешным внешним действием и checkpoint, результат шага имеет состояние unknown, а resume обязан сначала сверить внешний эффект, а не повторять его. Это уже требование к workflow, не к dashboard.
Правило главы
Trace хранит причинность, metrics — агрегаты, structured logs — диагностические события, audit log — доказательство действий. Стандартные GenAI spans и attributes используйте без локального переименования; orchestration описывайте явно версионированной custom-схемой. Контент по умолчанию не попадает в telemetry.
Чеклист главы
- Один
run_idпереживает retry и resume; попытки различаютсяattempt. - Model calls используют атрибуты GenAI conventions поддерживаемой версии, custom spans имеют собственный namespace.
- В trace видны model call, tool execution, внешний side effect, checkpoint и verification.
- Prompt, model config, workflow и tool schema имеют immutable версии/digests.
- В metrics нет идентификаторов пользователей, runs и свободного текста.
- Content capture выключен по умолчанию; artifact store имеет ACL и retention.
- Sampling сохраняет ошибки и продуктовые выбросы; потери Collector измеряются.
- По trace можно связать действие с audit event и idempotency key.
Практикум
Задача 28.1 — инструментировать refund-agent. ⭑⭑⭑
Добавьте OpenTelemetry в агент с model call, lookup_order, issue_refund, checkpoint и verify. Экспортируйте traces через Collector, метрики — в Prometheus-совместимый backend. Создайте сценарии: успех, policy deny, timeout после успешного refund, retry/resume, ошибка exporter-а.
Критерии приёмки:
- Trace восстанавливает порядок шагов и все попытки, но не содержит prompt, PII и tool payload.
- Стандартные и custom attributes перечислены в отдельном schema-файле и не смешаны.
- Resume связан с исходным run; повторный refund предотвращён одним
operation_id. - Ошибочный и медленный traces сохраняются tail sampling, успешные семплируются.
- Суммарные usage/cost metrics не меняются при изменении trace sampling.
- При недоступном Collector запрос завершается, а счётчик dropped telemetry растёт.
Первоисточники
- OpenTelemetry: GenAI semantic conventions
- OpenTelemetry: GenAI spans
- OpenTelemetry: Trace semantic conventions
- W3C Trace Context
- OpenTelemetry Collector: tail sampling processor