Skip to main content
Glama
logger.ts9.8 kB
/** * Centralized Logging System * * Replaces scattered console.log usage with structured, configurable logging. * Integrates with the centralized Config system for environment-aware behavior. */ import { ConfigHelpers } from "@/config/Config.js"; export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; export type LogContext = Record<string, unknown>; export interface LogEntry { readonly timestamp: string; readonly level: LogLevel; readonly message: string; readonly context?: LogContext | undefined; readonly component?: string | undefined; readonly requestId?: string | undefined; readonly siteId?: string | undefined; readonly userId?: string | undefined; } export interface LoggerOptions { readonly component?: string | undefined; readonly context?: LogContext | undefined; readonly siteId?: string | undefined; readonly requestId?: string | undefined; } /** * Log level priorities (higher number = more important) */ const LOG_LEVELS: Record<LogLevel, number> = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5, }; /** * Get minimum log level from configuration */ function getMinLogLevel(): LogLevel { const configInstance = ConfigHelpers.get(); const appConfig = configInstance.get(); const configLevel = appConfig.debug.logLevel.toLowerCase(); return LOG_LEVELS[configLevel as LogLevel] !== undefined ? (configLevel as LogLevel) : "info"; } /** * Check if log level should be output */ function shouldLog(level: LogLevel): boolean { const minLevel = getMinLogLevel(); return LOG_LEVELS[level] >= LOG_LEVELS[minLevel]; } /** * Format log entry for output */ function formatLogEntry(entry: LogEntry): string { const timestamp = entry.timestamp; const level = entry.level.toUpperCase().padEnd(5); const component = entry.component ? `[${entry.component}]` : ""; const siteId = entry.siteId ? `{site:${entry.siteId}}` : ""; const requestId = entry.requestId ? `{req:${entry.requestId.slice(0, 8)}}` : ""; let message = `${timestamp} ${level} ${component}${siteId}${requestId} ${entry.message}`; if (entry.context && Object.keys(entry.context).length > 0) { const contextStr = JSON.stringify(entry.context); message += ` ${contextStr}`; } return message; } /** * Sanitize sensitive data from log context */ function sanitizeContext(context: LogContext): LogContext { const sanitized: LogContext = {}; for (const [key, value] of Object.entries(context)) { const keyLower = key.toLowerCase(); const isSensitive = keyLower.includes("password") || keyLower.includes("secret") || keyLower.includes("token") || keyLower.includes("key") || keyLower.includes("credential"); if (isSensitive) { if (typeof value === "string") { sanitized[key] = value.length > 0 ? `[REDACTED:${value.length}chars]` : "[EMPTY]"; } else if (Array.isArray(value)) { sanitized[key] = "[EMPTY]"; // Redact entire array for sensitive fields } else { sanitized[key] = value; // Keep non-string, non-array values as-is } } else { sanitized[key] = value; } } return sanitized; } /** * Output log entry to appropriate destination */ function outputLog(entry: LogEntry): void { // In test environment, only log errors and fatals if (ConfigHelpers.isTest() && LOG_LEVELS[entry.level] < LOG_LEVELS.error) { return; } // In DXT mode, suppress most logging if (ConfigHelpers.isDXT() && LOG_LEVELS[entry.level] < LOG_LEVELS.warn) { return; } const formatted = formatLogEntry(entry); // Use stderr for all log output to avoid STDIO interference console.error(formatted); } /** * Main Logger Class */ export class Logger { private readonly options: LoggerOptions; constructor(options: LoggerOptions = {}) { this.options = options; } /** * Create child logger with additional context */ child(additionalOptions: LoggerOptions): Logger { return new Logger({ component: additionalOptions.component ?? this.options.component, siteId: additionalOptions.siteId ?? this.options.siteId, requestId: additionalOptions.requestId ?? this.options.requestId, context: { ...this.options.context, ...additionalOptions.context, }, }); } /** * Log at specific level */ log(level: LogLevel, message: string, context?: LogContext): void { if (!shouldLog(level)) { return; } const mergedContext = context ? sanitizeContext({ ...this.options.context, ...context }) : this.options.context; const entry: LogEntry = { timestamp: new Date().toISOString(), level, message, ...(mergedContext && Object.keys(mergedContext).length > 0 && { context: mergedContext }), ...(this.options.component && { component: this.options.component }), ...(this.options.requestId && { requestId: this.options.requestId }), ...(this.options.siteId && { siteId: this.options.siteId }), }; outputLog(entry); } // Convenience methods trace(message: string, context?: LogContext): void { this.log("trace", message, context); } debug(message: string, context?: LogContext): void { this.log("debug", message, context); } info(message: string, context?: LogContext): void { this.log("info", message, context); } warn(message: string, context?: LogContext): void { this.log("warn", message, context); } error(message: string, context?: LogContext): void; error(error: Error, context?: LogContext): void; error(messageOrError: string | Error, context?: LogContext): void { if (messageOrError instanceof Error) { this.log("error", messageOrError.message, { ...context, errorName: messageOrError.name, errorStack: messageOrError.stack, }); } else { this.log("error", messageOrError, context); } } fatal(message: string, context?: LogContext): void; fatal(error: Error, context?: LogContext): void; fatal(messageOrError: string | Error, context?: LogContext): void { if (messageOrError instanceof Error) { this.log("fatal", messageOrError.message, { ...context, errorName: messageOrError.name, errorStack: messageOrError.stack, }); } else { this.log("fatal", messageOrError, context); } } /** * Time a function execution */ time<T>(message: string, fn: () => T): T; time<T>(message: string, fn: () => Promise<T>): Promise<T>; time<T>(message: string, fn: () => T | Promise<T>): T | Promise<T> { const start = Date.now(); this.debug(`Starting: ${message}`); try { const result = fn(); // Check if result is a Promise by checking for then method if (result && typeof result === "object" && "then" in result) { // Cast to Promise<T> since we know it has a then method const promiseResult = result as Promise<T>; return promiseResult .then((value: T) => { const duration = Date.now() - start; this.debug(`Completed: ${message}`, { duration: `${duration}ms` }); return value; }) .catch((error: unknown) => { const duration = Date.now() - start; this.error(`Failed: ${message}`, { duration: `${duration}ms`, error: error instanceof Error ? error.message : String(error), }); throw error; }); } else { const duration = Date.now() - start; this.debug(`Completed: ${message}`, { duration: `${duration}ms` }); return result; } } catch (_error) { const duration = Date.now() - start; this.error(`Failed: ${message}`, { duration: `${duration}ms`, _error: _error instanceof Error ? _error.message : String(_error), }); throw _error; } } } /** * Default logger instance */ export const logger = new Logger(); /** * Create component-specific logger */ export function createLogger(component: string, options: Omit<LoggerOptions, "component"> = {}): Logger { return new Logger({ ...options, component }); } /** * Create site-specific logger */ export function createSiteLogger(siteId: string, component?: string | undefined): Logger { return new Logger({ siteId, ...(component && { component }), }); } /** * Create request-specific logger */ export function createRequestLogger( requestId: string, component?: string | undefined, siteId?: string | undefined, ): Logger { return new Logger({ requestId, ...(component && { component }), ...(siteId && { siteId }), }); } /** * Logger factory for common scenarios */ export const LoggerFactory = { /** * Create logger for WordPress API operations */ api: (siteId?: string | undefined) => createLogger("API", siteId ? { siteId } : {}), /** * Create logger for cache operations */ cache: (siteId?: string | undefined) => createLogger("CACHE", siteId ? { siteId } : {}), /** * Create logger for tool operations */ tool: (toolName: string, siteId?: string | undefined) => createLogger(`TOOL:${toolName}`, siteId ? { siteId } : {}), /** * Create logger for authentication */ auth: (siteId?: string | undefined) => createLogger("AUTH", siteId ? { siteId } : {}), /** * Create logger for configuration */ config: () => createLogger("CONFIG"), /** * Create logger for security operations */ security: () => createLogger("SECURITY"), /** * Create logger for performance monitoring */ performance: () => createLogger("PERF"), /** * Create logger for server operations */ server: () => createLogger("SERVER"), };

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/docdyhr/mcp-wordpress'

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