Skip to main content
Glama
logger.util.ts11.1 kB
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 };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AbdallahAHO/lokalise-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server