diff --git a/lib/Logger.js b/lib/Logger.js new file mode 100644 index 0000000..81c2155 --- /dev/null +++ b/lib/Logger.js @@ -0,0 +1,133 @@ +/** + * Custom Logger for Osmosis Web Scraping + * Provides structured logging with multiple log levels and configurable outputs + */ +class Logger { + /** + * Create a new Logger instance + * @param {Object} options - Logger configuration options + * @param {string} [options.level='info'] - Minimum log level to output + * @param {boolean} [options.timestamp=true] - Include timestamp in log messages + * @param {boolean} [options.colorize=true] - Colorize log output + */ + constructor(options = {}) { + this.levels = { + error: 0, + warn: 1, + info: 2, + debug: 3 + }; + + this.options = { + level: options.level || 'info', + timestamp: options.timestamp !== false, + colorize: options.colorize !== false + }; + } + + /** + * Generate a formatted timestamp + * @returns {string} Formatted timestamp + * @private + */ + _getTimestamp() { + return new Date().toISOString(); + } + + /** + * Determine if a log should be output based on current log level + * @param {string} messageLevel - Level of the log message + * @returns {boolean} Whether the message should be logged + * @private + */ + _shouldLog(messageLevel) { + return this.levels[messageLevel] <= this.levels[this.options.level]; + } + + /** + * Color-code log messages + * @param {string} level - Log level + * @param {string} message - Log message + * @returns {string} Colored log message + * @private + */ + _colorize(level, message) { + if (!this.options.colorize) return message; + + const colors = { + error: '\x1b[31m', // Red + warn: '\x1b[33m', // Yellow + info: '\x1b[36m', // Cyan + debug: '\x1b[90m' // Gray + }; + const reset = '\x1b[0m'; + return `${colors[level]}${message}${reset}`; + } + + /** + * Core logging method + * @param {string} level - Log level + * @param {string} message - Log message + * @param {Object} [metadata] - Additional log metadata + */ + _log(level, message, metadata = {}) { + if (!this._shouldLog(level)) return; + + const timestamp = this.options.timestamp ? `[${this._getTimestamp()}] ` : ''; + const logMessage = `${timestamp}[${level.toUpperCase()}] ${message}`; + const coloredMessage = this._colorize(level, logMessage); + + switch (level) { + case 'error': + console.error(coloredMessage, metadata); + break; + case 'warn': + console.warn(coloredMessage, metadata); + break; + case 'info': + console.info(coloredMessage, metadata); + break; + case 'debug': + console.debug(coloredMessage, metadata); + break; + } + } + + /** + * Log an error message + * @param {string} message - Error message + * @param {Object} [metadata] - Error metadata + */ + error(message, metadata = {}) { + this._log('error', message, metadata); + } + + /** + * Log a warning message + * @param {string} message - Warning message + * @param {Object} [metadata] - Warning metadata + */ + warn(message, metadata = {}) { + this._log('warn', message, metadata); + } + + /** + * Log an informational message + * @param {string} message - Information message + * @param {Object} [metadata] - Info metadata + */ + info(message, metadata = {}) { + this._log('info', message, metadata); + } + + /** + * Log a debug message + * @param {string} message - Debug message + * @param {Object} [metadata] - Debug metadata + */ + debug(message, metadata = {}) { + this._log('debug', message, metadata); + } +} + +module.exports = Logger; \ No newline at end of file diff --git a/package.json b/package.json index 5aa2568..e976e89 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,12 @@ }, "devDependencies": { "jscs": ">=3.0.2", - "nodeunit": "0.11.3" + "nodeunit": "0.11.3", + "jest": "^29.7.0" }, "scripts": { - "test": "node ./node_modules/.bin/nodeunit test" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", + "test:logger": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/logger.test.js" }, "license": "MIT", "main": "index", @@ -38,5 +40,8 @@ "readmeFilename": "Readme.md", "bugs": { "url": "https://github.com/rchipka/node-osmosis/issues" + }, + "jest": { + "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/test/logger.js b/test/logger.js new file mode 100644 index 0000000..293c722 --- /dev/null +++ b/test/logger.js @@ -0,0 +1,87 @@ +const assert = require('assert'); +const Logger = require('../lib/Logger'); + +describe('Logger', () => { + let logger; + let originalConsole; + + beforeEach(() => { + // Capture console methods + originalConsole = { ...console }; + console.error = console.warn = console.info = console.debug = jest.fn(); + }); + + afterEach(() => { + // Restore original console methods + console.error = originalConsole.error; + console.warn = originalConsole.warn; + console.info = originalConsole.info; + console.debug = originalConsole.debug; + }); + + describe('Log Level Configuration', () => { + it('should not log messages below configured level', () => { + logger = new Logger({ level: 'warn' }); + logger.info('Test Info Message'); + logger.debug('Test Debug Message'); + + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('should log messages at or above configured level', () => { + logger = new Logger({ level: 'warn' }); + logger.warn('Test Warning'); + logger.error('Test Error'); + + expect(console.warn).toHaveBeenCalled(); + expect(console.error).toHaveBeenCalled(); + }); + }); + + describe('Log Methods', () => { + beforeEach(() => { + logger = new Logger({ timestamp: false, colorize: false }); + }); + + it('should call correct console method for each log level', () => { + logger.error('Error Message'); + logger.warn('Warning Message'); + logger.info('Info Message'); + logger.debug('Debug Message'); + + expect(console.error).toHaveBeenCalledWith(expect.stringContaining('ERROR'), {}); + expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('WARN'), {}); + expect(console.info).toHaveBeenCalledWith(expect.stringContaining('INFO'), {}); + expect(console.debug).toHaveBeenCalledWith(expect.stringContaining('DEBUG'), {}); + }); + + it('should support metadata with log messages', () => { + const metadata = { url: 'https://example.com', status: 404 }; + logger.error('Scraping Error', metadata); + + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('ERROR'), + metadata + ); + }); + }); + + describe('Optional Features', () => { + it('should support disabling timestamp', () => { + logger = new Logger({ timestamp: false }); + logger.info('No Timestamp'); + + const consoleCall = console.info.mock.calls[0][0]; + expect(consoleCall).not.toMatch(/^\[\d{4}-\d{2}-\d{2}/); + }); + + it('should support disabling colorization', () => { + logger = new Logger({ colorize: false }); + logger.info('Uncolored Message'); + + const consoleCall = console.info.mock.calls[0][0]; + expect(consoleCall).not.toMatch(/\x1b/); + }); + }); +}); \ No newline at end of file diff --git a/test/logger.test.js b/test/logger.test.js new file mode 100644 index 0000000..d035d13 --- /dev/null +++ b/test/logger.test.js @@ -0,0 +1,43 @@ +const assert = require('assert'); +const Logger = require('../lib/Logger'); + +describe('Logger Tests', () => { + let logger; + let originalConsole; + + beforeEach(() => { + originalConsole = { ...console }; + console.error = jest.fn(); + console.warn = jest.fn(); + console.info = jest.fn(); + console.debug = jest.fn(); + }); + + afterEach(() => { + console.error = originalConsole.error; + console.warn = originalConsole.warn; + console.info = originalConsole.info; + console.debug = originalConsole.debug; + }); + + it('should log messages at or above the configured level', () => { + logger = new Logger({ level: 'warn' }); + logger.error('Test Error'); + logger.warn('Test Warning'); + logger.info('Test Info'); + logger.debug('Test Debug'); + + assert(console.error.mock.calls.length > 0, 'Error should be logged'); + assert(console.warn.mock.calls.length > 0, 'Warning should be logged'); + assert(console.info.mock.calls.length === 0, 'Info should not be logged'); + assert(console.debug.mock.calls.length === 0, 'Debug should not be logged'); + }); + + it('should support optional metadata', () => { + logger = new Logger(); + const metadata = { url: 'https://example.com', status: 404 }; + logger.error('Scraping Error', metadata); + + assert(console.error.mock.calls[0][1] === metadata, 'Metadata should be logged'); + }); +}); \ No newline at end of file