# Наблюдаемость ## Эксплуатационные журналы 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-ы; - понять, какие инструменты реально используются.