Skip to content

Commit fb78d63

Browse files
authored
fix(logger): avoid recursive error handling (#71)
* avoid using log to broadcast its own errors * move log format into a helper
1 parent 870fd68 commit fb78d63

File tree

3 files changed

+86
-25
lines changed

3 files changed

+86
-25
lines changed

src/__tests__/__snapshots__/logger.test.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ exports[`createLogger should attempt to subscribe and unsubscribe from a channel
3333
}
3434
`;
3535

36+
exports[`formatLogEvent should return a formatted log event, all available fields 1`] = `"[INFO]: lorem ipsum, debug :1 2 3"`;
37+
38+
exports[`formatLogEvent should return a formatted log event, default 1`] = `"[DEBUG]: lorem ipsum, debug"`;
39+
40+
exports[`formatLogEvent should return a formatted log event, null 1`] = `"[INFO]:"`;
41+
42+
exports[`formatLogEvent should return a formatted log event, partial 1`] = `"[DEBUG]:"`;
43+
44+
exports[`formatLogEvent should return a formatted log event, undefined 1`] = `"[INFO]:"`;
45+
3646
exports[`formatUnknownError should attempt to return a formatted error on non-errors, bigint 1`] = `"9007199254740991n"`;
3747

3848
exports[`formatUnknownError should attempt to return a formatted error on non-errors, boolean 1`] = `"Non-Error thrown: true"`;

src/__tests__/logger.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import diagnostics_channel from 'node:diagnostics_channel';
22
import { setOptions, getLoggerOptions } from '../options.context';
3-
import { logSeverity, truncate, formatUnknownError, publish, subscribeToChannel, registerStderrSubscriber, createLogger } from '../logger';
3+
import { logSeverity, truncate, formatUnknownError, formatLogEvent, publish, subscribeToChannel, registerStderrSubscriber, createLogger } from '../logger';
44

55
describe('logSeverity', () => {
66
it.each([
@@ -113,6 +113,47 @@ describe('formatUnknownError', () => {
113113
});
114114
});
115115

116+
describe('formatLogEvent', () => {
117+
it.each([
118+
{
119+
description: 'default',
120+
event: {
121+
level: 'debug',
122+
timestamp: new Date('2025-11-01T00:00:00Z'),
123+
msg: 'lorem ipsum, debug'
124+
}
125+
},
126+
{
127+
description: 'all available fields',
128+
event: {
129+
level: 'info',
130+
timestamp: new Date('2025-11-01T00:00:00.000Z'),
131+
msg: 'lorem ipsum, debug',
132+
fields: { lorem: 'ipsum dolor sit amet' },
133+
source: 'loremIpsum',
134+
args: [1, 2, 3],
135+
transport: 'dolorSit'
136+
}
137+
},
138+
{
139+
description: 'undefined',
140+
event: undefined
141+
},
142+
{
143+
description: 'null',
144+
event: null
145+
},
146+
{
147+
description: 'partial',
148+
event: {
149+
level: 'debug'
150+
}
151+
}
152+
])('should return a formatted log event, $description', ({ event }) => {
153+
expect(formatLogEvent(event as any)).toMatchSnapshot();
154+
});
155+
});
156+
116157
describe('publish', () => {
117158
let channelSpy: jest.SpyInstance;
118159
const mockPublish = jest.fn();

src/logger.ts

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,29 @@ const formatUnknownError = (value: unknown): string => {
114114
}
115115
};
116116

117+
/**
118+
* Format a structured log event for output to stderr.
119+
*
120+
* @param event - Log event to format
121+
*/
122+
const formatLogEvent = (event: LogEvent) => {
123+
const level = event?.level?.toUpperCase() || 'INFO';
124+
const eventLevel = `[${level}]`;
125+
const message = event?.msg || '';
126+
127+
const rest = event?.args?.map(arg => {
128+
try {
129+
return typeof arg === 'string' ? arg : JSON.stringify(arg);
130+
} catch {
131+
return String(arg);
132+
}
133+
}).join(' ') || '';
134+
135+
const separator = rest ? '\t:' : '';
136+
137+
return `${eventLevel}: ${message}${separator}${rest}`.trim();
138+
};
139+
117140
/**
118141
* Publish a structured log event to the diagnostics channel.
119142
*
@@ -207,7 +230,7 @@ const subscribeToChannel = (
207230
try {
208231
handler.call(null, event);
209232
} catch (error) {
210-
log.debug('Error invoking logging subscriber', event, error);
233+
process.stderr.write(`Error invoking logging subscriber: ${formatUnknownError(error)}\n`);
211234
}
212235
};
213236

@@ -227,28 +250,14 @@ const subscribeToChannel = (
227250
* @param [formatter] - Optional custom formatter for log events. Default prints: `[LEVEL] msg ...args`
228251
* @returns Unsubscribe function to remove the subscriber
229252
*/
230-
const registerStderrSubscriber = (options: LoggingSession, formatter?: (e: LogEvent) => string): Unsubscribe => {
231-
const format = formatter || ((event: LogEvent) => {
232-
const eventLevel = `[${event.level.toUpperCase()}]`;
233-
const message = event.msg || '';
234-
const rest = event?.args?.map(arg => {
235-
try {
236-
return typeof arg === 'string' ? arg : JSON.stringify(arg);
237-
} catch {
238-
return String(arg);
239-
}
240-
}).join(' ') || '';
241-
const separator = rest ? '\t:' : '';
242-
243-
return `${eventLevel}: ${message}${separator}${rest}`.trim();
244-
});
245-
246-
return subscribeToChannel((event: LogEvent) => {
247-
if (logSeverity(event.level) >= logSeverity(options.level)) {
248-
process.stderr.write(`${format(event)}\n`);
249-
}
250-
});
251-
};
253+
const registerStderrSubscriber = (
254+
options: LoggingSession,
255+
formatter: (e: LogEvent) => string = formatLogEvent
256+
): Unsubscribe => subscribeToChannel((event: LogEvent) => {
257+
if (logSeverity(event.level) >= logSeverity(options.level)) {
258+
process.stderr.write(`${formatter(event)}\n`);
259+
}
260+
});
252261

253262
/**
254263
* Creates a logger initialization function and supports registering logging subscribers.
@@ -268,7 +277,7 @@ const createLogger = (options: LoggingSession = getLoggerOptions()): Unsubscribe
268277
try {
269278
unsubscribe();
270279
} catch (error) {
271-
log.debug('Error unsubscribing from diagnostics channel', error);
280+
process.stderr.write(`Error unsubscribing from diagnostics channel: ${formatUnknownError(error)}\n`);
272281
}
273282
});
274283

@@ -279,6 +288,7 @@ const createLogger = (options: LoggingSession = getLoggerOptions()): Unsubscribe
279288
export {
280289
LOG_LEVELS,
281290
createLogger,
291+
formatLogEvent,
282292
formatUnknownError,
283293
log,
284294
logSeverity,

0 commit comments

Comments
 (0)