FA-10360 «Verdaccio: Провести рефакторинг обсервабилити сервиса» — пустой шаблон (Backlog). Смысл берётся из родительского эпика FA-10357 (In Progress): «Подключить логирование, метрики и алерты для всех сущностей команды FrontOps».
Драйвер: после переезда на STaaS (S3+Mongo) выросло число проблем, а текущего логирования не хватило для диагностики.
Цель этого плана — только часть про логи: перевести бизнес-логику сервиса на структурированное логирование через @avito/logger, чтобы логи попадали в Observer Avito. Метрики (@avito/metrics/node-monitor) и алерты (alert.yaml) уже есть и в рамках этого плана не переделываются.
Текущее состояние (инвентаризация)
Слой
Сейчас
Проблема
services/logger.js
обёртка над console (debug/info/error + prefix)
plain-текст в stdout, без уровней/JSON/request_id, не уходит в Observer
~60 вызовов в server.js, config.js, plugins/**, scripts/*, duty-cli/*
без контекста, без уровня error на падениях STaaS
@avito/logger
не установлен
—
@avito/metrics, node-monitor, Sentry
есть
метрики и исключения есть, логов нет
Архитектурные решения
Источник истины логов — @avito/logger/server. Сервис пишет структурированный JSON в stdout; PaaS-коллектор забирает его и отдаёт в Observer. Прямых network-вызовов в Observer из кода нет.
Заменить, не дублировать. Кастомный services/logger.js переписывается как тонкая обёртка над @avito/logger (сохраняя forComponent(name) и методы debug/info/error), чтобы не трогать ~30 мест вызова. console.* в бизнес-коде удаляется/заменяется на инстанс логгера.
Verdaccio ≠ чистый express. Точка расширения — register_middlewares(app, auth) в plugins/avito/index.js. requestHandler() и errorHandler(logger) вешаются туда. platform.listen(app)не используется — слушателем остаётся app.listen(3001) в server.js.
Два логгера. Встроенный verdaccio-логгер (config.log) отвечает за ядро registry — его оставляем как есть (либо минимизируем уровень). Бизнес-логика (plugins/repositories/scripts) переводится на @avito/logger.
CommonJS. Используем require('@avito/logger/server'). Перед стартом фазы 1 проверяем exports в package.json пакета на совместимость с CJS.
Scope CLI и разовых скриптов.duty-cli/ и человеко-читаемые scripts/ (кроме cron check-history.js) оставляем как есть — их вывод для человека, не для Observer. check-history.js переводим (крон в проде, app.toml).
Канонический паттерн (референс подтверждён на 8+ сервисах Avito)
Карта событий → уровень → поля (спецификация логов)
Поля должны быть вынесены в structured fields, а не вшиты в message. Минимальный общий набор: timestamp (ставит logger), level, service=verdaccio, component (через forComponent), request_id (для HTTP — из requestHandler).
A. HTTP-запросы (per-request) — plugins/avito/index.js
Событие
Уровень
Доп. поля
Каждый входящий запрос
info
method, path, user
Завершение запроса
info
method, path, status, duration_ms
Необработанная ошибка запроса
error
method, path, err.message, err.stack
B. Publish-флоу — plugins/avito/package-analyzer.js, publish-history.js
F. Cron check-history — scripts/check-history.js (03:23 daily)
Событие
Уровень
Старт/завершение прогона
info (duration_ms, records_processed)
updateUnfinished returns false
error
Ошибка апдейта
error
G. Жизненный цикл сервиса — server.js
Событие
Уровень
Старт (mongo connect, init repos, listen)
info
SIGINT/shutdown
info
Фатал старта (сейчас console.error(e))
fatal + stack
Definition of Done (сквозной бар проекта)
Стоящий барьер качества, который каждая задача проходит перед зачтением — помимо своих acceptance criteria. AC отвечает «построили ли мы нужное?», DoD отвечает «готово ли это к нашему стандарту?». Источник: definition-of-done.md (см. skill planning-and-task-breakdown).
На каждую задачу (Correctness + Quality)
Acceptance criteria задачи выполнены.
Код работает в рантайме (не только «компилируется / typecheck») — проверено запуском/тестом.
Новое поведение покрыто тестом, который падает без изменения и проходит с ним.
Существующие тесты зелёные; регрессий нет (npm test).
Обработаны edge-cases и error-пути, не только happy path (особенно для STaaS-обёрток — ошибки репозитория, упавшие S3-стримы).
Имена и структура выражают намерение; комментарии объясняют почему, а не что.
Нет дублирования логики, мёртвого кода, debug-вывода, закомментированных блоков.
Внутри задачи — только задача; нет побочных рефакторингов.
npm run lint зелёный.
Ни одного console.* не осталось в области задачи (grep -rn "console\." на затронутых файлах пуст).
На фичу / фазу (Integration + Documentation)
Изменение работает в составе системы (verdaccio register_middlewares, service-locator, плагины вместе), не только изолированно.
Совместимость с Verdaccio: плагин-хуки (register_middlewares, storage-плагин) не сломаны; supertest на publish/_info/_history зелёные.
Ни одного секрета в логах — только user.name, packageName; никогда пароли/токены/LDAP-креды.
Backward compat публичного API services/logger.js (forComponent, debug/info/error) сохранён — ~30 call-site'ов не ломаются.
3rd-party зависимости не патчатся без записи в patches/ и веской причины (вариант C — Proxy, а не patch-package).
Public-контракт логов (поля, уровни) зафиксирован в docs/observability.md.
Архитектурное решение (почему @avito/logger/server единственный источник, почему Proxy для minio) записано как ADR в docs/decisions/ (см. skill documentation-and-adrs).
На релиз (Ship-readiness) — Checkpoint 6
Observability на критическом пути в месте: структурированные логи в Observer, существующие метрики/alerts не тронуты.
Rollback-путь существует (изменение только логирования — откат безопасен; метрики/alerts остаются страховкой).
Шаталити на staging (Task 6.1) зелёное; карта событий A–G подтверждена в Observer.
Backup сделан перед prod-роллаутом (требование эпика).
Человек ревьюнёт перед merge/деплоем.
Красный флаг: «работает, я просто не запускал» — это не done. AC + DoD вместе, без исключений под дедлайн.
Фазы и задачи
Phase 0: Spike / снятие неизвестностей (до начала имплементации)
Task 0.1: Проверить совместимость @avito/logger с CommonJS + Node 20.11. Прочитать exports/main/engines в package.json пакета (через docs-mcp/curiosity). Цель: подтвердить, что require('@avito/logger/server') работает и даёт default Logger + requestHandler.
AC: зафиксирован точный импорт и версия; если пакет ESM-only — выбран обходной путь (dynamic import / mjs-обёртка / версионный пин).
Verify: локальный node -e требует пакет и вызывает logger.info('test').
Task 0.2: Подтвердить канал Observer. Проверить, что stdout сервиса в JSON действительно попадает в Observer (через docs-mcp «Observer» / платформу), и какой формат полей ожидается (level, message, request_id).
AC: записан ответ: канал = stdout+PaaS-коллектор; требуемые поля зафиксированы.
Task 0.3: Заполнить FA-10360. Перенести эту карту событий в описание задачи (раздел «Шаги»/«Критерии приёмки») и согласовать с исполнителем.
Checkpoint 0
Решены все архитектурные неизвестные; импорт и канал подтверждены.
План согласован человеком перед кодингом.
Phase 1: Фундамент — @avito/logger + адаптер
Task 1.1: Добавить зависимость.npm i @avito/logger (версию зафиксировать), обновить package.json/package-lock.json. Проверить patches/ (есть patch-package) — не нужен ли патч.
AC: пакет ставится, require('@avito/logger/server') не падает.
Files:package.json, package-lock.json.
Task 1.2: Переписать services/logger.js как обёртку над @avito/logger. Сохранить публичный API: конструктор ({ logger, prefix }), методы debug/info/error, forComponent(name). Внутрь — инстанс @avito/logger; prefix → структурное поле component.
AC: все существующие вызовы (logger.debug(...), logger.forComponent('X')) продолжают работать без правок в местах вызова.
Verify: юнит-тест на services/logger.js (создать services/logger.spec.js): вызывает info('msg') и проверяет, что делегирует в @avito/logger с правильным component.
Локально: logger.info(...) выводит JSON в stdout (видно level, component).
Phase 2: HTTP-слой — request_id и access-логи
Task 2.1: Подключить requestHandler() в plugins/avito/index.js. В register_middlewares добавить app.use(requestHandler()) (перед роутами). Добавить terminal express-middleware, логирующий завершение запроса (status, duration_ms, path, method) на info.
AC: каждый HTTP-запрос логируется с request_id; ошибки запроса логируются на error через errorHandler(logger) (или ручной error-middleware, если @avito/node-platform не подключаем).
Verify: supertest-тест на publish/_info/_history проверяет наличие логов (spy на логгере).
Files:plugins/avito/index.js, новый тест plugins/avito/__tests__/access-log.spec.js.
Size: M.
Task 2.2: Завести единый service-locator.logger и пробросить во все плагины. Убедиться, что package-analyzer, publish-history, mongo-minio, repositories получают логгер из локатора, а не создают свой console.
AC: ни один бизнес-модуль не ссылается на голый console.
Локальный прогон: curl publish/unpublish → в stdout видны структурированные access-логи с request_id.
npm test зелёный.
Phase 3: Хранилище STaaS (приоритет — главная боль эпика)
Task 3.1: plugins/mongo-minio/package-storage-wrapper.js. Заменить console.log(err.stack) (стр. 57) и console.log('diff...') (стр. 134) на logger.error/warn с полями из карты C. В каждый catch добавить структурированный error-лог (packageName, operation, err.code, err.stack).
AC: нет console.* в файле; все ошибки storage логируются на error с контекстом.
Verify: юнит-тесты на readPackage/createPackage/updatePackage с mock-ошибкой репозитория проверяют вызов logger.error с ожидаемыми полями.
Files:package-storage-wrapper.js, тест package-storage-wrapper.spec.js.
Size: M.
Task 3.2: Минио/S3 latency-логи. Обернуть S3-операции (через verdaccio-minio/wrapper) таймингом и логировать долгие/упавшие операции с bucket, endpoint, operation, latency_ms.
AC: медленные S3-операции видны в логах.
Files:plugins/mongo-minio/wrapper.js/index.js.
Size: S.
Task 3.3: Проксировать внутренние логи verdaccio-minio в @avito/logger (вариант C — Proxy/Reflect).verdaccio-minio (3rd-party, pinned 0.2.5) пишет во встроенный логгер verdaccio в 2-арг-формате с шаблонами @{token}: logger.debug({bucket,key}, 'uploading @{key}'), logger.error({error}, 'init failed: @{error}'), logger.debug(_objectSpread({...})). Задача — обернуть @avito/logger в Proxy, который притворяется verdaccio-логгером (парсит @{token}, Error поднимает в err), и передать его в new Minio(..., { logger: proxy }) в plugins/mongo-minio/index.js. Без патча node_modules. POC уже создан: plugins/mongo-minio/verdaccio-logger-proxy.js.
AC:
Внутренние события minio (retry, client-init failure, db.search, token refresh) идут в @avito/logger с component=verdaccio-minio.
logger.error({ error }, '...@{error}') сериализуется как err: { message, stack, code }, а не строка.
Все найденные в node_modules/verdaccio-minio/lib/*.js формы вызова покрыты тестом (регрессионный охранитель при апгрейде пакета).
Verify:
npm test -- verdaccio-logger-proxy — юнит-тест на 4 формы вызова: (fields, '@{k} string'), ('plain string'), ({fields}) без message, ({error}) → проверка err.message/stack/code.
Ручная проверка на staging: в Observer видны retry/[Minio]-сообщения под component=verdaccio-minio.
grep -rn "console\.\|@{" node_modules/verdaccio-minio/lib/*.js сохранён как «источник истины» для форм вызова (читается тестом, а не хардкод).
Dependencies: Task 1.2 (services/logger.js даёт forComponent), Task 2.2 (serviceLocator.logger доступен). Не зависит от 3.1/3.2 — разные поверхности.
Отношение к 3.2: комплементарно, не взаимоисключающе. 3.2 даёт latency_ms на tarball-стримах (минио не эмитит тайминги); 3.3 даёт видимость внутренностей минио (retry/init/db.search), которых 3.2 не видит.
Риски: зависит от стабильности синтаксиса @{token} (митигация — pinned 0.2.5 + регрессионный тест); Proxy get срабатывает на каждое чтение свойства incl. символы — держать handler минимальным.
Checkpoint 3
Все падения storage теперь логируются с контекстом пакета и ошибкой.
Внутренние события verdaccio-minio (Task 3.3) видны под component=verdaccio-minio.
npm test зелёный.
DoD (Correctness+Quality) выполнен для задач 3.1–3.3.
Phase 4: Publish-флоу и интеграции
Task 4.1: package-analyzer.js. Добавить логи из карты B (начало публикации, reject-причины, already published, bx/core блок, success, new latest). Заменить logger.error(error.stack) на структурированный error-лог.
AC: publish/unpublish полностью отслеживаем в логах; reject-причины доступны как поля.
Verify: обновить package-analyzer.spec.js — проверить логи на success и на каждом типе reject.
End-to-end: локальный publish пакета → в логах видна вся цепочка (HTTP → analyzer → storage → external integrations) с общим request_id.
npm test зелёный.
Phase 5: Репозитории, cron и сервер
Task 5.1: repositories/full-packages.js, history-logs.js. Логи init на info; ошибки assert* и DB-операций на error с контекстом (карта D). Заменить logger.info(... JSON.stringify) на поля.
Task 5.2: scripts/check-history.js. Перевести на @avito/logger (сейчас console.log/error). Логи из карты F.
Files:scripts/check-history.js.
Size: XS.
Task 5.3: server.js и config.js. Заменить console.error старта (стр. 17, 64) на logger.fatal/error. Логи жизненного цикла (карта G). Убрать/понизить console.log про htpasswd в config.js:103.
Подтверждение канала Observer = stdout+PaaS (Task 0.2).
Версия @avito/logger для пина (актуальная в Avito registry).
Подключать ли @avito/node-platform (даёт errorHandler/serverMetrics из коробки) или обойтись только @avito/logger?
Границы scope: трогаем ли duty-cli/ и прочие scripts/ (по умолчанию — нет, кроме cron).
Нужно ли хранить verdaccio built-in pretty-логи или полностью подавить их в пользу @avito/logger.
Параллелизация
Последовательно: Phase 0 → 1 → 2 (фундамент, есть зависимости).
Параллельно после Phase 2: Phase 3 (storage), Phase 4 (publish/integrations), Phase 5 (repos/cron/server) можно вести в разных worktree одновременно — разные файлы, общий контракт — логгер из service-locator.