Skip to content

Commit b3f9621

Browse files
authored
feature: logger hook functions (#10)
* added hooks functions ability for each log level or all * updated jsdoc
1 parent a918731 commit b3f9621

File tree

4 files changed

+127
-21
lines changed

4 files changed

+127
-21
lines changed

src/context-logger.spec.ts

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ describe('ContextLogger', () => {
1515

1616
beforeEach(() => {
1717
jest.clearAllMocks();
18-
18+
1919
jest.spyOn(ContextStore, 'getContext').mockReturnValue(CONTEXT);
20-
20+
2121
mockLogger = {
2222
log: spyLog,
2323
debug: spyDebug,
@@ -29,7 +29,7 @@ describe('ContextLogger', () => {
2929
// Reset the internal logger for each test to ensure clean state
3030
(ContextLogger as any).internalLogger = null;
3131
ContextLogger.init(mockLogger);
32-
32+
3333
contextLogger = new ContextLogger(MODULE_NAME);
3434
});
3535

@@ -59,7 +59,7 @@ describe('ContextLogger', () => {
5959

6060
expect(mockLogger.log).toHaveBeenCalledWith(CONTEXT, message, MODULE_NAME);
6161
});
62-
62+
6363
it('should call log method when info is called', () => {
6464
const message = 'Test message';
6565
const bindings = { someBinding: 'value' };
@@ -245,7 +245,7 @@ describe('ContextLogger', () => {
245245
// Reset the internal logger for each test to ensure clean state
246246
(ContextLogger as any).internalLogger = null;
247247
});
248-
248+
249249
it('should group only bindings when bindingsKey provided', () => {
250250
const logger = new ContextLogger(MODULE_NAME);
251251
ContextLogger.init(mockLogger, { groupFields: { bindingsKey: 'params' } });
@@ -318,7 +318,7 @@ describe('ContextLogger', () => {
318318
// Reset the internal logger for each test to ensure clean state
319319
(ContextLogger as any).internalLogger = null;
320320
});
321-
321+
322322
it('should group both bindings and context under specified keys', () => {
323323
const logger = new ContextLogger(MODULE_NAME);
324324
ContextLogger.init(mockLogger, {
@@ -369,7 +369,7 @@ describe('ContextLogger', () => {
369369
// Reset the internal logger for each test to ensure clean state
370370
(ContextLogger as any).internalLogger = null;
371371
});
372-
372+
373373
it('should adapt context when adapter is provided', () => {
374374
const logger = new ContextLogger(MODULE_NAME);
375375
ContextLogger.init(mockLogger, {
@@ -413,7 +413,7 @@ describe('ContextLogger', () => {
413413
// Reset the internal logger for each test to ensure clean state
414414
(ContextLogger as any).internalLogger = null;
415415
});
416-
416+
417417
it('should ignore bootstrap logs when ignoreBootstrapLogs is true', () => {
418418
const logger = new ContextLogger(MODULE_NAME);
419419
ContextLogger.init(mockLogger, { ignoreBootstrapLogs: true });
@@ -452,4 +452,65 @@ describe('ContextLogger', () => {
452452
);
453453
});
454454
});
455+
456+
describe('hook functions', () => {
457+
it('should call hook function when provided', () => {
458+
const logger = new ContextLogger(MODULE_NAME);
459+
const logMessage = 'Test message';
460+
const bindings = { someBinding: 'value' };
461+
const hookSpy = jest.fn();
462+
463+
ContextLogger.init(mockLogger, {
464+
hooks: {
465+
log: [hookSpy],
466+
},
467+
});
468+
469+
logger.log(logMessage, bindings);
470+
471+
expect(hookSpy).toHaveBeenCalledWith(logMessage, { ...bindings, ...CONTEXT });
472+
});
473+
474+
it('should call multiple hook function when provided', () => {
475+
const logger = new ContextLogger(MODULE_NAME);
476+
const logMessage = 'Test message';
477+
const bindings = { someBinding: 'value' };
478+
const hookSpy1 = jest.fn();
479+
const hookSpy2 = jest.fn();
480+
const hookSpy3 = jest.fn();
481+
482+
ContextLogger.init(mockLogger, {
483+
hooks: {
484+
log: [hookSpy1, hookSpy2, hookSpy3],
485+
},
486+
});
487+
488+
logger.log(logMessage, bindings);
489+
490+
expect(hookSpy1).toHaveBeenCalledWith(logMessage, { ...bindings, ...CONTEXT });
491+
expect(hookSpy2).toHaveBeenCalledWith(logMessage, { ...bindings, ...CONTEXT });
492+
expect(hookSpy3).toHaveBeenCalledWith(logMessage, { ...bindings, ...CONTEXT });
493+
});
494+
495+
it('should call \'all\' hook function when provided', () => {
496+
const logger = new ContextLogger(MODULE_NAME);
497+
const logMessage = 'Test message';
498+
const bindings = { someBinding: 'value' };
499+
const hookSpy = jest.fn();
500+
const allHookSpy = jest.fn();
501+
502+
ContextLogger.init(mockLogger, {
503+
hooks: {
504+
log: [hookSpy],
505+
all: [allHookSpy],
506+
},
507+
});
508+
509+
logger.log(logMessage, bindings);
510+
logger.debug(logMessage, bindings);
511+
512+
expect(hookSpy).toHaveBeenCalledTimes(1);
513+
expect(allHookSpy).toHaveBeenCalledTimes(2);
514+
});
515+
});
455516
});

src/context-logger.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import { Logger as NestLogger } from '@nestjs/common';
1+
import { LogLevel, Logger as NestLogger } from '@nestjs/common';
22
import { Logger as NestJSPinoLogger } from 'nestjs-pino';
33
import { ContextStore } from './store/context-store';
44
import { omitBy, isNil, isEmpty } from 'lodash';
55
import { ContextLoggerFactoryOptions } from './interfaces/context-logger.interface';
6-
7-
type Bindings = Record<string, any>;
8-
9-
export interface LogEntry {
10-
[key: string]: any;
11-
err?: Error | string;
12-
}
6+
import { Bindings, LogEntry } from './types';
137

148
export class ContextLogger {
159
private static internalLogger: NestJSPinoLogger;
@@ -85,7 +79,7 @@ export class ContextLogger {
8579
this.callInternalLogger('error', message, adaptedBindings, error);
8680
}
8781

88-
private callInternalLogger(level: string, message: string, bindings: Bindings, error?: Error | string) {
82+
private callInternalLogger(level: LogLevel, message: string, bindings: Bindings, error?: Error | string) {
8983
// If it's a bootstrap log and ignoreBootstrapLogs is true, do nothing
9084
if (typeof bindings === 'string' && ContextLogger.options?.ignoreBootstrapLogs) {
9185
return;
@@ -99,6 +93,7 @@ export class ContextLogger {
9993
logObject = this.createLogEntry(bindings, error);
10094
}
10195
const logger = ContextLogger.internalLogger ?? this.fallbackLogger;
96+
this.callHooks(level, message, logObject);
10297
return logger[level](logObject, ...[message, this.moduleName]);
10398
}
10499

@@ -133,4 +128,18 @@ export class ContextLogger {
133128

134129
return { [key]: obj };
135130
}
131+
132+
private callHooks(level: LogLevel, message: string, bindings: Bindings): void {
133+
const hooks = ContextLogger.options?.hooks;
134+
135+
if (!hooks) {
136+
return;
137+
}
138+
139+
[...(hooks[level] || []), ...(hooks['all'] || [])].forEach((hook) => {
140+
hook(message, bindings);
141+
});
142+
143+
return;
144+
}
136145
}

src/interfaces/context-logger.interface.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ExecutionContext, ModuleMetadata } from '@nestjs/common';
22
import { Params } from 'nestjs-pino';
3+
import { Bindings, LoggerHookKeys } from '../types';
34

45
export interface ContextLoggerFactoryOptions extends Params {
56
/**
@@ -8,14 +9,14 @@ export interface ContextLoggerFactoryOptions extends Params {
89
* Specify keys to group specific fields:
910
* - bindingsKey: Groups runtime bindings under this key
1011
* - contextKey: Groups context data under this key
11-
*
12+
*
1213
* @example
1314
* // Group both bindings and context
1415
* groupFields: { bindingsKey: 'params', contextKey: 'metadata' }
15-
*
16+
*
1617
* // Group only bindings, spread context at root
1718
* groupFields: { bindingsKey: 'params' }
18-
*
19+
*
1920
* // Group only context, spread bindings at root
2021
* groupFields: { contextKey: 'metadata' }
2122
*/
@@ -43,7 +44,7 @@ export interface ContextLoggerFactoryOptions extends Params {
4344
/**
4445
* Optional function to transform the context before it is included in the log entry.
4546
* Useful for filtering, renaming, or restructuring context data.
46-
*
47+
*
4748
* @param context - The current context object
4849
* @returns The transformed context object
4950
*/
@@ -59,6 +60,31 @@ export interface ContextLoggerFactoryOptions extends Params {
5960
enrichContext?: (
6061
context: ExecutionContext
6162
) => Record<string, any> | Promise<Record<string, any>>;
63+
64+
/**
65+
* Optional hooks to execute when a log is created.
66+
* These callbacks allow you to extend logging behavior, such as reporting to external systems,
67+
* incrementing metrics, or performing side effects based on log level.
68+
*
69+
* ⚠️ Note: All callbacks are executed **synchronously and sequentially**.
70+
* This means each hook will block the next one, potentially introducing latency
71+
* to the logging process. Use with caution, especially when performing async-like tasks.
72+
*
73+
* @example
74+
* hooks: {
75+
* all: [
76+
* (message: string, bindings: Bindings) => {
77+
* // do something for all logs
78+
* },
79+
* ],
80+
* error: [
81+
* (message: string, bindings: Bindings) => {
82+
* // do something specific for error logs
83+
* },
84+
* ],
85+
* }
86+
*/
87+
hooks?: Partial<Record<LoggerHookKeys, Array<(message: string, bindings: Bindings) => void>>>;
6288
}
6389

6490
export interface ContextLoggerAsyncOptions extends Pick<ModuleMetadata, 'imports'> {

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { LogLevel } from '@nestjs/common';
2+
3+
export type Bindings = Record<string, any>;
4+
5+
export interface LogEntry {
6+
[key: string]: any;
7+
err?: Error | string;
8+
}
9+
10+
export type LoggerHookKeys = LogLevel | 'all';

0 commit comments

Comments
 (0)