@seawingai/winglog
Version:
A powerful TypeScript/JavaScript logging library built on top of Pino for structured logging with enhanced features
386 lines (306 loc) • 14.5 kB
text/typescript
import { WingLog, LogType } from './winglog';
import * as fs from 'fs';
import * as path from 'path';
import pino from 'pino';
// Mock pino to avoid actual file operations during tests
jest.mock('pino');
jest.mock('fs');
jest.mock('path');
const mockPino = pino as jest.Mocked<typeof pino>;
const mockFs = fs as jest.Mocked<typeof fs>;
const mockPath = path as jest.Mocked<typeof path>;
describe('WingLog', () => {
let wingLog: WingLog;
let mockLogger: any;
let mockStream: any;
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Setup mock logger
mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};
mockStream = {
write: jest.fn(),
};
// Mock pino methods
(mockPino as any).mockReturnValue(mockLogger);
(mockPino.destination as any) = jest.fn().mockReturnValue(mockStream);
(mockPino.transport as any) = jest.fn().mockReturnValue(mockStream);
(mockPino.multistream as any) = jest.fn().mockReturnValue(mockStream);
// Mock fs methods
mockFs.existsSync.mockReturnValue(false);
mockFs.mkdirSync.mockImplementation(() => undefined);
// Mock path methods
mockPath.join.mockImplementation((...args) => args.join('/'));
mockPath.resolve.mockImplementation((...args) => args.join('/'));
// Mock process.cwd
Object.defineProperty(process, 'cwd', {
value: jest.fn().mockReturnValue('/test/cwd'),
writable: true,
});
wingLog = new WingLog('test-logger');
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('Constructor', () => {
it('should create a WingLog instance with the given name', () => {
expect(wingLog).toBeInstanceOf(WingLog);
});
it('should create logs directory if it does not exist', () => {
expect(mockFs.existsSync).toHaveBeenCalledWith('/test/cwd/logs');
expect(mockFs.mkdirSync).toHaveBeenCalledWith('/test/cwd/logs', { recursive: true });
});
it('should create logs directory even if it already exists (mkdirSync handles this)', () => {
mockFs.existsSync.mockReturnValue(true);
new WingLog('test-logger-2');
expect(mockFs.mkdirSync).toHaveBeenCalledWith('/test/cwd/logs', { recursive: true });
});
it('should initialize pino logger with correct configuration', () => {
expect(mockPino).toHaveBeenCalledWith(
{
level: 'debug',
timestamp: expect.any(Function),
},
mockStream
);
});
it('should log initialization message', () => {
expect(mockLogger.debug).toHaveBeenCalledWith('Logger initialized: [test-logger]');
});
});
describe('Logging Methods', () => {
beforeEach(() => {
// Mock Date.now for consistent testing
jest.spyOn(Date, 'now').mockReturnValue(1000000);
});
it('should log started message correctly', () => {
const result = wingLog.started('Test started');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Test started');
expect(result).toBe(0);
});
it('should log finished message correctly', () => {
const result = wingLog.finished('Test finished');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Test finished');
expect(result).toBe(0);
});
it('should log success message correctly', () => {
const result = wingLog.success('Test success');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Test success');
expect(result).toBe(0);
});
it('should log failed message correctly', () => {
const result = wingLog.failed('Test failed');
expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Test failed');
expect(result).toBe(0);
});
it('should log info message correctly', () => {
const result = wingLog.info('Test info');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Test info');
expect(result).toBe(0);
});
it('should log warn message correctly', () => {
const result = wingLog.warn('Test warning');
expect(mockLogger.warn).toHaveBeenCalledWith('[test-logger]:Test warning');
expect(result).toBe(0);
});
it('should log debug message correctly', () => {
const result = wingLog.debug('Test debug');
expect(mockLogger.debug).toHaveBeenCalledWith('[test-logger]:Test debug');
expect(result).toBe(0);
});
});
describe('Duration Calculation', () => {
beforeEach(() => {
// Mock Date.now to return a fixed timestamp
jest.spyOn(Date, 'now').mockReturnValue(1000000);
});
it('should calculate duration correctly when startTime is provided', () => {
const startTime = 999000; // 1 second ago
const result = wingLog.started('Test with duration', startTime);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[00:01]:Test with duration');
expect(result).toBe(1);
});
it('should handle zero duration correctly', () => {
const startTime = 1000000; // Same time
const result = wingLog.started('Test with zero duration', startTime);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[00:00]:Test with zero duration');
expect(result).toBe(0);
});
it('should handle duration with minutes and seconds', () => {
const startTime = 940000; // 60 seconds ago
const result = wingLog.started('Test with minutes', startTime);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[01:00]:Test with minutes');
expect(result).toBe(60);
});
it('should handle duration with decimal seconds', () => {
const startTime = 999500; // 0.5 seconds ago
const result = wingLog.started('Test with decimal duration', startTime);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[00:01]:Test with decimal duration');
expect(result).toBe(0.5);
});
it('should handle very large duration values', () => {
const startTime = 1; // Very long time ago (but not 0 to avoid falsy check)
const result = wingLog.started('Long running task', startTime);
expect(result).toBe(1000); // 999.99 seconds rounded to 1000.00 by toFixed(2)
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[16:40]:Long running task');
});
});
describe('Error Handling', () => {
it('should handle Error objects correctly', () => {
const error = new Error('Test error message');
wingLog.error('Operation failed', error);
expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Operation failed: Test error message {"error":{}}');
});
it('should handle non-Error objects correctly', () => {
const error = 'String error';
wingLog.error('Operation failed', error);
expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Operation failed: String error {"error":{}}');
});
it('should handle null/undefined errors', () => {
wingLog.error('Operation failed', null);
expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Operation failed: null {"error":{}}');
});
});
describe('Overloaded Methods', () => {
beforeEach(() => {
// Mock Date.now to return a fixed timestamp
jest.spyOn(Date, 'now').mockReturnValue(1000000);
});
it('should handle info with simple message', () => {
const result = wingLog.info('Simple message');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Simple message');
expect(result).toBe(0);
});
it('should handle info with structured data', () => {
const record = { userId: 123, action: 'login' };
wingLog.info('User action', record);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:User action {"userId":123,"action":"login"}');
});
it('should handle debug with simple message', () => {
const result = wingLog.debug('Simple debug message');
expect(mockLogger.debug).toHaveBeenCalledWith('[test-logger]:Simple debug message');
expect(result).toBe(0);
});
it('should handle debug with structured data', () => {
const record = { query: 'SELECT * FROM users', duration: 45 };
wingLog.debug('Database query', record);
expect(mockLogger.debug).toHaveBeenCalledWith('[test-logger]:Database query {"query":"SELECT * FROM users","duration":45}');
});
it('should handle failed with simple message', () => {
const result = wingLog.failed('Simple error message');
expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Simple error message');
expect(result).toBe(0);
});
it('should handle failed with structured data', () => {
const record = { userId: 123, reason: 'Invalid token', ip: '192.168.1.1' };
wingLog.failed('Authentication failed', record);
expect(mockLogger.error).toHaveBeenCalledWith('[test-logger]:Authentication failed {"userId":123,"reason":"Invalid token","ip":"192.168.1.1"}');
});
it('should handle warn with simple message', () => {
const result = wingLog.warn('Simple warning message');
expect(mockLogger.warn).toHaveBeenCalledWith('[test-logger]:Simple warning message');
expect(result).toBe(0);
});
it('should handle warn with structured data', () => {
const record = { memoryUsage: '85%', threshold: '80%' };
wingLog.warn('High memory usage', record);
expect(mockLogger.warn).toHaveBeenCalledWith('[test-logger]:High memory usage {"memoryUsage":"85%","threshold":"80%"}');
});
it('should handle success with simple message', () => {
const result = wingLog.success('Simple success message');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Simple success message');
expect(result).toBe(0);
});
it('should handle success with structured data', () => {
const record = { userId: 123, email: 'user@example.com' };
wingLog.success('User created successfully', record);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:User created successfully {"userId":123,"email":"user@example.com"}');
});
it('should handle started with simple message', () => {
const result = wingLog.started('Simple start message');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Simple start message');
expect(result).toBe(0);
});
it('should handle started with structured data', () => {
const record = { jobId: 'job-123', priority: 'high' };
wingLog.started('Job processing started', record);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Job processing started {"jobId":"job-123","priority":"high"}');
});
it('should handle finished with simple message', () => {
const result = wingLog.finished('Simple finish message');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Simple finish message');
expect(result).toBe(0);
});
it('should handle finished with structured data', () => {
const record = { jobId: 'job-123', duration: 45, status: 'completed' };
wingLog.finished('Job processing finished', record);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Job processing finished {"jobId":"job-123","duration":45,"status":"completed"}');
});
it('should handle duration calculation with simple message', () => {
const startTime = 999000; // 1 second ago
const result = wingLog.info('Message with duration', startTime);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Duration:[00:01]:Message with duration');
expect(result).toBe(1);
});
it('should handle duration calculation with structured data', () => {
const record = { userId: 123, action: 'login' };
wingLog.info('User action with duration', record);
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:User action with duration {"userId":123,"action":"login"}');
});
});
describe('LogType Enum', () => {
it('should have all expected log types', () => {
expect(LogType.FAILED).toBe('FAILED');
expect(LogType.WARN).toBe('WARN');
expect(LogType.DEBUG).toBe('DEBUG');
expect(LogType.SUCCESS).toBe('SUCCESS');
expect(LogType.STARTED).toBe('STARTED');
expect(LogType.FINISHED).toBe('FINISHED');
expect(LogType.INFO).toBe('INFO');
});
});
describe('Edge Cases', () => {
it('should handle empty messages', () => {
wingLog.info('');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:');
});
it('should handle messages with special characters', () => {
wingLog.info('Message with "quotes" and \'apostrophes\'');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Message with "quotes" and \'apostrophes\'');
});
it('should handle pino transport fallback', () => {
// Mock pino.transport to throw an error
(mockPino.transport as any).mockImplementation(() => {
throw new Error('Transport not available');
});
// Should not throw when creating new instance
expect(() => new WingLog('fallback-test')).not.toThrow();
});
});
describe('Integration Tests', () => {
it('should work with multiple log calls', () => {
wingLog.started('Task started');
wingLog.info('Task in progress');
wingLog.success('Task completed');
expect(mockLogger.info).toHaveBeenCalledTimes(3);
expect(mockLogger.info).toHaveBeenNthCalledWith(1, '[test-logger]:Task started');
expect(mockLogger.info).toHaveBeenNthCalledWith(2, '[test-logger]:Task in progress');
expect(mockLogger.info).toHaveBeenNthCalledWith(3, '[test-logger]:Task completed');
});
it('should handle mixed log levels', () => {
wingLog.debug('Debug message');
wingLog.info('Info message');
wingLog.warn('Warning message');
wingLog.error('Error message', new Error('Test error'));
expect(mockLogger.debug).toHaveBeenCalledWith('[test-logger]:Debug message');
expect(mockLogger.info).toHaveBeenCalledWith('[test-logger]:Info message');
expect(mockLogger.warn).toHaveBeenCalledWith('[test-logger]:Warning message');
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[test-logger]:Error message'));
});
});
});