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
32 changes: 32 additions & 0 deletions src/__tests__/__snapshots__/logger.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ exports[`createLogger should attempt to subscribe and unsubscribe from a channel
}
`;

exports[`formatUnknownError should attempt to return a formatted error on non-errors, bigint 1`] = `"9007199254740991n"`;

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

exports[`formatUnknownError should attempt to return a formatted error on non-errors, error, non-error 1`] = `"Non-Error thrown: {"message":"lorem ipsum dolor sit amet"}"`;

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

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

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

exports[`formatUnknownError should attempt to return a formatted error on non-errors, object 1`] = `"Non-Error thrown: {"lorem":"ipsum dolor sit amet","dolor":"sit amet","amet":"consectetur adipiscing elit"}"`;

exports[`formatUnknownError should attempt to return a formatted error on non-errors, string 1`] = `"lorem ipsum dolor sit amet"`;

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

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

exports[`logSeverity should return log severity, debug 1`] = `0`;

exports[`logSeverity should return log severity, default 1`] = `-1`;
Expand Down Expand Up @@ -195,3 +215,15 @@ exports[`subscribeToChannel should attempt to subscribe and unsubscribe from a c
`;

exports[`subscribeToChannel should throw an error attempting to subscribe and unsubscribe from a channel: missing channel name 1`] = `"subscribeToChannel called without a configured logging channelName"`;

exports[`truncate should truncate a string, default 1`] = `"lorem ipsum...[truncated]"`;

exports[`truncate should truncate a string, null 1`] = `null`;

exports[`truncate should truncate a string, number 1`] = `10000`;

exports[`truncate should truncate a string, object string 1`] = `"{"lorem":"i...[truncated]"`;

exports[`truncate should truncate a string, suffix overrides max 1`] = `"...[truncated]"`;

exports[`truncate should truncate a string, undefined 1`] = `undefined`;
86 changes: 85 additions & 1 deletion src/__tests__/logger.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import diagnostics_channel from 'node:diagnostics_channel';
import { setOptions, getLoggerOptions } from '../options.context';
import { logSeverity, publish, subscribeToChannel, registerStderrSubscriber, createLogger } from '../logger';
import { logSeverity, truncate, formatUnknownError, publish, subscribeToChannel, registerStderrSubscriber, createLogger } from '../logger';

describe('logSeverity', () => {
it.each([
Expand Down Expand Up @@ -29,6 +29,90 @@ describe('logSeverity', () => {
});
});

describe('truncate', () => {
it.each([
{
description: 'default',
value: 'lorem ipsum dolor sit amet',
max: 25
},
{
description: 'object string',
value: JSON.stringify({ lorem: 'ipsum dolor sit amet' }),
max: 25
},
{
description: 'suffix overrides max',
value: 'lorem',
max: 5
},
{
description: 'number',
value: 10_000,
max: 25
},
{
description: 'undefined',
value: undefined,
max: 25
},
{
description: 'null',
value: null,
max: 25
}
])(`should truncate a string, $description`, ({ value, max }) => {
expect(truncate(value as any, { max })).toMatchSnapshot();
});
});

describe('formatUnknownError', () => {
it.each([
{
description: 'error, non-error',
err: { ...new Error('lorem ipsum dolor sit amet', { cause: 'dolor' }), message: 'lorem ipsum dolor sit amet' }
},
{
description: 'symbol',
err: Symbol('lorem ipsum')
},
{
description: 'function',
err: () => {}
},
{
description: 'boolean',
err: true
},
{
description: 'string',
err: 'lorem ipsum dolor sit amet'
},
{
description: 'object',
err: { lorem: 'ipsum dolor sit amet', dolor: 'sit amet', amet: 'consectetur adipiscing elit' }
},
{
description: 'bigint',
err: BigInt(Number.MAX_SAFE_INTEGER)
},
{
description: 'number',
err: 10_000
},
{
description: 'undefined',
err: undefined
},
{
description: 'null',
err: null
}
])('should attempt to return a formatted error on non-errors, $description', ({ err }) => {
expect(formatUnknownError(err)).toMatchSnapshot();
});
});

describe('publish', () => {
let channelSpy: jest.SpyInstance;
const mockPublish = jest.fn();
Expand Down
66 changes: 66 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { channel, unsubscribe, subscribe } from 'node:diagnostics_channel';
import { inspect } from 'node:util';
import { type LoggingSession } from './options.defaults';
import { getLoggerOptions } from './options.context';

Expand Down Expand Up @@ -50,6 +51,69 @@ const LOG_LEVELS: LogLevel[] = ['debug', 'info', 'warn', 'error'];
const logSeverity = (level: unknown): number =>
LOG_LEVELS.indexOf(level as LogLevel);

/**
* Basic string HARD truncate.
*
* - Passing a non-string returns the original value.
* - Suffix length is counted against `max`. If `suffix.length >= max`, only
* `suffix` is returned, which may exceed the set `max`.
*
* @param str
* @param options
* @param options.max
* @param options.suffix - Appended suffix string. Suffix length is counted against max length.
* @returns Truncated string, or the suffix only, or the original string, or the original non-string value.
*/
const truncate = (str: string, { max = 250, suffix = '...[truncated]' }: { max?: number, suffix?: string } = {}) => {
if (typeof str === 'string') {
const updatedMax = Math.max(0, max - suffix.length);

if (updatedMax <= 0) {
return suffix;
}

return str.length > updatedMax ? `${str.slice(0, updatedMax)}${suffix}` : str;
}

return str;
};

/**
* Format an unknown value as a string, for logging.
*
* @param value
* @returns Formatted string
*/
const formatUnknownError = (value: unknown): string => {
if (value instanceof Error) {
const message = value.stack || value.message;

if (message) {
return message;
}

try {
return String(value);
} catch {
return Object.prototype.toString.call(value);
}
}

if (typeof value === 'string') {
return value;
}

try {
return `Non-Error thrown: ${truncate(JSON.stringify(value))}`;
} catch {
try {
return truncate(inspect(value, { depth: 3, maxArrayLength: 50, breakLength: 120 }));
} catch {
return Object.prototype.toString.call(value);
}
}
};

/**
* Publish a structured log event to the diagnostics channel.
*
Expand Down Expand Up @@ -215,11 +279,13 @@ const createLogger = (options: LoggingSession = getLoggerOptions()): Unsubscribe
export {
LOG_LEVELS,
createLogger,
formatUnknownError,
log,
logSeverity,
publish,
registerStderrSubscriber,
subscribeToChannel,
truncate,
type LogEvent,
type LogLevel,
type Unsubscribe
Expand Down
Loading