Files
crank/docs/observability.md
T

405 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Наблюдаемость
## Эксплуатационные журналы stdout
`admin-api` и `mcp-server` используют один жизненный цикл наблюдаемости и
одинаковую однострочную JSON-схему:
```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-совместимый канал включается одним значением:
```env
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`:
```yaml
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`](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-конфигурации](runtime-config.md).
## Прикладные журналы и использование
Crank сохраняет в PostgreSQL данные о тестовых запусках и вызовах
опубликованных MCP-инструментов. Это отдельная прикладная история, а не
копия stdout.
Новые записи сохраняют точную Operation version и закрытую классификацию
`execution_stage`, `execution_error_code`, `retryability` и
`outcome_certainty`. Admin Draft Test и MCP используют одну taxonomy; transport
может отличаться, но не смысл результата. Legacy-записи до migration v5 имеют
`NULL` в этих полях — Crank не выдумывает исторические исходы.
`outcome_unknown` означает, что внешний side effect мог произойти до timeout
или transport failure. Такой исход требует ручной сверки и никогда не является
сигналом для автоматического повтора. Request ID и Trace ID остаются полями
корреляции и не используются как Prometheus labels.
Если внешнее действие завершилось, но 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 и Trace ID;
- статус;
- HTTP status code внешнего API;
- время выполнения;
- краткий preview запроса и ответа;
- execution stage, error code, retryability и certainty, если вызов завершился
ошибкой.
Previews проходят единый redaction/size limit до записи в PostgreSQL, Admin API
и CSV. Raw credentials, arbitrary headers/body и неограниченный текст ошибки не
являются частью Invocation History. Legacy-записи могут иметь пустые typed
execution fields; новые записи должны содержать Request ID и Trace ID.
Admin Logs API поддерживает workspace-scoped фильтры `period`, `level`,
`status`, `source`, `operation_id`, `agent_id`, `search` и deterministic opaque
cursor pagination. Detail view показывает те же безопасные поля и не раскрывает
foreign-scope metadata через counts или разные формы ошибок. CSV export
использует те же filters/auth/redaction и дополнительно экранирует
spreadsheet-formula cells.
При обновлении опубликованного 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;
- распределение вызовов по операциям;
- распределение по Agent;
- outcome groups `success`, `upstream`, `client`, `schema`, `crank`.
Периоды считаются в UTC как half-open interval `[start, end)`, поэтому запись
на правой границе периода не дублируется в соседнем окне. Retention удаляет
только старые Invocation History rows по typed observable outcome. Cleanup
сохраняет публичный usage horizon последних 90 дней даже при более агрессивном
requested cutoff и возвращает typed policy/outcome (`requested_cutoff`,
`effective_cutoff`, deleted count, preserved window). Published Version audit,
Agent snapshots и immutable release evidence не являются объектами retention
cleanup.
## Для чего это нужно
- проверить, вызывают ли агенты нужные инструменты;
- увидеть ошибки маппинга или внешнего API;
- найти медленные endpoint-ы;
- понять, какие инструменты реально используются.