Skip to content

Commit 2024117

Browse files
authored
Merge pull request #12 from yao206cc/main
feat(logger): support function messages based on context
2 parents 6d85884 + 1a249a4 commit 2024117

File tree

5 files changed

+212
-31
lines changed

5 files changed

+212
-31
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { DeepPartial, LoggerMessage } from '../types/index.js';
1+
import type { DeepPartial, RawLoggerMessage } from '../types/index.js';
22
import type { LoggerContext } from '../types/type-logger.js';
33
import type { Logger as BaseLogger } from '../types/type-logger.js';
44
import { Logger } from './logger.js';
55

66
export const createLogger = <
77
Context extends object = object,
8-
Message extends LoggerMessage = LoggerMessage,
8+
Message extends RawLoggerMessage<Context> = RawLoggerMessage<Context>,
99
>(
1010
options?: LoggerContext<Context> & {
1111
setup?: () =>
@@ -15,4 +15,4 @@ export const createLogger = <
1515
}
1616
): BaseLogger<LoggerContext<Context>, Message> => {
1717
return new Logger<Context, Message>(options);
18-
};
18+
};

packages/logger/src/core/logger.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
LoggerContext,
1616
} from '../types/type-logger.js';
1717
import type { LoggerPlugin } from '../types/type-logger-plugin.js';
18-
import type { LoggerMessage } from '../types/type-message.js';
18+
import type { LoggerMessage, RawLoggerMessage } from '../types/type-message.js';
1919
import type { DeepPartial } from '../types/type-partial-deep.js';
2020

2121
/**
@@ -26,18 +26,18 @@ import type { DeepPartial } from '../types/type-partial-deep.js';
2626
* is created with the build() method.
2727
*
2828
* @template Context The logger context type, including log level, name, etc.
29-
* @template Message The log message type
29+
* @template RawMessage The log message type
3030
*/
3131
export class Logger<
3232
Context extends object = object,
33-
Message extends LoggerMessage = LoggerMessage,
34-
> implements BaseLogger<LoggerContext<Context>, Message>
35-
{
33+
Message extends RawLoggerMessage<Context> = RawLoggerMessage<Context>,
34+
> implements BaseLogger<LoggerContext<Context>, Message> {
3635
private ctx: LoggerContext<Context>;
3736
private errorHandling: ((error: Error) => void) | undefined;
3837
private pipeline: Pipeline<{
3938
ctx: LoggerContext<Context>;
40-
message: Message;
39+
rawMessage: Message;
40+
message?: LoggerMessage;
4141
level: LogLevel;
4242
}> = new Pipeline();
4343

@@ -59,6 +59,7 @@ export class Logger<
5959
});
6060
this.errorHandling = errorHandling;
6161
this.useSetupCtx(setup);
62+
this.useMessageParser();
6263
}
6364

6465
private useSetupCtx = async (
@@ -73,17 +74,32 @@ export class Logger<
7374
});
7475
};
7576

