Часть 2: Обработка ошибок в Node.js - Логгирование, мониторинг и отладка

mr. Cooper 5 часов назад Веб-разработка
Часть 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

Ключевые выводы:

  1. Используйте структурированное логирование (pino/winston) - JSON-логи спасут при поиске

  2. Логируйте контекст ошибки - порядок ID, пользователь, время

  3. Добавьте correlation ID - чтобы видеть всю цепочку запроса

  4. Настройте APM (Sentry/NewRelic/Datadog) - ошибки не будут теряться

  5. Санитайзьте логи - пароли и токены не должны попадать в логи

  6. Обрезайте стеки в продакшене - но сохраняйте их для логов

  7. Используйте diagnostics_channel для отладки в разработке

  8. Валидируйте входные данные - избегайте ошибок из-за неверного ввода

  9. Используйте схемы (Joi/Zod) для декларативной валидации

  10. Настройте rate limiting для защиты от DoS

Комментарии

Пока нет комментариев. Будьте первым, кто напишет.

Чтобы оставить комментарий, войдите в аккаунт.

Похожие статьи