Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions lib/Logger.js
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -38,5 +40,8 @@
"readmeFilename": "Readme.md",
"bugs": {
"url": "https://github.com/rchipka/node-osmosis/issues"
},
"jest": {
"testEnvironment": "node"
}
}
}
87 changes: 87 additions & 0 deletions test/logger.js
Original file line number Diff line number Diff line change
@@ -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/);
});
});
});
43 changes: 43 additions & 0 deletions test/logger.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});