diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..916f185 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,14 @@ +name: Node CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..889f2bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# =============================== +# Node dependencies +# =============================== +node_modules/ + +# =============================== +# Logs +# =============================== +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# =============================== +# Environment variables +# =============================== +.env +.env.* +!.env.example + +# =============================== +# OS / Editor junk +# =============================== +.DS_Store +Thumbs.db +.idea/ +.vscode/ +*.swp +*.swo + +# =============================== +# Build / cache +# =============================== +dist/ +build/ +coverage/ +.cache/ +.tmp/ + +# =============================== +# Test artifacts +# =============================== +jest-cache/ +.nyc_output/ + +# =============================== +# npm / yarn / pnpm +# =============================== +package-lock.json +yarn.lock +pnpm-lock.yaml + +# =============================== +# Optional: local test logs +# =============================== +logs-test/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..faa0ceb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This project follows [Semantic Versioning](https://semver.org/). + +--- + +## [1.2.0] – 2025-01-XX + +### Added +- Support for multiple arguments (console-style logging). +- Automatic detection and logging of `Error` objects with stack traces. +- Optional JSON logging mode for structured logs. +- Optional size-based log rotation. +- Factory API: `Logger.createLogger(options)`. + +### Changed +- Improved internal log formatting and safety. +- Console output now safely falls back to `console.log` when needed. + +### Fixed +- README now accurately reflects error logging behavior. + +### Backward Compatibility +- All existing v1.x usage remains fully supported. +- No breaking changes introduced. + +--- + +## [1.1.0] – 2024-XX-XX + +### Added +- Error object support (message + stack trace). +- Improved README accuracy and clarity. + +--- + +## [1.0.4] – Initial Stable Release + +### Added +- Basic file and console logging. +- Support for `info`, `warn`, and `error` levels. +- Daily log files organized by level. diff --git a/README.md b/README.md index 496ccbb..cb06f11 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,201 @@ -# Node-Error-Logger.JS +# Node-Error-Logger.js -> A simple and efficient Node.js package for logging errors and messages to files and the console. +![CI](https://github.com/makstyle119/node-error-logger.js/actions/workflows/test.yml/badge.svg) -## Features: -- Supports error, warning, and info logs. -- Logs messages with timestamps and severity levels. -- Saves logs to separate files based on date and level. -- Provides clear and concise API for logging. +> A lightweight, zero-dependency logger for Node.js with file logging, error stacks, and console-style usage. -## Installation: -```Bash +This package is designed for developers who want **simple, predictable logging** without pulling in large logging frameworks. + +--- + +## ✨ Features + +* ✅ Zero dependencies +* ✅ Supports `info`, `warn`, and `error` levels +* ✅ Console-style logging (multiple arguments like `console.log`) +* ✅ Automatic error stack trace logging +* ✅ Logs stored by **date and level** +* ✅ Optional JSON logs (production-friendly) +* ✅ Optional size-based log rotation +* ✅ Async, non-blocking file writes +* ✅ Backward compatible across all v1.x versions + +--- + +## 📦 Installation + +```bash npm i node-error-logger.js ``` -## Usage -1. Import the Logger module: -```JavaScript +--- + +## 🚀 Basic Usage (Backward Compatible) + +```js const Logger = require('node-error-logger.js'); + +Logger.info('Application started'); +Logger.warn('Low memory warning'); +Logger.error('Something went wrong'); ``` -2. Use the provided logging functions: -```JavaScript -Logger.info('Application started successfully.'); -// Log an error with an optional error object -const myError = new Error('Something went wrong!'); -Logger.error('An error occurred!', myError); +This works exactly the same as earlier versions. + +--- + +## 🧩 Multiple Arguments (Console-style) + +Just like `console.log`, you can pass multiple values: + +```js +Logger.info('User created', userId, userData); +Logger.warn('Invalid input', { field: 'email' }); ``` -## API: -- `Logger.info(message):` Logs an informational message. -- `Logger.warn(message):` Logs a warning message. -- `Logger.error(message, error):` Logs an error message. (Optional error object for stack trace) +Extra values are treated as **metadata**. -## Example: -```JavaScript -const Logger = require('node-error-logger.js'); +--- + +## ❌ Error Object Support (Automatic) -// Log messages throughout your application -Logger.info('Processing data...'); +Pass an `Error` anywhere in the arguments: +```js try { - // Your application logic here -} catch (error) { - Logger.error('An error occurred!', error); + throw new Error('Database connection failed'); +} catch (err) { + Logger.error('Unhandled exception', err, { retry: false }); } +``` + +✔ Logs the error message +✔ Logs the full stack trace +✔ No extra configuration required -Logger.warn('A potential issue might arise.'); +--- + +## 📂 Log Output Structure + +Logs are written to the application root: + +``` +logs/ + ├── info/ + │ └── 2025-01-01-info.log + ├── warn/ + │ └── 2025-01-01-warn.log + └── error/ + └── 2025-01-01-error.log +``` + +--- + +## 🧪 JSON Logging Mode (Optional) + +Ideal for production environments and log processors. + +```js +const Logger = require('node-error-logger.js'); + +const logger = Logger.createLogger({ json: true }); + +logger.error('Payment failed', new Error('timeout'), { orderId: 123 }); +``` + +### Example JSON Output + +```json +{ + "timestamp": "2025-01-01T12:00:00.000Z", + "level": "error", + "message": "Payment failed", + "meta": [{ "orderId": 123 }], + "error": { + "name": "Error", + "message": "timeout", + "stack": "..." + } +} ``` -## Benefits: -- Improves debugging by providing detailed logs. -- Helps monitor application behavior and identify errors. -- Easy to integrate into your Node.js projects. \ No newline at end of file +--- + +## 🔁 Log Rotation (Optional) + +Enable size-based log rotation to prevent large log files. + +```js +const logger = Logger.createLogger({ + rotate: true, + maxSizeMB: 10 +}); +``` + +* Rotation is **disabled by default** +* Old files are renamed with a timestamp + +--- + +## 🛠 API Reference + +### `Logger.info(...args)` + +Logs informational messages. + +### `Logger.warn(...args)` + +Logs warnings. + +### `Logger.error(...args)` + +Logs errors and automatically captures stack traces when an `Error` is provided. + +### `Logger.createLogger(options)` + +Creates a new logger instance with custom options. + +#### Options + +| Option | Type | Default | Description | +| ----------- | ------- | ------- | --------------------------------- | +| `json` | boolean | `false` | Enable JSON log output | +| `rotate` | boolean | `false` | Enable size-based log rotation | +| `maxSizeMB` | number | `5` | Max log file size before rotation | + +--- + +## 🔒 Backward Compatibility Guarantee + +All existing usage patterns are **fully supported** in all `v1.x` releases. + +New features are: + +* optional +* opt-in +* non-breaking + +--- + +## ❗ What This Package Is (and Is Not) + +### ✅ This package is: + +* Simple +* Lightweight +* Predictable +* Ideal for small services, scripts, and APIs + +### ❌ This package is NOT: + +* A replacement for Winston or Pino. +* A plugin-based logging framework. +* Designed for massive distributed systems. + +If you need advanced transports or integrations, a full-featured logger may be a better fit. + +--- + +## 📄 License + +MIT © Mohammad Moiz Ali (MAKSTYLE119) diff --git a/__tests__/logger.test.js b/__tests__/logger.test.js new file mode 100644 index 0000000..21f41e5 --- /dev/null +++ b/__tests__/logger.test.js @@ -0,0 +1,89 @@ +const fs = require('fs'); +const path = require('path'); +const Logger = require('../index'); + +const projectRoot = process.cwd(); +const logsDir = path.join(projectRoot, 'logs'); + +const waitForLog = (level) => { + const levelDir = path.join(logsDir, level); + + for (let i = 0; i < 20; i++) { + if (fs.existsSync(levelDir)) { + const files = fs.readdirSync(levelDir); + if (files.length) { + const latest = files.sort().pop(); + return fs.readFileSync(path.join(levelDir, latest), 'utf8'); + } + } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50); + } + + throw new Error(`Log file not found for level: ${level}`); +}; + +describe('node-error-logger.js', () => { + + beforeAll(() => { + jest.spyOn(console, 'info').mockImplementation(() => { }); + jest.spyOn(console, 'warn').mockImplementation(() => { }); + jest.spyOn(console, 'error').mockImplementation(() => { }); + }); + + + test('logs info message to file', () => { + Logger.info('Info test message'); + + const content = waitForLog('info'); + expect(content).toContain('Info test message'); + expect(content).toContain('[INFO]'); + }); + + test('logs warning with metadata', () => { + Logger.warn('Warn test', { userId: 123 }); + + const content = waitForLog('warn'); + expect(content).toContain('Warn test'); + expect(content).toContain('userId'); + }); + + test('logs error with stack trace', () => { + const err = new Error('Test failure'); + + Logger.error('Error occurred', err); + + const content = waitForLog('error'); + expect(content).toContain('Error occurred'); + expect(content).toContain('Test failure'); + expect(content).toContain('Error: Test failure'); + }); + + test('supports multiple arguments like console.log', () => { + Logger.info('Multi arg', 42, { ok: true }); + + const content = waitForLog('info'); + expect(content).toContain('Multi arg'); + expect(content).toContain('42'); + expect(content).toContain('"ok":true'); + }); + + test('JSON logger outputs valid JSON', () => { + const jsonLogger = Logger.createLogger({ json: true }); + + jsonLogger.error('JSON error', new Error('json-fail'), { env: 'test' }); + + const content = waitForLog('error'); + const lastLine = content.trim().split('\n').pop(); + const parsed = JSON.parse(lastLine); + + expect(parsed.level).toBe('error'); + expect(parsed.message).toBe('JSON error'); + expect(parsed.error.message).toBe('json-fail'); + expect(parsed.meta[0].env).toBe('test'); + }); + afterAll(() => { + console.info.mockRestore(); + console.warn.mockRestore(); + console.error.mockRestore(); + }); +}); diff --git a/index.js b/index.js index 48f8759..812da25 100644 --- a/index.js +++ b/index.js @@ -1,47 +1,152 @@ const fs = require('fs'); const path = require('path'); -const appRoot = path.dirname(require.main.filename); // Get the root directory of the app +/* ================= CONFIG ================= */ +const appRoot = process.cwd(); const logDirectory = path.join(appRoot, 'logs'); const logLevels = ['info', 'warn', 'error']; +const defaultOptions = { + json: false, + rotate: false, + maxSizeMB: 5 +}; + +/* ================= HELPERS ================= */ + +const timestamp = () => new Date().toISOString(); + const createLogDirectory = (level) => { - const levelDir = path.join(logDirectory, level); - if (!fs.existsSync(levelDir)) { - fs.mkdirSync(levelDir, { recursive: true }); + const dir = path.join(logDirectory, level); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); } }; -const getTimestamp = () => new Date().toISOString(); +const rotateIfNeeded = (file, options) => { + if (!options.rotate || !fs.existsSync(file)) return; -const log = (level, message) => { - createLogDirectory(level); // Ensure directory exists for the level + const { size } = fs.statSync(file); + if (size < options.maxSizeMB * 1024 * 1024) return; - const date = new Date().toISOString().slice(0, 10); - const logFile = path.join(logDirectory, level, `${date}-${level}.log`); - const logEntry = `[${getTimestamp()}] [${level.toUpperCase()}] ${message}\n`; + const rotatedFile = file.replace('.log', `-${Date.now()}.log`); + fs.renameSync(file, rotatedFile); +}; + +const serializeValue = (value) => { + if (typeof value === 'string') return value; + try { + return JSON.stringify(value); + } catch { + return String(value); + } +}; + +/* ================= FORMATTER ================= */ - fs.appendFile(logFile, logEntry, 'utf8', (err) => { - if (err) { - console.error(`[LoggerService] Failed to write to ${logFile}: ${err.message}`); +const formatLog = (level, args, options) => { + const time = timestamp(); + + let message = ''; + let error = null; + const meta = []; + + args.forEach(arg => { + if (arg instanceof Error && !error) { + error = arg; + } else if (typeof arg === 'string' && !message) { + message = arg; + } else { + meta.push(arg); } }); - console[level](logEntry.trim()); + + if (!message) message = '[no message]'; + + const payload = { + timestamp: time, + level, + message + }; + + if (meta.length) payload.meta = meta; + + if (error) { + payload.error = { + name: error.name, + message: error.message, + stack: error.stack + }; + } + + /* ---------- JSON MODE ---------- */ + if (options.json) { + return JSON.stringify(payload) + '\n'; + } + + /* ---------- TEXT MODE ---------- */ + let output = `[${time}] [${level.toUpperCase()}] ${message}`; + + if (meta.length) { + output += ' | ' + meta.map(serializeValue).join(' '); + } + + if (error) { + output += ` | ${error.message}\n${error.stack}`; + } + + return output + '\n'; }; -// Handle default logging level (info) -const Logger = (message) => { - if (typeof message === 'string') { - log('info', message); +/* ================= CORE LOGGER ================= */ + +const log = (level, args, options = defaultOptions) => { + createLogDirectory(level); + + const date = timestamp().slice(0, 10); + const logFile = path.join(logDirectory, level, `${date}-${level}.log`); + + rotateIfNeeded(logFile, options); + + const entry = formatLog(level, args, options); + + if (process.env.NODE_ENV === 'test') { + try { + fs.appendFileSync(logFile, entry, 'utf8'); + } catch (err) { + console.error('[node-error-logger] Failed to write log:', err.message); + } } else { - throw new Error('Invalid log message. Expected a string.'); + fs.appendFile(logFile, entry, 'utf8', (err) => { + if (err) { + console.error('[node-error-logger] Failed to write log:', err.message); + } + }); } + + + (console[level] || console.log)(entry.trim()); }; -// Add logging functions for each level +/* ================= DEFAULT EXPORT (BACKWARD COMPATIBLE) ================= */ + +const Logger = (...args) => log('info', args); + logLevels.forEach(level => { - Logger[level] = (message) => log(level, message); + Logger[level] = (...args) => log(level, args); }); -module.exports = Logger; \ No newline at end of file +/* ================= OPTIONAL FACTORY API ================= */ + +Logger.createLogger = (options = {}) => { + const cfg = { ...defaultOptions, ...options }; + + return { + info: (...args) => log('info', args, cfg), + warn: (...args) => log('warn', args, cfg), + error: (...args) => log('error', args, cfg) + }; +}; + +module.exports = Logger; diff --git a/package.json b/package.json index 81300e1..739b672 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,34 @@ { "name": "node-error-logger.js", - "version": "1.0.4", - "description": "A npm package to handle logs", + "version": "1.2.0", + "description": "A lightweight, zero-dependency logger for Node.js", "main": "index.js", - "scripts": {}, - "bin": "./index.js", + "files": [ + "index.js", + "CHANGELOG.md", + "README.md" + ], + "scripts": { + "test": "NODE_ENV=test jest" + }, "repository": { "type": "git", "url": "https://github.com/makstyle119/node-error-logger.js.git" }, "keywords": [ - "Node", - "Logger", - "Node.js-Log", - "Node-Logger.js", - "Node-Error", - "Node-Error-Log", - "Node-Error-Logger" + "node", + "logger", + "error", + "logging", + "nodejs" ], "author": "Mohammad Moiz Ali (MAKSTYLE119)", "license": "MIT", "bugs": { "url": "https://github.com/makstyle119/node-error-logger.js/issues" }, - "homepage": "https://github.com/makstyle119/node-error-logger.js#readme" + "homepage": "https://github.com/makstyle119/node-error-logger.js#readme", + "devDependencies": { + "jest": "^29.0.0" + } }