23 KiB
Наблюдаемость
Эксплуатационные журналы 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. Для сетевого сборщика необходимо явно:
- задать
CRANK_ADMIN_METRICS_BIND=0.0.0.0:9464иCRANK_MCP_METRICS_BIND=0.0.0.0:9465; - передать непустой
CRANK_METRICS_BEARER_TOKEN; - подключить сборщик к внутренней сети 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_request_duration_seconds,crank_mcp_active_sessions,crank_mcp_session_metrics_fresh,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 классифицированы как process_constant: они валидируются один
раз при запуске и образуют ровно одну tuple на процесс. Имена, типы, help,
process ownership, допустимые значения product labels и buckets определены в
независимом нижнем crate crank-metrics. Его deterministic machine snapshot —
schemas/metrics-registry-v1.json.
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-потоков. crank_mcp_session_metrics_fresh=0 означает,
что последнее bounded чтение session store завершилось ошибкой/timeout и
значение active sessions нельзя считать свежим; успешное чтение возвращает
freshness в 1. Прикладные ошибки 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. Registry v1
содержит 23 family и для всего lifetime одного процесса ограничен 5 000
logical labelsets и 15 000 Prometheus sample series; текущий worst case —
4 513 и 14 338 соответственно. Histogram budget включает finite buckets,
+Inf, _sum и _count; изменение domain или buckets требует обновления
versioned snapshot и evidence.
Обычный scrape сохраняет Prometheus text 0.0.4. Клиент с
Accept: application/openmetrics-text получает OpenMetrics 1.0 и # EOF.
Validated 32-hex Trace ID может присутствовать только как latest bounded
histogram exemplar; он никогда не является label и не меняет aggregate.
Отсутствующий, invalid или unsampled Trace ID просто не создаёт exemplar.
Размер готового exposition ограничен 8 MiB; превышение возвращает статический
503 без частично усечённого тела и не влияет на product listener.
Распределённые трассы
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-ы;
- понять, какие инструменты реально используются.