streamflow/backend/utils/logger.js

125 lines
3.7 KiB
JavaScript
Raw Normal View History

const winston = require('winston');
const path = require('path');
const fs = require('fs');
// Ensure logs directory exists
const logsDir = path.join(__dirname, '../../logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true, mode: 0o755 });
}
/**
* Custom format to sanitize sensitive data from logs
* Removes passwords, tokens, secrets, and other sensitive information
*/
const sanitizeFormat = winston.format((info) => {
// Convert info to string for pattern matching
const infoStr = JSON.stringify(info);
// Patterns to redact
const sensitivePatterns = [
{ pattern: /"password"\s*:\s*"[^"]*"/gi, replacement: '"password":"[REDACTED]"' },
{ pattern: /"token"\s*:\s*"[^"]*"/gi, replacement: '"token":"[REDACTED]"' },
{ pattern: /"secret"\s*:\s*"[^"]*"/gi, replacement: '"secret":"[REDACTED]"' },
{ pattern: /"apiKey"\s*:\s*"[^"]*"/gi, replacement: '"apiKey":"[REDACTED]"' },
{ pattern: /"api_key"\s*:\s*"[^"]*"/gi, replacement: '"api_key":"[REDACTED]"' },
{ pattern: /"authorization"\s*:\s*"Bearer\s+[^"]*"/gi, replacement: '"authorization":"Bearer [REDACTED]"' },
{ pattern: /"privateKey"\s*:\s*"[^"]*"/gi, replacement: '"privateKey":"[REDACTED]"' },
{ pattern: /"private_key"\s*:\s*"[^"]*"/gi, replacement: '"private_key":"[REDACTED]"' }
];
let sanitized = infoStr;
sensitivePatterns.forEach(({ pattern, replacement }) => {
sanitized = sanitized.replace(pattern, replacement);
});
try {
return JSON.parse(sanitized);
} catch (e) {
return info; // Return original if parsing fails
}
});
/**
* Production format: Structured JSON logs without sensitive data
*/
const productionFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
sanitizeFormat(),
winston.format.json()
);
/**
* Development format: Human-readable with colors
*/
const developmentFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
sanitizeFormat(),
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(meta).length > 0) {
msg += ` ${JSON.stringify(meta, null, 2)}`;
}
return msg;
})
);
const isProduction = process.env.NODE_ENV === 'production';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug'),
format: productionFormat,
defaultMeta: { service: 'streamflow-iptv' },
transports: [
// Error logs - separate file for errors only
new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5,
tailable: true
}),
// Combined logs - all levels
new winston.transports.File({
filename: path.join(logsDir, 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 5,
tailable: true
})
],
// Don't exit on uncaught exceptions
exitOnError: false
});
// Console transport for development
if (!isProduction) {
logger.add(new winston.transports.Console({
format: developmentFormat,
handleExceptions: true
}));
}
// Security audit log helper
logger.security = (action, details) => {
logger.info('SECURITY_EVENT', {
action,
timestamp: new Date().toISOString(),
...details
});
};
// Performance monitoring helper
logger.performance = (operation, duration, details = {}) => {
logger.info('PERFORMANCE', {
operation,
duration_ms: duration,
timestamp: new Date().toISOString(),
...details
});
};
module.exports = logger;