Skip to main content
Glama
logger.tsβ€’15.8 kB
/** * Enhanced structured logging utility for consistent logging across the application */ import { randomUUID } from 'crypto'; import { safeJsonStringify } from '@/utils/json-serializer.js'; import type { JsonObject } from '@/types/attio.js'; import { sanitizeErrorDetail, sanitizeLogMessage, sanitizeLogPayload, sanitizeMetadata, } from '@/utils/log-sanitizer.js'; /** * Log level enum for controlling verbosity */ export enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, NONE = 4, } /** * Operation types for better log categorization */ export enum OperationType { API_CALL = 'api_call', TOOL_EXECUTION = 'tool_execution', DATA_PROCESSING = 'data_processing', VALIDATION = 'validation', TRANSFORMATION = 'transformation', SYSTEM = 'system', } /** * Structured log metadata interface */ export interface LogMetadata { timestamp: string; level: string; module: string; operation?: string; operationType?: OperationType; correlationId?: string; sessionId?: string; duration?: number; requestId?: string; userId?: string; [key: string]: unknown; } /** * Structured log entry interface */ export interface LogEntry { message: string; metadata: LogMetadata; data?: JsonObject; error?: { message: string; name: string; stack?: string; code?: string | number; }; } /** * Context for tracking related operations */ interface LogContext { correlationId?: string; sessionId?: string; requestId?: string; userId?: string; operation?: string; operationType?: OperationType; } /** * Parse log level from environment variable */ function parseLogLevel(envValue?: string): LogLevel { if (!envValue) return LogLevel.DEBUG; const normalized = envValue.toUpperCase(); switch (normalized) { case 'DEBUG': return LogLevel.DEBUG; case 'INFO': return LogLevel.INFO; case 'WARN': case 'WARNING': return LogLevel.WARN; case 'ERROR': return LogLevel.ERROR; case 'NONE': case 'OFF': return LogLevel.NONE; default: // If invalid, fallback to environment-based defaults return process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG; } } /** * Current log level based on environment configuration * Priority: MCP_LOG_LEVEL > LOG_LEVEL > NODE_ENV-based defaults */ export const CURRENT_LOG_LEVEL = (() => { // Force DEBUG during Vitest runs for deterministic logging in tests if (process.env.VITEST === 'true') { return LogLevel.DEBUG; } // First check MCP-specific log level if (process.env.MCP_LOG_LEVEL) { return parseLogLevel(process.env.MCP_LOG_LEVEL); } // Then check generic log level if (process.env.LOG_LEVEL) { return parseLogLevel(process.env.LOG_LEVEL); } // Finally fallback to NODE_ENV-based defaults return process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG; })(); /** * Global log context storage */ let globalContext: LogContext = {}; /** * Set global logging context for correlation tracking */ export function setLogContext(context: Partial<LogContext>): void { globalContext = { ...globalContext, ...context }; } /** * Get current logging context */ export function getLogContext(): LogContext { return { ...globalContext }; } /** * Clear logging context */ export function clearLogContext(): void { globalContext = {}; } /** * Generate a new correlation ID */ export function generateCorrelationId(): string { return randomUUID(); } /** * Create structured log metadata */ function createLogMetadata( level: string, module: string, operation?: string, operationType?: OperationType, additionalMetadata?: JsonObject ): LogMetadata { const sanitizedMetadata = sanitizeMetadata(additionalMetadata); return { timestamp: new Date().toISOString(), level, module, operation: operation || globalContext.operation, operationType: operationType || globalContext.operationType, correlationId: globalContext.correlationId, sessionId: globalContext.sessionId, requestId: globalContext.requestId, userId: globalContext.userId, ...(sanitizedMetadata || {}), }; } /** * Format and output structured log entry */ function outputLog( entry: LogEntry, logFunction: (message: string, ...args: unknown[]) => void ): void { if (process.env.LOG_FORMAT === 'json') { // Output compact JSON using safe stringify to prevent errors logFunction(safeJsonStringify(entry, { indent: 0 })); } else { // Output pretty-printed JSON to maintain human readability for console, // while ensuring it's a single, valid JSON string to prevent MCP parsing errors. logFunction(safeJsonStringify(entry, { indent: 2 })); } } /** * Log a debug message (only in development) * * @param module - Name of the module/function logging the message * @param message - Message to log * @param data - Optional data to include with the log * @param operation - Optional operation name * @param operationType - Optional operation type */ export function debug( module: string, message: string, data?: JsonObject, operation?: string, operationType?: OperationType ): void { if (CURRENT_LOG_LEVEL <= LogLevel.DEBUG) { const sanitizedMessage = sanitizeLogMessage(message); const sanitizedData = data ? (sanitizeLogPayload(data) as JsonObject) : undefined; const entry: LogEntry = { message: sanitizedMessage, metadata: createLogMetadata('DEBUG', module, operation, operationType), ...(sanitizedData ? { data: sanitizedData } : {}), }; outputLog(entry, console.error); // Use stderr instead of stdout to avoid interfering with MCP protocol } } /** * Log an info message * * @param module - Name of the module/function logging the message * @param message - Message to log * @param data - Optional data to include with the log * @param operation - Optional operation name * @param operationType - Optional operation type */ export function info( module: string, message: string, data?: JsonObject, operation?: string, operationType?: OperationType ): void { if (CURRENT_LOG_LEVEL <= LogLevel.INFO) { const sanitizedMessage = sanitizeLogMessage(message); const sanitizedData = data ? (sanitizeLogPayload(data) as JsonObject) : undefined; const entry: LogEntry = { message: sanitizedMessage, metadata: createLogMetadata('INFO', module, operation, operationType), ...(sanitizedData ? { data: sanitizedData } : {}), }; outputLog(entry, console.error); // Use stderr instead of stdout to avoid interfering with MCP protocol } } /** * Log a warning message * * @param module - Name of the module/function logging the message * @param message - Message to log * @param data - Optional data to include with the log * @param operation - Optional operation name * @param operationType - Optional operation type */ export function warn( module: string, message: string, data?: JsonObject, operation?: string, operationType?: OperationType ): void { if (CURRENT_LOG_LEVEL <= LogLevel.WARN) { const sanitizedMessage = sanitizeLogMessage(message); const sanitizedData = data ? (sanitizeLogPayload(data) as JsonObject) : undefined; const entry: LogEntry = { message: sanitizedMessage, metadata: createLogMetadata('WARN', module, operation, operationType), ...(sanitizedData ? { data: sanitizedData } : {}), }; outputLog(entry, console.warn); } } /** * Log an error message * * @param module - Name of the module/function logging the message * @param message - Message to log * @param errorObj - Optional error object * @param data - Optional additional data * @param operation - Optional operation name * @param operationType - Optional operation type */ export function error( module: string, message: string, errorObj?: unknown, data?: JsonObject, operation?: string, operationType?: OperationType ): void { if (CURRENT_LOG_LEVEL <= LogLevel.ERROR) { const sanitizedMessage = sanitizeLogMessage(message); const sanitizedData = data ? (sanitizeLogPayload(data) as JsonObject) : undefined; const sanitizedError = sanitizeErrorDetail(errorObj); const entry: LogEntry = { message: sanitizedMessage, metadata: createLogMetadata('ERROR', module, operation, operationType), ...(sanitizedData ? { data: sanitizedData } : {}), ...(sanitizedError ? { error: sanitizedError } : {}), }; outputLog(entry, console.error); } } /** * Performance timing utility for tracking operation duration */ export class PerformanceTimer { private startTime: number; private module: string; private operation: string; private operationType: OperationType; constructor( module: string, operation: string, operationType: OperationType = OperationType.SYSTEM ) { this.module = module; this.operation = operation; this.operationType = operationType; this.startTime = Date.now(); } /** * End timing and log the duration */ end(message?: string, data?: Record<string, unknown>): number { const duration = Date.now() - this.startTime; debug( this.module, message || `Operation completed: ${this.operation}`, { ...data, duration: `${duration}ms` }, this.operation, this.operationType ); return duration; } } /** * Enhanced operation start logging with timing * * @param module - Name of the module/function * @param operation - Name of the operation being performed * @param operationType - Type of operation for categorization * @param params - Parameters for the operation * @returns PerformanceTimer instance for tracking duration */ export function operationStart( module: string, operation: string, operationType: OperationType = OperationType.SYSTEM, params?: JsonObject ): PerformanceTimer { debug( module, `Starting operation: ${operation}`, params, operation, operationType ); return new PerformanceTimer(module, operation, operationType); } /** * Logs the successful completion of an API operation * * @param module - Name of the module/function * @param operation - Name of the operation being performed * @param resultSummary - Summary of the operation result (e.g., count of items) * @param operationType - Type of operation for categorization * @param duration - Optional duration in milliseconds */ export function operationSuccess( module: string, operation: string, resultSummary?: JsonObject, operationType: OperationType = OperationType.SYSTEM, duration?: number ): void { const logData = { ...resultSummary, ...(duration && { duration: `${duration}ms` }), }; info( module, `Operation successful: ${operation}`, logData, operation, operationType ); } /** * Logs the failure of an API operation * * @param module - Name of the module/function * @param operation - Name of the operation that failed * @param errorObj - The error object * @param context - Additional context information * @param operationType - Type of operation for categorization * @param duration - Optional duration in milliseconds */ export function operationFailure( module: string, operation: string, errorObj: unknown, context?: JsonObject, operationType: OperationType = OperationType.SYSTEM, duration?: number ): void { const logData = { ...context, ...(duration && { duration: `${duration}ms` }), }; error( module, `Operation failed: ${operation}`, errorObj, logData, operation, operationType ); } /** * Logs the start of a fallback API operation * * @param module - Name of the module/function * @param operation - Name of the fallback operation * @param reason - Reason for falling back * @param operationType - Type of operation for categorization */ export function fallbackStart( module: string, operation: string, reason: string, operationType: OperationType = OperationType.API_CALL ): void { warn( module, `Trying fallback: ${operation}. Reason: ${reason}`, { reason }, operation, operationType ); } /** * Creates a scoped logger instance with pre-configured context */ export class SecureLogger { constructor( private readonly module: string, private readonly operation?: string, private readonly operationType: OperationType = OperationType.SYSTEM ) {} debug(message: string, data?: JsonObject): void { debug(this.module, message, data, this.operation, this.operationType); } info(message: string, data?: JsonObject): void { info(this.module, message, data, this.operation, this.operationType); } warn(message: string, data?: JsonObject): void { warn(this.module, message, data, this.operation, this.operationType); } error(message: string, errorObj?: unknown, data?: JsonObject): void { error( this.module, message, errorObj, data, this.operation, this.operationType ); } operationStart( op?: string, opType?: OperationType, params?: JsonObject ): PerformanceTimer { return operationStart( this.module, op || this.operation || 'unknown', opType || this.operationType, params ); } operationSuccess( op?: string, resultSummary?: JsonObject, opType?: OperationType, duration?: number ): void { operationSuccess( this.module, op || this.operation || 'unknown', resultSummary, opType || this.operationType, duration ); } operationFailure( op?: string, errorObj?: unknown, context?: JsonObject, opType?: OperationType, duration?: number ): void { operationFailure( this.module, op || this.operation || 'unknown', errorObj, context, opType || this.operationType, duration ); } } export function createScopedLogger( module: string, operation?: string, operationType?: OperationType ) { return new SecureLogger(module, operation, operationType); } /** * Utility for wrapping async operations with automatic logging */ export async function withLogging<T>( module: string, operation: string, operationType: OperationType, fn: () => Promise<T>, context?: JsonObject ): Promise<T> { const timer = operationStart(module, operation, operationType, context); try { const result = await fn(); const duration = timer.end(); operationSuccess( module, operation, { success: true }, operationType, duration ); return result; } catch (error: unknown) { const duration = timer.end(); operationFailure( module, operation, error, context, operationType, duration ); throw error; } } export function safeMcpLog(message: string, ...args: unknown[]): void { // Always use console.error to avoid interfering with MCP protocol const sanitizedMessage = sanitizeLogMessage(message); const sanitizedArgs = args.map((arg) => { if (typeof arg === 'string') { return sanitizeLogMessage(arg); } if (typeof arg === 'object' && arg !== null) { return sanitizeLogPayload(arg); } return arg; }); console.error(`[MCP_SAFE_LOG] ${sanitizedMessage}`, ...sanitizedArgs); } export default { debug, info, warn, error, operationStart, operationSuccess, operationFailure, fallbackStart, setLogContext, getLogContext, clearLogContext, generateCorrelationId, createScopedLogger, SecureLogger, withLogging, safeMcpLog, PerformanceTimer, LogLevel, OperationType, };

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/kesslerio/attio-mcp-server'

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