Skip to main content
Glama
logger.ts6.1 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { isError } from './outcomes'; /* * Once upon a time, we used Winston, and that was fine. * Then the log4j fiasco happened, and everyone started auditing logging libraries. * And we decided that we did not use any fancy logging features, * and that logging to console.log was actually perfectly adequate. */ /** * Logging level, with greater values representing more detailed logs emitted. * * The zero value means no server logs will be emitted. */ export const LogLevel = { NONE: 0, ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4, }; export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; export const LogLevelNames = ['NONE', 'ERROR', 'WARN', 'INFO', 'DEBUG'] as const; export interface LogMessage { level: (typeof LogLevelNames)[number]; msg: string; timestamp: string; [key: string]: string | boolean | number; } export interface LoggerOptions { prefix?: string; } export interface ILoggerConfig { level: LogLevel; options?: LoggerOptions; metadata: Record<string, any>; } export interface LoggerConfig extends ILoggerConfig { write: (msg: string) => void; } export type LoggerConfigOverride = Partial<LoggerConfig>; export interface ILogger { level: LogLevel; error(msg: string, data?: Record<string, any> | Error): void; warn(msg: string, data?: Record<string, any> | Error): void; info(msg: string, data?: Record<string, any> | Error): void; debug(msg: string, data?: Record<string, any> | Error): void; clone(overrides?: Partial<ILoggerConfig>): ILogger; } export class Logger implements ILogger { readonly write: (msg: string) => void; readonly metadata: Record<string, any>; readonly options?: LoggerOptions; readonly prefix?: string; level: LogLevel; constructor( write: (msg: string) => void, metadata: Record<string, any> = {}, level: LogLevel = LogLevel.INFO, options: LoggerOptions = {} ) { this.write = write; this.metadata = metadata; this.level = level; this.options = options; if (options?.prefix) { this.prefix = options.prefix; } this.error = this.error.bind(this); this.warn = this.warn.bind(this); this.info = this.info.bind(this); this.debug = this.debug.bind(this); this.log = this.log.bind(this); } clone(override?: LoggerConfigOverride): Logger { const config = this.getLoggerConfig(); const mergedConfig = override ? { ...config, override, options: { ...config.options, ...override.options } } : config; return new Logger(mergedConfig.write, mergedConfig.metadata, mergedConfig.level, mergedConfig.options); } private getLoggerConfig(): LoggerConfig { const { write, metadata, level, options } = this; return { write, metadata, level, options }; } error(msg: string, data?: Record<string, any> | Error): void { this.log(LogLevel.ERROR, msg, data); } warn(msg: string, data?: Record<string, any> | Error): void { this.log(LogLevel.WARN, msg, data); } info(msg: string, data?: Record<string, any> | Error): void { this.log(LogLevel.INFO, msg, data); } debug(msg: string, data?: Record<string, any> | Error): void { this.log(LogLevel.DEBUG, msg, data); } log(level: LogLevel, msg: string, data?: Record<string, any> | Error): void { if (level > this.level) { return; } let processedData: Record<string, any> | undefined; if (isError(data)) { processedData = serializeError(data); } else if (data) { processedData = { ...data }; for (const [key, value] of Object.entries(processedData)) { if (value instanceof Error) { processedData[key] = serializeError(value); } } } this.write( JSON.stringify({ level: LogLevelNames[level], timestamp: new Date().toISOString(), msg: this.prefix ? `${this.prefix}${msg}` : msg, ...processedData, ...this.metadata, }) ); } } export function parseLogLevel(level: string): LogLevel { const value = LogLevel[level.toUpperCase() as keyof typeof LogLevel]; if (value === undefined) { throw new Error(`Invalid log level: ${level}`); } return value; } /** * Serializes an Error object into a plain object, including nested causes and custom properties. * @param error - The error to serialize. * @param depth - The current depth of recursion. * @param maxDepth - The maximum depth of recursion. * @returns A serialized representation of the error. */ export function serializeError(error: Error, depth = 0, maxDepth = 10): Record<string, any> { // Prevent infinite recursion if (depth >= maxDepth) { return { error: 'Max error depth reached' }; } const serialized: Record<string, any> = { error: error.toString(), stack: error.stack?.split('\n'), }; // Include error name if it's not the default "Error" if (error.name && error.name !== 'Error') { serialized.name = error.name; } // Include message explicitly for clarity if (error.message) { serialized.message = error.message; } // Handle Error.cause recursively if ('cause' in error && error.cause !== undefined) { if (error.cause instanceof Error) { serialized.cause = serializeError(error.cause, depth + 1, maxDepth); } else { // cause might not be an Error object serialized.cause = error.cause; } } // Include any custom properties on the error const customProps = Object.getOwnPropertyNames(error).filter( (prop) => !['name', 'message', 'stack', 'cause'].includes(prop) ); for (const prop of customProps) { try { const value = (error as any)[prop]; // Recursively handle nested errors in custom properties if (value instanceof Error) { serialized[prop] = serializeError(value, depth + 1, maxDepth); } else { serialized[prop] = value; } } catch { // Skip properties that can't be accessed } } return serialized; }

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/medplum/medplum'

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