Skip to main content
Glama
logger.test.ts13.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { randomUUID } from 'crypto'; import { LogLevel, Logger, parseLogLevel, serializeError } from './logger'; describe('Logger', () => { let testLogger: Logger; let testOutput: jest.Mock<void, [Record<string, any>]>; beforeEach(() => { testOutput = jest.fn(); testLogger = new Logger((msg) => testOutput(JSON.parse(msg)), undefined, LogLevel.DEBUG); }); test('Info', () => { testLogger.info('Boing!'); expect(testOutput).toHaveBeenCalledWith(expect.objectContaining({ level: 'INFO', msg: 'Boing!' })); }); test('Warn', () => { testLogger.warn('Warning'); expect(testOutput).toHaveBeenCalledWith(expect.objectContaining({ level: 'WARN', msg: 'Warning' })); }); test('Error', () => { testLogger.error('Fatal error', new Error('Catastrophe!')); expect(testOutput).toHaveBeenCalledWith( expect.objectContaining({ level: 'ERROR', msg: 'Fatal error', error: 'Error: Catastrophe!', stack: expect.arrayContaining(['Error: Catastrophe!']), }) ); }); test('Error as property', () => { testLogger.error('Fatal error', { foo: new Error('Catastrophe!') }); expect(testOutput).toHaveBeenCalledWith( expect.objectContaining({ level: 'ERROR', msg: 'Fatal error', foo: expect.objectContaining({ error: 'Error: Catastrophe!', stack: expect.arrayContaining(['Error: Catastrophe!']), }), }) ); }); test('Does not write when logger is disabled', () => { const unlogger = new Logger((msg) => testOutput(JSON.parse(msg)), undefined, LogLevel.NONE); unlogger.error('Annihilation imminent'); expect(testOutput).not.toHaveBeenCalled(); }); test('Does not log when level is above configured maximum', () => { const logger = new Logger((msg) => testOutput(JSON.parse(msg)), undefined, LogLevel.INFO); logger.debug('Evil bit unset'); expect(testOutput).not.toHaveBeenCalled(); }); test('Logger metadata attached to logs', () => { const logger = new Logger((msg) => testOutput(JSON.parse(msg)), { foo: 'bar' }, LogLevel.INFO); logger.info('Patient merged', { id: randomUUID() }); expect(testOutput).toHaveBeenCalledWith( expect.objectContaining({ level: 'INFO', msg: 'Patient merged', id: expect.stringMatching(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/), foo: 'bar', }) ); }); test('With prefix', () => { const logger = new Logger((msg) => testOutput(JSON.parse(msg)), undefined, LogLevel.INFO, { prefix: '[TEST] ' }); logger.info('Testing prefix'); expect(testOutput).toHaveBeenCalledWith( expect.objectContaining({ level: 'INFO', msg: '[TEST] Testing prefix', }) ); }); test('Clone logger', () => { const logger = new Logger((msg) => testOutput(JSON.parse(msg)), undefined, LogLevel.INFO); const clonedLogger1 = logger.clone(); const clonedLogger2 = logger.clone({ options: { prefix: '[CLONED] ' } }); logger.info('Testing clone'); clonedLogger1.info('Testing clone'); clonedLogger2.info('Testing clone'); expect(testOutput).toHaveBeenNthCalledWith( 1, expect.objectContaining({ level: 'INFO', msg: 'Testing clone', }) ); expect(testOutput).toHaveBeenNthCalledWith( 2, expect.objectContaining({ level: 'INFO', msg: 'Testing clone', }) ); expect(testOutput).toHaveBeenLastCalledWith( expect.objectContaining({ level: 'INFO', msg: '[CLONED] Testing clone', }) ); }); test('parseLogLevel', () => { expect(parseLogLevel('DEbug')).toBe(LogLevel.DEBUG); expect(parseLogLevel('INFO')).toBe(LogLevel.INFO); expect(parseLogLevel('none')).toBe(LogLevel.NONE); expect(parseLogLevel('WARN')).toBe(LogLevel.WARN); expect(parseLogLevel('error')).toBe(LogLevel.ERROR); expect(() => { parseLogLevel('foo'); }).toThrow('Invalid log level: foo'); }); }); describe('serializeError', () => { describe('basic error serialization', () => { test('should serialize a simple error', () => { const error = new Error('Something went wrong'); const result = serializeError(error); expect(result.error).toBe('Error: Something went wrong'); expect(result.message).toBe('Something went wrong'); expect(result.stack).toBeInstanceOf(Array); expect(result.stack[0]).toContain('Error: Something went wrong'); }); test('should handle error without stack trace', () => { const error = new Error('No stack'); delete error.stack; const result = serializeError(error); expect(result.error).toBe('Error: No stack'); expect(result.message).toBe('No stack'); expect(result.stack).toBeUndefined(); }); test('should include custom error name', () => { class CustomError extends Error { constructor(message: string) { super(message); this.name = 'CustomError'; } } const error = new CustomError('Custom problem'); const result = serializeError(error); expect(result.name).toBe('CustomError'); expect(result.error).toBe('CustomError: Custom problem'); }); test('should not include name property for default Error name', () => { const error = new Error('Standard error'); const result = serializeError(error); expect(result.name).toBeUndefined(); }); }); describe('nested errors with cause', () => { test('should serialize error with Error cause', () => { const rootCause = new Error('Root cause'); const error = new Error('Main error', { cause: rootCause }); const result = serializeError(error); expect(result.error).toBe('Error: Main error'); expect(result.cause).toBeDefined(); expect(result.cause.error).toBe('Error: Root cause'); expect(result.cause.message).toBe('Root cause'); expect(result.cause.stack).toBeInstanceOf(Array); }); test('should handle multiple levels of nested causes', () => { const level3 = new Error('Level 3'); const level2 = new Error('Level 2', { cause: level3 }); const level1 = new Error('Level 1', { cause: level2 }); const result = serializeError(level1); expect(result.error).toBe('Error: Level 1'); expect(result.cause.error).toBe('Error: Level 2'); expect(result.cause.cause.error).toBe('Error: Level 3'); expect(result.cause.cause.cause).toBeUndefined(); }); test('should handle non-Error cause', () => { const error = new Error('Main error', { cause: 'String cause' }); const result = serializeError(error); expect(result.error).toBe('Error: Main error'); expect(result.cause).toBe('String cause'); }); test('should handle object cause', () => { const cause = { code: 'ECONNREFUSED', port: 5432 }; const error = new Error('Connection failed', { cause }); const result = serializeError(error); expect(result.error).toBe('Error: Connection failed'); expect(result.cause).toEqual({ code: 'ECONNREFUSED', port: 5432 }); }); test('should handle undefined cause', () => { const error = new Error('Error with undefined cause'); (error as any).cause = undefined; const result = serializeError(error); expect(result.error).toBe('Error: Error with undefined cause'); expect(result.cause).toBeUndefined(); }); }); describe('custom properties', () => { test('should include custom properties on error', () => { const error = new Error('Custom props error') as any; error.code = 'ERR_001'; error.statusCode = 500; error.details = { userId: 123 }; const result = serializeError(error); expect(result.code).toBe('ERR_001'); expect(result.statusCode).toBe(500); expect(result.details).toEqual({ userId: 123 }); }); test('should serialize nested Error in custom property', () => { const nestedError = new Error('Nested error'); const error = new Error('Main error') as any; error.innerError = nestedError; const result = serializeError(error); expect(result.innerError).toBeDefined(); expect(result.innerError.error).toBe('Error: Nested error'); expect(result.innerError.stack).toBeInstanceOf(Array); }); test('should handle property that throws on access', () => { const error = new Error('Error with throwing property'); Object.defineProperty(error, 'throwingProp', { get() { throw new Error('Cannot access this property'); }, enumerable: true, }); // Should not throw expect(() => serializeError(error)).not.toThrow(); const result = serializeError(error); expect(result.error).toBe('Error: Error with throwing property'); }); }); describe('depth limiting', () => { test('should respect default maxDepth of 10', () => { // Create a chain of 12 nested errors let error = new Error('Level 12'); for (let i = 11; i >= 1; i--) { error = new Error(`Level ${i}`, { cause: error }); } const result = serializeError(error); // Navigate to the deepest level let current = result; let depth = 0; while (current.cause && typeof current.cause === 'object' && 'error' in current.cause) { current = current.cause; depth++; } expect(depth).toBe(10); // 10 levels total (0-9) }); test('should respect custom maxDepth parameter', () => { const level3 = new Error('Level 3'); const level2 = new Error('Level 2', { cause: level3 }); const level1 = new Error('Level 1', { cause: level2 }); const result = serializeError(level1, 0, 2); expect(result.error).toBe('Error: Level 1'); expect(result.cause.error).toBe('Error: Level 2'); expect(result.cause.cause).toEqual({ error: 'Max error depth reached' }); }); test('should handle circular reference in custom properties', () => { const error1 = new Error('Error 1') as any; const error2 = new Error('Error 2') as any; error1.related = error2; error2.related = error1; // Should not cause infinite recursion expect(() => serializeError(error1)).not.toThrow(); const result = serializeError(error1); expect(result.error).toBe('Error: Error 1'); expect(result.related.error).toBe('Error: Error 2'); // Should eventually hit max depth }); }); describe('edge cases', () => { test('should handle error with null prototype', () => { const error = Object.create(null); error.message = 'Null prototype error'; error.toString = () => 'CustomError: Null prototype error'; const result = serializeError(error); expect(result.error).toBe('CustomError: Null prototype error'); }); test('should handle error with empty message', () => { const error = new Error(''); const result = serializeError(error); expect(result.error).toBe('Error'); // Empty string message might not be included in serialization // depending on implementation - check if it exists before asserting if ('message' in result) { expect(result.message).toBe(''); } }); test('should handle error with very long stack trace', () => { const error = new Error('Long stack'); // Simulate a very long stack error.stack = Array(1000).fill('at someFunction()').join('\n'); const result = serializeError(error); expect(result.stack).toBeInstanceOf(Array); expect(result.stack.length).toBe(1000); }); test('should handle AggregateError with multiple errors', () => { const errors = [new Error('First error'), new Error('Second error'), 'String error']; const aggregateError = new AggregateError(errors, 'Multiple errors occurred'); const result = serializeError(aggregateError as any); expect(result.error).toContain('Multiple errors occurred'); expect(result.message).toBe('Multiple errors occurred'); // Note: AggregateError.errors property would be captured as custom property if (result.errors) { expect(result.errors).toBeInstanceOf(Array); } }); }); describe('starting depth parameter', () => { test('should handle non-zero starting depth', () => { const error = new Error('Starting at depth 5'); const cause = new Error('Cause'); (error as any).cause = cause; const result = serializeError(error, 8, 10); expect(result.error).toBe('Error: Starting at depth 5'); expect(result.cause.error).toBe('Error: Cause'); expect(result.cause.cause).toBeUndefined(); }); test('should return max depth message when starting at maxDepth', () => { const error = new Error('At max depth'); const result = serializeError(error, 10, 10); expect(result).toEqual({ error: 'Max error depth reached' }); }); }); });

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