Логи и конфигурация
Урок о том, как сделать логи пригодными для эксплуатации и как правильно настраивать приложение через окружение, не вшивая секреты в репозиторий.
Структурированный лог — это запись в машиночитаемом формате (обычно JSON) с полями вместо вольной строки, которую можно искать, фильтровать и агрегировать.
Локально console.log('user', user) кажется достаточным. Но в продакшене логи читают не глазами в терминале, а системами вроде Loki, Elasticsearch или CloudWatch — и им нужны поля, уровни и контекст, а не «простыня» текста. Параллельно встаёт вопрос конфигурации: один и тот же код должен работать локально, на staging и на проде с разными адресами БД и ключами. Этот урок — про обе задачи сразу, потому что на практике они идут рука об руку.
Зачем это нужно на практике
Представьте ночной инцидент: сервис тормозит. С console.log вы получите разрозненные строки без таймстемпов и без связи между ними. Со структурированными логами вы за секунды отфильтруете записи по requestId, увидите уровень error и поле durationMs, и поймёте, какой запрос завис. Логи — это ваши «глаза» в проде; от их качества напрямую зависит, найдёте вы причину за минуту или за час.
Структурированные логи вместо console.log
У console.log три беды для продакшена: нет уровней, нет машиночитаемого формата, нет единого контекста. Решение — логгер вроде pino или winston, который пишет JSON-строки с полями.
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
});
// вместо console.log('user logged in', userId)
logger.info({ userId, ip: req.ip }, 'user logged in');
// вместо console.error(err)
logger.error({ err, route: req.path }, 'request failed');
На выходе — строки, которые система сбора логов разберёт на поля:
{"level":"info","time":1718900000000,"userId":42,"ip":"10.0.0.5","msg":"user logged in"}
{"level":"error","time":1718900000050,"route":"/api/orders","msg":"request failed"}
Первым аргументом идёт объект с полями, вторым — человекочитаемое сообщение. Так вы потом ищете «все ошибки на маршруте /api/orders за последний час» одним запросом, а не grep-ом по тексту.
Уровни логирования
Уровни позволяют управлять «громкостью» без правки кода: на проде вы держите info, а при разборе инцидента временно включаете debug через переменную окружения. Типовая лестница:
| Уровень | Когда использовать |
fatal | Ошибка, после которой процесс завершается |
error | Операция провалилась, но сервис жив |
warn | Подозрительно, но не сломалось (деградация, ретрай) |
info | Бизнес-события: старт сервиса, вход пользователя |
debug | Подробности для отладки, на проде обычно выключены |
Главное правило: уровень отражает серьёзность для эксплуатации, а не «насколько мне интересно». В error попадает то, на что должен среагировать дежурный; в debug — то, что нужно лишь при глубоком разборе.
Конфигурация через переменные окружения
Настройки, которые меняются от среды к среде (адрес БД, порт, ключи), нельзя зашивать в код. Их место — переменные окружения. Node читает их из process.env.
const config = {
port: Number(process.env.PORT) || 3000,
databaseUrl: process.env.DATABASE_URL,
logLevel: process.env.LOG_LEVEL || 'info',
};
// валидируйте обязательное при старте — лучше упасть сразу
if (!config.databaseUrl) {
throw new Error('DATABASE_URL is required');
}
Локально удобно держать переменные в файле .env и подгружать их пакетом dotenv. Критично: сам .env добавляют в .gitignore, а в репозиторий кладут .env.example без значений — как шаблон.
# .env (локально, НЕ в git)
PORT=3000
DATABASE_URL=postgres://localhost:5432/app
LOG_LEVEL=debug
# запуск с переопределением переменной
LOG_LEVEL=debug node server.js
Принципы 12-factor
Методология 12-factor app формулирует, как строить сервисы для облака. Для Node особенно важны три фактора:
- Config (III). Конфигурация — в окружении, а не в коде. Один и тот же артефакт деплоится в любую среду, отличается только набором переменных.
- Logs (XI). Приложение пишет логи в
stdoutкак поток событий и не заботится о файлах и ротации — этим занимается инфраструктура (Docker, systemd, агрегатор). - Dev/prod parity (X). Среды максимально похожи; различия — только в значениях переменных, а не в логике.
Практический вывод: ваш Node-процесс должен просто писать JSON-логи в stdout и читать конфиг из process.env. Куда эти логи поедут и где хранятся секреты — забота окружения, а не кода.
Секреты не в коде
Пароли, токены API, приватные ключи нельзя коммитить в git ни при каких условиях. Однажды попавший в историю секрет считается скомпрометированным навсегда — историю читают и боты, и подрядчики, и форки.
- Секреты передавайте через переменные окружения, а на проде — через хранилище секретов (Docker/K8s secrets, Vault, параметры облака).
- Добавьте
.envв.gitignoreдо первого коммита. - Если секрет всё же утёк в репозиторий — недостаточно удалить строку: его нужно отозвать и перевыпустить, потому что он остаётся в истории git.
- Не печатайте секреты в логи. Логгеры позволяют настроить «редактирование» чувствительных полей.
// pino умеет вырезать секреты из логов
const logger = pino({
redact: ['req.headers.authorization', 'password', '*.token'],
});
Как это работает под капотом
Переменные окружения — это пары «ключ-значение», которые ОС передаёт процессу при запуске; Node при старте копирует их в объект process.env, где все значения — строки (поэтому PORT приходится приводить через Number(...)). Дочерние процессы наследуют окружение родителя — на этом и держатся dotenv и менеджеры процессов.
Структурированный логгер вроде pino работает быстро потому, что не форматирует красивый текст в основном потоке: он сериализует объект в JSON и пишет строку в stdout, а тяжёлую обработку (форматирование для человека, отправку в агрегатор) выносит в отдельный процесс-транспорт. Запись в stdout — это просто поток, который ОС или Docker перенаправляет куда нужно: в файл, в журнал systemd или в сетевой сборщик. Именно поэтому 12-factor советует не открывать файлы логов из кода — вы лишаете инфраструктуру возможности управлять этим потоком.
Частые ошибки
console.logв проде. Нет уровней и полей — логи невозможно фильтровать и агрегировать.- Секреты в коде или в
.envпод git. Компрометация; забыли.gitignore— секрет навсегда в истории. - Логирование чувствительных данных. Токен или пароль попал в лог — это утечка; настройте redaction.
- Запись логов в файлы из кода. Ломает 12-factor: оставьте поток
stdoutинфраструктуре. - Нет валидации конфига на старте. Пропущенный
DATABASE_URLвсплывёт не при запуске, а на первом запросе.
Итоги
- Пишите структурированные JSON-логи с полями и уровнями, а не вольный текст через
console.log. - Уровень отражает серьёзность для эксплуатации:
error— для дежурного,debug— для глубокого разбора. - Конфигурацию держите в переменных окружения и валидируйте обязательные значения при старте.
- Следуйте 12-factor: config в окружении, логи в
stdout, паритет сред. - Секреты никогда не коммитьте; утёкший секрет отзывайте и перевыпускайте, чувствительные поля вырезайте из логов.