diff --git a/lib/Logger.js b/lib/Logger.js new file mode 100644 index 0000000..c023ae7 --- /dev/null +++ b/lib/Logger.js @@ -0,0 +1,101 @@ +/** + * Custom Logger for Osmosis Web Scraping + * Provides flexible logging mechanisms with configurable levels + */ +class Logger { + /** + * Logging levels from most verbose to least + * @enum {number} + */ + static LEVELS = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + SILENT: 4 + }; + + /** + * Create a new Logger instance + * @param {Object} options - Logger configuration + * @param {number} [options.level=0] - Logging level + * @param {boolean} [options.timestamp=true] - Include timestamp in logs + * @param {function} [options.writer=console.log] - Custom log writer + */ + constructor(options = {}) { + this.level = options.level !== undefined ? options.level : Logger.LEVELS.DEBUG; + this.timestamp = options.timestamp !== false; + this.writer = options.writer || console.log; + } + + /** + * Generate log message with optional timestamp + * @param {string} level - Log level name + * @param {string} message - Log message + * @param {Object} [context] - Additional logging context + * @returns {string} Formatted log message + * @private + */ + _formatMessage(level, message, context = {}) { + const timestamp = this.timestamp ? `[${new Date().toISOString()}] ` : ''; + const contextStr = Object.keys(context).length > 0 + ? ` ${JSON.stringify(context)}` + : ''; + return `[${level}] ${message}${contextStr}`; + } + + /** + * Shared logging method + * @param {number} logLevel - Logging level + * @param {string} levelName - Log level name + * @param {string} message - Log message + * @param {Object} [context] - Additional context + * @private + */ + _log(logLevel, levelName, message, context) { + if (this.level <= logLevel) { + this.writer(this._formatMessage(levelName, message, context)); + } + } + + /** + * Log a debug message + * @param {string} message - Debug message + * @param {Object} [context] - Additional context + */ + debug(message, context) { + this._log(Logger.LEVELS.DEBUG, 'DEBUG', message, context); + } + + /** + * Log an informational message + * @param {string} message - Info message + * @param {Object} [context] - Additional context + */ + info(message, context) { + this._log(Logger.LEVELS.INFO, 'INFO', message, context); + } + + /** + * Log a warning message + * @param {string} message - Warning message + * @param {Object} [context] - Additional context + */ + warn(message, context) { + this._log(Logger.LEVELS.WARN, 'WARN', message, context); + } + + /** + * Log an error message + * @param {string} message - Error message + * @param {Object} [context] - Additional context + */ + error(message, context) { + this._log(Logger.LEVELS.ERROR, 'ERROR', message, context); + } +} + +// Export as a singleton to ensure consistent logging across the library +const loggerInstance = new Logger(); +module.exports = loggerInstance; +module.exports.Logger = Logger; \ No newline at end of file diff --git a/package.json b/package.json index 5aa2568..ef875a8 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": "jest", + "test:logger": "jest test/logger.js" }, "license": "MIT", "main": "index", @@ -37,6 +39,9 @@ }, "readmeFilename": "Readme.md", "bugs": { - "url": "https://github.com/rchipka/node-osmosis/issues" + "url": "https://github.com/rchipka/node-osmosis/issues"}, + "jest": { + "testEnvironment": "node", + "testMatch": ["**/test/**/*.js"] } -} +} \ No newline at end of file diff --git a/test/logger.js b/test/logger.js new file mode 100644 index 0000000..89520c6 --- /dev/null +++ b/test/logger.js @@ -0,0 +1,58 @@ +const { Logger } = require('../lib/Logger'); + +describe('Logger', () => { + let capturedLogs = []; + const mockWriter = (message) => capturedLogs.push(message); + + beforeEach(() => { + capturedLogs = []; + }); + + test('log messages at different levels', () => { + const logger = new Logger({ + level: Logger.LEVELS.DEBUG, + timestamp: false, + writer: mockWriter + }); + + logger.debug('Debug message'); + logger.info('Info message'); + logger.warn('Warning message'); + + expect(capturedLogs.length).toBe(3); + expect(capturedLogs[0]).toBe('[DEBUG] Debug message'); + expect(capturedLogs[1]).toBe('[INFO] Info message'); + expect(capturedLogs[2]).toBe('[WARN] Warning message'); + }); + + test('respect logging levels', () => { + const logger = new Logger({ + level: Logger.LEVELS.WARN, + timestamp: false, + writer: mockWriter + }); + + logger.debug('Debug message'); + logger.info('Info message'); + logger.warn('Warning message'); + logger.error('Error message'); + + expect(capturedLogs.length).toBe(2); + expect(capturedLogs[0]).toBe('[WARN] Warning message'); + expect(capturedLogs[1]).toBe('[ERROR] Error message'); + }); + + test('support logging context', () => { + const logger = new Logger({ + level: Logger.LEVELS.DEBUG, + timestamp: false, + writer: mockWriter + }); + + logger.info('Scraping page', { url: 'https://example.com', pageNumber: 1 }); + + expect(capturedLogs[0]).toBe( + '[INFO] Scraping page {"url":"https://example.com","pageNumber":1}' + ); + }); +}); \ No newline at end of file diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..3caeb1b --- /dev/null +++ b/test/setup.js @@ -0,0 +1,4 @@ +// Enable using describe and it globally +global.describe = describe; +global.it = it; +global.beforeEach = beforeEach; \ No newline at end of file