import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { PACKAGE_NAME } from "./constants.util.js";
/**
* Format a timestamp for logging
* @returns Formatted timestamp [HH:MM:SS]
*/
function getTimestamp(): string {
const now = new Date();
return `[${now.toISOString().split("T")[1].split(".")[0]}]`;
}
/**
* Safely convert object to string with size limits
* @param obj Object to stringify
* @param maxLength Maximum length of the resulting string
* @returns Safely stringified object
*/
function safeStringify(obj: unknown, maxLength = 1000): string {
try {
const str = JSON.stringify(obj);
if (str.length <= maxLength) {
return str;
}
return `${str.substring(0, maxLength)}... (truncated, ${str.length} chars total)`;
} catch {
return "[Object cannot be stringified]";
}
}
/**
* Extract essential values from larger objects for logging
* @param obj The object to extract values from
* @param keys Keys to extract (if available)
* @returns Object containing only the specified keys
*/
function extractEssentialValues(
obj: Record<string, unknown>,
keys: string[],
): Record<string, unknown> {
const result: Record<string, unknown> = {};
keys.forEach((key) => {
result[key] = obj[key];
});
return result;
}
/**
* Format source path consistently using the standardized format:
* [module/file.ts@function] or [module/file.ts]
*
* @param filePath File path (with or without src/ prefix)
* @param functionName Optional function name
* @returns Formatted source path according to standard pattern
*/
function formatSourcePath(filePath: string, functionName?: string): string {
// Always strip 'src/' prefix for consistency
const normalizedPath = filePath.replace(/^src\//, "");
return functionName
? `[${normalizedPath}@${functionName}]`
: `[${normalizedPath}]`;
}
/**
* Check if debug logging is enabled for a specific module
*
* This function parses the DEBUG environment variable to determine if a specific
* module should have debug logging enabled. The DEBUG variable can be:
* - 'true' or '1': Enable all debug logging
* - Comma-separated list of modules: Enable debug only for those modules
* - Module patterns with wildcards: e.g., 'controllers/*' enables all controllers
*
* Examples:
* - DEBUG=true
* - DEBUG=controllers/*,services/aws.sso.auth.service.ts
* - DEBUG=transport,utils/formatter*
*
* @param modulePath The module path to check against DEBUG patterns
* @returns true if debug is enabled for this module, false otherwise
*/
function isDebugEnabledForModule(modulePath: string): boolean {
// Avoid circular dependency by directly checking process.env
const debugEnv = process.env.DEBUG;
if (!debugEnv) {
return false;
}
// If debug is set to true or 1, enable all debug logging
if (debugEnv === "true" || debugEnv === "1") {
return true;
}
// Parse comma-separated debug patterns
const debugPatterns = debugEnv.split(",").map((p) => p.trim());
// Check if the module matches any pattern
return debugPatterns.some((pattern) => {
// Convert glob-like patterns to regex
// * matches anything within a path segment
// ** matches across path segments
const regexPattern = pattern
.replace(/\*/g, ".*") // Convert * to regex .*
.replace(/\?/g, "."); // Convert ? to regex .
const regex = new RegExp(`^${regexPattern}$`);
return (
regex.test(modulePath) ||
// Check for pattern matches without the 'src/' prefix
regex.test(modulePath.replace(/^src\//, ""))
);
});
}
// Generate a unique session ID for this process
const SESSION_ID = crypto.randomUUID();
// Get the package name from constants
const getPkgName = (): string => {
return PACKAGE_NAME;
};
// MCP logs directory setup
const HOME_DIR = os.homedir();
const MCP_DATA_DIR = path.join(HOME_DIR, ".mcp", "data");
const CLI_NAME = getPkgName();
// Create the log file path with session ID
const LOG_FILENAME = `${CLI_NAME}.${SESSION_ID}.log`;
const LOG_FILEPATH = path.join(MCP_DATA_DIR, LOG_FILENAME);
// Flag to track if file logging is available
let FILE_LOGGING_AVAILABLE = false;
// Flag to track if we've attempted to initialize file logging
let FILE_LOGGING_INITIALIZED = false;
// Logger singleton to track initialization
let isLoggerInitialized = false;
// Lazy initialization of file logging
function initializeFileLogging(): void {
if (FILE_LOGGING_INITIALIZED) {
return;
}
FILE_LOGGING_INITIALIZED = true;
try {
// Ensure the MCP data directory exists
if (!fs.existsSync(MCP_DATA_DIR)) {
fs.mkdirSync(MCP_DATA_DIR, { recursive: true });
}
// Write initial log header
fs.writeFileSync(
LOG_FILEPATH,
`# ${CLI_NAME} Log Session\n` +
`Session ID: ${SESSION_ID}\n` +
`Started: ${new Date().toISOString()}\n` +
`Process ID: ${process.pid}\n` +
`Working Directory: ${process.cwd()}\n` +
`Command: ${process.argv.join(" ")}\n\n` +
"## Log Entries\n\n",
"utf8",
);
FILE_LOGGING_AVAILABLE = true;
// Log initialization message only once
if (!isLoggerInitialized) {
console.info(
`[${getTimestamp()}] [INFO] [domains/registry.ts] Logger initialized with session ID: ${SESSION_ID}`,
);
console.info(
`[${getTimestamp()}] [INFO] [domains/registry.ts] Logs will be saved to: ${LOG_FILEPATH}`,
);
isLoggerInitialized = true;
}
} catch (error) {
// File logging failed, but don't crash the process
console.warn(
`Warning: Could not initialize log file at ${LOG_FILEPATH}: ${error}. Logs will only appear in console.`,
);
FILE_LOGGING_AVAILABLE = false;
}
}
/**
* Logger class for consistent logging across the application.
*
* RECOMMENDED USAGE:
*
* 1. Create a file-level logger using the static forContext method:
* ```
* const logger = Logger.forContext('controllers/myController.ts');
* ```
*
* 2. For method-specific logging, create a method logger:
* ```
* const methodLogger = Logger.forContext('controllers/myController.ts', 'myMethod');
* ```
*
* 3. Avoid using raw string prefixes in log messages. Instead, use contextualized loggers.
*
* 4. For debugging objects, use the debugResponse method to log only essential properties.
*
* 5. Set DEBUG environment variable to control which modules show debug logs:
* - DEBUG=true (enable all debug logs)
* - DEBUG=controllers/*,services/* (enable for specific module groups)
* - DEBUG=transport,utils/formatter* (enable specific modules, supports wildcards)
*/
class Logger {
private context?: string;
private modulePath: string;
private static sessionId = SESSION_ID;
private static logFilePath = LOG_FILEPATH;
constructor(context?: string, modulePath = "") {
this.context = context;
this.modulePath = modulePath;
}
/**
* Create a contextualized logger for a specific file or component.
* This is the preferred method for creating loggers.
*
* @param filePath The file path (e.g., 'controllers/aws.sso.auth.controller.ts')
* @param functionName Optional function name for more specific context
* @returns A new Logger instance with the specified context
*
* @example
* // File-level logger
* const logger = Logger.forContext('controllers/myController.ts');
*
* // Method-level logger
* const methodLogger = Logger.forContext('controllers/myController.ts', 'myMethod');
*/
static forContext(filePath: string, functionName?: string): Logger {
return new Logger(formatSourcePath(filePath, functionName), filePath);
}
/**
* Create a method level logger from a context logger
* @param method Method name
* @returns A new logger with the method context
*/
forMethod(method: string): Logger {
return Logger.forContext(this.modulePath, method);
}
private _formatMessage(message: string): string {
return this.context ? `${this.context} ${message}` : message;
}
private _formatArgs(args: unknown[]): unknown[] {
// If the first argument is an object and not an Error, safely stringify it
if (
args.length > 0 &&
typeof args[0] === "object" &&
args[0] !== null &&
!(args[0] instanceof Error)
) {
args[0] = safeStringify(args[0]);
}
return args;
}
_log(
level: "info" | "warn" | "error" | "debug",
message: string,
...args: unknown[]
) {
// Skip debug messages if not enabled for this module
if (level === "debug" && !isDebugEnabledForModule(this.modulePath)) {
return;
}
const timestamp = getTimestamp();
const prefix = `${timestamp} [${level.toUpperCase()}]`;
let logMessage = `${prefix} ${this._formatMessage(message)}`;
const formattedArgs = this._formatArgs(args);
if (formattedArgs.length > 0) {
// Handle errors specifically
if (formattedArgs[0] instanceof Error) {
const error = formattedArgs[0] as Error;
logMessage += ` Error: ${error.message}`;
if (error.stack) {
logMessage += `\n${error.stack}`;
}
// If there are more args, add them after the error
if (formattedArgs.length > 1) {
logMessage += ` ${formattedArgs
.slice(1)
.map((arg) => (typeof arg === "string" ? arg : safeStringify(arg)))
.join(" ")}`;
}
} else {
logMessage += ` ${formattedArgs.map((arg) => (typeof arg === "string" ? arg : safeStringify(arg))).join(" ")}`;
}
}
// Initialize file logging on first use
initializeFileLogging();
// Write to log file if available
if (FILE_LOGGING_AVAILABLE) {
try {
fs.appendFileSync(Logger.logFilePath, `${logMessage}\n`, "utf8");
} catch (err) {
// If we can't write to the log file, disable file logging and log the error to console
FILE_LOGGING_AVAILABLE = false;
console.error(
`Failed to write to log file, disabling file logging: ${err}`,
);
}
}
if (process.env.NODE_ENV === "test") {
console[level](logMessage);
} else {
console.error(logMessage);
}
}
info(message: string, ...args: unknown[]) {
this._log("info", message, ...args);
}
warn(message: string, ...args: unknown[]) {
this._log("warn", message, ...args);
}
error(message: string, ...args: unknown[]) {
this._log("error", message, ...args);
}
debug(message: string, ...args: unknown[]) {
this._log("debug", message, ...args);
}
/**
* Log essential information about an API response
* @param message Log message
* @param response API response object
* @param essentialKeys Keys to extract from the response
*/
debugResponse(
message: string,
response: Record<string, unknown>,
essentialKeys: string[],
) {
const essentialInfo = extractEssentialValues(response, essentialKeys);
this.debug(message, essentialInfo);
}
/**
* Get the current session ID
* @returns The UUID for the current logging session
*/
static getSessionId(): string {
return Logger.sessionId;
}
/**
* Get the current log file path
* @returns The path to the current log file
*/
static getLogFilePath(): string {
return Logger.logFilePath;
}
}
// Only export the Logger class to enforce contextual logging via Logger.forContext
export { Logger };