Observability
Сторінка описує, як бекенд пише логи, що потрапляє в Sentry, як влаштовані liveness і readiness і чого в шаблоні поки немає (метрики). Змінні оточення Sentry перелічені в Конфігурації, обробка помилок HTTP — у Життєвому циклі запиту, Sentry-аргументи збірки фронтенду — у Деплої.
Логування
Логер — pino 10, єдиний екземпляр експортується з logger.ts (імпорт #vendor/utils/logger.js).
- Рівень.
LOG_LEVEL(config/logging.ts):fatal,error,warn,info(за замовчуванням),debug,traceабоsilent, без огляду на регістр. Невідоме значення зупиняє старт (Invalid logging configuration). - Формат. Якщо
APP_ENVдорівнюєprodабоproduction, логи йдуть у stdout як JSON-рядки (destination({ sync: false })). В інших оточеннях використовуєтьсяpino-prettyз кольорами; пакет імпортується динамічно лише поза production. - Потоки.
multistreamмає два потоки: основний (stdout) іsentryStreamрівняerror, який пересилає помилки в Sentry (див. нижче). - Серіалізатори. Додано лише серіалізатор
bigint→ рядок. - Redaction. Опції pino
redactнемає. Маскування робиться вручну: хелпериmaskEmailForLogs/ маскування телефону в mask-sensitive.ts.
Токени в логах
Оскільки автоматичного redact немає, усе, що передано в об'єкт логу, пишеться як є. Відома проблема №3 у code-issues-found-during-docs.md — токени потрапляють у логи. Не логуйте httpData, заголовки чи тіла запитів цілком.
Логер запиту
Для кожного HTTP-запиту і WS-повідомлення створюється дочірній логер logger.child({ requestId }), де requestId — randomUUID(). HTTP-requestId генерується на першому кроці обробки (server.ts), ще до читання тіла, тож його мають і помилки 413/415/422. Коли session_web відновив сесію з користувачем, логер запиту замінюється на child({ userId }); WS-логер отримує userId з даних з'єднання. Він доступний у контролерах як ctx.logger (http-context.ts, ws-context.ts).
Що логується при помилці
handleError у server.ts пише через logger.child({ requestId, userId, route, method }) — кожен запис нижче має ці поля (userId — коли відомий):
| Помилка | Рівень | Поля |
|---|---|---|
PayloadTooLargeError | warn | limit, contentType |
MalformedBodyError | warn | bodyKind (без тіла запиту й тексту помилки парсера) |
UnsupportedMediaTypeError | warn | receivedContentType, allowedKinds |
ValidationError, ParameterValidationError | warn | err |
RedisUnavailableError (відповідь 503) | error | err + контекст; у Sentry не йде (див. нижче) |
| інше (відповідь 500) | error | err + requestId, userId, route, method (контекст) |
Для маршрутів API контекст містить requestId, userId, route.url, route.method; для службового обробника документації маршрутів — лише route і method. userId береться в момент помилки: для 413/415/422 його немає, бо тіло читається й валідується до middleware сесії. Коди відповідей — у Життєвому циклі запиту.
Також у index.ts: uncaughtException логується як error з sentrySkip: true і окремо відправляється через sentryService.captureException; при graceful shutdown викликається sentryService.flush(2000).
Sentry (бекенд)
Ініціалізація
sentry-bootstrap.ts — це перший імпорт у index.ts: він завантажує dotenv/config і викликає sentryService.init(). Конфіг — config/sentry.ts:
- Sentry активний, лише якщо
SENTRY_DSNнепорожній іSENTRY_ENABLEDнеfalse. Інакше всі методи сервісу — no-op. - В
Sentry.initпередаютьсяdsn,environment(SENTRY_ENVIRONMENTабоAPP_ENV),release,tracesSampleRate(дефолт0). Додатковіintegrationsне задаються — діють стандартні з@sentry/node11. - Помилка
Sentry.initне зупиняє застосунок: пишетьсяconsole.warn, Sentry лишається вимкненим.
SENTRY_DSN=https://<public-key>@<org>.ingest.sentry.io/<project-id>
SENTRY_ENVIRONMENT=production
SENTRY_RELEASE=<git-sha>
SENTRY_TRACES_SAMPLE_RATE=0PII і санітизація
У Sentry 11 sendDefaultPii прибрано, тому sentry-service.ts явно вимикає збір через dataCollection: userInfo, cookies, httpBodies: [], genAI inputs/outputs, databaseQueryData, queues, stackFrameVariables — усе false.
Додатково event processor sanitizeEvent:
- видаляє з
tags,extra,contextsключі, що збігаються з/token|secret|password|cookie|auth|api[_-]?key|bearer/i; - видаляє заголовки
authorization,cookieі ті, що збігаються з тим самим regex; - видаляє
request.data, якщоstatus_code >= 400, інакше обрізає його до 10 КБ.
З користувача в Sentry передається лише id (scope.setUser({ id })).
Що потрапляє в Sentry
Логи пересилає sentryStream у logger.ts. Правила:
- Рівень нижче
error(50) ігнорується —warn(4xx уhandleError) у Sentry не йде. sentrySkip: true— пропустити (використовується, коли помилку відправлено вручну). Так само пропускаються записи зerr.type === "RedisUnavailableError": інцидент уже зафіксовано модулем здоров'я Redis, див. Відмова Redis.- Є поле
errабоerror→captureException. - Інакше лише при
sentryCapture: true→captureMessage(msg, "error").
До події додаються теги requestId, route, method і user.id, якщо вони є в записі логу. AppError (об'єкти з _tag) нормалізуються в Error з ім'ям AppError.<tag>.
// повідомлення без exception — лише з явним прапорцем
ctx.logger.error({ sentryCapture: true, orderId }, 'Payment webhook mismatch')
// вже відправлено вручну — не дублювати
logger.error({ err, sentrySkip: true }, 'event uncaughtException')Відмова Redis
Стан основного Redis-клієнта тримає redis-health.ts: healthy або unavailable.
- Одна подія на інцидент. Перехід
healthy → unavailable(перша подіяerror/closeклієнта або першаRedisUnavailableErrorзwithRedis) пише одинlogger.error({ sentryCapture: true }, "Redis unavailable")— це єдина подія Sentry за інцидент. - Поки інцидент триває, модуль пише
warn"Redis still unavailable"не частіше разу на хвилину і раз на 2 с шле фоновийPING. - Відновлення — за першим із сигналів: успішна команда через
withRedis, успішний фоновийPINGабо подіяreadyпісля reconnect. Тоді пишетьсяinfo"Redis recovered"зdownMs, стан стаєhealthy, і наступний збій знову дасть одну подію. - На процес, а не на кластер. Кожен процес має власний стан, тож N інстансів дадуть N подій; групує їх сам Sentry.
- Інші записи не дублюють подію.
sentryStreamпропускає записи, деerr.type === "RedisUnavailableError"(поле, яке пише серіалізатор pino), — одне правило в транспорті замість правок у кожному місці. - Свідоме рішення щодо stdout.
logger.errorізhandleErrorі з login/logout/logoutAllпід час збою лишаються в stdout на кожен запит: це діагностика конкретних запитів, і їхній обсяг обмежений самими запитами. Обмеження «раз на хвилину» діє лише для модуля здоров'я. Якщо обсяг логів стане проблемою, наступний крок — понизити ці записи доwarn, а не прибирати.
Сервіси observability
У app/services/observability є лише sentryService: init, isEnabled, captureException, captureMessage, withScope, flush. Тести — sentry-service.spec.ts.
Sentry (фронтенд)
Фронтенд використовує @sentry/vue (sentry-service.ts, sentry-config.ts). Налаштування задаються на етапі збірки через VITE_SENTRY_DSN, VITE_SENTRY_ENABLED, VITE_SENTRY_ENVIRONMENT (дефолт — Vite MODE), VITE_SENTRY_RELEASE, VITE_SENTRY_TRACES_SAMPLE_RATE, VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE, VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE. Так само, як на бекенді, dataCollection вимикає збір PII. Як передати ці значення в Docker-збірку — у Деплої.
Health check
GET /api/ping (http-routes.ts → MainController.ping у main-controller.ts → mainService.ping у main-service.ts) повертає статичне:
{ "status": "ok" }/api/ping — liveness: підтверджує лише, що процес слухає порт, і нічого не перевіряє.
GET /api/ready — readiness (readiness-service.ts): паралельно SELECT 1 у PostgreSQL (через healthRepository) і PING основного Redis, кожна перевірка з таймаутом READINESS_CHECK_TIMEOUT_MS (за замовчуванням 1000 мс). Обидві відповіли — 200 { "status": "ok" }; інакше 503 { "status": "unavailable" } без деталей, а що саме впало — у warn «Readiness check failed» (failed: ["redis"] тощо). Маршрут без сесії й CSRF; глобальний ліміт за IP діє, але частота healthcheck на порядки нижча за поріг. S3 і SMTP не перевіряються. На /ready налаштований HEALTHCHECK Docker-образу (див. Деплой).
Перевірено наживо: зупинка Redis → 503 за ~0,05 с; зупинка PostgreSQL → 503, /ping далі ok; після повернення БД /ready знову 200.
Обрив з'єднання з PostgreSQL
pg.Pool при обриві простою з'єднання (рестарт, failover) емітить error. Без обробника Node перетворював це на uncaughtException, і сервер зупинявся — знайдено під час feat-030. Тепер db.ts логує подію (PostgreSQL pool: idle client error), пул сам замінює клієнт, а /ready відповідає 503, доки БД недоступна.
Метрики і трасування
- Prometheus/OpenTelemetry-метрик у бекенді немає: серед залежностей package.json є лише
@sentry/node,pino,pino-pretty.@opentelemetry/apiу lockfile — транзитивна залежність, у коді не використовується. - Трасування можливе лише через Sentry performance:
SENTRY_TRACES_SAMPLE_RATE> 0 (дефолт0— трасування вимкнене).
Практика
Читати логи локально. pnpm dev:backend виводить pretty-логи. Щоб знайти всі записи одного запиту, шукайте його requestId. У production (APP_ENV=production) логи — JSON, їх зручно фільтрувати через jq:
docker logs <container> | jq 'select(.level >= 50)'
docker logs <container> | jq 'select(.requestId == "<uuid>")'Перевірити Sentry. Задайте SENTRY_DSN і перезапустіть бекенд, потім викличте в коді (наприклад, тимчасово в обробнику локального маршруту) logger.error({ err: new Error('sentry test') }, 'sentry test') — запис рівня error іде в Sentry. Окремого тестового маршруту немає: мертвий MainController.testSentryError видалено (п. 27 у code-issues-found-during-docs.md).