Skip to content

Commit fef1e80

Browse files
Merge pull request #425 from splitio/custom-logger
Custom logger support
2 parents 57e1608 + 0cbcb2b commit fef1e80

File tree

14 files changed

+237
-74
lines changed

14 files changed

+237
-74
lines changed

CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
2.7.0 (October 7, 2025)
2+
- Added support for custom loggers: added `logger` configuration option and `LoggerAPI.setLogger` method to allow the SDK to use a custom logger.
3+
14
2.6.0 (September 18, 2025)
25
- Added `storage.wrapper` configuration option to allow the SDK to use a custom storage wrapper for the storage type `LOCALSTORAGE`. Default value is `window.localStorage`.
36

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-commons",
3-
"version": "2.6.0",
3+
"version": "2.7.0",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/logger/__tests__/index.spec.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@ test('SPLIT LOGGER / isLogLevelString utility function', () => {
1616
expect(isLogLevelString(LOG_LEVELS.DEBUG)).toBe(true); // Calling isLogLevelString should return true with a LOG_LEVELS value
1717
expect(isLogLevelString('ERROR')).toBe(true); // Calling isLogLevelString should return true with a string equal to some LOG_LEVELS value
1818
expect(isLogLevelString('INVALID LOG LEVEL')).toBe(false); // Calling isLogLevelString should return false with a string not equal to any LOG_LEVELS value
19-
2019
});
2120

2221
test('SPLIT LOGGER / LogLevels exposed mappings', () => {
2322
expect(LogLevels).toEqual(LOG_LEVELS); // Exposed log levels should contain the levels we want.
24-
2523
});
2624

