Files
crank/docs/observability.md
T
bsodfather 9b1a739e39
CI / Rust Checks (pull_request) Successful in 6m15s
CI / UI Checks (pull_request) Successful in 5s
CI / Community Image Smoke (pull_request) Successful in 4m25s
CI / Frontend E2E (pull_request) Successful in 5m17s
CI / Deploy (pull_request) Has been skipped
CI / Rust Checks (push) Successful in 6m9s
CI / UI Checks (push) Successful in 5s
CI / Community Image Smoke (push) Successful in 1m3s
CI / Frontend E2E (push) Successful in 3m47s
CI / Deploy (push) Failing after 3s
наблюдаемость: ввести безопасный контракт метрик
2026-07-31 05:04:08 +03:00

338 lines
20 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...",
"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.
Один идентификатор:
- возвращается в `x-request-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`;
- статическое сообщение без исходного текста ошибки.
До отправки удаляются 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_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-вызовов. `x-request-id` остаётся отдельным
идентификатором запроса. Baggage не извлекается и не передаётся.
Экспорт отключён по умолчанию: при пустых
`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` и `OTEL_EXPORTER_OTLP_ENDPOINT`
provider, batch processor и фоновый поток не создаются. После включения
используется только 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.
Если внешнее действие завершилось, но 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-ы;
- понять, какие инструменты реально используются.