netakhoj-api / services /logger.js
Rakeshops71
Deploy app with LFS for large files
3eedfc9
Raw
History Blame Contribute Delete
7.31 kB
import winston from 'winston';
import DailyRotateFile from 'winston-daily-rotate-file';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import geoip from 'geoip-lite';
import useragent from 'useragent';
import crypto from 'crypto';
import os from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const logsDir = path.join(__dirname, '..', 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const consoleFormat = winston.format.printf(({ level, message }) => {
const emoji = {
error: '❌',
warn: '⚠️',
info: '✅',
http: '🌐',
verbose: '📝',
debug: '🔍',
silly: '💭'
}[level] || 'ℹ️';
return `${emoji} ${message}`;
});
const fileFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
winston.format.errors({ stack: true }),
winston.format.json()
);
const transports = {
combined: new DailyRotateFile({
filename: path.join(logsDir, 'combined-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
format: fileFormat
}),
error: new DailyRotateFile({
filename: path.join(logsDir, 'error-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '30d',
level: 'error',
format: fileFormat
}),
access: new DailyRotateFile({
filename: path.join(logsDir, 'access-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '50m',
maxFiles: '7d',
format: fileFormat
}),
websocket: new DailyRotateFile({
filename: path.join(logsDir, 'websocket-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '7d',
format: fileFormat
}),
security: new DailyRotateFile({
filename: path.join(logsDir, 'security-%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '30d',
format: fileFormat
})
};
const fileLogger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: fileFormat,
transports: [
transports.combined,
transports.error
]
});
const accessLogger = winston.createLogger({
transports: [transports.access]
});
const wsLogger = winston.createLogger({
transports: [transports.websocket]
});
const securityLogger = winston.createLogger({
transports: [transports.security]
});
const consoleLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.colorize(),
consoleFormat
),
transports: [
new winston.transports.Console({
silent: process.env.SILENT === 'true'
})
]
});
class Logger {
static generateFingerprint(req) {
const components = [
req.headers['user-agent'] || '',
req.headers['accept-language'] || '',
req.headers['accept-encoding'] || '',
req.ip || req.connection?.remoteAddress || ''
];
return crypto.createHash('md5').update(components.join('|')).digest('hex');
}
static getClientInfo(req) {
const ip = req.ip || req.connection?.remoteAddress || 'unknown';
const cleanIp = ip.replace('::ffff:', '');
const geo = geoip.lookup(cleanIp);
const agent = useragent.parse(req.headers['user-agent'] || '');
return {
ip: cleanIp,
fingerprint: this.generateFingerprint(req),
userAgent: req.headers['user-agent'] || 'unknown',
browser: agent.toAgent(),
os: agent.os.toString(),
device: agent.device.toString(),
location: geo ? {
country: geo.country,
region: geo.region,
city: geo.city,
timezone: geo.timezone,
coordinates: geo.ll
} : null,
referer: req.headers.referer || null,
sessionId: req.sessionId || null
};
}
static logRequest(req, res, responseTime) {
const clientInfo = this.getClientInfo(req);
accessLogger.info({
type: 'http_request',
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
responseTime: `${responseTime}ms`,
...clientInfo,
timestamp: new Date().toISOString()
});
if (res.statusCode >= 500) {
consoleLogger.error(`${req.method} ${req.originalUrl}${res.statusCode} (${responseTime}ms)`);
} else if (res.statusCode >= 400) {
consoleLogger.warn(`${req.method} ${req.originalUrl}${res.statusCode}`);
} else if (process.env.LOG_REQUESTS === 'true') {
consoleLogger.info(`${req.method} ${req.originalUrl}${res.statusCode}`);
}
}
static logWebSocket(event, ws, data = {}) {
wsLogger.info({
type: 'websocket',
event,
sessionId: ws.sessionId || 'unknown',
ip: ws.clientIp || ws._socket?.remoteAddress || 'unknown',
...data,
timestamp: new Date().toISOString()
});
if (event === 'error') {
consoleLogger.error(`WebSocket error: ${data.error}`);
} else if (process.env.LOG_WS === 'true') {
consoleLogger.info(`WS ${event}: ${ws.sessionId}`);
}
}
static logSecurity(event, req, details = {}) {
const clientInfo = this.getClientInfo(req);
securityLogger.warn({
type: 'security',
event,
...clientInfo,
details,
timestamp: new Date().toISOString()
});
consoleLogger.warn(`Security: ${event} from ${clientInfo.ip}`);
}
static logError(error, context = {}) {
fileLogger.error({
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString()
});
consoleLogger.error(error.message);
}
static info(message, metadata = {}) {
fileLogger.info({ message, ...metadata });
if (process.env.LOG_VERBOSE === 'true') {
consoleLogger.info(message);
}
}
static warn(message, metadata = {}) {
fileLogger.warn({ message, ...metadata });
consoleLogger.warn(message);
}
static debug(message, metadata = {}) {
fileLogger.debug({ message, ...metadata });
if (process.env.DEBUG === 'true') {
consoleLogger.info(`[DEBUG] ${message}`);
}
}
static console(message, type = 'info') {
const messages = {
info: () => consoleLogger.info(message),
warn: () => consoleLogger.warn(message),
error: () => consoleLogger.error(message),
success: () => consoleLogger.info(message)
};
(messages[type] || messages.info)();
}
// Get system info
static getSystemInfo() {
return {
platform: process.platform,
cpus: os.cpus().length,
totalMemory: `${Math.round(os.totalmem() / 1024 / 1024 / 1024)} GB`,
nodeVersion: process.version
};
}
static cleanOldLogs(daysToKeep = 30) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
const logFiles = fs.readdirSync(logsDir);
let deletedCount = 0;
for (const file of logFiles) {
if (file.includes('audit')) continue;
const filePath = path.join(logsDir, file);
const stat = fs.statSync(filePath);
if (stat.mtime < cutoffDate) {
fs.unlinkSync(filePath);
deletedCount++;
}
}
if (deletedCount > 0) {
fileLogger.info(`Cleaned ${deletedCount} old log files`);
}
}
}
export default Logger;