Наблюдаемость¶
Каждый запуск фиксирует трейс. Трейсы наблюдаемы по умолчанию: демо-дашборды работают офлайн без внешних сервисов, а трейсы дополнительно можно отправлять в Langfuse или Postgres.
Что содержит трейс¶
RunTrace — один запуск runtime (до завершения astream/run):
- Спаны агентов (
AgentSpan) — что делал каждый агент: чтения и записи артефактов, суммарные ключ/значение патча, который он произвёл. - LLM-вызовы (
LLMCall) — промпты, ответы (урезанные), расход токенов, задержки. - Тайминги и порядок — вся причинная цепочка запуска по порядку.
RecordingLLM оборачивает ваш провайдер бесплатно, поэтому трекинг LLM-вызовов
не требует кода — достаточно подключить его при настройке ресурсов.
Хранение и просмотр¶
SQLite-хранилище + веб-дашборд (локально, офлайн)¶
from reactifact.tracing import TraceStore
store = TraceStore("traces.db") # SQLite-приёмник; также отдаёт запуски UI
Интерфейс стореджа — async (export/query/get); SQLite-ядро выполняется в
рабочем потоке, поэтому один и тот же объект работает и в веб-приложении, и в
обычном синхронном коде.
Дашборд — это FastAPI-роутер, монтируемый на ваше приложение:
from reactifact.tracing.web import create_trace_router
app.include_router(create_trace_router(store), prefix="/traces")
Он отдаёт traces.html (список запусков, фильтрация) и run.html (спаны,
чтения, записи, ввод/вывод LLM, тайминги, плюс две живые Mermaid-диаграммы:
sequence запуска и его граф доказательств — записанные артефакты с
рёбрами провенанса patch.link, §34). Пример devops монтирует этот роутер
и служит эталонным UI.

Открытый запуск показывает работу по агентам (число спанов, типы затронутых
артефактов) и полную sequence-диаграмму — здесь HITL ask/resume поток примера
devops: k8s несколько раз вызывает модель через artifact_created-спаны,
затем render собирает финальный ответ:

Langfuse и Postgres как дополнительные приёмники¶
Трейс можно отправлять в несколько мест сразу через CompositeTracer, передаваемый
в Runtime. Tracer — тонкий: on_turn_begin → on_span → on_turn_end
(I/O выполняет только on_turn_end; приёмники экспортируют асинхронно).
from reactifact import Runtime
from reactifact.tracing import LangfuseTracer, PostgresStore, TraceStore
runtime = Runtime(
ctx,
agents=[...],
tracer=[
TraceStore("traces.db"), # локальный дашборд
LangfuseTracer(public_key="...", private_key="...",
host="https://cloud.langfuse.com"),
PostgresStore(dsn="postgresql://…"), # требуется extra pg
],
)
Трейсы доставляются один раз в конце хода (семантика доставки-один-раз); трейсер
Langfuse мапит суммарные чтения/записи в input/output, чтобы таймлайн читался
в их UI. SQLite TraceStore остаётся одним из источников для веб-дашборда;
Postgres зеркалирует ту же схему runs/spans.
И TraceStore, и PostgresStore реализуют TraceReader (async query/get),
поэтому create_trace_router работает и с теми, и с другими: укажите дашборду
PostgresStore(dsn) — и он покажет трейсы из Postgres без локального
SQLite-файла.
OTLPTracer: любой OTLP/HTTP-коллектор¶
LangfuseTracer привязан к Langfuse (собственное пространство атрибутов
langfuse.*). OTLPTracer экспортирует тот же запуск как вендор-нейтральные
спаны — только атрибуты GenAI semantic conventions (gen_ai.*, плюс
пространство reactifact.* для reads/writes/provenance) — в любой
OTLP/HTTP-коллектор: Jaeger, Tempo, Honeycomb, Datadog Agent, локальный
otel-collector, ...
from reactifact.tracing import OTLPTracer
runtime = Runtime(
ctx,
agents=[...],
tracer=OTLPTracer(endpoint="http://localhost:4318/v1/traces"),
)
Без зависимости от opentelemetry-sdk — как и LangfuseTracer, он
отправляет OTLP/HTTP JSON-payload напрямую через httpx (уже core-зависимость),
поэтому не требует установки дополнительного extra. Передайте headers= для
коллектора, требующего авторизацию.
Модель эмиссии¶
- Спаны эмитят только меняющие состояние агенты (чистый read/verify-produce ничего не эмитит — меньше шума).
- Один
TracerнаCompositeTracer: отдавайте один композит нескольким приёмникам. on_turn_end— единая точка доставки: новыйastream/arunпродвигает id запуска (повтор считается новым запуском), и трейс каждого хода финализируется один раз.
Трассировка никогда не роняет запуск¶
Наблюдаемость best-effort по контракту. Недоступный приёмник (Langfuse лёг,
Postgres отказывает в соединении), падающий колбэк кастомного Tracer или
битый payload трейса перехватывается, логируется предупреждением и
пропускается — бизнес-запуск завершается нормально, а следующий ход
трассируется как обычно. Если настроено несколько приёмников или трейсеров,
падение одного никогда не мешает остальным получить трейс. Потеря
наблюдаемости не должна означать потерю запуска, который она лишь
обслуживала.
Редактирование чувствительных данных¶
Трейсы покидают процесс (SQLite, Postgres, Langfuse, дашборд, строка лога);
рабочий Context — и, при сохранении сессии, возобновляемый диалог — нет.
Укажите redactor= на RuntimeResources, чтобы вычищать текст, который
уходит: data артефактов, messages/response LLM-вызовов и error
спанов/LLM. Живое состояние и сохранённые сессии намеренно не
редактируются — маскировка рабочей копии сломала бы диалог, который должен
возобновляться.
from reactifact import RuntimeResources
from reactifact.redaction import RegexRedactor
resources = RuntimeResources(llm=from_env(), redactor=RegexRedactor())
Встроенный RegexRedactor намеренно консервативен — email, US SSN, IBAN,
Bearer-токены и api-ключи вида sk-… — чтобы никогда не замаскировать
законную финансовую цифру; передавайте patterns=[(name, regex), …] для своих
форматов (карты, телефоны, …). Подойдёт любой объект с методом
redact(text: str) -> str, так что PII-сервис подключается без наследования.
None (по умолчанию) оставляет трейсы байт-в-байт как раньше.
События прогресса (реактивность UI)¶
Для анимированных строк «Думаю… / Составляю план… / Считаю смету…» Produce
вызывают context.announce(message, kind=..., **payload). Это превращается в
ProgressEvent на EventHub, который runtime отдаёт в SSE-поток:
async for event in runtime.astream():
if event.kind == "status":
yield sse("status", {"message": event.message})
announce — не лог, а первоклассный канал UI, который web-демо потребляют
буквально.