77+
/**
78+
* Registers message parser pipeline
79+
*/
80+
private useMessageParser() {
81+
this.pipeline.use(async (ctx, next) => {
82+
const rawMessage = ctx.rawMessage as Message;
83+
ctx.message = this.parseMessage(rawMessage, ctx.ctx);
84+
await next();
85+
});
86+
}
87+
7688
/**
7789
* Registers a Logger plugin into the logging pipeline.
7890
* @param plugins LoggerPlugin instances to be used in the pipeline
7991
* @returns this, for chaining
8092
*/
8193
public use(
82-
...plugins: LoggerPlugin<LoggerContext<Context>, Message>[]
94+
...plugins: LoggerPlugin<LoggerContext<Context>, LoggerMessage>[]
8395
): Pick<BaseLogger<LoggerContext<Context>, Message>, 'use' | 'build'> {
8496
for (const plugin of plugins) {
8597
this.pipeline.use(async (ctx, next) => {
8698
const { level, message, ctx: pluginCtx } = ctx;
99+
if (!message) {
100+
await next();
101+
return;
102+
}
87103
const options = {
88104
ctx: { ...simpleDeepClone(pluginCtx), pluginName: plugin.pluginName },
89105
level: level,
@@ -126,7 +142,7 @@ export class Logger<
126142
public debug(message: Message) {
127143
this.pipeline.execute({
128144
ctx: this.ctx,
129-
message: message,
145+
rawMessage: message,
130146
level: LogLevel.Debug,
131147
});
132148
}
@@ -138,7 +154,7 @@ export class Logger<
138154
public info(message: Message) {
139155
this.pipeline.execute({
140156
ctx: this.ctx,
141-
message: message,
157+
rawMessage: message,
142158
level: LogLevel.Info,
143159
});
144160
}
@@ -150,7 +166,7 @@ export class Logger<
150166
public warn(message: Message) {
151167
this.pipeline.execute({
152168
ctx: this.ctx,
153-
message: message,
169+
rawMessage: message,
154170
level: LogLevel.Warn,
155171
});
156172
}
@@ -162,7 +178,7 @@ export class Logger<
162178
public error(message: Message) {
163179
this.pipeline.execute({
164180
ctx: this.ctx,
165-
message: message,
181+
rawMessage: message,
166182
level: LogLevel.Error,
167183
});
168184
}
@@ -174,8 +190,31 @@ export class Logger<
174190
public verbose(message: Message) {
175191
this.pipeline.execute({
176192
ctx: this.ctx,
177-
message: message,
193+
rawMessage: message,
178194
level: LogLevel.Verbose,
179195
});
180196
}
197+
198+
/**
199+
* The log message, which can be a function or a static message.
200+
* If it's a function, it will be executed with the current context.
201+
* @param message The log message to be parsed.
202+
* @returns message The parsed log message.
203+
*/
204+
private parseMessage(
205+
message: Message,
206+
ctx: LoggerContext<Context>
207+
): LoggerMessage {
208+
try {
209+
if (typeof message === 'function') {
210+
return message(ctx);
211+
}
212+
return message;
213+
} catch (err: any) {
214+
return {
215+
message: 'Failed to execute message function',
216+
stack: err?.stack,
217+
};
218+
}
219+
}
181220
}

packages/logger/src/types/type-logger.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { LogLevel } from '../constant/log-level.js';
22
import type { LoggerPlugin } from './type-logger-plugin.js';
3-
import type { LoggerMessage } from './type-message.js';
3+
import type { LoggerMessage, RawLoggerMessage } from './type-message.js';
44

55
export type LoggerContext<Context extends object = object> = Context & {
66
name: string;
@@ -14,10 +14,10 @@ export type LoggerPluginList<
1414

1515
export interface Logger<
1616
Context extends LoggerContext = LoggerContext,
17-
Message extends LoggerMessage = LoggerMessage,
17+
Message extends RawLoggerMessage<Context> = RawLoggerMessage<Context>,
1818
> {
1919
use: (
20-
...plugins: LoggerPlugin<Context, Message>[]
20+
...plugins: LoggerPlugin<Context, LoggerMessage>[]
2121
) => Pick<Logger<Context, Message>, 'use' | 'build'>;
2222
build: () => Pick<
2323
Logger<Context, Message>,
@@ -28,4 +28,4 @@ export interface Logger<
2828
warn: (message: Message) => void;
2929
error: (message: Message) => void;
3030
verbose: (message: Message) => void;
31-
}
31+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import type { LoggerContext } from "./type-logger.js";
2+
13
export type LoggerMessageObject = {
24
message: string | object;
35
prefix?: string;
46
name?: string;
57
stack?: string | undefined | null;
68
};
79

8-
export type LoggerMessage = string | LoggerMessageObject;
10+
export type LoggerMessage = string | LoggerMessageObject
11+
12+
export type RawLoggerMessage<Context extends object = object> = LoggerMessage | ((ctx: LoggerContext<Context>) => LoggerMessage)

packages/logger/tests/logger.spec.ts

Lines changed: 149 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ describe('Logger Basic Functionality', () => {
1212
let mockConsoleInfo: ReturnType<typeof vi.spyOn>;
1313

1414
beforeEach(() => {
15-
mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
16-
mockConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {});
17-
mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
18-
mockConsoleDebug = vi.spyOn(console, 'debug').mockImplementation(() => {});
19-
mockConsoleInfo = vi.spyOn(console, 'info').mockImplementation(() => {});
15+
mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => { });
16+
mockConsoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => { });
17+
mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
18+
mockConsoleDebug = vi.spyOn(console, 'debug').mockImplementation(() => { });
19+
mockConsoleInfo = vi.spyOn(console, 'info').mockImplementation(() => { });
2020
});
2121

2222
afterEach(() => {
@@ -136,6 +136,11 @@ describe('Logger Basic Functionality', () => {
136136
thresholdLevel: LogLevel.Verbose,
137137
name: 'sampleLogger',
138138
env: 'node',
139+
setup() {
140+
return {
141+
env: 'browser',
142+
};
143+
},
139144
})
140145
.use(consolePlugin)
141146
.build();
@@ -170,19 +175,152 @@ describe('Logger Basic Functionality', () => {
170175

171176
expect(executeMock).toHaveBeenCalledTimes(5);
172177
expect(executeMock.mock.calls[0][0]).toEqual(
173-
'[error] ctx: sampleLogger node 0 error error message'
178+
'[error] ctx: sampleLogger browser 0 error error message'
174179
);
175180
expect(executeMock.mock.calls[1][0]).toEqual(
176-
'[warn] ctx: sampleLogger node 1 warn warn message'
181+
'[warn] ctx: sampleLogger browser 1 warn warn message'
177182
);
178183
expect(executeMock.mock.calls[2][0]).toEqual(
179-
'[info] ctx: sampleLogger node 2 info info message'
184+
'[info] ctx: sampleLogger browser 2 info info message'
180185
);
181186
expect(executeMock.mock.calls[3][0]).toEqual(
182-
'[debug] ctx: sampleLogger node 3 debug debug message'
187+
'[debug] ctx: sampleLogger browser 3 debug debug message'
183188
);
184189
expect(executeMock.mock.calls[4][0]).toEqual(
185-
'[verbose] ctx: sampleLogger node 4 verbose verbose message'
190+
'[verbose] ctx: sampleLogger browser 4 verbose verbose message'
191+
);
192+
});
193+
194+
// Test case for function messages
195+
it('should handle function messages correctly', async () => {
196+
const executeMock = vi.fn();
197+
198+
const consolePlugin = definePlugin({
199+
pluginName: 'consolePlugin',
200+
execute({ message }) {
201+
if (typeof message === 'string') {
202+
executeMock(message);
203+
} else if (typeof message === 'object') {
204+
executeMock(`${message.prefix} ${message.message}`);
205+
}
206+
},
207+
});
208+
209+
type AppContext = {
210+
userId: string;
211+
environment: string;
212+
};
213+
214+
const logger = createLogger<AppContext>({
215+
thresholdLevel: LogLevel.Verbose,
216+
name: 'functionLogger',
217+
environment: 'production',
218+
userId: '',
219+
setup: async () => Promise.resolve({ userId: 'user-123' }),
220+
})
221+
.use(consolePlugin)
222+
.build();
223+
// 1. Test string-returning function message
224+
logger.info(
225+
(ctx) => `User ${ctx.userId} logged in from ${ctx.environment}`
226+
);
227+
228+
// 2. Test object-returning function message
229+
logger.warn((ctx) => ({
230+
message: `Warning for ${ctx.userId}`,
231+
prefix: '[WARNING]',
232+
}));
233+
234+
// 3. Test dynamic context modification
235+
logger.debug((ctx) => {
236+
// Test that context is immutable - create a new object instead
237+
const localCtx = { ...ctx, userId: 'user-456' };
238+
return `Updated user: ${localCtx.userId}`;
239+
});
240+
241+
// 4. Test using modified context
242+
logger.info((ctx) => `Now user is ${ctx.userId}`);
243+
244+
// 5. Test error handling
245+
logger.error(() => {
246+
throw new Error('Test error in message function');
247+
});
248+
249+
await sleep(100);
250+
251+
// Verify results
252+
expect(executeMock).toHaveBeenCalledTimes(5);
253+
expect(executeMock.mock.calls[0][0]).toBe(
254+
'User user-123 logged in from production'
255+
);
256+
expect(executeMock.mock.calls[1][0]).toBe('[WARNING] Warning for user-123');
257+
expect(executeMock.mock.calls[2][0]).toBe('Updated user: user-456');
258+
expect(executeMock.mock.calls[3][0]).toBe('Now user is user-123');
259+
260+
// Verify error message handling
261+
expect(executeMock.mock.calls[4][0]).toContain(
262+
'Message function execution failed'
263+
);
264+
});
265+
266+
// New test case: Handling function messages in pipeline plugins
267+
it('should handle function messages in pipeline plugins', async () => {
268+
type AppContext = {
269+
env: string;
270+
version: string;
271+
};
272+
273+
const executeMock = vi.fn();
274+
275+
const consolePlugin = definePlugin<AppContext>({
276+
pluginName: 'consolePlugin',
277+
execute({ ctx, level, message, pipe }) {
278+
pipe(
279+
(ctx: AppContext) => ctx,
280+
// Resolve function message
281+
() => message,
282+
(msg) => {
283+
if (typeof msg === 'string') {
284+
executeMock(`[${LogLevel[level]}] ${msg}`);
285+
} else {
286+
executeMock(`[${LogLevel[level]}] ${msg.message}`);
287+
}
288+
}
289+
)(ctx);
290+
},
291+
});
292+
293+
const logger = createLogger<AppContext>({
294+
thresholdLevel: LogLevel.Verbose,
295+
name: 'pipelineLogger',
296+
env: 'production',
297+
version: '1.0.0',
298+
})
299+
.use(consolePlugin)
300+
.build();
301+
302+
// Function message - string
303+
logger.info((ctx) => `App v${ctx.version} running in ${ctx.env}`);
304+
305+
// Function message - object
306+
logger.warn((ctx) => ({
307+
message: `Deprecated feature in ${ctx.env} v${ctx.version}`,
308+
}));
309+
310+
// Regular string message
311+
logger.error('Critical error occurred');
312+
313+
await sleep(100);
314+
315+
expect(executeMock).toHaveBeenCalledTimes(3);
316+
expect(executeMock.mock.calls[0][0]).toBe(
317+
'[Info] App v1.0.0 running in production'
318+
);
319+
expect(executeMock.mock.calls[1][0]).toBe(
320+
'[Warn] Deprecated feature in production v1.0.0'
321+
);
322+
expect(executeMock.mock.calls[2][0]).toBe(
323+
'[Error] Critical error occurred'
186324
);
187325
});
188-
});
326+
});

0 commit comments

Comments
 (0)