Files
crank/docs/observability.md
T

22 KiB
Raw Blame History

Наблюдаемость

Эксплуатационные журналы stdout

admin-api и mcp-server используют один жизненный цикл наблюдаемости и одинаковую однострочную JSON-схему:

{
  "timestamp": "2026-07-26T12:34:56.789Z",
  "level": "INFO",
  "service": "admin-api",
  "version": "0.3.1",
  "environment": "production",
  "target": "admin_api::request_context",
  "event": "admin.request.completed",
  "request_id": "019...",
  "trace_id": "0af7651916cd43dd8448eb211c80319c",
  "fields": {
    "method": "GET",
    "route": "/api/operations",
    "status": 200
  }
}

Обязательны timestamp, level, service, version, environment, target, event и объект fields. Поля request_id и trace_id независимы и появляются только при наличии соответствующего контекста. Имена событий статичны и имеют вид <компонент>.<объект>.<исход>.

Форматирование не использует ANSI. Одна строка stdout всегда соответствует одному JSON-объекту.

Сквозная корреляция

Admin API и MCP принимают x-request-id как непрозрачный идентификатор. Допустимое входное значение сохраняется без изменений. Если заголовок отсутствует или содержит пробелы, управляющие символы, ,, ;, не-ASCII символы либо больше 128 байт, Crank создаёт UUIDv7.

Request ID и отдельный Trace ID:

  • возвращаются в x-request-id/x-trace-id успешного или ошибочного ответа;
  • записываются в корневой span входного запроса;
  • типизированно передаются в runtime;
  • заменяет статические или полученные из mapping значения x-request-id и x-correlation-id перед исходящим REST-запросом;
  • сохраняются в прикладной истории вызова.

request id не является trace id и не подменяет распределённую трассировку.

Очистка

Перед сериализацией рекурсивно очищаются пароли, секреты, токены, ключи доступа, authorization, cookie, query, полные payload, body, arguments, result и response. Составные имена полей проверяются по тем же правилам. Из URL удаляются учётные данные, query и fragment.

Пределы одного события:

  • строка: 1024 байта;
  • массив: 32 элемента;
  • объект: 64 поля;
  • вложенность: 8 уровней;
  • вся строка JSON вместе с завершающим переводом строки: 16 КиБ.

Усечение сохраняет корректный UTF-8 и JSON. Секрет заменяется целиком на [REDACTED]; его часть, длина или хеш не выводятся. Если после очистки событие превышает 16 КиБ, fields заменяется ограниченным признаком усечения. Произвольное представление Debug не считается безопасным структурированным значением: для вложенных данных используется предварительно очищенный JSON.

Уровень и окружение задаются через CRANK_LOG_LEVEL и CRANK_ENVIRONMENT. Для stdout не требуются внешние службы, фоновые очереди или сетевые endpoint. Их отсутствие не влияет на readiness.

Критические ошибки

Необязательный Sentry-совместимый канал включается одним значением:

CRANK_SENTRY_DSN=https://public-key@errors.example.com/1

Пустой или отсутствующий CRANK_SENTRY_DSN полностью отключает канал. Неверное непустое значение останавливает запуск до приёма запросов, при этом само значение не попадает в ошибку. Стандартная переменная SENTRY_DSN не используется.

Канал получает только необработанные panic и явно классифицированные критические ошибки. Ожидаемые ошибки HTTP, MCP, runtime и внешних API остаются в обычных журналах, показателях и трассах. Для критического события допустимы только:

  • закрытая категория panic, startup, internal или data_integrity;
  • service, release и environment;
  • доступные request_id и trace_id;
  • статическое сообщение без исходного текста ошибки.

События группируются по паре service и закрытой категории, поэтому одинаковая ошибка admin-api и mcp-server не объединяется в один инцидент. Если обязательные поля идентичности не помещаются в заданный предел события, запуск останавливается безопасной типизированной ошибкой до приёма запросов.

До отправки удаляются request, user, breadcrumbs, URL, query, cookie, authorization, payload, произвольные contexts и extra, а также исходный текст panic или ошибки. Performance tracing, журналы, показатели и отслеживание сессий Sentry SDK отключены. Отказ внешнего приёмника не меняет результат продуктового запроса и не создаёт рекурсивное событие.

GlitchTip и другие Sentry-совместимые приёмники не входят в Community Compose. Их доступность не участвует в /health и /ready.

Показатели Prometheus

admin-api и mcp-server создают отдельные поверхности показателей. Они не смешиваются с продуктовыми маршрутами:

Сервис Значение по умолчанию
admin-api 127.0.0.1:9464
mcp-server 127.0.0.1:9465

На каждом адресе существуют только:

  • /metrics — показатели Prometheus;
  • /health — статическая проверка самого listener-а.

