[observability][follow-up #355] route-лейбл содержит сырые имена ассетов с билд-хэшами (/assets/chunk-*.js) — неограниченная кардинальность #362

Open
opened 2026-07-05 01:19:02 +03:00 by agent_vscode · 1 comment
Collaborator

Суть

Приёмка метрик (#355) выявила нарушение собственного требования «route — только темплейты, bounded cardinality»: в http_request_duration_seconds лейбл route содержит сырые имена статических файлов с билд-хэшами:

route="/assets/chunk-3OPIFGDE-CJOt9nr5.js"
route="/assets/chunk-5ZQYHXKU-B5AklAHY.js"
route="/assets/index-CAbxDtto.js"
route="/assets/excalidraw-menu-DpsI0kFW.js"
... (~25 значений уже сейчас)

Каждый деплой генерирует новый набор хэшированных имён → серия лейблов растёт с каждым билдом навсегда (retention VM — 90 дней, деплоев в день бывает много). Это классическая утечка кардинальности, от которой защищались в API-роутах.

API-роуты при этом корректные (темплейты/статические пути), SSE исключён — проблема только со статикой, которую Fastify отдаёт как wildcard-роут: похоже, для static-хендлера req.routeOptions.url пуст и в лейбл попадает сырой URL (при том что для 404 уже есть схлопывание в unknown).

Что нужно

Схлопывать статику в один темплейт: всё, что отдаёт static-модуль (нет routeOptions.url или путь начинается с /assets/, /brand/, /locales/ и т.п.) → route="/assets/*" (или static). Либо вовсе не наблюдать статику в гистограмме — edge-латентность статики уже измеряет Traefik (traefik_router_request_duration_*).

Прочее из приёмки (не дефекты, для сведения)

  • bullmq_job_duration_seconds регистрируется лениво — серии появляются после первой завершённой джобы. ОК.
  • bullmq_queue_depth{queue="search"} был 1893 после рестартов — разгребается; выражение алерта queue-growing переоценим по живому поведению.
  • Виталы: пайплайн работает (валидный {events:[...]} → строка в client_metrics).
# Суть Приёмка метрик (#355) выявила нарушение собственного требования «route — только темплейты, bounded cardinality»: в `http_request_duration_seconds` лейбл `route` содержит **сырые имена статических файлов с билд-хэшами**: ``` route="/assets/chunk-3OPIFGDE-CJOt9nr5.js" route="/assets/chunk-5ZQYHXKU-B5AklAHY.js" route="/assets/index-CAbxDtto.js" route="/assets/excalidraw-menu-DpsI0kFW.js" ... (~25 значений уже сейчас) ``` Каждый деплой генерирует новый набор хэшированных имён → серия лейблов растёт с каждым билдом навсегда (retention VM — 90 дней, деплоев в день бывает много). Это классическая утечка кардинальности, от которой защищались в API-роутах. API-роуты при этом корректные (темплейты/статические пути), SSE исключён — проблема только со статикой, которую Fastify отдаёт как wildcard-роут: похоже, для static-хендлера `req.routeOptions.url` пуст и в лейбл попадает сырой URL (при том что для 404 уже есть схлопывание в `unknown`). # Что нужно Схлопывать статику в один темплейт: всё, что отдаёт static-модуль (нет `routeOptions.url` или путь начинается с `/assets/`, `/brand/`, `/locales/` и т.п.) → `route="/assets/*"` (или `static`). Либо вовсе не наблюдать статику в гистограмме — edge-латентность статики уже измеряет Traefik (`traefik_router_request_duration_*`). # Прочее из приёмки (не дефекты, для сведения) - `bullmq_job_duration_seconds` регистрируется лениво — серии появляются после первой завершённой джобы. ОК. - `bullmq_queue_depth{queue="search"}` был 1893 после рестартов — разгребается; выражение алерта queue-growing переоценим по живому поведению. - Виталы: пайплайн работает (валидный `{events:[...]}` → строка в `client_metrics`).
Author
Collaborator

Проверил по коду — диагноз по эффекту верный, но механизм другой, и это важно для фикса.

Корень: req.routeOptions.url для ассетов НЕ пуст. @fastify/static зарегистрирован с wildcard: false (static.module.ts#L74), а в этом режиме плагин регистрирует каждый файл из dist как отдельный Fastify-роут при старте. То есть /assets/chunk-3OPIFGDE-CJOt9nr5.js — это и есть «темплейт роута», просто роутов столько, сколько файлов, и их имена меняются каждым билдом. Схлопывание 404→unknown в http-metrics.hook.ts#L13-L16 сюда не срабатывает, потому что роут честно матчится.

Фикс — одна функция (resolveRouteLabel): весь API живёт под /api/, всё остальное (ассеты, локали, иконки, wildcard-рендер *) — статика, чью edge-латентность и так меряет Traefik (traefik_router_request_duration_*):

export function resolveRouteLabel(req: FastifyRequest): string {
  const url = req.routeOptions?.url;
  if (typeof url !== 'string' || url.length === 0) return 'unknown';
  // Every @fastify/static file is its OWN registered route (wildcard: false),
  // and build-hashed filenames change per deploy — unbounded cardinality.
  // Only /api/* templates are bounded; collapse everything else.
  return url.startsWith('/api/') ? url : 'static';
}

Плюс дописать кейс в существующий route-label юнит: ассетный путь → static, /api/pages/:id → без изменений, 404 → unknown. Существующие ~25 мусорных серий сами уйдут за retention (90 дней) — чистить VM руками не нужно.

Проверил по коду — диагноз по эффекту верный, но механизм другой, и это важно для фикса. **Корень:** `req.routeOptions.url` для ассетов НЕ пуст. `@fastify/static` зарегистрирован с `wildcard: false` ([static.module.ts#L74](apps/server/src/integrations/static/static.module.ts#L74)), а в этом режиме плагин **регистрирует каждый файл из dist как отдельный Fastify-роут при старте**. То есть `/assets/chunk-3OPIFGDE-CJOt9nr5.js` — это и есть «темплейт роута», просто роутов столько, сколько файлов, и их имена меняются каждым билдом. Схлопывание 404→`unknown` в [http-metrics.hook.ts#L13-L16](apps/server/src/integrations/metrics/http-metrics.hook.ts#L13-L16) сюда не срабатывает, потому что роут честно матчится. **Фикс — одна функция** (`resolveRouteLabel`): весь API живёт под `/api/`, всё остальное (ассеты, локали, иконки, wildcard-рендер `*`) — статика, чью edge-латентность и так меряет Traefik (`traefik_router_request_duration_*`): ```ts export function resolveRouteLabel(req: FastifyRequest): string { const url = req.routeOptions?.url; if (typeof url !== 'string' || url.length === 0) return 'unknown'; // Every @fastify/static file is its OWN registered route (wildcard: false), // and build-hashed filenames change per deploy — unbounded cardinality. // Only /api/* templates are bounded; collapse everything else. return url.startsWith('/api/') ? url : 'static'; } ``` Плюс дописать кейс в существующий route-label юнит: ассетный путь → `static`, `/api/pages/:id` → без изменений, 404 → `unknown`. Существующие ~25 мусорных серий сами уйдут за retention (90 дней) — чистить VM руками не нужно.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#362