Improve logging

This commit is contained in:
Sosthene 2025-09-07 23:23:10 +02:00
parent 06a6b5c7aa
commit 888ca48712

View File

@ -1,3 +1,5 @@
import { inspect } from 'util';
interface LogContext {
requestId?: string;
userId?: string;
@ -9,36 +11,194 @@ interface LogContext {
}
export class Logger {
private static getLogLevel(): string {
return process.env.LOG_LEVEL || (process.env.NODE_ENV === 'development' ? 'debug' : 'info');
}
private static shouldLog(level: string): boolean {
const currentLevel = this.getLogLevel().toLowerCase();
const levels = ['error', 'warn', 'info', 'debug'];
const currentIndex = levels.indexOf(currentLevel);
const messageIndex = levels.indexOf(level.toLowerCase());
return messageIndex <= currentIndex;
}
private static formatMessage(level: string, message: string, context?: LogContext): string {
const timestamp = new Date().toISOString();
const baseLog = {
timestamp,
level,
message,
...context
};
const isDevelopment = process.env.NODE_ENV === 'development';
const supportsColors = this.supportsColors();
return JSON.stringify(baseLog, null, process.env.NODE_ENV === 'development' ? 2 : 0);
// Create a more readable header with level, timestamp, and message
const levelSymbol = this.getLevelSymbol(level);
const levelColor = this.getLevelColor(level, isDevelopment && supportsColors);
const timestampFormatted = (isDevelopment && supportsColors) ?
`\x1b[90m${timestamp}\x1b[0m` : // Gray timestamp in dev
timestamp;
const header = (isDevelopment && supportsColors) ?
`${levelColor}${levelSymbol} [${level}]\x1b[0m ${timestampFormatted} \x1b[1m${message}\x1b[0m` :
`${levelSymbol} [${level}] ${timestamp} ${message}`;
// Format context data if present
if (context && Object.keys(context).length > 0) {
const contextFormatted = inspect(context, {
depth: isDevelopment ? 10 : 6, // Much deeper inspection
colors: isDevelopment && supportsColors,
compact: false,
breakLength: 100,
maxArrayLength: isDevelopment ? 20 : 10, // Show more array elements
maxStringLength: isDevelopment ? 500 : 200, // Longer strings
showHidden: false,
sorted: true,
getters: false,
showProxy: false, // Don't show proxy details
customInspect: true // Use custom inspect methods
});
// Add visual separation
const separator = (isDevelopment && supportsColors) ?
`\x1b[90m${'─'.repeat(80)}\x1b[0m` :
'─'.repeat(80);
return `${header}\n${separator}\n${contextFormatted}`;
}
return header;
}
private static supportsColors(): boolean {
// Check if colors are explicitly disabled
if (process.env.FORCE_COLOR === '0') return false;
if (process.env.NO_COLOR) return false;
if (process.env.TERM === 'dumb') return false;
// Check if colors are explicitly enabled
if (process.env.FORCE_COLOR === '1' || process.env.FORCE_COLOR === '2' || process.env.FORCE_COLOR === '3') return true;
// Check if we're in a TTY
if (process.stdout && !process.stdout.isTTY) return false;
// Check terminal capabilities - be more permissive
if (process.env.TERM && (
process.env.TERM.includes('256color') ||
process.env.TERM.includes('truecolor') ||
process.env.TERM.includes('xterm') ||
process.env.TERM.includes('screen') ||
process.env.TERM.includes('tmux') ||
process.env.TERM.includes('color')
)) return true;
// Check COLORTERM
if (process.env.COLORTERM) return true;
// Default to true for development mode if we have a TTY
return process.env.NODE_ENV === 'development' && process.stdout.isTTY;
}
private static getLevelSymbol(level: string): string {
const symbols = {
'INFO': ' ',
'DEBUG': '🐛',
'WARN': '⚠️ ',
'ERROR': '❌'
};
return symbols[level as keyof typeof symbols] || '📝';
}
private static getLevelPrefix(level: string, supportsColors: boolean): string {
if (supportsColors) {
return '';
}
// Provide visual distinction without colors
const prefixes = {
'ERROR': '>>> ',
'WARN': '>>> ',
'INFO': '>>> ',
'DEBUG': '>>> '
};
return prefixes[level as keyof typeof prefixes] || '>>> ';
}
private static getLevelColor(level: string, isDevelopment: boolean): string {
if (!isDevelopment) return '';
const colors = {
'INFO': '\x1b[36m', // Cyan
'DEBUG': '\x1b[35m', // Magenta
'WARN': '\x1b[33m', // Yellow
'ERROR': '\x1b[31m' // Red
};
return colors[level as keyof typeof colors] || '\x1b[37m'; // White default
}
static info(message: string, context?: LogContext): void {
if (this.shouldLog('info')) {
console.log(this.formatMessage('INFO', message, context));
}
}
static warn(message: string, context?: LogContext): void {
if (this.shouldLog('warn')) {
console.warn(this.formatMessage('WARN', message, context));
}
}
static error(message: string, context?: LogContext): void {
if (this.shouldLog('error')) {
console.error(this.formatMessage('ERROR', message, context));
}
}
static debug(message: string, context?: LogContext): void {
if (process.env.NODE_ENV === 'development') {
if (this.shouldLog('debug')) {
console.debug(this.formatMessage('DEBUG', message, context));
}
}
// Enhanced logging methods that automatically detect and format objects
static logObject(message: string, obj: any, level: 'info' | 'debug' | 'warn' | 'error' = 'info'): void {
const isDevelopment = process.env.NODE_ENV === 'development';
const supportsColors = this.supportsColors();
// Create a more readable object representation
const formattedObj = inspect(obj, {
depth: isDevelopment ? 10 : 6, // Much deeper inspection
colors: isDevelopment && supportsColors,
compact: false, // Always use expanded format for objects
breakLength: 100,
maxArrayLength: isDevelopment ? 20 : 10,
maxStringLength: isDevelopment ? 500 : 200,
showHidden: false,
sorted: true,
getters: false,
showProxy: false,
customInspect: true
});
const context = {
objectType: obj?.constructor?.name || typeof obj,
objectSize: JSON.stringify(obj).length,
formattedObject: formattedObj
};
switch (level) {
case 'info':
this.info(message, context);
break;
case 'debug':
this.debug(message, context);
break;
case 'warn':
this.warn(message, context);
break;
case 'error':
this.error(message, context);
break;
}
}
// Helper for HTTP request logging
static logRequest(req: any, res: any, duration: number): void {
const context = {
@ -78,4 +238,37 @@ export class Logger {
this.error(message, logContext);
}
}
// Helper for logging API responses with better formatting
static logApiResponse(operation: string, response: any, level: 'info' | 'debug' | 'warn' | 'error' = 'info'): void {
const context = {
operation,
responseType: response?.constructor?.name || typeof response,
statusCode: response?.status || response?.statusCode,
hasData: !!response?.data,
dataKeys: response?.data ? Object.keys(response.data) : [],
responseSize: JSON.stringify(response).length
};
// Add the actual response data for detailed inspection
if (process.env.NODE_ENV === 'development') {
context['responseData'] = response;
}
switch (level) {
case 'info':
this.info(`API Response: ${operation}`, context);
break;
case 'debug':
this.debug(`API Response: ${operation}`, context);
break;
case 'warn':
this.warn(`API Response: ${operation}`, context);
break;
case 'error':
this.error(`API Response: ${operation}`, context);
break;
}
}
}