Implementation Plan: Verdaccio — логирование в Observer Avito (FA-10360 / FA-10357)

Контекст задачи

Текущее состояние (инвентаризация)

Слой Сейчас Проблема
services/logger.js обёртка над console (debug/info/error + prefix) plain-текст в stdout, без уровней/JSON/request_id, не уходит в Observer
Verdaccio built-in (config.js) log: { type:'stdout', format:'pretty', level:'trace' } pretty, не структурированный
console.* ~60 вызовов в server.js, config.js, plugins/**, scripts/*, duty-cli/* без контекста, без уровня error на падениях STaaS
@avito/logger не установлен
@avito/metrics, node-monitor, Sentry есть метрики и исключения есть, логов нет

Архитектурные решения

  1. Источник истины логов — @avito/logger/server. Сервис пишет структурированный JSON в stdout; PaaS-коллектор забирает его и отдаёт в Observer. Прямых network-вызовов в Observer из кода нет.
  2. Заменить, не дублировать. Кастомный services/logger.js переписывается как тонкая обёртка над @avito/logger (сохраняя forComponent(name) и методы debug/info/error), чтобы не трогать ~30 мест вызова. console.* в бизнес-коде удаляется/заменяется на инстанс логгера.
  3. Verdaccio ≠ чистый express. Точка расширения — register_middlewares(app, auth) в plugins/avito/index.js. requestHandler() и errorHandler(logger) вешаются туда. platform.listen(app) не используется — слушателем остаётся app.listen(3001) в server.js.
  4. Два логгера. Встроенный verdaccio-логгер (config.log) отвечает за ядро registry — его оставляем как есть (либо минимизируем уровень). Бизнес-логика (plugins/repositories/scripts) переводится на @avito/logger.
  5. CommonJS. Используем require('@avito/logger/server'). Перед стартом фазы 1 проверяем exports в package.json пакета на совместимость с CJS.
  6. Scope CLI и разовых скриптов. duty-cli/ и человеко-читаемые scripts/ (кроме cron check-history.js) оставляем как есть — их вывод для человека, не для Observer. check-history.js переводим (крон в проде, app.toml).

Канонический паттерн (референс подтверждён на 8+ сервисах Avito)

// ESM reference; в Verdaccio — CommonJS require
const Logger = require('@avito/logger/server').default;
const { requestHandler } = require('@avito/logger/server');
// app.use([requestHandler(), ...]) в register_middlewares

Карта событий → уровень → поля (спецификация логов)

Поля должны быть вынесены в 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

Событие Уровень Поля
Начало публикации info packageName, version, publisher, isNewVersion
Отклонено валидацией warn packageName, errors[], publisher
Already published warn packageName, existingVersions[]
Блок bx/core warn packageName, publisher, repositoryUrl
Успешный publish (new latest) info packageName, latest, publisher
History record add/update info/debug action, packageName, versions[], publisher, recordIds, status, rejectReason
Unpublish — нет прав warn user, packageName

C. Хранилище STaaS/S3+Mongo — plugins/mongo-minio/package-storage-wrapper.jsглавная боль по эпику

Событие Уровень Поля
readPackage not found debug packageName
readPackage failed error packageName, err.code, err.message, err.stack
createPackage failed (сейчас console.log(err.stack)) error packageName, err, code
savePackage/upsert failed error packageName, err.stack
deletePackage/removePackage failed error packageName, err.stack
updatePackage: not found / normalize / write fail warn / error name, stage, err
Minio/S3-операции (через wrapper) error bucket, endpoint, operation, latency_ms

D. MongoDB-репозитории — repositories/full-packages.js, history-logs.js

Событие Уровень Поля
init коллекций/индексов info collection, duration_ms
Ошибки insert/update/find/delete (assert*) error collection, name, operation, err
historyLogsRepo.find info offset, limit, filter-поля (не весь JSON)

E. Внешние интеграции (RPC/HTTP) — plugins/avito/api/clients/*, helpers/stash.js

Клиент Поля
atlasApi, techWatchApi, paasApiApi endpoint, status, latency_ms, packageName, err
DataBus producer (npmpackage.published) topic, payload-size, success/err
frontend-info-api-composition fetch apiPath (atlas-techwatch/publish-sync), status, data.succeeded, err
LDAP auth (config.js) event (login ok/fail), user, err.code, latency_ms
Stash API endpoint, status, err (заменить 5 console.warn)

F. Cron check-historyscripts/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)

На фичу / фазу (Integration + Documentation)

На релиз (Ship-readiness) — Checkpoint 6

Красный флаг: «работает, я просто не запускал» — это не done. AC + DoD вместе, без исключений под дедлайн.


Фазы и задачи

Phase 0: Spike / снятие неизвестностей (до начала имплементации)

Checkpoint 0

Phase 1: Фундамент — @avito/logger + адаптер

Checkpoint 1

Phase 2: HTTP-слой — request_id и access-логи

Checkpoint 2

Phase 3: Хранилище STaaS (приоритет — главная боль эпика)

Checkpoint 3

Phase 4: Publish-флоу и интеграции

Checkpoint 4

Phase 5: Репозитории, cron и сервер

Checkpoint 5

Phase 6: Валидация на staging + Observer

Checkpoint 6 (готовность к релизу на прод)


Риски и митигации

Риск Impact Митигация
@avito/logger только ESM, конфликт с CommonJS Verdaccio High Spike в Task 0.1 до любого кода; при необходимости — dynamic import или mjs-обёртка
requestHandler() ломает verdaccio-роутинг/auth High Подключать в register_middlewares аккуратно; supertest-тесты на publish/_info/_history сразу в Task 2.1
Логи слишком шумные (level:'trace' built-in + наши) Med Уровень built-in verdaccio-логгера понизить; наши — info по умолчанию, debug только в storage
stdout-формат не совпадает с ожиданиями Observer Med Task 0.2 подтверждает формат; валидация на staging (Task 6.1)
Task 3.3: синтаксис @{token} меняется при апгрейде verdaccio-minio Med Пакет pinned 0.2.5; регрессионный тест читает inventory форм вызова из node_modules
Чувствительные данные в логах (publisher, LDAP user) Med Не логировать пароли/токены; только user.name, packageName; ревью security-агентом
Высокий объём логов (20 реплик в проде) Med Структурировать, не дублировать; access-лог на info, storage-debug на debug
Regressions в publish/STaaS (критичный сервис) High Поэтапная выкатка (staging → шаталити → prod), бекап, существующие метрики 5xx как страховка

Open questions (требуют ответа человека)

  1. Подтверждение канала Observer = stdout+PaaS (Task 0.2).
  2. Версия @avito/logger для пина (актуальная в Avito registry).
  3. Подключать ли @avito/node-platform (даёт errorHandler/serverMetrics из коробки) или обойтись только @avito/logger?
  4. Границы scope: трогаем ли duty-cli/ и прочие scripts/ (по умолчанию — нет, кроме cron).
  5. Нужно ли хранить verdaccio built-in pretty-логи или полностью подавить их в пользу @avito/logger.

Параллелизация