Skip to main content
Glama
logger.test.ts33.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { LogLevelNames } from '@medplum/core'; import { LogLevel, parseLogLevel, sleep } from '@medplum/core'; import { mkdtemp, rm } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; import { createWinstonFromLoggerConfig, DEFAULT_LOGGER_CONFIG, getWinstonLevelFromMedplumLevel, LoggerType, parseLoggerConfigFromArgs, WinstonWrapperLogger, } from './logger'; import type { AgentArgs } from './types'; describe('Agent Logger', () => { describe('parseLoggerConfigFromArgs', () => { test('should emit no warnings when given a fully valid partial config', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.logDir': '/tmp/logs', 'logger.main.logLevel': 'DEBUG', 'logger.channel.logDir': '/tmp/channel-logs', 'logger.channel.logLevel': 'WARN', }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toHaveLength(0); expect(config.main.logDir).toBe('/tmp/logs'); expect(config.main.logLevel).toBe(LogLevel.DEBUG); expect(config.channel.logDir).toBe('/tmp/channel-logs'); expect(config.channel.logLevel).toBe(LogLevel.WARN); expect(config.main.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); expect(config.main.filesToKeep).toBe(DEFAULT_LOGGER_CONFIG.filesToKeep); expect(config.channel.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); expect(config.channel.filesToKeep).toBe(DEFAULT_LOGGER_CONFIG.filesToKeep); }); test('should emit no warnings when given a fully valid full config', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.logDir': '/var/log/medplum', 'logger.main.logLevel': 'ERROR', 'logger.main.maxFileSizeMb': '100', 'logger.main.filesToKeep': '15', 'logger.channel.logDir': '/var/log/medplum/channels', 'logger.channel.logLevel': 'INFO', 'logger.channel.maxFileSizeMb': '50', 'logger.channel.filesToKeep': '20', }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toHaveLength(0); expect(config.main.logDir).toBe('/var/log/medplum'); expect(config.main.logLevel).toBe(LogLevel.ERROR); expect(config.channel.logDir).toBe('/var/log/medplum/channels'); expect(config.channel.logLevel).toBe(LogLevel.INFO); expect(config.main.maxFileSizeMb).toBe(100); expect(config.main.filesToKeep).toBe(15); expect(config.channel.maxFileSizeMb).toBe(50); expect(config.channel.filesToKeep).toBe(20); }); test('should emit warning when an invalid prop name is encountered', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.logDir': '/tmp/logs', 'logger.main.invalidProp': 'someValue', 'logger.main.anotherInvalid': 'anotherValue', }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toContain('logger.main.invalidProp is not a valid setting name'); expect(warnings).toContain('logger.main.anotherInvalid is not a valid setting name'); expect(config.main.logDir).toBe('/tmp/logs'); // Invalid props should not be set, defaults should be used expect(config.main.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); expect(config.main.filesToKeep).toBe(DEFAULT_LOGGER_CONFIG.filesToKeep); expect(config.main.logLevel).toBe(DEFAULT_LOGGER_CONFIG.logLevel); }); test('should emit warning when an invalid log level is encountered', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.logLevel': 'INVALID_LEVEL', 'logger.channel.logLevel': 'ANOTHER_INVALID', }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toContain('Error while parsing logger.main.logLevel: Invalid log level: INVALID_LEVEL'); expect(warnings).toContain('Error while parsing logger.channel.logLevel: Invalid log level: ANOTHER_INVALID'); // Should fallback to INFO when parsing fails expect(config.main.logLevel).toBe(LogLevel.INFO); expect(config.channel.logLevel).toBe(LogLevel.INFO); }); test('should emit warning when an invalid logger type is encountered', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.invalid.logDir': '/tmp/logs', 'logger.anotherInvalid.maxFileSizeMb': '5', }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toContain('invalid is not a valid config type, must be main or channel'); expect(warnings).toContain('anotherInvalid is not a valid config type, must be main or channel'); // Should use defaults since invalid types are ignored expect(config.main.logDir).toBe(DEFAULT_LOGGER_CONFIG.logDir); expect(config.main.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); expect(config.channel.logDir).toBe(DEFAULT_LOGGER_CONFIG.logDir); expect(config.channel.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); }); test('should emit warning when logDir is an invalid value', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.logDir': '', // Empty string 'logger.main.logLevel': 'DEBUG', // Valid value to ensure other configs work }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toContain('logger.main.logDir must be a valid filepath string'); // Invalid logDir should be cleaned up and default used expect(config.main.logDir).toBe(DEFAULT_LOGGER_CONFIG.logDir); // Other valid configs should still work expect(config.main.logLevel).toBe(LogLevel.DEBUG); }); test('should emit warning when maxFileSizeMb is an invalid value', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.maxFileSizeMb': '0', // Zero 'logger.channel.maxFileSizeMb': '-5', // Negative 'logger.main.logDir': '/tmp/logs', // Valid value to ensure other configs work }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toContain('logger.main.maxFileSizeMb must be a valid integer'); expect(warnings).toContain('logger.channel.maxFileSizeMb must be a valid integer'); // Invalid maxFileSizeMb should be cleaned up and default used expect(config.main.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); expect(config.channel.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); // Other valid configs should still work expect(config.main.logDir).toBe('/tmp/logs'); }); test('should emit warning when filesToKeep is an invalid value', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.filesToKeep': '0', // Zero 'logger.channel.filesToKeep': '-3', // Negative 'logger.main.logDir': '/tmp/logs', // Valid value to ensure other configs work }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toContain('logger.main.filesToKeep must be a valid integer'); expect(warnings).toContain('logger.channel.filesToKeep must be a valid integer'); // Invalid filesToKeep should be cleaned up and default used expect(config.main.filesToKeep).toBe(DEFAULT_LOGGER_CONFIG.filesToKeep); expect(config.channel.filesToKeep).toBe(DEFAULT_LOGGER_CONFIG.filesToKeep); // Other valid configs should still work expect(config.main.logDir).toBe('/tmp/logs'); }); test('should emit warning when logLevel is an invalid value', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.logLevel': '99', // Invalid number 'logger.channel.logLevel': '-1', // Invalid number 'logger.main.logLevel2': 'INVALID', // Invalid string (renamed to avoid duplicate) 'logger.main.logDir': '/tmp/logs', // Valid value to ensure other configs work }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toContain('Error while parsing logger.main.logLevel: Invalid log level: 99'); expect(warnings).toContain('Error while parsing logger.channel.logLevel: Invalid log level: -1'); expect(warnings).toContain('logger.main.logLevel2 is not a valid setting name'); // Invalid logLevel should be cleaned up and default used expect(config.main.logLevel).toBe(LogLevel.INFO); // Falls back to INFO from parseLogLevel error expect(config.channel.logLevel).toBe(LogLevel.INFO); // Falls back to INFO from parseLogLevel error // Other valid configs should still work expect(config.main.logDir).toBe('/tmp/logs'); }); test('should handle mixed valid and invalid configurations', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.logDir': '/tmp/logs', // Valid 'logger.main.filesToKeep': 'invalid', // Invalid 'logger.main.logLevel': 'DEBUG', // Valid 'logger.channel.logDir': '', // Invalid 'logger.channel.logLevel': 'INVALID_LEVEL', // Invalid 'logger.main.invalidProp': 'should-warn', // Invalid prop }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toContain('logger.main.filesToKeep must be a valid integer'); expect(warnings).toContain('logger.channel.logDir must be a valid filepath string'); expect(warnings).toContain('Error while parsing logger.channel.logLevel: Invalid log level: INVALID_LEVEL'); expect(warnings).toContain('logger.main.invalidProp is not a valid setting name'); // Valid configs should be preserved expect(config.main.logDir).toBe('/tmp/logs'); expect(config.main.logLevel).toBe(LogLevel.DEBUG); // Invalid configs should use defaults expect(config.main.filesToKeep).toBe(DEFAULT_LOGGER_CONFIG.filesToKeep); expect(config.channel.logDir).toBe(DEFAULT_LOGGER_CONFIG.logDir); expect(config.channel.logLevel).toBe(LogLevel.INFO); // Fallback from parseLogLevel error // Values not specified should also use defaults expect(config.main.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); expect(config.channel.maxFileSizeMb).toBe(DEFAULT_LOGGER_CONFIG.maxFileSizeMb); expect(config.channel.filesToKeep).toBe(DEFAULT_LOGGER_CONFIG.filesToKeep); }); test('should handle undefined values gracefully', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', 'logger.main.logDir': undefined, 'logger.main.logLevel': 'DEBUG', 'logger.channel.logLevel': undefined, }; const [config, warnings] = parseLoggerConfigFromArgs(args); // Should not have warnings for undefined values (they're skipped) expect(warnings).toHaveLength(0); expect(config.main.logDir).toBe(DEFAULT_LOGGER_CONFIG.logDir); expect(config.main.logLevel).toBe(LogLevel.DEBUG); expect(config.channel.logLevel).toBe(DEFAULT_LOGGER_CONFIG.logLevel); }); test('should handle args object without logger config options', () => { const args: AgentArgs = { baseUrl: 'https://api.medplum.com', clientId: 'test-client-id', clientSecret: 'test-client-secret', agentId: 'test-agent-id', }; const [config, warnings] = parseLoggerConfigFromArgs(args); expect(warnings).toHaveLength(0); expect(config.main).toEqual(DEFAULT_LOGGER_CONFIG); expect(config.channel).toEqual(DEFAULT_LOGGER_CONFIG); }); }); describe('getWinstonLevelFromMedplumLevel', () => { test.each([ ['NONE', 'error'], ['ERROR', 'error'], ['WARN', 'warn'], ['INFO', 'info'], ['DEBUG', 'debug'], ] as [(typeof LogLevelNames)[number], string][])('%s => %s', (medplumLogLevel, winstonLogLevel) => { expect(getWinstonLevelFromMedplumLevel(parseLogLevel(medplumLogLevel))).toStrictEqual(winstonLogLevel); }); test('invalid input throws', () => { expect(() => getWinstonLevelFromMedplumLevel(100)).toThrow('Invalid log level'); }); }); describe('WinstonWrapperLogger', () => { let logger: WinstonWrapperLogger; let consoleSpy: { log: jest.SpyInstance; warn: jest.SpyInstance; error: jest.SpyInstance; }; beforeEach(() => { // Spy on console methods that winston uses consoleSpy = { log: jest.spyOn(console, 'log').mockImplementation(() => {}), warn: jest.spyOn(console, 'warn').mockImplementation(() => {}), error: jest.spyOn(console, 'error').mockImplementation(() => {}), }; // Create a logger with a custom config const config = { ...DEFAULT_LOGGER_CONFIG, logLevel: LogLevel.INFO, }; logger = new WinstonWrapperLogger(config, LoggerType.MAIN); }); afterEach(() => { // Restore console methods consoleSpy.log.mockRestore(); consoleSpy.warn.mockRestore(); consoleSpy.error.mockRestore(); }); test('should not log anything when log level is lower than level of a log message', () => { // Set logger to WARN level, so DEBUG and INFO should be filtered out logger.level = LogLevel.WARN; logger.debug('Debug message'); logger.info('Info message'); logger.warn('Warn message'); logger.error('Error message'); // With forceConsole: true, all levels go to console.log // DEBUG and INFO should not be logged (filtered out) // WARN and ERROR should be logged expect(consoleSpy.log).toHaveBeenCalledTimes(2); expect(consoleSpy.warn).not.toHaveBeenCalled(); expect(consoleSpy.error).not.toHaveBeenCalled(); }); test('should handle data being an Error', () => { const error = new Error('Test error message'); error.stack = 'Error: Test error message\n at test (test.js:1:1)'; logger.error('Error occurred', error); expect(consoleSpy.log).toHaveBeenCalled(); // Check that the error was serialized properly by examining the call const errorCall = consoleSpy.log.mock.calls[0][0]; expect(errorCall).toContain('Error occurred'); expect(errorCall).toContain('Error: Test error message'); }); test('.debug logs message of level DEBUG', () => { // Create a logger with DEBUG level const debugLogger = new WinstonWrapperLogger( { ...DEFAULT_LOGGER_CONFIG, logLevel: LogLevel.DEBUG }, LoggerType.MAIN ); debugLogger.debug('Debug message', { key: 'value' }); expect(consoleSpy.log).toHaveBeenCalled(); const debugCall = consoleSpy.log.mock.calls[0][0]; expect(debugCall).toContain('Debug message'); expect(debugCall).toContain('"key":"value"'); }); test('.info logs message of level INFO', () => { logger.info('Info message', { key: 'value' }); expect(consoleSpy.log).toHaveBeenCalled(); const infoCall = consoleSpy.log.mock.calls[0][0]; expect(infoCall).toContain('Info message'); expect(infoCall).toContain('"key":"value"'); }); test('.warn logs message of level WARN', () => { logger.warn('Warn message', { key: 'value' }); expect(consoleSpy.log).toHaveBeenCalled(); const warnCall = consoleSpy.log.mock.calls[0][0]; expect(warnCall).toContain('Warn message'); expect(warnCall).toContain('"key":"value"'); }); test('.error logs message of level ERROR', () => { logger.error('Error message', { key: 'value' }); expect(consoleSpy.log).toHaveBeenCalled(); const errorCall = consoleSpy.log.mock.calls[0][0]; expect(errorCall).toContain('Error message'); expect(errorCall).toContain('"key":"value"'); }); test('.clone should use parent logger winston instance', () => { const clonedLogger = logger.clone(); // The cloned logger should use the same winston instance as the parent expect(clonedLogger.getWinston()).toBe(logger.getWinston()); // Test that both loggers log to the same winston instance logger.info('Parent message'); clonedLogger.info('Clone message'); expect(consoleSpy.log).toHaveBeenCalledTimes(2); const parentCall = consoleSpy.log.mock.calls[0][0]; const cloneCall = consoleSpy.log.mock.calls[1][0]; expect(parentCall).toContain('Parent message'); expect(cloneCall).toContain('Clone message'); }); test('.clone should work with a prefix', () => { const clonedLogger = logger.clone({ options: { prefix: '[TEST] ' }, }); // The cloned logger should use the same winston instance as the parent expect(clonedLogger.getWinston()).toBe(logger.getWinston()); clonedLogger.info('Test message'); expect(consoleSpy.log).toHaveBeenCalled(); const call = consoleSpy.log.mock.calls[0][0]; expect(call).toContain('[TEST] Test message'); }); test('should override metadata when cloning', () => { const loggerWithMetadata = new WinstonWrapperLogger( { ...DEFAULT_LOGGER_CONFIG, logLevel: LogLevel.INFO }, LoggerType.MAIN, { metadata: { service: 'test-service' } } ); const clonedLogger = loggerWithMetadata.clone({ metadata: { requestId: '123' }, }); // The cloned logger should use the same winston instance as the parent expect(clonedLogger.getWinston()).toBe(loggerWithMetadata.getWinston()); clonedLogger.info('Test message', { userId: '456' }); expect(consoleSpy.log).toHaveBeenCalled(); const call = consoleSpy.log.mock.calls[0][0]; expect(call).toContain('Test message'); expect(call).toContain('"requestId":"123"'); expect(call).toContain('"userId":"456"'); // The clone method overrides metadata entirely, it doesn't merge expect(call).not.toContain('"service":"test-service"'); }); test('should handle log method with different levels', () => { // Create a logger with DEBUG level to allow all levels const debugLogger = new WinstonWrapperLogger( { ...DEFAULT_LOGGER_CONFIG, logLevel: LogLevel.DEBUG }, LoggerType.MAIN ); debugLogger.log(LogLevel.DEBUG, 'Debug via log method'); debugLogger.log(LogLevel.INFO, 'Info via log method'); debugLogger.log(LogLevel.WARN, 'Warn via log method'); debugLogger.log(LogLevel.ERROR, 'Error via log method'); // With forceConsole: true, all levels go to console.log expect(consoleSpy.log).toHaveBeenCalledTimes(4); // DEBUG, INFO, WARN, ERROR expect(consoleSpy.warn).not.toHaveBeenCalled(); expect(consoleSpy.error).not.toHaveBeenCalled(); const debugCall = consoleSpy.log.mock.calls[0][0]; const infoCall = consoleSpy.log.mock.calls[1][0]; const warnCall = consoleSpy.log.mock.calls[2][0]; const errorCall = consoleSpy.log.mock.calls[3][0]; expect(debugCall).toContain('Debug via log method'); expect(infoCall).toContain('Info via log method'); expect(warnCall).toContain('Warn via log method'); expect(errorCall).toContain('Error via log method'); }); test('should handle Error object in log method', () => { const error = new Error('Test error'); error.stack = 'Error: Test error\n at test (test.js:1:1)'; logger.log(LogLevel.ERROR, 'Error occurred', error); expect(consoleSpy.log).toHaveBeenCalled(); const errorCall = consoleSpy.log.mock.calls[0][0]; expect(errorCall).toContain('Error occurred'); expect(errorCall).toContain('Error: Test error'); }); test('should preserve prefix when cloning with existing prefix', () => { const loggerWithPrefix = new WinstonWrapperLogger( { ...DEFAULT_LOGGER_CONFIG, logLevel: LogLevel.INFO }, LoggerType.MAIN, { prefix: '[PARENT] ' } ); const clonedLogger = loggerWithPrefix.clone({ options: { prefix: '[CHILD] ' }, }); // The cloned logger should use the same winston instance as the parent expect(clonedLogger.getWinston()).toBe(loggerWithPrefix.getWinston()); clonedLogger.info('Test message'); expect(consoleSpy.log).toHaveBeenCalled(); const call = consoleSpy.log.mock.calls[0][0]; expect(call).toContain('[CHILD] Test message'); }); test('should use parent logger winston instance when parent is provided', () => { const parentLogger = new WinstonWrapperLogger( { ...DEFAULT_LOGGER_CONFIG, logLevel: LogLevel.INFO }, LoggerType.MAIN ); const childLogger = new WinstonWrapperLogger( { ...DEFAULT_LOGGER_CONFIG, logLevel: LogLevel.INFO }, LoggerType.CHANNEL, { parentLogger } ); expect(childLogger.getWinston()).toBe(parentLogger.getWinston()); childLogger.info('Child message'); expect(consoleSpy.log).toHaveBeenCalled(); const call = consoleSpy.log.mock.calls[0][0]; expect(call).toContain('Child message'); }); }); describe('createWinstonFromLoggerConfig', () => { let tempDir: string; let originalNodeEnv: string | undefined; beforeAll(() => { console.log = jest.fn(); }); beforeEach(async () => { // Create a temporary directory for test logs tempDir = await mkdtemp(join(tmpdir(), 'medplum-logger-test-')); // Store original NODE_ENV and set to non-test originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; }); afterEach(async () => { // Restore original NODE_ENV process.env.NODE_ENV = originalNodeEnv; // Clean up temporary directory try { await rm(tempDir, { recursive: true, force: true }); } catch (_error) { // Ignore cleanup errors } }); test('should create winston logger with console transport in test environment', () => { // Set NODE_ENV back to test for this specific test process.env.NODE_ENV = 'test'; const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); expect(logger).toBeDefined(); expect(logger.transports).toHaveLength(1); expect((logger.transports[0] as any).name).toBe('console'); // Restore NODE_ENV for other tests process.env.NODE_ENV = 'production'; }); test('should create winston logger with daily rotate transport in non-test environment', () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, maxFileSizeMb: 5, filesToKeep: 3, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); expect(logger).toBeDefined(); expect(logger.transports).toHaveLength(2); // console + daily rotate const dailyRotateTransport = logger.transports.find((t) => (t as any).name === 'dailyRotateFile'); expect(dailyRotateTransport).toBeDefined(); }); test('should create correct filename for main logger type', () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); const dailyRotateTransport = logger.transports.find((t) => (t as any).name === 'dailyRotateFile'); expect(dailyRotateTransport).toBeDefined(); // The filename should contain 'medplum-agent-main' expect((dailyRotateTransport as any).options.filename).toContain('medplum-agent-main'); }); test('should create correct filename for channel logger type', () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.CHANNEL); const dailyRotateTransport = logger.transports.find((t) => (t as any).name === 'dailyRotateFile'); expect(dailyRotateTransport).toBeDefined(); // The filename should contain 'medplum-agent-channels' expect((dailyRotateTransport as any).options.filename).toContain('medplum-agent-channels'); }); test('should use correct dirname from config', () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); const dailyRotateTransport = logger.transports.find((t) => (t as any).name === 'dailyRotateFile'); expect(dailyRotateTransport).toBeDefined(); // The dirname property is stored in the options expect((dailyRotateTransport as any).options.dirname).toBe(tempDir); }); test('should use correct maxSize from config', () => { const maxFileSizeMb = 15; const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, maxFileSizeMb, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); const dailyRotateTransport = logger.transports.find((t) => (t as any).name === 'dailyRotateFile'); expect(dailyRotateTransport).toBeDefined(); // The maxSize property is stored in the options expect((dailyRotateTransport as any).options.maxSize).toBe(`${maxFileSizeMb}m`); }); test('should use correct maxFiles from config', () => { const filesToKeep = 7; const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, filesToKeep, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); const dailyRotateTransport = logger.transports.find((t) => (t as any).name === 'dailyRotateFile'); expect(dailyRotateTransport).toBeDefined(); // The maxFiles property is stored in the options expect((dailyRotateTransport as any).options.maxFiles).toBe(filesToKeep); }); test('should set correct log level from config', () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.DEBUG, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); expect(logger.level).toBe('debug'); }); test('should set logger to silent when log level is NONE', () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.NONE, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); expect(logger.silent).toBe(true); }); test('should not be silent when log level is not NONE', () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); expect(logger.silent).toBe(false); }); test('should have error handler attached to daily rotate transport', () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); const dailyRotateTransport = logger.transports.find((t) => (t as any).name === 'dailyRotateFile'); expect(dailyRotateTransport).toBeDefined(); // Check that the error handler is attached by examining the listeners const listeners = (dailyRotateTransport as any).listeners('error'); expect(listeners.length).toBeGreaterThan(0); // The error handler should be a function that calls console.error const errorHandler = listeners[0]; expect(typeof errorHandler).toBe('function'); }); test('should create log files when logging messages', async () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); // Log some messages logger.info('Test info message', { testData: 'value' }); logger.warn('Test warn message', { testData: 'value2' }); logger.error('Test error message', { testData: 'value3' }); // Give winston time to write to file await sleep(100); // Check if log files were created const fs = await import('fs/promises'); const files = await fs.readdir(tempDir); const logFiles = files.filter((file) => file.includes('medplum-agent-main')); expect(logFiles.length).toBeGreaterThan(0); // Check that the log file contains our messages const logFile = logFiles[0]; const logContent = await fs.readFile(join(tempDir, logFile), 'utf-8'); expect(logContent).toContain('Test info message'); expect(logContent).toContain('Test warn message'); expect(logContent).toContain('Test error message'); }); test('should format log messages correctly with timestamp and level transformation', async () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.INFO, }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); // Log a message logger.info('Test message', { key: 'value' }); // Give winston time to write to file await sleep(100); // Check the log file content const fs = await import('fs/promises'); const files = await fs.readdir(tempDir); const logFiles = files.filter((file) => file.includes('medplum-agent-main')); expect(logFiles.length).toBeGreaterThan(0); const logContent = await fs.readFile(join(tempDir, logFiles[0]), 'utf-8'); const logLines = logContent.trim().split('\n'); const logEntry = JSON.parse(logLines.at(-1) as string); // Check the format transformation expect(logEntry.level).toBe('INFO'); // Should be uppercase expect(logEntry.msg).toBe('Test message'); // Should be 'msg' not 'message' expect(logEntry.key).toBe('value'); expect(logEntry.timestamp).toBeDefined(); // Should have timestamp }); test('should respect log level filtering', async () => { const config = { ...DEFAULT_LOGGER_CONFIG, logDir: tempDir, logLevel: LogLevel.WARN, // Only WARN and ERROR should be logged }; const logger = createWinstonFromLoggerConfig(config, LoggerType.MAIN); // Log messages at different levels logger.debug('Debug message'); // Should be filtered out logger.info('Info message'); // Should be filtered out logger.warn('Warn message'); // Should be logged logger.error('Error message'); // Should be logged // Give winston time to write to file await sleep(100); // Check the log file content const fs = await import('fs/promises'); const files = await fs.readdir(tempDir); const logFiles = files.filter((file) => file.includes('medplum-agent-main')); const logContent = await fs.readFile(join(tempDir, logFiles[0]), 'utf-8'); // Should not contain debug or info messages expect(logContent).not.toContain('Debug message'); expect(logContent).not.toContain('Info message'); // Should contain warn and error messages expect(logContent).toContain('Warn message'); expect(logContent).toContain('Error message'); }); }); });

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