Skip to main content
Glama
logger.ts6.12 kB
import type { AuthInfo } from '../types/auth.js' export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' export interface LogFields { [key: string]: unknown correlationId?: string } interface LoggerOptions { level?: LogLevel json?: boolean base?: LogFields } /** * Lightweight, structured, context-aware logger with JSON output support and * timing utilities. Designed to run on Node.js and Workers without deps. */ export class Logger { private static level: LogLevel = ((): LogLevel => { const env = (globalThis as any)?.process?.env const raw = (env?.LOG_LEVEL || env?.NODE_LOG_LEVEL || 'info').toLowerCase() const allowed: LogLevel[] = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'] return (allowed.includes(raw as LogLevel) ? (raw as LogLevel) : 'info') as LogLevel })() private static json: boolean = ((): boolean => { const env = (globalThis as any)?.process?.env const raw = env?.LOG_FORMAT || env?.LOG_JSON if (!raw) return (env?.NODE_ENV === 'production') as boolean return String(raw).toLowerCase() === 'true' || String(raw).toLowerCase() === 'json' })() private static base: LogFields = {} static configure(opts: LoggerOptions): void { if (opts.level) this.level = opts.level if (typeof opts.json === 'boolean') this.json = opts.json if (opts.base) this.base = { ...this.base, ...sanitizeFields(opts.base) } } static with(fields: LogFields): typeof Logger { const merged = { ...this.base, ...sanitizeFields(fields) } const child = new Proxy(this, { get: (target, prop) => { if (prop === 'base') return merged return (target as any)[prop] }, }) as typeof Logger return child } static setLevel(level: LogLevel): void { this.level = level } static enableJSON(enabled: boolean): void { this.json = enabled } static getLevel(): LogLevel { return this.level } static trace(message: string, fields?: LogFields | unknown): void { const f = fieldsToLogFields(fields) this._log('trace', message, f) } static debug(message: string, fields?: LogFields | unknown): void { const envDebug = (globalThis as any)?.process?.env?.DEBUG const f = fieldsToLogFields(fields) if (envDebug || this.levelAllowed('debug')) this._log('debug', message, f) } static info(message: string, fields?: LogFields | unknown): void { const f = fieldsToLogFields(fields) this._log('info', message, f) } static warn(message: string, fields?: LogFields | unknown): void { const f = fieldsToLogFields(fields) this._log('warn', message, f) } static error(message: string, fields?: LogFields | unknown): void { const f = fieldsToLogFields(fields) this._log('error', message, f) } static fatal(message: string, fields?: LogFields | unknown): void { const f = fieldsToLogFields(fields) this._log('fatal', message, f) } /** * Structured auth event helper for backward compatibility. */ static logAuthEvent(event: string, context: AuthInfo): void { this.info('auth_event', { event, ...context }) } /** * Structured server event helper for backward compatibility. */ static logServerEvent(event: string, serverId: string, context?: unknown): void { const fields = fieldsToLogFields(context) this.info('server_event', { event, serverId, ...(fields ?? {}) }) } /** * Starts a performance timer, returning a function to log completion. * * Usage: * const done = Logger.time('load_config', { id }) * ...work... * done({ status: 'ok' }) */ static time(name: string, fields?: LogFields): (extra?: LogFields) => void { const start = now() const base = { name, ...(fields ? sanitizeFields(fields) : {}) } return (extra?: LogFields) => { const durationMs = Math.max(0, now() - start) this.info('perf', { ...base, ...(extra ? sanitizeFields(extra) : {}), durationMs }) } } /** * Low-level log method honoring level and output format. */ private static _log(level: LogLevel, message: string, fields?: LogFields): void { if (!this.levelAllowed(level)) return const ts = new Date().toISOString() const entry = { ts, level, msg: message, ...this.base, ...(fields ? sanitizeFields(fields) : {}), } // eslint-disable-next-line no-console if (this.json) console.log(JSON.stringify(entry)) else console.log(formatHuman(entry)) } private static levelAllowed(check: LogLevel): boolean { const order: LogLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] const curIdx = order.indexOf(this.level) const chkIdx = order.indexOf(check) return chkIdx >= curIdx } } function now(): number { if (typeof performance !== 'undefined' && typeof performance.now === 'function') { return performance.now() } return Date.now() } function sanitizeFields(fields: LogFields): LogFields { const out: LogFields = {} for (const [k, v] of Object.entries(fields)) { if (v === undefined) continue if (v instanceof Error) { out[k] = { name: v.name, message: v.message, stack: v.stack, } } else if (typeof v === 'object' && v !== null) { try { // Avoid circular structures out[k] = JSON.parse(JSON.stringify(v)) } catch { out[k] = String(v) } } else { out[k] = v as any } } return out } function formatHuman(entry: { [k: string]: unknown }): string { const { ts, level, msg, ...rest } = entry as any const head = `[${String(level).toUpperCase()}] ${ts} ${msg}` const restKeys = Object.keys(rest) if (restKeys.length === 0) return head return `${head} ${safeStringify(rest)}` } function safeStringify(obj: any): string { try { return JSON.stringify(obj) } catch { return '[object]' } } function fieldsToLogFields(f?: LogFields | unknown): LogFields | undefined { if (!f) return undefined if (typeof f === 'object' && !(f instanceof Error)) return f as LogFields return { detail: f } }

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/Jakedismo/master-mcp-server'

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