From 259cbb779afa977e682debc4cdee938cae1095bb Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Mon, 8 Dec 2025 12:20:55 -0800 Subject: [PATCH 1/5] feat: adding in logger to be fully implemented --- package.json | 30 +++++++++++++++--------------- test/eventified.test.ts | 6 ++++-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 58b0795..f0ccd4b 100644 --- a/package.json +++ b/package.json @@ -68,21 +68,21 @@ }, "homepage": "https://github.com/jaredwray/hookified#readme", "devDependencies": { - "@biomejs/biome": "2.3.8", - "@monstermann/tinybench-pretty-printer": "0.3.0", - "@types/node": "24.10.1", - "@vitest/coverage-v8": "4.0.15", - "docula": "0.31.1", - "emittery": "1.2.0", - "eventemitter3": "5.0.1", - "hookable": "5.5.3", - "pino": "10.1.0", - "rimraf": "6.1.2", - "tinybench": "6.0.0", - "tsup": "8.5.1", - "tsx": "4.21.0", - "typescript": "5.9.3", - "vitest": "4.0.15" + "@biomejs/biome": "^2.3.8", + "@monstermann/tinybench-pretty-printer": "^0.3.0", + "@types/node": "^24.10.1", + "@vitest/coverage-v8": "^4.0.15", + "docula": "^0.31.1", + "emittery": "^1.2.0", + "eventemitter3": "^5.0.1", + "hookable": "^5.5.3", + "pino": "^10.1.0", + "rimraf": "^6.1.2", + "tinybench": "^6.0.0", + "tsup": "^8.5.1", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.0.15" }, "files": [ "dist", diff --git a/test/eventified.test.ts b/test/eventified.test.ts index d53f0cf..7dffb05 100644 --- a/test/eventified.test.ts +++ b/test/eventified.test.ts @@ -23,10 +23,12 @@ describe("Eventified", () => { test("get / set logger", (t) => { const emitter = new Eventified(); const logger = { + trace() {}, + debug() {}, info() {}, - error() {}, warn() {}, - debug() {}, + error() {}, + fatal() {}, }; emitter.logger = logger; t.expect(emitter.logger).toBe(logger); From 8440b3f288d3cc66135abf30c40c03c6db76b022 Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Mon, 8 Dec 2025 12:21:02 -0800 Subject: [PATCH 2/5] feat: adding in logger to be fully implemented --- test/index.test.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index d267ead..f2dbe9d 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1010,10 +1010,12 @@ describe("Hookified", () => { test("should log deprecation warning to logger if available", () => { const deprecatedHooks = new Map([["oldHook", "Use newHook instead"]]); const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), warn: vi.fn(), error: vi.fn(), - info: vi.fn(), - debug: vi.fn(), + fatal: vi.fn(), }; const hookified = new Hookified({ deprecatedHooks, logger }); const handler = () => {}; @@ -1298,10 +1300,12 @@ describe("Hookified", () => { test("should handle logger.warn not being available", () => { const deprecatedHooks = new Map([["oldHook", "Use newHook instead"]]); const logger = { - error: vi.fn(), - info: vi.fn(), + trace: vi.fn(), debug: vi.fn(), - // no warn method + info: vi.fn(), + warn: undefined as unknown as () => void, + error: vi.fn(), + fatal: vi.fn(), }; const hookified = new Hookified({ deprecatedHooks, logger }); const handler = () => {}; @@ -1570,10 +1574,12 @@ describe("Hookified", () => { test("should work with logger when allowDeprecated is false", () => { const deprecatedHooks = new Map([["oldHook", "Use newHook instead"]]); const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), warn: vi.fn(), error: vi.fn(), - info: vi.fn(), - debug: vi.fn(), + fatal: vi.fn(), }; const hookified = new Hookified({ deprecatedHooks, From 9bfa4e3aea48843b4082ee0e1da9fe32a020414b Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Mon, 8 Dec 2025 12:56:01 -0800 Subject: [PATCH 3/5] adding in sendLog --- src/eventified.ts | 69 +++++++++++++++++++++++ src/index.ts | 8 --- test/eventified.test.ts | 118 +++++++++++++++++++++++++++++++++++++++- test/index.test.ts | 18 ++++++ 4 files changed, 204 insertions(+), 9 deletions(-) diff --git a/src/eventified.ts b/src/eventified.ts index 1cd3f3b..0c0d29b 100644 --- a/src/eventified.ts +++ b/src/eventified.ts @@ -47,6 +47,72 @@ export class Eventified implements IEventEmitter { this._logger = logger; } + /** + * Sends a log message using the configured logger based on the event name + * @param {string | symbol} eventName - The event name that determines the log level + * @param {unknown} data - The data to log + */ + private sendLog(eventName: string | symbol, data: any): void { + if (!this._logger) { + return; + } + + let message: string; + /* v8 ignore next -- @preserve */ + if (typeof data === "string") { + message = data; + } else if ( + Array.isArray(data) && + data.length > 0 && + data[0] instanceof Error + ) { + message = data[0].message; + /* v8 ignore next -- @preserve */ + } else if (data instanceof Error) { + message = data.message; + } else if ( + Array.isArray(data) && + data.length > 0 && + typeof data[0]?.message === "string" + ) { + message = data[0].message; + } else { + message = JSON.stringify(data); + } + + switch (eventName) { + case "error": { + this._logger.error?.(message, { event: eventName, data }); + break; + } + + case "warn": { + this._logger.warn?.(message, { event: eventName, data }); + break; + } + + case "trace": { + this._logger.trace?.(message, { event: eventName, data }); + break; + } + + case "debug": { + this._logger.debug?.(message, { event: eventName, data }); + break; + } + + case "fatal": { + this._logger.fatal?.(message, { event: eventName, data }); + break; + } + + default: { + this._logger.info?.(message, { event: eventName, data }); + break; + } + } + } + /** * Gets whether an error should be thrown when an emit throws an error. Default is false and only emits an error event. * @returns {boolean} @@ -282,6 +348,9 @@ export class Eventified implements IEventEmitter { } } + // send it to the logger + this.sendLog(event, arguments_); + return result; } diff --git a/src/index.ts b/src/index.ts index d2dafc0..6ba796c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -156,11 +156,6 @@ export class Hookified extends Eventified { // Emit deprecation warning event this.emit("warn", { hook: event, message: warningMessage }); - // Log to logger if available - if (this.logger?.warn) { - this.logger.warn(warningMessage); - } - // Return false if deprecated hooks are not allowed return this._allowDeprecated; } @@ -324,9 +319,6 @@ export class Hookified extends Eventified { } catch (error) { const message = `${event}: ${(error as Error).message}`; this.emit("error", new Error(message)); - if (this.logger) { - this.logger.error(message); - } if (this._throwOnHookError) { throw new Error(message); diff --git a/test/eventified.test.ts b/test/eventified.test.ts index 7dffb05..4040d0f 100644 --- a/test/eventified.test.ts +++ b/test/eventified.test.ts @@ -1,6 +1,6 @@ // biome-ignore-all lint/suspicious/noExplicitAny: this is a test file // biome-ignore-all lint/suspicious/noImplicitAnyLet: this is a test file -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; import { Eventified } from "../src/eventified.js"; describe("Eventified", () => { @@ -434,4 +434,120 @@ describe("Eventified", () => { emitter.emit("error", new Error("Test error")); }).toThrow("Test error"); }); + + describe("sendLog via emit", () => { + test("should not call logger when logger is not set", () => { + const emitter = new Eventified(); + // Should not throw when emitting without logger + expect(() => emitter.emit("info", "test")).not.toThrow(); + }); + + test("should log trace events", () => { + const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + }; + const emitter = new Eventified({ logger }); + emitter.emit("trace", "trace message"); + expect(logger.trace).toHaveBeenCalled(); + }); + + test("should log debug events", () => { + const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + }; + const emitter = new Eventified({ logger }); + emitter.emit("debug", "debug message"); + expect(logger.debug).toHaveBeenCalled(); + }); + + test("should log fatal events", () => { + const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + }; + const emitter = new Eventified({ logger }); + emitter.emit("fatal", "fatal message"); + expect(logger.fatal).toHaveBeenCalled(); + }); + + test("should log info for default/unknown events", () => { + const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + }; + const emitter = new Eventified({ logger }); + emitter.emit("custom-event", "custom message"); + expect(logger.info).toHaveBeenCalled(); + }); + + test("should handle string data in array", () => { + const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + }; + const emitter = new Eventified({ logger }); + emitter.emit("info", "string message"); + expect(logger.info).toHaveBeenCalledWith( + '["string message"]', + expect.objectContaining({ event: "info" }), + ); + }); + + test("should handle Error object directly", () => { + const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + }; + const emitter = new Eventified({ logger }); + emitter.on("error", () => {}); // Add listener to prevent throw + emitter.emit("error", new Error("direct error")); + expect(logger.error).toHaveBeenCalledWith( + "direct error", + expect.objectContaining({ event: "error" }), + ); + }); + + test("should JSON stringify objects without message property", () => { + const logger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), + }; + const emitter = new Eventified({ logger }); + emitter.emit("info", { key: "value" }); + expect(logger.info).toHaveBeenCalledWith( + '[{"key":"value"}]', + expect.objectContaining({ event: "info" }), + ); + }); + }); }); diff --git a/test/index.test.ts b/test/index.test.ts index f2dbe9d..3fb56ae 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1024,6 +1024,15 @@ describe("Hookified", () => { expect(logger.warn).toHaveBeenCalledWith( 'Hook "oldHook" is deprecated: Use newHook instead', + { + event: "warn", + data: [ + { + hook: "oldHook", + message: 'Hook "oldHook" is deprecated: Use newHook instead', + }, + ], + }, ); }); @@ -1592,6 +1601,15 @@ describe("Hookified", () => { expect(logger.warn).toHaveBeenCalledWith( 'Hook "oldHook" is deprecated: Use newHook instead', + { + event: "warn", + data: [ + { + hook: "oldHook", + message: 'Hook "oldHook" is deprecated: Use newHook instead', + }, + ], + }, ); expect(hookified.getHooks("oldHook")).toBeUndefined(); }); From 5df9a2bc860a7c51ce6677d5247b73c4d8cab19f Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Mon, 8 Dec 2025 12:56:38 -0800 Subject: [PATCH 4/5] Update eventified.ts --- src/eventified.ts | 132 +++++++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/src/eventified.ts b/src/eventified.ts index 0c0d29b..88289fd 100644 --- a/src/eventified.ts +++ b/src/eventified.ts @@ -47,72 +47,6 @@ export class Eventified implements IEventEmitter { this._logger = logger; } - /** - * Sends a log message using the configured logger based on the event name - * @param {string | symbol} eventName - The event name that determines the log level - * @param {unknown} data - The data to log - */ - private sendLog(eventName: string | symbol, data: any): void { - if (!this._logger) { - return; - } - - let message: string; - /* v8 ignore next -- @preserve */ - if (typeof data === "string") { - message = data; - } else if ( - Array.isArray(data) && - data.length > 0 && - data[0] instanceof Error - ) { - message = data[0].message; - /* v8 ignore next -- @preserve */ - } else if (data instanceof Error) { - message = data.message; - } else if ( - Array.isArray(data) && - data.length > 0 && - typeof data[0]?.message === "string" - ) { - message = data[0].message; - } else { - message = JSON.stringify(data); - } - - switch (eventName) { - case "error": { - this._logger.error?.(message, { event: eventName, data }); - break; - } - - case "warn": { - this._logger.warn?.(message, { event: eventName, data }); - break; - } - - case "trace": { - this._logger.trace?.(message, { event: eventName, data }); - break; - } - - case "debug": { - this._logger.debug?.(message, { event: eventName, data }); - break; - } - - case "fatal": { - this._logger.fatal?.(message, { event: eventName, data }); - break; - } - - default: { - this._logger.info?.(message, { event: eventName, data }); - break; - } - } - } - /** * Gets whether an error should be thrown when an emit throws an error. Default is false and only emits an error event. * @returns {boolean} @@ -404,4 +338,70 @@ export class Eventified implements IEventEmitter { return result; } + + /** + * Sends a log message using the configured logger based on the event name + * @param {string | symbol} eventName - The event name that determines the log level + * @param {unknown} data - The data to log + */ + private sendLog(eventName: string | symbol, data: any): void { + if (!this._logger) { + return; + } + + let message: string; + /* v8 ignore next -- @preserve */ + if (typeof data === "string") { + message = data; + } else if ( + Array.isArray(data) && + data.length > 0 && + data[0] instanceof Error + ) { + message = data[0].message; + /* v8 ignore next -- @preserve */ + } else if (data instanceof Error) { + message = data.message; + } else if ( + Array.isArray(data) && + data.length > 0 && + typeof data[0]?.message === "string" + ) { + message = data[0].message; + } else { + message = JSON.stringify(data); + } + + switch (eventName) { + case "error": { + this._logger.error?.(message, { event: eventName, data }); + break; + } + + case "warn": { + this._logger.warn?.(message, { event: eventName, data }); + break; + } + + case "trace": { + this._logger.trace?.(message, { event: eventName, data }); + break; + } + + case "debug": { + this._logger.debug?.(message, { event: eventName, data }); + break; + } + + case "fatal": { + this._logger.fatal?.(message, { event: eventName, data }); + break; + } + + default: { + this._logger.info?.(message, { event: eventName, data }); + break; + } + } + } } From 4c904573c64cb89ee6e18d2f38074fc265a159cc Mon Sep 17 00:00:00 2001 From: Jared Wray Date: Mon, 8 Dec 2025 13:00:03 -0800 Subject: [PATCH 5/5] Update README.md --- README.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/README.md b/README.md index 80021d2..9152abe 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ - [.eventNames()](#eventnames) - [.listenerCount(eventName?)](#listenercounteventname) - [.rawListeners(eventName?)](#rawlistenerseventname) +- [Logging](#logging) - [Benchmarks](#benchmarks) - [How to Contribute](#how-to-contribute) - [License and Copyright](#license-and-copyright) @@ -1239,6 +1240,88 @@ myClass.on('message', (message) => { console.log(myClass.rawListeners('message')); ``` +# Logging + +Hookified integrates logging directly into the event system. When a logger is configured, all emitted events are automatically logged to the appropriate log level based on the event name. + +## How It Works + +When you emit an event, Hookified automatically sends the event data to the configured logger using the appropriate log method: + +| Event Name | Logger Method | +|------------|---------------| +| `error` | `logger.error()` | +| `warn` | `logger.warn()` | +| `debug` | `logger.debug()` | +| `trace` | `logger.trace()` | +| `fatal` | `logger.fatal()` | +| Any other | `logger.info()` | + +The logger receives two arguments: +1. **message**: A string extracted from the event data (error messages, object messages, or JSON stringified data) +2. **context**: An object containing `{ event: eventName, data: originalData }` + +## Setting Up a Logger + +Any logger that implements the `Logger` interface is compatible. This includes popular loggers like [Pino](https://github.com/pinojs/pino), [Winston](https://github.com/winstonjs/winston), [Bunyan](https://github.com/trentm/node-bunyan), and others. + +```typescript +type Logger = { + trace: (message: string, ...args: unknown[]) => void; + debug: (message: string, ...args: unknown[]) => void; + info: (message: string, ...args: unknown[]) => void; + warn: (message: string, ...args: unknown[]) => void; + error: (message: string, ...args: unknown[]) => void; + fatal: (message: string, ...args: unknown[]) => void; +}; +``` + +## Usage Example with Pino + +```javascript +import { Hookified } from 'hookified'; +import pino from 'pino'; + +const logger = pino(); + +class MyService extends Hookified { + constructor() { + super({ logger }); + } + + async processData(data) { + // This will log to logger.info with the data + this.emit('info', { action: 'processing', data }); + + try { + // ... process data + this.emit('debug', { action: 'completed', result: 'success' }); + } catch (err) { + // This will log to logger.error with the error message + this.emit('error', err); + } + } +} + +const service = new MyService(); + +// All events are automatically logged +service.emit('info', 'Service started'); // -> logger.info() +service.emit('warn', { message: 'Low memory' }); // -> logger.warn() +service.emit('error', new Error('Failed')); // -> logger.error() +service.emit('custom-event', { foo: 'bar' }); // -> logger.info() (default) +``` + +You can also set or change the logger after instantiation: + +```javascript +const service = new MyService(); +service.logger = pino({ level: 'debug' }); + +// Or remove the logger +service.logger = undefined; +``` + # Benchmarks We are doing very simple benchmarking to see how this compares to other libraries using `tinybench`. This is not a full benchmark but just a simple way to see how it performs. Our goal is to be as close or better than the other libraries including native (EventEmitter).