nakama/backend/middleware/logger.js

201 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs');
const path = require('path');
// Создать директорию для логов если её нет
const logsDir = path.join(__dirname, '../logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const getDatePrefix = () => {
return new Date().toISOString().slice(0, 10);
};
const appendLog = (fileName, message) => {
const filePath = path.join(logsDir, fileName);
fs.appendFile(filePath, `${message}\n`, (err) => {
if (err) {
console.error('Ошибка записи в лог файл:', err);
}
});
};
// Цвета для консоли (ANSI коды)
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m'
};
// Эмодзи для уровней
const levelEmojis = {
debug: '🔍',
info: '📝',
success: '✅',
warn: '⚠️',
error: '❌',
security: '🔒'
};
// Функция для логирования
const log = (level, message, data = {}) => {
const timestamp = new Date().toISOString();
const emoji = levelEmojis[level] || '📋';
const logMessage = `[${timestamp}] ${emoji} [${level.toUpperCase()}] ${message}`;
const serializedData = Object.keys(data).length ? ` ${JSON.stringify(data, null, 2)}` : '';
const fullMessage = `${logMessage}${serializedData}`;
// Логирование в консоль с цветами
let colorCode = colors.reset;
if (level === 'error') {
colorCode = colors.red;
console.error(`${colors.bright}${colorCode}${logMessage}${colors.reset}`, data);
} else if (level === 'warn' || level === 'security') {
colorCode = colors.yellow;
console.warn(`${colors.bright}${colorCode}${logMessage}${colors.reset}`, data);
} else if (level === 'success') {
colorCode = colors.green;
console.log(`${colors.bright}${colorCode}${logMessage}${colors.reset}`, data);
} else if (level === 'debug') {
colorCode = colors.dim;
console.log(`${colorCode}${logMessage}${colors.reset}`, data);
} else {
colorCode = colors.cyan;
console.log(`${colorCode}${logMessage}${colors.reset}`, data);
}
const fileName = `${level}-${getDatePrefix()}.log`;
appendLog(fileName, fullMessage);
// Также писать все логи в общий файл
appendLog(`all-${getDatePrefix()}.log`, fullMessage);
};
// Middleware для логирования запросов
const requestLogger = (req, res, next) => {
const start = Date.now();
// Логировать входящий запрос (только для важных роутов)
if (req.path.startsWith('/api/') && !req.path.includes('/health')) {
log('debug', 'Incoming request', {
method: req.method,
path: req.path,
query: req.query,
body: req.method === 'POST' || req.method === 'PUT' ?
(req.body && Object.keys(req.body).length > 0 ?
{ ...req.body, password: req.body.password ? '***' : undefined } :
'empty') : undefined,
ip: req.ip,
userId: req.user?.id || req.user?._id || 'anonymous'
});
}
// Логировать после завершения запроса
res.on('finish', () => {
const duration = Date.now() - start;
const logData = {
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')?.substring(0, 100),
userId: req.user?.id || req.user?._id || 'anonymous'
};
// Пропустить логирование для публичных роутов (health, корневой роут)
if (req.path === '/health' || req.path === '/' || req.path === '/favicon.ico') {
// Логировать только ошибки для публичных роутов
if (res.statusCode >= 400) {
log('error', 'Request failed', logData);
}
return; // Не логировать успешные запросы к публичным роутам
}
if (res.statusCode >= 500) {
log('error', 'Server error', logData);
} else if (res.statusCode >= 400) {
log('warn', 'Client error', logData);
} else if (res.statusCode >= 300 && res.statusCode !== 304) {
// 304 - это нормально (кеш), не логируем
log('info', 'Redirect', logData);
} else {
// Успешный запрос
if (duration > 1000) {
// Медленный запрос
log('warn', 'Slow request', { ...logData, slow: true });
} else {
log('info', 'Request completed', logData);
}
}
});
next();
};
// Логирование подозрительной активности
const logSecurityEvent = (type, req, details = {}) => {
const securityData = {
type,
ip: req.ip,
userAgent: req.get('user-agent'),
path: req.path,
method: req.method,
userId: req.user?.id || 'anonymous',
...details
};
log('warn', 'Security event', securityData);
// В production можно отправить уведомление
const securityMessage = `[${new Date().toISOString()}] [SECURITY] ${type}: ${JSON.stringify(securityData)}`;
appendLog(`security-${getDatePrefix()}.log`, securityMessage);
};
// Дополнительные утилиты для логирования
const logError = (context, error, additionalData = {}) => {
log('error', `Error in ${context}`, {
error: error.message,
stack: error.stack,
name: error.name,
...additionalData
});
};
const logSuccess = (message, data = {}) => {
log('success', message, data);
};
const logDebug = (message, data = {}) => {
// Логировать debug только в development
if (process.env.NODE_ENV === 'development') {
log('debug', message, data);
}
};
// Логирование производительности
const logPerformance = (operation, duration, data = {}) => {
const level = duration > 1000 ? 'warn' : 'info';
log(level, `Performance: ${operation}`, {
duration: `${duration}ms`,
...data
});
};
module.exports = {
log,
logError,
logSuccess,
logDebug,
logPerformance,
requestLogger,
logSecurityEvent
};