Skip to content

Instantly share code, notes, and snippets.

@RyadPasha
Last active January 3, 2024 22:07
Show Gist options
  • Save RyadPasha/4d2d86c66e1c33be3252933dac0b1116 to your computer and use it in GitHub Desktop.
Save RyadPasha/4d2d86c66e1c33be3252933dac0b1116 to your computer and use it in GitHub Desktop.
Logger class that wraps 'winston' and provides custom logging capabilities with different log levels.
/**
* Logger class that wraps 'winston' and provides custom
* logging capabilities with different log levels.
*
* @class Logger
* @author Mohamed Riyad <m@ryad.dev>
* @link https://RyadPasha.com
* @copyright Copyright (C) 2024 RyadPasha. All rights reserved.
* @license MIT
* @version 1.0.2-2024.01.03
* @see {@link https://github.com/winstonjs/winston} for more information on Winston
* @see {@link https://gist.github.com/RyadPasha/4d2d86c66e1c33be3252933dac0b1116} for updates
*/
import CircularJSON from 'circular-json'
import winston, { format, transports } from 'winston'
import DailyRotateFile from 'winston-daily-rotate-file'
class Logger {
private static instance: Logger
private logger: winston.Logger
private maxFileSize: number = 5242880 // 5MB
private maxFiles: string = '14d' // Keep logs for 14 days
private validLevels: string[] = ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']
private defaultLevel: string = 'info'
private logLevel: string = process.env.LOG_LEVEL || 'silly' // Default log level
private consoleLogLevel: string = process.env.CONSOLE_LOG_LEVEL || this.logLevel // Default console log level
private fileLogLevel: string = process.env.FILE_LOG_LEVEL || this.logLevel // Default file log level
/**
* Private constructor for the Logger class.
* Configures the Winston logger with console and file transports.
*/
private constructor() {
// Log format for console with colorization
const consoleLogFormat = format.combine(
format.colorize({ all: true }),
format.timestamp({
format: 'YYYY-MM-DD hh:mm:ss A'
}),
format.printf(({ timestamp, level, message }) => `[${timestamp}]: ${level}: ${message}`)
)
// Log format for files without colorization
const fileLogFormat = format.combine(
format.timestamp({
format: 'YYYY-MM-DD hh:mm:ss A'
}),
format.printf(({ timestamp, level, message }) => `[${timestamp}]: ${level}: ${message}`)
)
/**
* Create and configure a Winston logger with file rotation.
*
* @type {Logger}
*/
this.logger = winston.createLogger({
level: this.logLevel,
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD hh:mm:ss A'
}),
format.printf(({ timestamp, level, message }) => `[${timestamp}]: ${level}: ${message}`)
),
transports: [
new transports.Console({
level: this.consoleLogLevel,
format: consoleLogFormat
}),
new DailyRotateFile({
filename: 'logs/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: this.fileLogLevel,
format: fileLogFormat,
maxFiles: this.maxFiles,
maxSize: this.maxFileSize
}),
new DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
format: fileLogFormat,
maxFiles: this.maxFiles,
maxSize: this.maxFileSize
})
]
})
}
/**
* Get the instance of the Logger class (Singleton pattern).
* If an instance doesn't exist, it creates one; otherwise, returns the existing instance.
*
* @returns {Logger} The Logger instance.
*/
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger()
}
return Logger.instance
}
/**
* Stringify an argument, handling objects (including Errors) and primitives.
*
* @private
* @param {any} arg - The argument to be stringified.
* @returns {string} The stringified representation of the argument.
*/
private stringifyArg(arg: any): string {
if (typeof arg === 'string') {
return arg
} else if (typeof arg === 'object' && arg !== null) {
if (arg instanceof Error) {
// If the argument is an Error object and not the only argument, stringify it
const errorData: Record<string, any> = {}
// Include the non-enumerable properties separately
// 1. the error message
if (arg.message) errorData.message = arg.message
// 2. the stack trace
if (arg.stack) errorData.stack = arg.stack
// Handle custom fields
for (const prop in arg) {
if (Object.prototype.hasOwnProperty.call(arg, prop) && prop !== 'message' && prop !== 'stack') {
errorData[prop] = arg[prop]
}
}
return CircularJSON.stringify(errorData, null, 2)
} else {
return CircularJSON.stringify(arg, null, 2)
}
} else {
return String(arg)
}
}
/**
* Public method to log messages with different log levels.
*
* @param {string} level - The log level ('info', 'warning', 'error', etc.).
* @param {...any[]} args - Arguments to be logged, which will be stringified.
*/
public log(level: string, ...args: any[]) {
const selectedLevel = this.validLevels.includes(level) ? level : this.defaultLevel
const message = args.map((arg) => this.stringifyArg(arg)).join(' ')
this.logger.log(selectedLevel, message)
}
/**
* Log an informational message.
*
* @param {...any[]} args - Arguments to be logged.
*/
public info(...args: any[]) {
this.log('info', ...args)
}
/**
* Log a warning message.
*
* @param {...any[]} args - Arguments to be logged.
*/
public warn(...args: any[]) {
this.log('warn', ...args)
}
/**
* Log an error message.
*
* @param {...any[]} args - Arguments to be logged.
*/
public error(...args: any[]) {
this.log('error', ...args)
}
/**
* Log a debug message.
*
* @param {...any[]} args - Arguments to be logged.
*/
public debug(...args: any[]) {
this.log('debug', ...args)
}
/**
* Log a silly message.
*/
public silly(...args: any[]) {
this.log('silly', ...args)
}
/**
* Log a verbose message.
*/
public verbose(...args: any[]) {
this.log('verbose', ...args)
}
}
export default Logger.getInstance() // Export a single instance of the Logger
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment