Логирование в TypeScript приложениях: лучшие практики на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Логирование в TypeScript приложениях: лучшие практики на практике

Логирование — это фундаментальная часть любого production-приложения. Оно позволяет не только отслеживать выполнение кода и находить ошибки, но и проводить аудит безопасности, анализировать производительность и отлаживать сложные сценарии. В TypeScript, с его мощной системой типов, подход к логированию становится ещё более гибким и надёжным. В этой статье мы разберём лучшие практики логирования на конкретных примерах.

1. Выбор библиотеки и настройка логгера

Первое, с чего стоит начать — выбрать подходящую библиотеку. Среди популярных решений выделяются winston и pino. Обе поддерживают TypeScript из коробки, но имеют разные приоритеты: winston — гибкость и богатый функционал, pino — максимальная производительность.

Рассмотрим пример настройки winston с типизацией:

import winston from 'winston';

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'user-service' },
  transports: [
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      ),
    }),
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

export default logger;

Для pino конфигурация будет лаконичнее, но не менее эффективной:

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'SYS:standard',
    },
  },
});

export default logger;

Совет: Всегда выносите настройку уровня логирования в переменные окружения (LOG_LEVEL). В продакшене обычно используют info или warn, а в разработке — debug.

2. Структурированное логирование и контекст

Одна из главных ошибок — логировать простые строки. Вместо этого используйте структурированные объекты. Они позволяют легко фильтровать и агрегировать логи в таких системах, как ELK, Grafana или Datadog.

Пример плохого логирования:

logger.info('User logged in successfully');

Пример хорошего логирования с контекстом:

logger.info({
  message: 'User logged in',
  userId: user.id,
  email: user.email,
  loginMethod: 'oauth',
  ip: request.ip,
  duration: Date.now() - startTime,
});

Для удобства можно создать типизированную обёртку:

interface LogContext {
  requestId?: string;
  userId?: string;
  action?: string;
  [key: string]: unknown;
}

function logWithContext(message: string, context: LogContext = {}): void {
  logger.info({ message, ...context });
}

// Использование
logWithContext('Order processed', { 
  orderId: '12345', 
  userId: 'user_678', 
  total: 99.99 
});

Такой подход гарантирует, что каждый лог содержит ключевую информацию для анализа.

3. Логирование ошибок и исключений

Обработка ошибок — самая важная часть логирования. Всегда логируйте полный стек ошибки и контекст, в котором она произошла. Не используйте console.error — он не даёт гибкости для ротации и форматирования.

Правильный паттерн с TypeScript:

import logger from './logger';

async function fetchUserData(userId: string): Promise<UserData> {
  try {
    const response = await api.get(`/users/${userId}`);
    return response.data;
  } catch (error) {
    logger.error({
      message: 'Failed to fetch user data',
      userId,
      error: error instanceof Error ? {
        name: error.name,
        message: error.message,
        stack: error.stack,
      } : error,
    });
    throw error; // или обработать иначе
  }
}

Для глобального перехвата необработанных ошибок и отклонённых промисов добавьте обработчики в точке входа приложения:

process.on('uncaughtException', (error: Error) => {
  logger.fatal({
    message: 'Uncaught Exception',
    error: { name: error.name, message: error.message, stack: error.stack },
  });
  process.exit(1); // Рекомендуется перезапустить процесс
});

process.on('unhandledRejection', (reason: unknown) => {
  logger.error({
    message: 'Unhandled Promise Rejection',
    reason: reason instanceof Error ? reason.message : String(reason),
  });
});

Важно: Логируйте ошибки на том уровне, который соответствует их серьёзности. Используйте error для восстанавливаемых ошибок, fatal — для критических, требующих немедленного вмешательства.

4. Управление производительностью и безопасностью

Логирование может стать узким местом, если не контролировать его объём. Следуйте этим правилам:

  • Не логируйте чувствительные данные. Никогда не записывайте пароли, токены, номера кредитных карт или персональные данные (GDPR/152-ФЗ). Используйте маскировку:
function maskSensitiveData(data: Record<string, unknown>): Record<string, unknown> {
  const sensitiveFields = ['password', 'token', 'secret', 'creditCard'];
  const masked = { ...data };
  for (const field of sensitiveFields) {
    if (masked[field]) {
      masked[field] = '***MASKED***';
    }
  }
  return masked;
}

logger.info(maskSensitiveData({ email: 'test@test.com', password: 'secret123' }));
// Результат: { email: 'test@test.com', password: '***MASKED***' }
  • Используйте асинхронное логирование. В production-среде синхронная запись в файл может заблокировать цикл событий. winston и pino поддерживают асинхронные транспорты.
  • Настройте ротацию логов. Используйте winston-daily-rotate-file или аналоги, чтобы логи не занимали всё дисковое пространство.
  • Контролируйте уровень debug.

Блоги

Рекомендаций по книгам