0e8f1ca03a
Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
321 lines
19 KiB
Markdown
321 lines
19 KiB
Markdown
# Наблюдаемость
|
||
|
||
## Эксплуатационные журналы 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_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_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 имеют закрытый набор значений.
|
||
Запрещено использовать workspace, идентификаторы агента, операции или
|
||
запроса, фактический URL, текст ошибки, payload и пользовательский текст.
|
||
HTTP route берётся только из шаблона Axum; неизвестные маршруты и методы
|
||
сворачиваются в `unmatched` и `OTHER`.
|
||
|
||
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-ы;
|
||
- понять, какие инструменты реально используются.
|