Перейти к содержимому

24. Логирование (pino, winston)

Иллюстрация к уроку

Логирование — глаза и уши твоего сервера. Без логов в production ты слепой.

// ❌ console.log в production
console.log('Пользователь зарегистрировался'); // нет даты, нет уровня
console.log('Error:', error); // нет контекста
console.log(`User ${id} logged in`); // нет структуры
// ✅ Правильное логирование
logger.info({ userId: 42, action: 'register' }, 'Пользователь зарегистрирован');
logger.error({ err, userId: 42 }, 'Ошибка при обновлении профиля');
Окно терминала
npm install pino pino-pretty
const pino = require('pino');
// Создание логгера
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// Красивый вывод в dev, JSON в production
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
});
// Уровни логирования
logger.trace('Очень подробно'); // 10
logger.debug('Для отладки'); // 20
logger.info('Информация'); // 30 (по умолчанию)
logger.warn('Предупреждение'); // 40
logger.error('Ошибка'); // 50
logger.fatal('Критичная ошибка'); // 60
// Структурированные логи (ключевое преимущество)
logger.info({ userId: 42, email: '[email protected]' }, 'Регистрация');
// {"level":30,"time":1705000000,"userId":42,"email":"[email protected]","msg":"Регистрация"}
// Логирование ошибок
try {
await riskyOperation();
} catch (err) {
logger.error({ err }, 'Ошибка операции');
// Pino автоматически сериализует Error (message, stack, type)
}
const pinoHttp = require('pino-http');
// Middleware для автоматического логирования запросов
app.use(pinoHttp({
logger,
// Кастомные поля
customProps: (req) => ({
userId: req.user?.id,
}),
// Уровень по статус-коду
customLogLevel: (req, res, err) => {
if (res.statusCode >= 500 || err) return 'error';
if (res.statusCode >= 400) return 'warn';
return 'info';
},
// Что логировать
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
// НЕ логируем заголовки (могут содержать токены!)
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
}));
// Использование в роутах
app.get('/users', (req, res) => {
req.log.info('Запрос списка пользователей');
// req.log — child логгер с request ID
res.json({ data: [] });
});
Окно терминала
npm install winston
const winston = require('winston');
const { combine, timestamp, json, printf, colorize } = winston.format;
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
// JSON формат для production
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
json()
),
// Транспорты — куда писать логи
transports: [
// Файл для ошибок
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 10 * 1024 * 1024, // 10 МБ
maxFiles: 5, // ротация — 5 файлов
}),
// Файл для всех логов
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 10 * 1024 * 1024,
maxFiles: 10,
}),
],
});
// Красивый вывод в dev
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: combine(
colorize(),
timestamp({ format: 'HH:mm:ss' }),
printf(({ timestamp, level, message, ...meta }) => {
const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : '';
return `${timestamp} ${level}: ${message} ${metaStr}`;
})
),
}));
}
// Использование
logger.info('Сервер запущен', { port: 3000 });
logger.error('Ошибка БД', { error: err.message, query: 'SELECT ...' });
// ✅ ЛОГИРУЙ
logger.info({ userId, action: 'login' }, 'Вход пользователя');
logger.info({ orderId, total }, 'Заказ создан');
logger.warn({ userId, attempts: 5 }, 'Много неудачных попыток входа');
logger.error({ err, userId, requestId }, 'Ошибка обработки запроса');
// ❌ НЕ ЛОГИРУЙ
logger.info({ password: '123' }); // НИКОГДА пароли!
logger.info({ token: 'eyJhb...' }); // НИКОГДА токены!
logger.info({ creditCard: '4111...' }); // НИКОГДА данные карт!
logger.debug('Here 1'); // бесполезно
logger.info(`User: ${JSON.stringify(userObj)}`); // лучше: logger.info({ user })
const { v4: uuid } = require('uuid');
// Middleware добавляет уникальный ID каждому запросу
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || uuid();
res.setHeader('X-Request-ID', req.requestId);
// Child логгер с requestId
req.log = logger.child({ requestId: req.requestId });
next();
});
// Теперь в любом месте обработки можно отследить запрос
app.get('/users', async (req, res) => {
req.log.info('Получение пользователей');
const users = await db.users.findMany();
req.log.info({ count: users.length }, 'Пользователи получены');
res.json({ data: users });
});
// В логах:
// {"requestId":"abc-123","msg":"Получение пользователей"}
// {"requestId":"abc-123","count":42,"msg":"Пользователи получены"}
Pino Winston
────────────────────────── ──────────────────────────
~5x быстрее Медленнее
JSON по умолчанию Гибкие форматы
Минимальный API Богатый API
pino-pretty для dev Встроенное форматирование
pino-http для Express express-winston
Меньше зависимостей Больше фич (ротация, и т.д.)
Рекомендация:
API / Микросервисы → Pino (скорость!)
Традиционные приложения → Winston (гибкость)
  1. Настрой pino логгер с pino-pretty в dev и JSON в production
  2. Подключи pino-http для автоматического логирования запросов
  3. Добавь Request ID middleware для отслеживания запросов
  4. Настрой winston с ротацией файлов и разными уровнями
  5. Создай helper sanitizeLog(obj), который маскирует чувствительные поля