2725
test('SPLIT LOGGER / Logger class shape', () => {
@@ -40,9 +38,9 @@ const LOG_LEVELS_IN_ORDER: SplitIO.LogLevel[] = ['DEBUG', 'INFO', 'WARN', 'ERROR
4038
/* Utility function to avoid repeating too much code */
4139
function testLogLevels(levelToTest: SplitIO.LogLevel) {
4240
// Builds the expected message.
43-
const buildExpectedMessage = (lvl: string, category: string, msg: string, showLevel?: boolean) => {
41+
const buildExpectedMessage = (lvl: string, category: string, msg: string, useDefaultLogger?: boolean) => {
4442
let res = '';
45-
if (showLevel) res += '[' + lvl + ']' + (lvl.length === 4 ? ' ' : ' ');
43+
if (useDefaultLogger) res += '[' + lvl + ']' + (lvl.length === 4 ? ' ' : ' ');
4644
res += category + ' => ';
4745
res += msg;
4846
return res;
@@ -51,24 +49,33 @@ function testLogLevels(levelToTest: SplitIO.LogLevel) {
5149
// Spy console.log
5250
const consoleLogSpy = jest.spyOn(global.console, 'log');
5351

54-
// Runs the suite with the given value for showLevel option.
55-
const runTests = (showLevel?: boolean, useCodes?: boolean) => {
52+
// Runs the suite with the given values
53+
const runTests = (useDefaultLogger?: boolean, useCodes?: boolean) => {
5654
let logLevelLogsCounter = 0;
5755
let testForNoLog = false;
5856
const logMethod = levelToTest.toLowerCase();
5957
const logCategory = `test-category-${logMethod}`;
60-
const instance = new Logger({ prefix: logCategory, showLevel },
61-
useCodes ? new Map([[1, 'Test log for level %s with showLevel: %s %s']]) : undefined);
58+
const instance = new Logger({ prefix: logCategory },
59+
useCodes ? new Map([[1, 'Test log for level %s with default logger: %s %s']]) : undefined);
60+
if (!useDefaultLogger) {
61+
instance.setLogger({
62+
debug: console.log,
63+
info: console.log,
64+
warn: console.log,
65+
error: console.log,
66+
});
67+
}
68+
6269

6370
LOG_LEVELS_IN_ORDER.forEach((logLevel, i) => {
64-
const logMsg = `Test log for level ${levelToTest} with showLevel: ${showLevel} ${logLevelLogsCounter}`;
65-
const expectedMessage = buildExpectedMessage(levelToTest, logCategory, logMsg, showLevel);
71+
const logMsg = `Test log for level ${levelToTest} with default logger: ${useDefaultLogger} ${logLevelLogsCounter}`;
72+
const expectedMessage = buildExpectedMessage(levelToTest, logCategory, logMsg, useDefaultLogger);
6673

6774
// Set the logLevel for this iteration.
6875
instance.setLogLevel(LogLevels[logLevel]);
6976
// Call the method
7077
// @ts-ignore
71-
if (useCodes) instance[logMethod](1, [levelToTest, showLevel, logLevelLogsCounter]); // @ts-ignore
78+
if (useCodes) instance[logMethod](1, [levelToTest, useDefaultLogger, logLevelLogsCounter]); // @ts-ignore
7279
else instance[logMethod](logMsg);
7380
// Assert if console.log was called.
7481
const actualMessage = consoleLogSpy.mock.calls[consoleLogSpy.mock.calls.length - 1][0];
@@ -85,36 +92,31 @@ function testLogLevels(levelToTest: SplitIO.LogLevel) {
8592
});
8693
};
8794

88-
// Show logLevel
95+
// Default console.log (Show level in logs)
8996
runTests(true);
90-
// Hide logLevel
97+
// Custom logger (Don't show level in logs)
9198
runTests(false);
92-
// Hide logLevel and use message codes
99+
// Custom logger (Don't show level in logs) and use message codes
93100
runTests(false, true);
94101

95102
// Restore spied object.
96103
consoleLogSpy.mockRestore();
97-
98104
}
99105

100106
test('SPLIT LOGGER / Logger class public methods behavior - instance.debug', () => {
101107
testLogLevels(LogLevels.DEBUG);
102-
103108
});
104109

105110
test('SPLIT LOGGER / Logger class public methods behavior - instance.info', () => {
106111
testLogLevels(LogLevels.INFO);
107-
108112
});
109113

110114
test('SPLIT LOGGER / Logger class public methods behavior - instance.warn', () => {
111115
testLogLevels(LogLevels.WARN);
112-
113116
});
114117

115118
test('SPLIT LOGGER / Logger class public methods behavior - instance.error', () => {
116119
testLogLevels(LogLevels.ERROR);
117-
118120
});
119121

120122
test('SPLIT LOGGER / _sprintf', () => {

src/logger/__tests__/sdkLogger.mock.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ export const loggerMock = {
66
debug: jest.fn(),
77
info: jest.fn(),
88
setLogLevel: jest.fn(),
9+
setLogger: jest.fn(),
910

1011
mockClear() {
1112
this.warn.mockClear();
1213
this.error.mockClear();
1314
this.debug.mockClear();
1415
this.info.mockClear();
1516
this.setLogLevel.mockClear();
17+
this.setLogger.mockClear();
1618
}
1719
};
1820

1921
export function getLoggerLogLevel(logger: any): SplitIO.LogLevel | undefined {
2022
if (logger) return logger.options.logLevel;
2123
}
24+
25+
export function getCustomLogger(logger: any): SplitIO.Logger | undefined {
26+
if (logger) return logger.logger;
27+
}

src/logger/__tests__/sdkLogger.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createLoggerAPI } from '../sdkLogger';
22
import { Logger, LogLevels } from '../index';
3-
import { getLoggerLogLevel } from './sdkLogger.mock';
3+
import { getLoggerLogLevel, getCustomLogger } from './sdkLogger.mock';
44

55
test('LoggerAPI / methods and props', () => {
66
// creates a LoggerAPI instance
@@ -26,4 +26,16 @@ test('LoggerAPI / methods and props', () => {
2626

2727
expect(API.LogLevel).toEqual(LogLevels); // API object should have LogLevel prop including all available levels.
2828

29+
// valid custom logger
30+
API.setLogger(console);
31+
expect(getCustomLogger(logger)).toBe(console);
32+
33+
// unset custom logger
34+
API.setLogger(undefined);
35+
expect(getCustomLogger(logger)).toBeUndefined();
36+
37+
// invalid custom logger
38+
// @ts-expect-error
39+
API.setLogger({});
40+
expect(getCustomLogger(logger)).toBeUndefined();
2941
});

src/logger/index.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { objectAssign } from '../utils/lang/objectAssign';
22
import { ILoggerOptions, ILogger } from './types';
33
import { find, isObject } from '../utils/lang';
44
import SplitIO from '../../types/splitio';
5+
import { isLogger } from '../utils/settingsValidation/logger/commons';
56

67
export const LogLevels: SplitIO.ILoggerAPI['LogLevel'] = {
78
DEBUG: 'DEBUG',
@@ -19,6 +20,13 @@ const LogLevelIndexes = {
1920
NONE: 5
2021
};
2122

23+
export const DEFAULT_LOGGER: SplitIO.Logger = {
24+
debug(msg) { console.log('[DEBUG] ' + msg); },
25+
info(msg) { console.log('[INFO] ' + msg); },
26+
warn(msg) { console.log('[WARN] ' + msg); },
27+
error(msg) { console.log('[ERROR] ' + msg); }
28+
};
29+
2230
export function isLogLevelString(str: string): str is SplitIO.LogLevel {
2331
return !!find(LogLevels, (lvl: string) => str === lvl);
2432
}
@@ -40,14 +48,14 @@ export function _sprintf(format: string = '', args: any[] = []): string {
4048
const defaultOptions = {
4149
prefix: 'splitio',
4250
logLevel: LogLevels.NONE,
43-
showLevel: true,
4451
};
4552

4653
export class Logger implements ILogger {
4754

4855
private options: Required<ILoggerOptions>;
4956
private codes: Map<number, string>;
5057
private logLevel: number;
58+
private logger?: SplitIO.Logger;
5159

5260
constructor(options?: ILoggerOptions, codes?: Map<number, string>) {
5361
this.options = objectAssign({}, defaultOptions, options);
@@ -60,48 +68,54 @@ export class Logger implements ILogger {
6068
this.logLevel = LogLevelIndexes[logLevel];
6169
}
6270

71+
setLogger(logger?: SplitIO.Logger) {
72+
if (logger) {
73+
if (isLogger(logger)) {
74+
this.logger = logger;
75+
// If custom logger is set, all logs are either enabled or disabled
76+
if (this.logLevel !== LogLevelIndexes.NONE) this.setLogLevel(LogLevels.DEBUG);
77+
return;
78+
} else {
79+
this.error('Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`');
80+
}
81+
}
82+
// unset
83+
this.logger = undefined;
84+
}
85+
6386
debug(msg: string | number, args?: any[]) {
64-
if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log(LogLevels.DEBUG, msg, args);
87+
if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log('debug', msg, args);
6588
}
6689

6790
info(msg: string | number, args?: any[]) {
68-
if (this._shouldLog(LogLevelIndexes.INFO)) this._log(LogLevels.INFO, msg, args);
91+
if (this._shouldLog(LogLevelIndexes.INFO)) this._log('info', msg, args);
6992
}
7093

7194
warn(msg: string | number, args?: any[]) {
72-
if (this._shouldLog(LogLevelIndexes.WARN)) this._log(LogLevels.WARN, msg, args);
95+
if (this._shouldLog(LogLevelIndexes.WARN)) this._log('warn', msg, args);
7396
}
7497

7598
error(msg: string | number, args?: any[]) {
76-
if (this._shouldLog(LogLevelIndexes.ERROR)) this._log(LogLevels.ERROR, msg, args);
99+
if (this._shouldLog(LogLevelIndexes.ERROR)) this._log('error', msg, args);
77100
}
78101

79-
private _log(level: SplitIO.LogLevel, msg: string | number, args?: any[]) {
102+
_log(method: keyof SplitIO.Logger, msg: string | number, args?: any[]) {
80103
if (typeof msg === 'number') {
81104
const format = this.codes.get(msg);
82105
msg = format ? _sprintf(format, args) : `Message code ${msg}${args ? ', with args: ' + args.toString() : ''}`;
83106
} else {
84107
if (args) msg = _sprintf(msg, args);
85108
}
86109

87-
const formattedText = this._generateLogMessage(level, msg);
88-
89-
console.log(formattedText);
90-
}
110+
if (this.options.prefix) msg = this.options.prefix + ' => ' + msg;
91111

92-
private _generateLogMessage(level: SplitIO.LogLevel, text: string) {
93-
const textPre = ' => ';
94-
let result = '';
95-
96-
if (this.options.showLevel) {
97-
result += '[' + level + ']' + (level === LogLevels.INFO || level === LogLevels.WARN ? ' ' : '') + ' ';
98-
}
99-
100-
if (this.options.prefix) {
101-
result += this.options.prefix + textPre;
112+
if (this.logger) {
113+
try {
114+
this.logger[method](msg);
115+
return;
116+
} catch (e) { /* empty */ }
102117
}
103-
104-
return result += text;
118+
DEFAULT_LOGGER[method](msg);
105119
}
106120

107121
private _shouldLog(level: number) {

src/logger/sdkLogger.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ export function createLoggerAPI(log: ILogger): SplitIO.ILoggerAPI {
3030
* @param logLevel - Custom LogLevel value.
3131
*/
3232
setLogLevel,
33+
/**
34+
* Sets a custom logger for the SDK logs.
35+
* @param logger - Custom logger.
36+
*/
37+
setLogger(logger?: ILogger) {
38+
log.setLogger(logger);
39+
},
3340
/**
3441
* Disables all the log levels.
3542
*/

src/logger/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import SplitIO from '../../types/splitio';
22

33
export interface ILoggerOptions {
4-
prefix?: string,
5-
logLevel?: SplitIO.LogLevel,
6-
showLevel?: boolean, // @TODO remove this param eventually since it is not being set `false` anymore
4+
prefix?: string;
5+
logLevel?: SplitIO.LogLevel;
76
}
87

98
export interface ILogger extends SplitIO.ILogger {
9+
setLogger(logger?: SplitIO.Logger): void;
10+
1011
debug(msg: any): void;
1112
debug(msg: string | number, args?: any[]): void;
1213

0 commit comments

Comments
 (0)