Любой другой путь возвращает 404. /health этой поверхности не заменяет /ready рабочего сервиса. Ошибка запуска listener-а останавливает процесс до приёма рабочих запросов.

Loopback доступен без авторизации. Для любого non-loopback адреса обязателен отдельный CRANK_METRICS_BEARER_TOKEN; без него конфигурация отклоняется при старте. Токен не переиспользует сессию администратора или ключ MCP. Оба маршрута внешней поверхности требуют Authorization: Bearer ....

Community Compose не содержит Prometheus или Grafana и не публикует порты 9464/9465 на host. Для сетевого сборщика необходимо явно:

  1. задать CRANK_ADMIN_METRICS_BIND=0.0.0.0:9464 и CRANK_MCP_METRICS_BIND=0.0.0.0:9465;
  2. передать непустой CRANK_METRICS_BEARER_TOKEN;
  3. подключить сборщик к внутренней сети Compose либо отдельно опубликовать нужные порты с ограничением firewall.

Токен сборщика хранится в файле, а не в prometheus.yml. Актуальная конфигурация Prometheus использует authorization.credentials_file:

scrape_configs:
  - job_name: crank-admin
    static_configs:
      - targets: ["admin-api:9464"]
    authorization:
      type: Bearer
      credentials_file: /run/secrets/crank_metrics_token

  - job_name: crank-mcp
    static_configs:
      - targets: ["mcp-server:9465"]
    authorization:
      type: Bearer
      credentials_file: /run/secrets/crank_metrics_token

Семейства первой версии:

  • crank_http_requests_total, crank_http_request_duration_seconds, crank_http_inflight;
  • crank_mcp_requests_total, crank_mcp_active_sessions, crank_mcp_active_streams;
  • crank_tool_invocations_total, crank_tool_invocation_duration_seconds;
  • crank_upstream_requests_total, crank_upstream_request_duration_seconds;
  • crank_runtime_inflight, crank_runtime_limit_rejections_total, crank_runtime_cache_total, crank_idempotency_total, crank_confirmation_total;
  • crank_db_pool_connections;
  • crank_catalog_tools, crank_catalog_estimated_context_tokens, crank_catalog_warnings;
  • crank_invocation_history_lost_total, crank_telemetry_export_failures_total.

Recorder добавляет к рядам проверенные статические labels service, version, environment. Остальные labels имеют закрытый набор значений. Имена, типы и допустимые значения labels определены в независимом нижнем crate crank-metrics. Product-код вызывает только его типизированный фасад; прямая production-зависимость от metrics вне crank-metrics и crank-observability запрещена архитектурной проверкой Cargo metadata. Запрещено использовать workspace, идентификаторы агента, операции или запроса, фактический URL, текст ошибки, payload и пользовательский текст. HTTP route берётся только из шаблона Axum; неизвестные маршруты и методы сворачиваются в unmatched и OTHER.

crank_mcp_active_sessions отражает число ещё не истёкших транспортных сессий в общем хранилище, а crank_mcp_active_streams — число открытых в этом экземпляре SSE-потоков. Прикладные ошибки JSON-RPC и tools/call.result.isError=true учитываются отдельно от успешного HTTP 200. Счётчики cache, idempotency и confirmation описывают переходы соответствующих стадий: hit/miss и ошибки хранилища, execute/replay/conflict и required/approved/invalid token. Отменённый future runtime или upstream завершает соответствующее измерение закрытым исходом aborted, поэтому фактически начатый вызов не исчезает из показателей.

Buckets длительности фиксированы кодом: 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60 секунд. Это пока техническая шкала, а не SLO. Фактические series и расход памяти измеряются в истории 1.8; произвольный численный бюджет до замеров не назначается.

Распределённые трассы

Crank принимает и передаёт стандартный W3C traceparent на границах Admin API, MCP и исходящих REST-вызовов. Ровно один canonical parent принимается; duplicate, malformed, uppercase и zero-ID значения заменяются новым local trace без echo входа. x-request-id остаётся отдельным идентификатором запроса, а x-trace-id — безопасным локальным support ID. Caller tracestate ограничен 512 байтами/32 members, baggage — 8192 байтами/64 members; Community allowlist пуста, поэтому они не извлекаются и не передаются.

Экспорт отключён по умолчанию: при пустых OTEL_EXPORTER_OTLP_TRACES_ENDPOINT и OTEL_EXPORTER_OTLP_ENDPOINT local provider сохраняет span topology и Trace ID, но exporter, batch processor, фоновая очередь и сетевой трафик не создаются. Поэтому response, runtime и Invocation History получают Trace ID независимо от sampling/export. После включения используется только OTLP/HTTP binary protobuf. Community Compose не включает Collector или хранилище трасс: оператор подключает внешний совместимый приёмник.

