/**
* Debug Logger Utility
* Provides detailed method entry/exit logging with timestamps and parameters
* Only active when debug mode is enabled
*/
import { ConfigLoader } from '../config/config-loader.js';
import * as fs from 'fs';
import * as path from 'path';
export class DebugLogger {
private static instance: DebugLogger;
private isDebugMode: boolean;
private config = ConfigLoader.getInstance();
private logStream?: fs.WriteStream;
private logFilePath: string;
private constructor() {
this.isDebugMode = this.config.getConfig().server.debug || false;
// Disable file logging - use stderr/stdout only
this.logFilePath = '';
this.logStream = undefined;
if (this.isDebugMode) {
console.error(`[DEBUG] Logging to stderr (file logging disabled)`);
}
}
/**
* Write a log entry to stderr only
*/
private writeLog(message: string): void {
console.error(message);
}
/**
* Write to log file (disabled - no-op)
*/
private writeToFile(message: string): void {
// File logging disabled - no-op
}
static getInstance(): DebugLogger {
if (!DebugLogger.instance) {
DebugLogger.instance = new DebugLogger();
}
return DebugLogger.instance;
}
/**
* Get formatted timestamp for log entries
*/
private getTimestamp(): string {
const now = new Date();
return `[${now.toISOString()}]`;
}
/**
* Safely stringify parameters, handling circular references and sensitive data
*/
private stringifyParams(params: any): string {
try {
const seen = new WeakSet();
return JSON.stringify(params, (key, value) => {
// Hide sensitive data - but not in debug mode for Authorization
if (key.toLowerCase().includes('password') ||
key.toLowerCase().includes('token') ||
key.toLowerCase().includes('secret')) {
return '[REDACTED]';
}
// Don't redact Authorization in debug mode - we need to see it
if (key.toLowerCase() === 'authorization') {
return value; // Show the actual value for debugging
}
// Handle circular references
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
// Limit string length to avoid log flooding
if (typeof value === 'string' && value.length > 500) {
return value.substring(0, 500) + '... [truncated]';
}
return value;
}, 2);
} catch (error) {
return '[Unable to stringify]';
}
}
/**
* Log method entry with parameters
*/
methodEntry(className: string, methodName: string, params?: any): void {
if (!this.isDebugMode) return;
const timestamp = this.getTimestamp();
const paramStr = params ? this.stringifyParams(params) : 'no parameters';
this.writeLog(`${timestamp} [DEBUG] >>> ENTER ${className}.${methodName}()`);
this.writeLog(`${timestamp} [DEBUG] Parameters: ${paramStr}`);
}
/**
* Log method exit with optional return value
*/
methodExit(className: string, methodName: string, returnValue?: any): void {
if (!this.isDebugMode) return;
const timestamp = this.getTimestamp();
const returnStr = returnValue !== undefined ? this.stringifyParams(returnValue) : 'void';
this.writeLog(`${timestamp} [DEBUG] <<< EXIT ${className}.${methodName}()`);
if (returnValue !== undefined) {
this.writeLog(`${timestamp} [DEBUG] Return: ${returnStr}`);
}
}
/**
* Log important state changes or decisions
*/
logState(className: string, message: string, data?: any): void {
if (!this.isDebugMode) return;
const timestamp = this.getTimestamp();
const dataStr = data ? this.stringifyParams(data) : '';
this.writeLog(`${timestamp} [DEBUG] [${className}] ${message}`);
if (data) {
this.writeLog(`${timestamp} [DEBUG] Data: ${dataStr}`);
}
}
/**
* Log API calls with details
*/
logApiCall(method: string, url: string, headers?: any, body?: any): void {
if (!this.isDebugMode) return;
const timestamp = this.getTimestamp();
this.writeLog(`${timestamp} [DEBUG] === API CALL ===`);
this.writeLog(`${timestamp} [DEBUG] Method: ${method}`);
this.writeLog(`${timestamp} [DEBUG] URL: ${url}`);
if (headers) {
this.writeLog(`${timestamp} [DEBUG] Headers: ${this.stringifyParams(headers)}`);
}
if (body) {
this.writeLog(`${timestamp} [DEBUG] Body: ${this.stringifyParams(body)}`);
}
}
/**
* Log API response
*/
logApiResponse(url: string, status: number, data?: any): void {
if (!this.isDebugMode) return;
const timestamp = this.getTimestamp();
this.writeLog(`${timestamp} [DEBUG] === API RESPONSE ===`);
this.writeLog(`${timestamp} [DEBUG] URL: ${url}`);
this.writeLog(`${timestamp} [DEBUG] Status: ${status}`);
if (data) {
this.writeLog(`${timestamp} [DEBUG] Data: ${this.stringifyParams(data)}`);
}
}
/**
* Log errors with context
*/
logError(className: string, methodName: string, error: any, context?: any): void {
// Always log errors, even in release mode
const timestamp = this.getTimestamp();
const errorMsg1 = `${timestamp} [ERROR] !!! ERROR in ${className}.${methodName}()`;
const errorMsg2 = `${timestamp} [ERROR] Message: ${error.message || error}`;
// Always write to stderr for errors
console.error(errorMsg1);
console.error(errorMsg2);
// Also write to file if in debug mode
if (this.isDebugMode) {
this.writeToFile(errorMsg1);
this.writeToFile(errorMsg2);
if (error.stack) {
const stackMsg = `${timestamp} [ERROR] Stack: ${error.stack}`;
console.error(stackMsg);
this.writeToFile(stackMsg);
}
if (context) {
const contextMsg = `${timestamp} [ERROR] Context: ${this.stringifyParams(context)}`;
console.error(contextMsg);
this.writeToFile(contextMsg);
}
}
}
/**
* Check if debug mode is enabled
*/
isDebugEnabled(): boolean {
return this.isDebugMode;
}
/**
* Get the current log file path
*/
getLogFilePath(): string | undefined {
return this.isDebugMode ? this.logFilePath : undefined;
}
/**
* Clean up and close the log file (disabled - no-op)
*/
close(): void {
// File logging disabled - no-op
}
}
// Export singleton instance for convenience
export const debugLog = DebugLogger.getInstance();
// Ensure cleanup on process exit
process.on('exit', () => {
debugLog.close();
});
process.on('SIGINT', () => {
debugLog.close();
process.exit();
});
process.on('SIGTERM', () => {
debugLog.close();
process.exit();
});