Skip to content
Merged
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
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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).
Expand Down
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
69 changes: 69 additions & 0 deletions src/eventified.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@ export class Eventified implements IEventEmitter {
}
}

// send it to the logger
this.sendLog(event, arguments_);

return result;
}

Expand Down Expand Up @@ -335,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;
}
}
}
}
8 changes: 0 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
124 changes: 121 additions & 3 deletions test/eventified.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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);
Expand Down Expand Up @@ -432,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" }),
);
});
});
});
Loading
Loading