Логирование в 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.