Уровень эксплуатационных журналов не отключает трассы. В OTLP попадают только явно отмеченные spans с внутренней целью crank::trace; события tracing не экспортируются, чтобы их поля не обходили очистку JSON-журналов. Новые экспортируемые spans должны использовать ту же цель и содержать только ограниченные безопасные атрибуты.

Очередь экспорта конечна. Экспорт выполняется вне обработки продуктового запроса, поэтому недоступность приёмника не меняет HTTP, MCP или runtime результат. Ошибки доставки учитываются в crank_telemetry_export_failures_total{signal_type="trace",exporter="otlp"} без журналирования endpoint, headers и без создания новой OTLP-трассы.

Входящий корректный traceparent становится родителем корневого span. Некорректный заголовок трактуется как отсутствующий: запрос продолжается с новой трассой, а исходное значение не возвращается и не журналируется. Перед исходящим REST-запросом доверенный текущий контекст заменяет любой traceparent, заданный в настройках операции.

Стадии выполнения инструмента

Crank создаёт дочерний span только тогда, когда соответствующая стадия фактически выполняется:

Span Значение
mcp.rate_limit проверка ограничения частоты MCP-запроса
mcp.access.check машинная проверка доступа
mcp.catalog.load получение опубликованного каталога
mcp.tools.resolve разрешение конкретного инструмента
approval.check применимая проверка подтверждения
runtime.execute выполнение инструмента в runtime
runtime.arguments.map проверка схемы и отображение аргументов
runtime.idempotency применимая проверка идемпотентности
upstream.http фактический HTTP-вызов внешней системы
runtime.response.transform отображение и проверка ответа
auth.resolve применимое разрешение профиля авторизации и секретов
approval.recovery фоновое восстановление подтверждённого вызова
history.write запись прикладной истории
db.query значимая операция PostgreSQL

Например, ответ из кэша или повтор идемпотентного результата не создаёт upstream.http. Операция без подтверждения и идемпотентности не создаёт соответствующие spans. Отсутствующая стадия никогда не изображается успешной.

Атрибуты стадий имеют закрытый словарь: outcome, error.category, db.system и db.operation. Значения задаются перечислениями из crank-trace; пользовательские идентификаторы, URL, заголовки, query, аргументы, ответы и текст ошибок туда передать нельзя. Ошибка содержит только категорию, достаточную для локализации стадии. request_id присутствует только в проверенном корневом span и сопоставляется с записью прикладной истории.

Перед OTLP-сериализацией экспортёр повторно применяет разрешительный список имён и атрибутов, удаляет события и ссылки span и очищает текстовое описание статуса ошибки. Эта последняя граница действует независимо от очистки эксплуатационных JSON-журналов и не позволяет новому инструментированию случайно экспортировать пользовательские данные.

Точный список переменных и правила OpenBao приведены в описании runtime-конфигурации.

Прикладные журналы и использование

Crank сохраняет в PostgreSQL данные о тестовых запусках и вызовах опубликованных MCP-инструментов. Это отдельная прикладная история, а не копия stdout.

Если внешнее действие завершилось, но PostgreSQL отклонил запись истории, Crank не меняет фактический результат и не повторяет действие. Вместо синтетической записи создаётся эксплуатационный инцидент DC-08:

  • admin.invocation_history.lost или mcp.invocation_history.lost в stdout;
  • внутренний монотонный счётчик без пользовательских меток;
  • crank_invocation_history_lost_total и закрытый показатель потери экспорта;
  • только закрытые поля source, invocation_status и error_category.

Текст ошибки PostgreSQL, payload и секреты в событие не передаются. Внешняя публикация не влияет на результат уже завершённого действия.

Журналы

В журнал попадают:

  • операция;
  • агент, если вызов пришел через MCP;
  • request id;
  • статус;
  • HTTP status code внешнего API;
  • время выполнения;
  • краткий preview запроса и ответа;
  • категория ошибки, если вызов завершился ошибкой.

При обновлении опубликованного MCP-каталога событие mcp.catalog.analyzed содержит tool_count, serialized_bytes, estimated_context_tokens, largest_tool_estimated_context_tokens, recommended_context_tokens, exceeds_recommended_budget и число предупреждений качества. По этим полям можно заметить рост цены tools/list до того, как он ухудшит выбор инструментов моделью.

Использование

Раздел использования агрегирует:

  • количество вызовов;
  • успешные и ошибочные вызовы;
  • долю ошибок;
  • задержки p50, p95 и p99;
  • распределение вызовов по операциям.

Для чего это нужно

  • проверить, вызывают ли агенты нужные инструменты;
  • увидеть ошибки маппинга или внешнего API;
  • найти медленные endpoint-ы;
  • понять, какие инструменты реально используются.