Часть 2: Обработка ошибок в Node.js - Логгирование, мониторинг и отладка
Вы поймали ошибку в продакшене. И теперь у вас есть 5 минут, чтобы понять, что случилось, пока пользователь пишет в поддержку. Но ваши логи - это просто console.log в разных местах. Знакомая ситуация?
В этой части мы разберем, как сделать логирование и мониторинг инструментом, а не головной болью. Вы узнаете:
Как использовать структурированное логирование (JSON) вместо
console.logКак добавить correlation ID для трассировки запросов
Как настроить APM (Sentry, New Relic, Datadog)
Как обезопасить логи от конфиденциальных данных
11. Используйте структурированное логгирование (JSON)
Не используйте console.log в продакшене.
// ❌ ПЛОХО
console.log(`User ${user.id} logged in at ${new Date()}`);
// ✅ ХОРОШО: pino (самый быстрый)
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label }),
},
transport:
process.env.NODE_ENV !== 'production'
? {
target: 'pino-pretty',
options: { colorize: true, translateTime: true },
}
: undefined,
});
logger.info({ userId: user.id, ip: req.ip }, 'User logged in');
// ✅ ХОРОШО: winston
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
})
);
}Почему JSON-логи:
Легко парсятся системами (ELK, Loki, Datadog)
Можно фильтровать по любым полям
Все данные структурированы, а не разбросаны по тексту
12. Всегда логируйте контекст ошибки
Ошибка без контекста - как иголка в стоге сена.
// ❌ ПЛОХО
try {
await processOrder(orderId);
} catch (err) {
logger.error(err.message);
}
// ✅ ХОРОШО
try {
await processOrder(orderId);
} catch (err) {
logger.error({
msg: 'Не удалось обработать заказ',
orderId,
userId: req.user.id,
error: err.message,
stack: err.stack,
timestamp: new Date().toISOString(),
correlationId: req.correlationId,
});
throw new OrderProcessingError(orderId, { cause: err });
}13. Используйте correlation ID для трассировки
Чтобы отследить ошибку через всю цепочку запросов.
const { v4: uuidv4 } = require('uuid');
const cls = require('cls-hooked');
const ns = cls.createNamespace('app');
// Мидлвэр для Express
app.use((req, res, next) => {
const correlationId = req.headers['x-correlation-id'] || uuidv4();
req.correlationId = correlationId;
res.setHeader('X-Correlation-ID', correlationId);
ns.run(() => {
ns.set('correlationId', correlationId);
ns.set('req', req);
next();
});
});
// Функция-хелпер для логов
function logWithContext(level, message, meta = {}) {
const correlationId = ns.get('correlationId');
logger[level]({
...meta,
correlationId,
message,
});
}
// Использование
try {
await doSomething();
} catch (err) {
logWithContext('error', 'Что-то сломалось', {
error: err.message,
userId: req.user?.id,
});
}14. Используйте APM для глубокого мониторинга
В продакшене без APM - как без рук.
// New Relic
require('newrelic');
const newrelic = require('newrelic');
try {
await criticalOperation();
} catch (err) {
newrelic.noticeError(err, {
userId: req.user.id,
orderId: orderId,
correlationId: req.correlationId,
});
throw err;
}
// Sentry
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
integrations: [new Sentry.Integrations.Http({ tracing: true })],
});
try {
await dangerousOperation();
} catch (err) {
Sentry.captureException(err, {
extra: {
userId: req.user.id,
data: sanitizeLog(req.body),
correlationId: req.correlationId,
},
});
throw err;
}
// Datadog (dd-trace)
const tracer = require('dd-trace').init({
service: 'my-api',
env: process.env.NODE_ENV,
logInjection: true,
});
tracer.use('express');15. Не логируйте конфиденциальные данные
Эти данные не должны попадать в логи никогда.
// blacklist.js
const SENSITIVE_FIELDS = [
'password',
'secret',
'token',
'key',
'authorization',
'cookie',
'credit_card',
'cvv',
'ssn',
'api_key',
'private_key',
'access_token',
'refresh_token',
'jwt',
];
function sanitizeLog(data) {
if (!data || typeof data !== 'object') return data;
const cleaned = Array.isArray(data) ? [...data] : { ...data };
for (const key of Object.keys(cleaned)) {
const lowerKey = key.toLowerCase();
if (SENSITIVE_FIELDS.some((field) => lowerKey.includes(field))) {
cleaned[key] = '[REDACTED]';
} else if (typeof cleaned[key] === 'object') {
cleaned[key] = sanitizeLog(cleaned[key]);
}
}
return cleaned;
}
// Использование
logger.info({
msg: 'Запрос получен',
body: sanitizeLog(req.body),
headers: sanitizeLog(req.headers),
});16. Обрезайте стеки ошибок в продакшене
class AppError extends Error {
constructor(message, code, statusCode = 500, options = {}) {
super(message, { cause: options.cause });
this.code = code;
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
// В продакшене стек не нужен в ответе клиенту
if (process.env.NODE_ENV === 'production') {
this.stack = undefined;
}
}
}
// Но для логов стек сохраняем
logger.error({
error: err.message,
stack: err.stack,
correlationId: req.correlationId,
});17. Используйте node:diagnostics_channel для отладки
В Node.js 18+ появился встроенный канал для диагностики.
const diagnosticsChannel = require('diagnostics_channel');
// Создаём канал
const errorChannel = diagnosticsChannel.channel('error:captured');
// Подписываемся на все ошибки (только в development)
if (process.env.NODE_ENV !== 'production') {
errorChannel.subscribe((message) => {
console.log('🔍 Диагностика:', {
error: message.error.message,
context: message.context,
timestamp: message.timestamp,
correlationId: message.correlationId,
});
});
}
// В коде
function captureError(error, context = {}) {
const correlationId = ns.get('correlationId');
// Логируем
logger.error({
error: error.message,
stack: error.stack,
...context,
correlationId,
});
// Отправляем в диагностический канал
if (errorChannel.hasSubscribers) {
errorChannel.publish({
error,
context,
correlationId,
timestamp: Date.now(),
pid: process.pid,
});
}
return error;
}
// Использование
try {
await processOrder(orderId);
} catch (err) {
throw captureError(err, { orderId, userId: req.user.id });
}18. Валидируйте входные данные до обработки
Большая часть ошибок - из-за невалидного ввода.
const { body, validationResult } = require('express-validator');
app.post(
'/users',
[
body('email').isEmail().normalizeEmail(),
body('age').isInt({ min: 18, max: 120 }),
body('name').isLength({ min: 2, max: 50 }).trim(),
],
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
const messages = errors.array().map((e) => ({
field: e.param,
message: e.msg,
}));
return next(new ValidationError('Ошибка валидации', null, { errors: messages }));
}
next();
}
);19. Используйте схемы (Joi, Zod) для валидации
const Joi = require('joi');
const userSchema = Joi.object({
id: Joi.number().integer().positive().required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).max(64).required(),
role: Joi.string().valid('admin', 'user', 'guest').default('user'),
});
try {
const { error, value } = userSchema.validate(input, { abortEarly: false });
if (error) {
const messages = error.details.map((d) => ({
field: d.path.join('.'),
message: d.message,
}));
throw new ValidationError('Ошибка валидации', null, { errors: messages });
}
// value - валидированный объект
} catch (err) {
if (err instanceof ValidationError) throw err;
throw new ValidationError('Невалидные данные', null, { cause: err });
}20. Используйте rate limiting для защиты
const rateLimit = require('express-rate-limit');
const { RateLimitError } = require('./errors');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100, // 100 запросов с одного IP
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
throw new RateLimitError('Слишком много запросов, попробуйте позже');
},
});
app.use('/api/', limiter);Итог: части 2
Ключевые выводы:
Используйте структурированное логирование (pino/winston) - JSON-логи спасут при поиске
Логируйте контекст ошибки - порядок ID, пользователь, время
Добавьте correlation ID - чтобы видеть всю цепочку запроса
Настройте APM (Sentry/NewRelic/Datadog) - ошибки не будут теряться
Санитайзьте логи - пароли и токены не должны попадать в логи
Обрезайте стеки в продакшене - но сохраняйте их для логов
Используйте diagnostics_channel для отладки в разработке
Валидируйте входные данные - избегайте ошибок из-за неверного ввода
Используйте схемы (Joi/Zod) для декларативной валидации
Настройте rate limiting для защиты от DoS
Комментарии
Чтобы оставить комментарий, войдите в аккаунт.