Skip to main content
Glama
logger.ts5.82 kB
/** * Structured logging with correlation IDs and sampling * For Phase 1D observability implementation */ // AsyncLocalStorage is not available in Cloudflare Workers, so we'll use a simple context stack class SimpleAsyncLocalStorage<T> { private store: T | undefined; run<R>(store: T, fn: () => R): R { const previous = this.store; this.store = store; try { return fn(); } finally { this.store = previous; } } getStore(): T | undefined { return this.store; } } interface LogContext { correlationId?: string; userId?: string; sessionId?: string; operation?: string; [key: string]: unknown; } interface LogEntry { timestamp: string; level: "debug" | "info" | "warn" | "error"; message: string; correlationId?: string; context?: LogContext; error?: { name: string; message: string; stack?: string; }; } export class Logger { private als = new SimpleAsyncLocalStorage<LogContext>(); private sampleRate: number; private parentContext?: LogContext; constructor(sampleRate: number = 1.0) { this.sampleRate = sampleRate; } private shouldLog(): boolean { return Math.random() < this.sampleRate; } private formatLog(entry: LogEntry): string { return JSON.stringify(entry); } private writeLog(entry: LogEntry): void { if (!this.shouldLog()) return; const formatted = this.formatLog(entry); switch (entry.level) { case "error": console.error(formatted); break; case "warn": console.warn(formatted); break; case "debug": // eslint-disable-next-line no-console console.debug(formatted); break; default: // eslint-disable-next-line no-console console.log(formatted); } } private getCurrentContext(): LogContext { const alsContext = this.als.getStore(); if (alsContext) { return { ...this.parentContext, ...alsContext }; } return this.parentContext || {}; } withCorrelation<T>(correlationId: string, fn: () => T): T; withCorrelation<T>(correlationId: string, context: Partial<LogContext>, fn: () => T): T; withCorrelation<T>( correlationId: string, contextOrFn: Partial<LogContext> | (() => T), fn?: () => T ): T { const context = typeof contextOrFn === "function" ? {} : contextOrFn; const callback = typeof contextOrFn === "function" ? contextOrFn : (fn ?? (() => ({}) as T)); context.correlationId = correlationId; const currentContext = this.getCurrentContext(); const newContext = { ...currentContext, ...context }; return this.als.run(newContext, callback); } timer<T>(operation: string, fn: () => T): T; timer<T>(operation: string, context: Partial<LogContext>, fn: () => T): T; timer<T>(operation: string, contextOrFn: Partial<LogContext> | (() => T), fn?: () => T): T { const context = typeof contextOrFn === "function" ? {} : contextOrFn; const callback = typeof contextOrFn === "function" ? contextOrFn : (fn ?? (() => ({}) as T)); const startTime = Date.now(); const currentContext = this.getCurrentContext(); const timerContext = { ...currentContext, ...context, operation }; this.als.run(timerContext, () => { this.info(`Starting operation: ${operation}`); }); try { const result = this.als.run(timerContext, callback); const duration = Date.now() - startTime; this.als.run(timerContext, () => { this.info(`Completed operation: ${operation}`, { duration }); }); return result; } catch (error) { const duration = Date.now() - startTime; this.als.run(timerContext, () => { this.error(`Failed operation: ${operation}`, { duration }, error as Error); }); throw error; } } debug(message: string, context?: Partial<LogContext>): void { const currentContext = this.getCurrentContext(); const entry: LogEntry = { timestamp: new Date().toISOString(), level: "debug", message, correlationId: currentContext.correlationId, context: { ...currentContext, ...context }, }; this.writeLog(entry); } info(message: string, context?: Partial<LogContext>): void { const currentContext = this.getCurrentContext(); const entry: LogEntry = { timestamp: new Date().toISOString(), level: "info", message, correlationId: currentContext.correlationId, context: { ...currentContext, ...context }, }; this.writeLog(entry); } warn(message: string, context?: Partial<LogContext>): void { const currentContext = this.getCurrentContext(); const entry: LogEntry = { timestamp: new Date().toISOString(), level: "warn", message, correlationId: currentContext.correlationId, context: { ...currentContext, ...context }, }; this.writeLog(entry); } error(message: string, context?: Partial<LogContext>, error?: Error): void { const currentContext = this.getCurrentContext(); const entry: LogEntry = { timestamp: new Date().toISOString(), level: "error", message, correlationId: currentContext.correlationId, context: { ...currentContext, ...context }, error: error ? { name: error.name, message: error.message, stack: error.stack, } : undefined, }; this.writeLog(entry); } child(context: Partial<LogContext>): Logger { const childLogger = new Logger(this.sampleRate); // For simplicity in Cloudflare Workers, just return a logger that will inherit context when used // In a real implementation, we'd share the ALS instance childLogger.parentContext = { ...this.getCurrentContext(), ...context }; return childLogger; } }

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/hummbl-dev/mcp-server'

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