178 lines
4.7 KiB
JavaScript
178 lines
4.7 KiB
JavaScript
const winston = require('winston');
|
|
const DailyRotateFile = require('winston-daily-rotate-file');
|
|
|
|
// Define log levels and colors
|
|
const logLevels = {
|
|
error: 0,
|
|
warn: 1,
|
|
info: 2,
|
|
http: 3,
|
|
debug: 4,
|
|
};
|
|
|
|
winston.addColors({
|
|
error: 'red',
|
|
warn: 'yellow',
|
|
info: 'green',
|
|
http: 'magenta',
|
|
debug: 'white',
|
|
});
|
|
|
|
// Determine log level based on environment
|
|
const level = () => {
|
|
const env = process.env.NODE_ENV || 'dev';
|
|
const isDevelopment = env === 'dev' || env === 'development';
|
|
return isDevelopment ? 'debug' : process.env.LOG_LEVEL || 'info';
|
|
};
|
|
|
|
// Custom format to extract stack traces from Error objects in metadata
|
|
const extractErrorStack = winston.format((info) => {
|
|
// Check if any metadata value is an Error object and extract its stack
|
|
Object.keys(info).forEach(key => {
|
|
if (info[key] instanceof Error) {
|
|
info[key] = {
|
|
message: info[key].message,
|
|
stack: info[key].stack,
|
|
name: info[key].name
|
|
};
|
|
}
|
|
});
|
|
return info;
|
|
});
|
|
|
|
// Define log format
|
|
const logFormat = winston.format.combine(
|
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
|
|
winston.format.colorize({ all: true }),
|
|
extractErrorStack(),
|
|
winston.format.printf((info) => {
|
|
const { timestamp, level, message, stack, ...metadata } = info;
|
|
let output = `${timestamp} ${level}: ${message}`;
|
|
|
|
// Include relevant metadata in console output
|
|
const metaKeys = Object.keys(metadata).filter(key =>
|
|
!['splat', 'Symbol(level)', 'Symbol(message)'].includes(key) &&
|
|
!key.startsWith('Symbol')
|
|
);
|
|
|
|
if (metaKeys.length > 0) {
|
|
const metaOutput = {};
|
|
metaKeys.forEach(key => {
|
|
// For Error objects, extract message and stack
|
|
if (metadata[key] && metadata[key].stack) {
|
|
metaOutput[key] = { message: metadata[key].message, stack: metadata[key].stack };
|
|
} else {
|
|
metaOutput[key] = metadata[key];
|
|
}
|
|
});
|
|
output += ` ${JSON.stringify(metaOutput, null, 2)}`;
|
|
}
|
|
|
|
// Check for stack trace in the info object itself
|
|
if (stack) {
|
|
output += `\nStack: ${stack}`;
|
|
}
|
|
|
|
return output;
|
|
}),
|
|
);
|
|
|
|
// Define JSON format for file logging
|
|
const jsonFormat = winston.format.combine(
|
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
|
|
winston.format.errors({ stack: true }),
|
|
extractErrorStack(),
|
|
winston.format.json()
|
|
);
|
|
|
|
// Create transports array
|
|
const transports = [
|
|
// Console transport for development
|
|
new winston.transports.Console({
|
|
level: level(),
|
|
format: logFormat,
|
|
}),
|
|
|
|
// Daily rotate file for all logs
|
|
new DailyRotateFile({
|
|
filename: 'logs/application-%DATE%.log',
|
|
datePattern: 'YYYY-MM-DD',
|
|
maxSize: '20m',
|
|
maxFiles: '14d',
|
|
level: 'info',
|
|
format: jsonFormat,
|
|
}),
|
|
|
|
// Daily rotate file for error logs only
|
|
new DailyRotateFile({
|
|
filename: 'logs/error-%DATE%.log',
|
|
datePattern: 'YYYY-MM-DD',
|
|
maxSize: '20m',
|
|
maxFiles: '14d',
|
|
level: 'error',
|
|
format: jsonFormat,
|
|
}),
|
|
];
|
|
|
|
// Create the logger
|
|
const logger = winston.createLogger({
|
|
level: level(),
|
|
levels: logLevels,
|
|
format: jsonFormat,
|
|
transports,
|
|
exceptionHandlers: [
|
|
new winston.transports.File({ filename: 'logs/exceptions.log' })
|
|
],
|
|
rejectionHandlers: [
|
|
new winston.transports.File({ filename: 'logs/rejections.log' })
|
|
],
|
|
});
|
|
|
|
// Create a stream object for Morgan
|
|
logger.stream = {
|
|
write: (message) => {
|
|
// Remove trailing newline from Morgan and log as http level
|
|
logger.http(message.trim());
|
|
},
|
|
};
|
|
|
|
// Add request ID correlation function
|
|
logger.withRequestId = (requestId) => {
|
|
return {
|
|
error: (message, meta = {}) => logger.error(message, { ...meta, requestId }),
|
|
warn: (message, meta = {}) => logger.warn(message, { ...meta, requestId }),
|
|
info: (message, meta = {}) => logger.info(message, { ...meta, requestId }),
|
|
http: (message, meta = {}) => logger.http(message, { ...meta, requestId }),
|
|
debug: (message, meta = {}) => logger.debug(message, { ...meta, requestId }),
|
|
};
|
|
};
|
|
|
|
// Add sanitization helper for sensitive data
|
|
logger.sanitize = (data) => {
|
|
if (typeof data !== 'object' || data === null) {
|
|
return data;
|
|
}
|
|
|
|
const sensitiveFields = [
|
|
'password', 'token', 'secret', 'key', 'authorization', 'cookie',
|
|
'ssn', 'credit_card', 'cvv', 'pin', 'account_number'
|
|
];
|
|
|
|
const sanitized = JSON.parse(JSON.stringify(data));
|
|
|
|
const sanitizeObject = (obj) => {
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const lowerKey = key.toLowerCase();
|
|
if (sensitiveFields.some(field => lowerKey.includes(field))) {
|
|
obj[key] = '[REDACTED]';
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
sanitizeObject(value);
|
|
}
|
|
}
|
|
};
|
|
|
|
sanitizeObject(sanitized);
|
|
return sanitized;
|
|
};
|
|
|
|
module.exports = logger; |