Skip to main content
Glama
structured-logger.ts6.5 kB
/** * Structured Logger * * Production-ready structured logging with log levels, context, and JSON output. * Writes to stderr to avoid interfering with MCP protocol on stdout. * * Requirements: 11.1, 11.2, 11.3, 11.4, 11.5 */ import type { LogEntry, LogLevel } from "./types.js"; /** * Log level priority (higher = more severe) */ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4, }; /** * Structured Logger class * * Provides structured logging with configurable log levels, context propagation, * and JSON output for production environments. */ export class StructuredLogger { private minLevel: LogLevel; private structuredOutput: boolean; private defaultContext: Record<string, unknown>; private component: string; constructor( options: { minLevel?: LogLevel; structuredOutput?: boolean; component?: string; defaultContext?: Record<string, unknown>; } = {} ) { this.minLevel = options.minLevel ?? this.getLogLevelFromEnv(); this.structuredOutput = options.structuredOutput ?? process.env.NODE_ENV === "production"; this.component = options.component ?? "thoughtmcp"; this.defaultContext = options.defaultContext ?? {}; } /** * Get log level from environment variable */ private getLogLevelFromEnv(): LogLevel { const envLevel = process.env.LOG_LEVEL?.toLowerCase(); if (envLevel && envLevel in LOG_LEVEL_PRIORITY) { return envLevel as LogLevel; } return process.env.NODE_ENV === "production" ? "warn" : "debug"; } /** * Check if a log level should be output */ private shouldLog(level: LogLevel): boolean { return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.minLevel]; } /** * Format a log entry for output */ private formatEntry(entry: LogEntry): string { if (this.structuredOutput) { return JSON.stringify({ ...entry, timestamp: entry.timestamp.toISOString(), }); } // Human-readable format for development const timestamp = entry.timestamp.toISOString(); const level = entry.level.toUpperCase().padEnd(5); const component = entry.component ? `[${entry.component}]` : ""; const operation = entry.operation ? `(${entry.operation})` : ""; const duration = entry.durationMs !== undefined ? ` [${entry.durationMs}ms]` : ""; const traceId = entry.traceId ? ` trace=${entry.traceId}` : ""; let message = `${timestamp} ${level} ${component}${operation} ${entry.message}${duration}${traceId}`; if (entry.context && Object.keys(entry.context).length > 0) { message += ` ${JSON.stringify(entry.context)}`; } if (entry.error) { message += `\n Error: ${entry.error.name}: ${entry.error.message}`; if (entry.error.stack) { message += `\n ${entry.error.stack}`; } } return message; } /** * Write a log entry */ private write(entry: LogEntry): void { if (!this.shouldLog(entry.level)) { return; } const formatted = this.formatEntry(entry); process.stderr.write(`${formatted}\n`); } /** * Create a log entry with common fields */ private createEntry( level: LogLevel, message: string, options: { operation?: string; traceId?: string; context?: Record<string, unknown>; error?: Error; durationMs?: number; } = {} ): LogEntry { const entry: LogEntry = { level, message, timestamp: new Date(), component: this.component, operation: options.operation, traceId: options.traceId, context: { ...this.defaultContext, ...options.context }, durationMs: options.durationMs, }; if (options.error) { entry.error = { name: options.error.name, message: options.error.message, stack: options.error.stack, code: (options.error as Error & { code?: string }).code, }; } return entry; } /** * Log a debug message */ debug( message: string, options?: { operation?: string; traceId?: string; context?: Record<string, unknown>; durationMs?: number; } ): void { this.write(this.createEntry("debug", message, options)); } /** * Log an info message */ info( message: string, options?: { operation?: string; traceId?: string; context?: Record<string, unknown>; durationMs?: number; } ): void { this.write(this.createEntry("info", message, options)); } /** * Log a warning message */ warn( message: string, options?: { operation?: string; traceId?: string; context?: Record<string, unknown>; error?: Error; durationMs?: number; } ): void { this.write(this.createEntry("warn", message, options)); } /** * Log an error message */ error( message: string, options?: { operation?: string; traceId?: string; context?: Record<string, unknown>; error?: Error; durationMs?: number; } ): void { this.write(this.createEntry("error", message, options)); } /** * Log a fatal error message */ fatal( message: string, options?: { operation?: string; traceId?: string; context?: Record<string, unknown>; error?: Error; durationMs?: number; } ): void { this.write(this.createEntry("fatal", message, options)); } /** * Create a child logger with additional context */ child(options: { component?: string; context?: Record<string, unknown> }): StructuredLogger { return new StructuredLogger({ minLevel: this.minLevel, structuredOutput: this.structuredOutput, component: options.component ?? this.component, defaultContext: { ...this.defaultContext, ...options.context }, }); } /** * Set the minimum log level */ setLevel(level: LogLevel): void { this.minLevel = level; } /** * Get the current minimum log level */ getLevel(): LogLevel { return this.minLevel; } /** * Enable or disable structured output */ setStructuredOutput(enabled: boolean): void { this.structuredOutput = enabled; } /** * Check if structured output is enabled */ isStructuredOutput(): boolean { return this.structuredOutput; } } /** * Global structured logger instance */ export const logger = new StructuredLogger();

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/keyurgolani/ThoughtMcp'

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