Skip to content

Commit e551293

Browse files
committed
feat: Add better common logger support.
1 parent 67b8cb8 commit e551293

File tree

3 files changed

+144
-33
lines changed

3 files changed

+144
-33
lines changed

packages/shared/common/__tests__/logging/BasicLogger.test.ts

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { BasicLogger, LDLogLevel } from '../../src';
22

33
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
44

5+
beforeEach(() => {
6+
jest.clearAllMocks();
7+
});
8+
59
describe.each<[LDLogLevel, string[]]>([
610
[
711
'debug',
@@ -64,10 +68,6 @@ describe('given a logger with a custom name', () => {
6468
describe('given a default logger', () => {
6569
const logger = new BasicLogger({});
6670

67-
beforeEach(() => {
68-
jest.clearAllMocks();
69-
});
70-
7171
it('logs to the console', () => {
7272
logger.warn('potato', 'bacon');
7373
expect(spy).toHaveBeenCalledWith('potato', 'bacon');
@@ -81,10 +81,6 @@ describe('given a logger with a destination that throws', () => {
8181
},
8282
});
8383

84-
beforeEach(() => {
85-
jest.clearAllMocks();
86-
});
87-
8884
it('logs to the console instead of throwing', () => {
8985
logger.error('a');
9086
expect(spy).toHaveBeenCalledWith('error: [LaunchDarkly] a');
@@ -94,10 +90,6 @@ describe('given a logger with a destination that throws', () => {
9490
describe('given a logger with a formatter that throws', () => {
9591
const strings: string[] = [];
9692

97-
beforeEach(() => {
98-
jest.clearAllMocks();
99-
});
100-
10193
const logger = new BasicLogger({
10294
destination: (...args: any) => {
10395
strings.push(args.join(' '));
@@ -112,3 +104,102 @@ describe('given a logger with a formatter that throws', () => {
112104
expect(spy).toHaveBeenCalledTimes(0);
113105
});
114106
});
107+
108+
it('dispatches logs correctly with multiple destinations', () => {
109+
const debug = jest.fn();
110+
const info = jest.fn();
111+
const warn = jest.fn();
112+
const error = jest.fn();
113+
114+
const logger = new BasicLogger({
115+
destination: {
116+
debug,
117+
info,
118+
warn,
119+
error,
120+
},
121+
level: 'debug',
122+
});
123+
124+
logger.debug('toDebug');
125+
logger.info('toInfo');
126+
logger.warn('toWarn');
127+
logger.error('toError');
128+
129+
expect(debug).toHaveBeenCalledTimes(1);
130+
expect(debug).toHaveBeenCalledWith('debug: [LaunchDarkly] toDebug');
131+
132+
expect(info).toHaveBeenCalledTimes(1);
133+
expect(info).toHaveBeenCalledWith('info: [LaunchDarkly] toInfo');
134+
135+
expect(warn).toHaveBeenCalledTimes(1);
136+
expect(warn).toHaveBeenCalledWith('warn: [LaunchDarkly] toWarn');
137+
138+
expect(error).toHaveBeenCalledTimes(1);
139+
expect(error).toHaveBeenCalledWith('error: [LaunchDarkly] toError');
140+
});
141+
142+
it('handles destinations which throw', () => {
143+
const debug = jest.fn(() => {
144+
throw new Error('bad');
145+
});
146+
const info = jest.fn(() => {
147+
throw new Error('bad');
148+
});
149+
const warn = jest.fn(() => {
150+
throw new Error('bad');
151+
});
152+
const error = jest.fn(() => {
153+
throw new Error('bad');
154+
});
155+
156+
const logger = new BasicLogger({
157+
destination: {
158+
debug,
159+
info,
160+
warn,
161+
error,
162+
},
163+
level: 'debug',
164+
});
165+
166+
logger.debug('toDebug');
167+
logger.info('toInfo');
168+
logger.warn('toWarn');
169+
logger.error('toError');
170+
171+
expect(spy).toHaveBeenCalledTimes(4);
172+
expect(spy).toHaveBeenCalledWith('debug: [LaunchDarkly] toDebug');
173+
expect(spy).toHaveBeenCalledWith('info: [LaunchDarkly] toInfo');
174+
expect(spy).toHaveBeenCalledWith('warn: [LaunchDarkly] toWarn');
175+
expect(spy).toHaveBeenCalledWith('error: [LaunchDarkly] toError');
176+
});
177+
178+
it('handles destinations which are not defined', () => {
179+
const debug = jest.fn();
180+
const info = jest.fn();
181+
const logger = new BasicLogger({
182+
// @ts-ignore
183+
destination: {
184+
debug,
185+
info,
186+
},
187+
level: 'debug',
188+
});
189+
190+
logger.debug('toDebug');
191+
logger.info('toInfo');
192+
logger.warn('toWarn');
193+
logger.error('toError');
194+
195+
expect(debug).toHaveBeenCalledTimes(1);
196+
expect(debug).toHaveBeenCalledWith('debug: [LaunchDarkly] toDebug');
197+
198+
expect(info).toHaveBeenCalledTimes(1);
199+
expect(info).toHaveBeenCalledWith('info: [LaunchDarkly] toInfo');
200+
201+
expect(spy).toHaveBeenCalledTimes(2);
202+
203+
expect(spy).toHaveBeenCalledWith('toWarn');
204+
expect(spy).toHaveBeenCalledWith('toError');
205+
});

packages/shared/common/src/api/logging/BasicLoggerOptions.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,23 @@ export interface BasicLoggerOptions {
2121
name?: string;
2222

2323
/**
24-
* An optional function to use to print each log line.
24+
* An optional function, or collection of functions to use to print each log line.
2525
*
26-
* If this is specified, `basicLogger` calls it to write each line of output. The
26+
* If not specified, the default is `console.error`.
27+
*
28+
* If a function is specified, `basicLogger` calls it to write each line of output. The
2729
* argument is a fully formatted log line, not including a linefeed. The function
2830
* is only called for log levels that are enabled.
2931
*
30-
* If not specified, the default is `console.error`.
32+
* If a map is specified, then each entry will be used as the destination for the corresponding
33+
* log level. Any level that is not specified will use the default of `console.error`.
3134
*
3235
* Setting this property to anything other than a function will cause SDK
3336
* initialization to fail.
3437
*/
35-
destination?: (line: string) => void;
38+
destination?:
39+
| ((line: string) => void)
40+
| Record<'debug' | 'info' | 'warn' | 'error', (line: string) => void>;
3641

3742
/**
3843
* An optional formatter to use. The formatter should be compatible

packages/shared/common/src/logging/BasicLogger.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { BasicLoggerOptions, LDLogger } from '../api';
1+
import { BasicLoggerOptions, LDLogger, LDLogLevel } from '../api';
22
import format from './format';
33

4-
const LogPriority = {
5-
debug: 0,
6-
info: 1,
7-
warn: 2,
8-
error: 3,
9-
none: 4,
10-
};
4+
enum LogPriority {
5+
debug = 0,
6+
info = 1,
7+
warn = 2,
8+
error = 3,
9+
none = 4,
10+
}
1111

12-
const LevelNames = ['debug', 'info', 'warn', 'error', 'none'];
12+
const LEVEL_NAMES: LDLogLevel[] = ['debug', 'info', 'warn', 'error', 'none'];
1313

1414
/**
1515
* A basic logger which handles filtering by level.
@@ -27,7 +27,7 @@ export default class BasicLogger implements LDLogger {
2727

2828
private _name: string;
2929

30-
private _destination?: (line: string) => void;
30+
private _destinations?: Record<number, (line: string) => void>;
3131

3232
private _formatter?: (...args: any[]) => string;
3333

@@ -43,9 +43,23 @@ export default class BasicLogger implements LDLogger {
4343
constructor(options: BasicLoggerOptions) {
4444
this._logLevel = LogPriority[options.level ?? 'info'] ?? LogPriority.info;
4545
this._name = options.name ?? 'LaunchDarkly';
46-
// eslint-disable-next-line no-console
47-
this._destination = options.destination;
4846
this._formatter = options.formatter;
47+
if (typeof options.destination === 'object') {
48+
this._destinations = {
49+
[LogPriority.debug]: options.destination.debug,
50+
[LogPriority.info]: options.destination.info,
51+
[LogPriority.warn]: options.destination.warn,
52+
[LogPriority.error]: options.destination.error,
53+
};
54+
} else if (typeof options.destination === 'function') {
55+
const { destination } = options;
56+
this._destinations = {
57+
[LogPriority.debug]: destination,
58+
[LogPriority.info]: destination,
59+
[LogPriority.warn]: destination,
60+
[LogPriority.error]: destination,
61+
};
62+
}
4963
}
5064

5165
private _tryFormat(...args: any[]): string {
@@ -60,9 +74,9 @@ export default class BasicLogger implements LDLogger {
6074
}
6175
}
6276

63-
private _tryWrite(msg: string) {
77+
private _tryWrite(destination: (msg: string) => void, msg: string) {
6478
try {
65-
this._destination!(msg);
79+
destination(msg);
6680
} catch {
6781
// eslint-disable-next-line no-console
6882
console.error(msg);
@@ -71,10 +85,11 @@ export default class BasicLogger implements LDLogger {
7185

7286
private _log(level: number, args: any[]) {
7387
if (level >= this._logLevel) {
74-
const prefix = `${LevelNames[level]}: [${this._name}]`;
88+
const prefix = `${LEVEL_NAMES[level]}: [${this._name}]`;
7589
try {
76-
if (this._destination) {
77-
this._tryWrite(`${prefix} ${this._tryFormat(...args)}`);
90+
const destination = this._destinations?.[level];
91+
if (destination) {
92+
this._tryWrite(destination, `${prefix} ${this._tryFormat(...args)}`);
7893
} else {
7994
// `console.error` has its own formatter.
8095
// So we don't need to do anything.

0 commit comments

Comments
 (0)