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

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

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

Начать курс

Введение: почему логирование важно в TypeScript

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

TypeScript, как надстройка над JavaScript, предоставляет строгую типизацию, что позволяет создавать более предсказуемые и безопасные системы логирования. Однако использование простого console.log() в продакшене — это антипаттерн, который приводит к информационному шуму и потере важных данных.

В этой статье мы рассмотрим лучшие практики логирования в TypeScript: от выбора библиотеки до структурирования логов и работы с контекстом.

Выбор библиотеки для логирования

В экосистеме TypeScript/Node.js существует несколько популярных библиотек. Рассмотрим три основных варианта:

  • Winston — наиболее популярная библиотека с гибкой настройкой транспортов (вывод в консоль, файл, базу данных). Поддерживает уровни логирования и форматирование.
  • Pino — современная библиотека, ориентированная на производительность. Идеальна для высоконагруженных систем.
  • Bunyan — менее популярный, но стабильный вариант. Все логгеры выводят данные в JSON-формате.

Для типового приложения на TypeScript мы рекомендуем Winston или Pino. Пример настройки Winston с TypeScript:

import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
  ],
});

logger.info('Сервер запущен', { port: 3000 });

Интеграция с TypeScript: типы и интерфейсы

Чтобы получить максимум от типизации, создайте обёртку для логгера с использованием интерфейсов:

interface LoggerOptions {
  level?: string;
  service?: string;
}

interface ILogger {
  info(message: string, meta?: unknown): void;
  error(message: string, error?: Error): void;
  warn(message: string, meta?: unknown): void;
}

Это позволит вам строго контролировать типы передаваемых данных и избежать ошибок на этапе компиляции.

Уровни логирования и их правильное использование

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

  • error — только для невосстановимых ошибок, требующих внимания разработчика.
  • warn — для потенциально проблемных ситуаций, которые не нарушают работу приложения.
  • info — для ключевых событий: запуск сервера, обработка запроса, успешная авторизация.
  • debug — для отладки в процессе разработки. Не включайте этот уровень в продакшене.

Пример динамического изменения уровня в зависимости от окружения:

const logLevel = process.env.NODE_ENV === 'production' ? 'warn' : 'debug';
const logger = winston.createLogger({ level: logLevel });

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

Вместо строковых сообщений используйте структурированные логи в формате JSON. Это позволяет легко парсить логи с помощью ELK-стека (Elasticsearch, Logstash, Kibana) или других систем.

Обязательно добавляйте контекстную информацию: ID запроса, имя пользователя, название сервиса, временную метку. Пример с использованием Pino:

import pino from 'pino';

const logger = pino({
  level: 'info',
  mixin() {
    return { service: 'my-api' };
  },
});

// Добавление контекста в конкретный лог
logger.info({ requestId: 'abc-123', userId: 42 }, 'Пользователь авторизован');

Для TypeScript также полезно создать middleware для Express или NestJS, который автоматически добавляет requestId в каждый лог:

import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';

function requestLogger(req: Request, res: Response, next: NextFunction) {
  req.requestId = uuidv4();
  logger.info({ requestId: req.requestId, method: req.method, url: req.url }, 'Входящий запрос');
  next();
}

Логирование ошибок и стектрейсов

Одна из частых ошибок — запись ошибки без стектрейса. Всегда передавайте объект ошибки целиком, а не только message:

try {
  // опасный код
} catch (err) {
  // Правильно:
  logger.error('Ошибка при обработке запроса', err);
  // Неправильно:
  logger.error(`Ошибка: ${err.message}`);
}

Используйте встроенную поддержку ошибок в Winston: библиотека автоматически сериализует Error-объекты, сохраняя стек.

Производительность и ротация логов

Для продакшена критично настроить ротацию логов, чтобы файлы не занимали всё дисковое пространство. Winston поддерживает транспорты с ротацией через пакет winston-daily-rotate-file:

import DailyRotateFile from 'winston-daily-rotate-file';

const transport = new DailyRotateFile({
  filename: 'logs/application-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  maxFiles: '14d', // хранить логи 14 дней
});

Также не забывайте про асинхронность: в Pino логирование по умолчанию асинхронное, что минимизирует задержки в обработке запросов.

Заключение

Логирование в TypeScript — это не просто техническая необходимость, а стратегический инструмент для поддержания качества кода и стабильности приложения. Мы рассмотрели ключевые принципы:

  • Выбор подходящей библиотеки (Winston для гибкости, Pino для скорости).
  • Использование уровней логирования для фильтрации шума.
  • Структурированные логи с контекстом для анализа.
  • Корректная обработка ошибок с сохранением стектрейсов.
  • Настройка ротации и производительности.

Начните с малого — замените console.log на структу

Блоги

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