[observability][follow-up #355] Метрики collab-цикла и MCP: load/lifecycle/connect/per-tool — слепые зоны, вскрытые инцидентом per-cell правок (#400) #402

Closed
opened 2026-07-07 01:44:19 +03:00 by agent_vscode · 0 comments
Collaborator

Суть

Расследование инцидента с per-cell правками MCP (#400) опёрлось на два числа из контура #355collab_store_duration_seconds (~1.48 с) и nodejs_eventloop_lag_max (спайки 1.3–1.7 с) — но вся остальная механика восстанавливалась чтением кода и гипотезами, потому что цикл connect → auth → load → sync → diff и жизненный цикл документов hocuspocus не наблюдаемы:

  • меряем только store-половину цикла; onLoadDocument (чтение ~400 КБ + двойная перекодировка) — невидим;
  • главный нелинейный механизм инцидента — трэшинг «выпал из 10-секундного дебаунса → store+unload+reload на каждую правку» — статистически не детектируем: нет ни gauge открытых доков, ни счётчиков load/unload;
  • 25-секундные connect-таймауты живут только в логах — гонка connect-vs-unload из отчёта так и останется гипотезой, пока нет счётчика для корреляции;
  • MCP-инструменты неразличимы: все вызовы под одним route-лейблом, table_update_cell и get_outline — одно и то же;
  • нет размера документа как дименсии — не отличить «болеют большие страницы» от «болеют все».

Без этих метрик фиксы #400 (кэш CollabSession) и #399 (resolve в очередь) будут проверяться вслепую.

Решение: 6 дополнений в существующий registry

Все имена — в духе контракта #355; всё за существующим гейтом METRICS_PORT (не задан → ни коллекторов, ни листенера).

1. collab_doc_load_duration_seconds (histogram, labels: size_bucket)

Инструментировать onLoadDocument в persistence.extension.ts (чтение из БД + Y.applyUpdate). Вторая половина цикла, симметрично store.

2. Жизненный цикл документов: collab_docs_open (gauge) + collab_doc_loads_total / collab_doc_unloads_total (counters)

Хуки afterLoadDocument / afterUnloadDocument (extension или collaboration.gateway.ts). Gauge не инкрементами, а collect()-колбэком из hocuspocus.documents.size — не дрейфует при ошибках/рестартах. Трэшинг-детектор: rate(collab_doc_loads_total) ≈ rate(collab_doc_unloads_total) при стабильно малом collab_docs_open — та самая пила load→store→unload.

3. collab_connect_duration_seconds (histogram) + collab_connect_timeouts_total (counter)

Серверная длительность: onConnect → отправлена начальная синхронизация (хуки hocuspocus). Счётчик таймаутов — на MCP-стороне (25-секундный таймер в packages/mcp collaboration.ts:198): packages/mcp не должен зависеть от prom-client → опциональный колбэк-интерфейс onMetric?(name, value) в конструкторе клиента, который хост (mcp.service мост) подключает к registry. Standalone-режим MCP колбэк не передаёт — метрик нет, поведение не меняется.

4. collab_auth_duration_seconds (histogram)

Вокруг onAuthenticate в authentication.extension.ts — 4–5 DB-запросов на каждое подключение, сейчас растворены в общем DB-агрегате.

5. mcp_tool_duration_seconds{tool} (histogram)

В точке диспатча инструментов in-process MCP-хоста (мост в server). Кардинальность bounded: лейбл — имя из реестра зарегистрированных инструментов, всё вне реестра → other.

6. Дименсия размера: label size_bucket на load (п. 1) и на существующий collab_store_duration_seconds

Фиксированные бакеты: lt64k | lt256k | lt1m | ge1m (по размеру ydoc-стейта в байтах). Кардинальность ×4. Функция бакетирования — одна, разделяемая обоими местами.

Дашборд (DevOps, после выката)

Четыре панели: load p95 по size_bucket; docs_open + rate(loads/unloads) (трэшинг-детектор); connect p95 + timeouts_total; MCP p95 по tool. Алерт на трэшинг — не сразу: сначала неделю посмотреть на живой паттерн, потом порог (урок queue-growing: правило по живым данным, а не по интуиции).

Границы изменения

apps/server (metrics registry/constants + 3 точки инструментирования в collaboration) + packages/mcp (опциональный onMetric-колбэк, нейтральный интерфейс без зависимостей). Поведение фич 1:1. Контракт имён — дополнение к таблице #355.

Крайние случаи

  • Кардинальность: tool — из реестра инструментов + other; size_bucket — 4 фиксированных значения; никаких id/имён документов в лейблах (docs by name — НЕ добавлять, это unbounded).
  • collab_docs_open через collect() — читает актуальный размер map на скрейпе; инкременты запрещены (дрейф).
  • packages/mcp в standalone-режиме (внешний процесс): колбэк не передан → no-op; сборка build/* коммитится, проверить пересборку.
  • Оверхед: все точки — уже дорогие пути (load/store/connect), +1 observe ничего не меняет; в hot-path per-message ничего не добавляем.
  • Смена лейблов существующей метрики (collab_store_duration_seconds + size_bucket): в VM появится новая серия рядом со старой безлейбловой — запросы дашборда обновить на sum without(size_bucket); старая серия дотлеет за retention.

Тесты / проверка

  • Юнит: функция size_bucket (границы), tool-лейбл (реестр/other), no-op колбэка в standalone.
  • Интеграция: все 6 семейств в curl :9464/metrics после открытия/правки/закрытия документа через MCP-вызов.
  • Прод: панель трэшинга показывает пилу на воспроизведении сценария #400 (серия медленных per-cell правок) — это и есть приёмочный критерий полезности.

Вне скоупа

  • Сам фикс per-cell цикла — #400 (кэш CollabSession); эти метрики — его «до/после».
  • Верификация #399 — те же метрики покроют.
  • Алерт на трэшинг — после недели живых данных.
  • Гонка connect-vs-unload в hocuspocus — если счётчик таймаутов подтвердит корреляцию с unload'ами, отдельная ишью (возможно, апстрим-фикс).

План работ

  1. Registry + константы: 5 новых семейств + size_bucket на store (пп. 1, 2, 4, 6).
  2. Колбэк-интерфейс в packages/mcp + подключение в хосте + mcp_tool_duration_seconds и collab_connect_timeouts_total (пп. 3, 5), пересборка build/*.
  3. Тесты, обновление таблицы контракта в комментарии к #355.
  4. DevOps: 4 панели + правка store-запросов на sum without(size_bucket).
  5. После выката — воспроизведение сценария #400 и фиксация базовой линии для его фикса.
# Суть Расследование инцидента с per-cell правками MCP (#400) опёрлось на два числа из контура #355 — `collab_store_duration_seconds` (~1.48 с) и `nodejs_eventloop_lag_max` (спайки 1.3–1.7 с) — но **вся остальная механика восстанавливалась чтением кода и гипотезами**, потому что цикл `connect → auth → load → sync → diff` и жизненный цикл документов hocuspocus не наблюдаемы: - меряем только **store-половину** цикла; `onLoadDocument` (чтение ~400 КБ + двойная перекодировка) — невидим; - главный нелинейный механизм инцидента — **трэшинг** «выпал из 10-секундного дебаунса → store+unload+reload на каждую правку» — статистически не детектируем: нет ни gauge открытых доков, ни счётчиков load/unload; - 25-секундные **connect-таймауты** живут только в логах — гонка connect-vs-unload из отчёта так и останется гипотезой, пока нет счётчика для корреляции; - **MCP-инструменты неразличимы**: все вызовы под одним route-лейблом, `table_update_cell` и `get_outline` — одно и то же; - нет **размера документа** как дименсии — не отличить «болеют большие страницы» от «болеют все». Без этих метрик фиксы #400 (кэш CollabSession) и #399 (resolve в очередь) будут проверяться вслепую. # Решение: 6 дополнений в существующий registry Все имена — в духе контракта #355; всё за существующим гейтом `METRICS_PORT` (не задан → ни коллекторов, ни листенера). ## 1. `collab_doc_load_duration_seconds` (histogram, labels: `size_bucket`) Инструментировать `onLoadDocument` в [persistence.extension.ts](apps/server/src/collaboration/extensions/persistence.extension.ts) (чтение из БД + `Y.applyUpdate`). Вторая половина цикла, симметрично store. ## 2. Жизненный цикл документов: `collab_docs_open` (gauge) + `collab_doc_loads_total` / `collab_doc_unloads_total` (counters) Хуки `afterLoadDocument` / `afterUnloadDocument` (extension или [collaboration.gateway.ts](apps/server/src/collaboration/collaboration.gateway.ts)). **Gauge не инкрементами, а `collect()`-колбэком** из `hocuspocus.documents.size` — не дрейфует при ошибках/рестартах. Трэшинг-детектор: `rate(collab_doc_loads_total) ≈ rate(collab_doc_unloads_total)` при стабильно малом `collab_docs_open` — та самая пила load→store→unload. ## 3. `collab_connect_duration_seconds` (histogram) + `collab_connect_timeouts_total` (counter) Серверная длительность: `onConnect` → отправлена начальная синхронизация (хуки hocuspocus). Счётчик таймаутов — на MCP-стороне (25-секундный таймер в `packages/mcp` collaboration.ts:198): `packages/mcp` не должен зависеть от prom-client → **опциональный колбэк-интерфейс** `onMetric?(name, value)` в конструкторе клиента, который хост ([mcp.service](apps/server/src) мост) подключает к registry. Standalone-режим MCP колбэк не передаёт — метрик нет, поведение не меняется. ## 4. `collab_auth_duration_seconds` (histogram) Вокруг `onAuthenticate` в [authentication.extension.ts](apps/server/src/collaboration/extensions/authentication.extension.ts) — 4–5 DB-запросов на каждое подключение, сейчас растворены в общем DB-агрегате. ## 5. `mcp_tool_duration_seconds{tool}` (histogram) В точке диспатча инструментов in-process MCP-хоста (мост в server). Кардинальность bounded: лейбл — имя из реестра зарегистрированных инструментов, всё вне реестра → `other`. ## 6. Дименсия размера: label `size_bucket` на load (п. 1) и на существующий `collab_store_duration_seconds` Фиксированные бакеты: `lt64k | lt256k | lt1m | ge1m` (по размеру ydoc-стейта в байтах). Кардинальность ×4. Функция бакетирования — одна, разделяемая обоими местами. ## Дашборд (DevOps, после выката) Четыре панели: load p95 по size_bucket; docs_open + rate(loads/unloads) (трэшинг-детектор); connect p95 + timeouts_total; MCP p95 по tool. Алерт на трэшинг — **не сразу**: сначала неделю посмотреть на живой паттерн, потом порог (урок queue-growing: правило по живым данным, а не по интуиции). # Границы изменения `apps/server` (metrics registry/constants + 3 точки инструментирования в collaboration) + `packages/mcp` (опциональный `onMetric`-колбэк, нейтральный интерфейс без зависимостей). Поведение фич 1:1. Контракт имён — дополнение к таблице #355. # Крайние случаи - **Кардинальность**: `tool` — из реестра инструментов + `other`; `size_bucket` — 4 фиксированных значения; никаких id/имён документов в лейблах (docs by name — НЕ добавлять, это unbounded). - **`collab_docs_open` через `collect()`** — читает актуальный размер map на скрейпе; инкременты запрещены (дрейф). - **`packages/mcp` в standalone-режиме** (внешний процесс): колбэк не передан → no-op; сборка build/* коммитится, проверить пересборку. - **Оверхед**: все точки — уже дорогие пути (load/store/connect), +1 observe ничего не меняет; в hot-path per-message ничего не добавляем. - **Смена лейблов существующей метрики** (`collab_store_duration_seconds` + `size_bucket`): в VM появится новая серия рядом со старой безлейбловой — запросы дашборда обновить на `sum without(size_bucket)`; старая серия дотлеет за retention. # Тесты / проверка - Юнит: функция size_bucket (границы), tool-лейбл (реестр/`other`), no-op колбэка в standalone. - Интеграция: все 6 семейств в `curl :9464/metrics` после открытия/правки/закрытия документа через MCP-вызов. - Прод: панель трэшинга показывает пилу на воспроизведении сценария #400 (серия медленных per-cell правок) — это и есть приёмочный критерий полезности. # Вне скоупа - Сам фикс per-cell цикла — #400 (кэш CollabSession); эти метрики — его «до/после». - Верификация #399 — те же метрики покроют. - Алерт на трэшинг — после недели живых данных. - Гонка connect-vs-unload в hocuspocus — если счётчик таймаутов подтвердит корреляцию с unload'ами, отдельная ишью (возможно, апстрим-фикс). # План работ 1. Registry + константы: 5 новых семейств + `size_bucket` на store (пп. 1, 2, 4, 6). 2. Колбэк-интерфейс в `packages/mcp` + подключение в хосте + `mcp_tool_duration_seconds` и `collab_connect_timeouts_total` (пп. 3, 5), пересборка build/*. 3. Тесты, обновление таблицы контракта в комментарии к #355. 4. DevOps: 4 панели + правка store-запросов на `sum without(size_bucket)`. 5. После выката — воспроизведение сценария #400 и фиксация базовой линии для его фикса.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#402