Skip to content

Commit 21670c4

Browse files
authored
feat: Enhance basic logger destination support. (#650)
This adds support to the common basic logger that makes it more suitable for client SDKs. Instead of a single destination for all logs you can now control the destination per log level. This allow you to use all the default formatting, log level handling, and log tagging while also getting logs that are nice for use in a browser.
1 parent 473e0cb commit 21670c4

File tree

7 files changed

+215
-49
lines changed

7 files changed

+215
-49
lines changed

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
AutoEnvAttributes,
33
base64UrlEncode,
4+
BasicLogger,
45
LDClient as CommonClient,
56
Configuration,
6-
createSafeLogger,
77
Encoding,
88
FlagManager,
99
internal,
@@ -98,15 +98,18 @@ export class BrowserClient extends LDClientImpl implements LDClient {
9898
// Overrides the default logger from the common implementation.
9999
const logger =
100100
customLogger ??
101-
createSafeLogger({
102-
// eslint-disable-next-line no-console
103-
debug: debug ? console.debug : () => {},
104-
// eslint-disable-next-line no-console
105-
info: console.info,
106-
// eslint-disable-next-line no-console
107-
warn: console.warn,
108-
// eslint-disable-next-line no-console
109-
error: console.error,
101+
new BasicLogger({
102+
destination: {
103+
// eslint-disable-next-line no-console
104+
debug: console.debug,
105+
// eslint-disable-next-line no-console
106+
info: console.info,
107+
// eslint-disable-next-line no-console
108+
warn: console.warn,
109+
// eslint-disable-next-line no-console
110+
error: console.error,
111+
},
112+
level: debug ? 'debug' : 'info',
110113
});
111114

112115
// TODO: Use the already-configured baseUri from the SDK config. SDK-560

packages/sdk/browser/src/index.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
*/
1313
import {
1414
AutoEnvAttributes,
15+
BasicLogger,
16+
BasicLoggerOptions,
1517
EvaluationSeriesContext,
1618
EvaluationSeriesData,
1719
Hook,
@@ -84,3 +86,54 @@ export function initialize(clientSideId: string, options?: LDOptions): LDClient
8486
// AutoEnvAttributes are not supported yet in the browser SDK.
8587
return new BrowserClient(clientSideId, AutoEnvAttributes.Disabled, options);
8688
}
89+
90+
/**
91+
* Provides a simple {@link LDLogger} implementation.
92+
*
93+
* This logging implementation uses a simple format that includes only the log level
94+
* and the message text. By default the output is written to `console.error`.
95+
*
96+
* To use the logger created by this function, put it into {@link LDOptions.logger}. If
97+
* you do not set {@link LDOptions.logger} to anything, the SDK uses a default logger
98+
* that will log "info" level and higher priorty messages and it will log messages to
99+
* console.info, console.warn, and console.error.
100+
*
101+
* @param options Configuration for the logger. If no options are specified, the
102+
* logger uses `{ level: 'info' }`.
103+
*
104+
* @example
105+
* This example shows how to use `basicLogger` in your SDK options to enable console
106+
* logging only at `warn` and `error` levels.
107+
* ```javascript
108+
* const ldOptions = {
109+
* logger: basicLogger({ level: 'warn' }),
110+
* };
111+
* ```
112+
*
113+
* @example
114+
* This example shows how to use `basicLogger` in your SDK options to cause all
115+
* log output to go to `console.log`
116+
* ```javascript
117+
* const ldOptions = {
118+
* logger: basicLogger({ destination: console.log }),
119+
* };
120+
* ```
121+
*
122+
* * @example
123+
* The configuration also allows you to control the destination for each log level.
124+
* ```javascript
125+
* const ldOptions = {
126+
* logger: basicLogger({
127+
* destination: {
128+
* debug: console.debug,
129+
* info: console.info,
130+
* warn: console.warn,
131+
* error:console.error
132+
* }
133+
* }),
134+
* };
135+
* ```
136+
*/
137+
export function basicLogger(options: BasicLoggerOptions): LDLogger {
138+
return new BasicLogger(options);
139+
}

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 map of levels to 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.

packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ describe('Configuration', () => {
2222
withReasons: false,
2323
eventsUri: 'https://events.launchdarkly.com',
2424
flushInterval: 30,
25-
logger: {
26-
_destination: console.error,
27-
_logLevel: 1,
28-
_name: 'LaunchDarkly',
29-
},
25+
logger: expect.anything(),
3026
maxCachedContexts: 5,
3127
privateAttributes: [],
3228
sendEvents: true,

packages/shared/sdk-client/src/api/LDOptions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ export interface LDOptions {
126126
* @remarks
127127
* Set a custom {@link LDLogger} if you want full control of logging behavior.
128128
*
129-
* @defaultValue A {@link BasicLogger} which outputs to the console at `info` level.
129+
* @defaultValue The default logging implementation will varybased on platform. For the browser
130+
* the default logger will log "info" level and higher priorty messages and it will log messages to
131+
* console.info, console.warn, and console.error. Other platforms may use a `BasicLogger` instance
132+
* also defaulted to the "info" level.
130133
*/
131134
logger?: LDLogger;
132135

0 commit comments

Comments
 (0)