diff --git a/CHANGES.txt b/CHANGES.txt index da38cdb1..711d3ca3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,6 @@ +2.7.0 (October 7, 2025) + - Added support for custom loggers: added `logger` configuration option and `LoggerAPI.setLogger` method to allow the SDK to use a custom logger. + 2.6.0 (September 18, 2025) - 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`. diff --git a/package-lock.json b/package-lock.json index d40871e0..f669aefa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-commons", - "version": "2.6.0", + "version": "2.7.0", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", diff --git a/package.json b/package.json index 9301b5bf..4f3fccb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "2.6.0", + "version": "2.7.0", "description": "Split JavaScript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/logger/__tests__/index.spec.ts b/src/logger/__tests__/index.spec.ts index 6e76c0a5..55b659a4 100644 --- a/src/logger/__tests__/index.spec.ts +++ b/src/logger/__tests__/index.spec.ts @@ -16,12 +16,10 @@ test('SPLIT LOGGER / isLogLevelString utility function', () => { expect(isLogLevelString(LOG_LEVELS.DEBUG)).toBe(true); // Calling isLogLevelString should return true with a LOG_LEVELS value expect(isLogLevelString('ERROR')).toBe(true); // Calling isLogLevelString should return true with a string equal to some LOG_LEVELS value expect(isLogLevelString('INVALID LOG LEVEL')).toBe(false); // Calling isLogLevelString should return false with a string not equal to any LOG_LEVELS value - }); test('SPLIT LOGGER / LogLevels exposed mappings', () => { expect(LogLevels).toEqual(LOG_LEVELS); // Exposed log levels should contain the levels we want. - }); test('SPLIT LOGGER / Logger class shape', () => { @@ -40,9 +38,9 @@ const LOG_LEVELS_IN_ORDER: SplitIO.LogLevel[] = ['DEBUG', 'INFO', 'WARN', 'ERROR /* Utility function to avoid repeating too much code */ function testLogLevels(levelToTest: SplitIO.LogLevel) { // Builds the expected message. - const buildExpectedMessage = (lvl: string, category: string, msg: string, showLevel?: boolean) => { + const buildExpectedMessage = (lvl: string, category: string, msg: string, useDefaultLogger?: boolean) => { let res = ''; - if (showLevel) res += '[' + lvl + ']' + (lvl.length === 4 ? ' ' : ' '); + if (useDefaultLogger) res += '[' + lvl + ']' + (lvl.length === 4 ? ' ' : ' '); res += category + ' => '; res += msg; return res; @@ -51,24 +49,33 @@ function testLogLevels(levelToTest: SplitIO.LogLevel) { // Spy console.log const consoleLogSpy = jest.spyOn(global.console, 'log'); - // Runs the suite with the given value for showLevel option. - const runTests = (showLevel?: boolean, useCodes?: boolean) => { + // Runs the suite with the given values + const runTests = (useDefaultLogger?: boolean, useCodes?: boolean) => { let logLevelLogsCounter = 0; let testForNoLog = false; const logMethod = levelToTest.toLowerCase(); const logCategory = `test-category-${logMethod}`; - const instance = new Logger({ prefix: logCategory, showLevel }, - useCodes ? new Map([[1, 'Test log for level %s with showLevel: %s %s']]) : undefined); + const instance = new Logger({ prefix: logCategory }, + useCodes ? new Map([[1, 'Test log for level %s with default logger: %s %s']]) : undefined); + if (!useDefaultLogger) { + instance.setLogger({ + debug: console.log, + info: console.log, + warn: console.log, + error: console.log, + }); + } + LOG_LEVELS_IN_ORDER.forEach((logLevel, i) => { - const logMsg = `Test log for level ${levelToTest} with showLevel: ${showLevel} ${logLevelLogsCounter}`; - const expectedMessage = buildExpectedMessage(levelToTest, logCategory, logMsg, showLevel); + const logMsg = `Test log for level ${levelToTest} with default logger: ${useDefaultLogger} ${logLevelLogsCounter}`; + const expectedMessage = buildExpectedMessage(levelToTest, logCategory, logMsg, useDefaultLogger); // Set the logLevel for this iteration. instance.setLogLevel(LogLevels[logLevel]); // Call the method // @ts-ignore - if (useCodes) instance[logMethod](1, [levelToTest, showLevel, logLevelLogsCounter]); // @ts-ignore + if (useCodes) instance[logMethod](1, [levelToTest, useDefaultLogger, logLevelLogsCounter]); // @ts-ignore else instance[logMethod](logMsg); // Assert if console.log was called. const actualMessage = consoleLogSpy.mock.calls[consoleLogSpy.mock.calls.length - 1][0]; @@ -85,36 +92,31 @@ function testLogLevels(levelToTest: SplitIO.LogLevel) { }); }; - // Show logLevel + // Default console.log (Show level in logs) runTests(true); - // Hide logLevel + // Custom logger (Don't show level in logs) runTests(false); - // Hide logLevel and use message codes + // Custom logger (Don't show level in logs) and use message codes runTests(false, true); // Restore spied object. consoleLogSpy.mockRestore(); - } test('SPLIT LOGGER / Logger class public methods behavior - instance.debug', () => { testLogLevels(LogLevels.DEBUG); - }); test('SPLIT LOGGER / Logger class public methods behavior - instance.info', () => { testLogLevels(LogLevels.INFO); - }); test('SPLIT LOGGER / Logger class public methods behavior - instance.warn', () => { testLogLevels(LogLevels.WARN); - }); test('SPLIT LOGGER / Logger class public methods behavior - instance.error', () => { testLogLevels(LogLevels.ERROR); - }); test('SPLIT LOGGER / _sprintf', () => { diff --git a/src/logger/__tests__/sdkLogger.mock.ts b/src/logger/__tests__/sdkLogger.mock.ts index b7c4aa25..b715461d 100644 --- a/src/logger/__tests__/sdkLogger.mock.ts +++ b/src/logger/__tests__/sdkLogger.mock.ts @@ -6,6 +6,7 @@ export const loggerMock = { debug: jest.fn(), info: jest.fn(), setLogLevel: jest.fn(), + setLogger: jest.fn(), mockClear() { this.warn.mockClear(); @@ -13,9 +14,14 @@ export const loggerMock = { this.debug.mockClear(); this.info.mockClear(); this.setLogLevel.mockClear(); + this.setLogger.mockClear(); } }; export function getLoggerLogLevel(logger: any): SplitIO.LogLevel | undefined { if (logger) return logger.options.logLevel; } + +export function getCustomLogger(logger: any): SplitIO.Logger | undefined { + if (logger) return logger.logger; +} diff --git a/src/logger/__tests__/sdkLogger.spec.ts b/src/logger/__tests__/sdkLogger.spec.ts index a69a587c..5409f554 100644 --- a/src/logger/__tests__/sdkLogger.spec.ts +++ b/src/logger/__tests__/sdkLogger.spec.ts @@ -1,6 +1,6 @@ import { createLoggerAPI } from '../sdkLogger'; import { Logger, LogLevels } from '../index'; -import { getLoggerLogLevel } from './sdkLogger.mock'; +import { getLoggerLogLevel, getCustomLogger } from './sdkLogger.mock'; test('LoggerAPI / methods and props', () => { // creates a LoggerAPI instance @@ -26,4 +26,16 @@ test('LoggerAPI / methods and props', () => { expect(API.LogLevel).toEqual(LogLevels); // API object should have LogLevel prop including all available levels. + // valid custom logger + API.setLogger(console); + expect(getCustomLogger(logger)).toBe(console); + + // unset custom logger + API.setLogger(undefined); + expect(getCustomLogger(logger)).toBeUndefined(); + + // invalid custom logger + // @ts-expect-error + API.setLogger({}); + expect(getCustomLogger(logger)).toBeUndefined(); }); diff --git a/src/logger/index.ts b/src/logger/index.ts index 662e1f86..09c3c7a9 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -2,6 +2,7 @@ import { objectAssign } from '../utils/lang/objectAssign'; import { ILoggerOptions, ILogger } from './types'; import { find, isObject } from '../utils/lang'; import SplitIO from '../../types/splitio'; +import { isLogger } from '../utils/settingsValidation/logger/commons'; export const LogLevels: SplitIO.ILoggerAPI['LogLevel'] = { DEBUG: 'DEBUG', @@ -19,6 +20,13 @@ const LogLevelIndexes = { NONE: 5 }; +export const DEFAULT_LOGGER: SplitIO.Logger = { + debug(msg) { console.log('[DEBUG] ' + msg); }, + info(msg) { console.log('[INFO] ' + msg); }, + warn(msg) { console.log('[WARN] ' + msg); }, + error(msg) { console.log('[ERROR] ' + msg); } +}; + export function isLogLevelString(str: string): str is SplitIO.LogLevel { return !!find(LogLevels, (lvl: string) => str === lvl); } @@ -40,7 +48,6 @@ export function _sprintf(format: string = '', args: any[] = []): string { const defaultOptions = { prefix: 'splitio', logLevel: LogLevels.NONE, - showLevel: true, }; export class Logger implements ILogger { @@ -48,6 +55,7 @@ export class Logger implements ILogger { private options: Required; private codes: Map; private logLevel: number; + private logger?: SplitIO.Logger; constructor(options?: ILoggerOptions, codes?: Map) { this.options = objectAssign({}, defaultOptions, options); @@ -60,23 +68,38 @@ export class Logger implements ILogger { this.logLevel = LogLevelIndexes[logLevel]; } + setLogger(logger?: SplitIO.Logger) { + if (logger) { + if (isLogger(logger)) { + this.logger = logger; + // If custom logger is set, all logs are either enabled or disabled + if (this.logLevel !== LogLevelIndexes.NONE) this.setLogLevel(LogLevels.DEBUG); + return; + } else { + this.error('Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); + } + } + // unset + this.logger = undefined; + } + debug(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log(LogLevels.DEBUG, msg, args); + if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log('debug', msg, args); } info(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.INFO)) this._log(LogLevels.INFO, msg, args); + if (this._shouldLog(LogLevelIndexes.INFO)) this._log('info', msg, args); } warn(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.WARN)) this._log(LogLevels.WARN, msg, args); + if (this._shouldLog(LogLevelIndexes.WARN)) this._log('warn', msg, args); } error(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelIndexes.ERROR)) this._log(LogLevels.ERROR, msg, args); + if (this._shouldLog(LogLevelIndexes.ERROR)) this._log('error', msg, args); } - private _log(level: SplitIO.LogLevel, msg: string | number, args?: any[]) { + _log(method: keyof SplitIO.Logger, msg: string | number, args?: any[]) { if (typeof msg === 'number') { const format = this.codes.get(msg); msg = format ? _sprintf(format, args) : `Message code ${msg}${args ? ', with args: ' + args.toString() : ''}`; @@ -84,24 +107,15 @@ export class Logger implements ILogger { if (args) msg = _sprintf(msg, args); } - const formattedText = this._generateLogMessage(level, msg); - - console.log(formattedText); - } + if (this.options.prefix) msg = this.options.prefix + ' => ' + msg; - private _generateLogMessage(level: SplitIO.LogLevel, text: string) { - const textPre = ' => '; - let result = ''; - - if (this.options.showLevel) { - result += '[' + level + ']' + (level === LogLevels.INFO || level === LogLevels.WARN ? ' ' : '') + ' '; - } - - if (this.options.prefix) { - result += this.options.prefix + textPre; + if (this.logger) { + try { + this.logger[method](msg); + return; + } catch (e) { /* empty */ } } - - return result += text; + DEFAULT_LOGGER[method](msg); } private _shouldLog(level: number) { diff --git a/src/logger/sdkLogger.ts b/src/logger/sdkLogger.ts index 42d9be42..821945de 100644 --- a/src/logger/sdkLogger.ts +++ b/src/logger/sdkLogger.ts @@ -30,6 +30,13 @@ export function createLoggerAPI(log: ILogger): SplitIO.ILoggerAPI { * @param logLevel - Custom LogLevel value. */ setLogLevel, + /** + * Sets a custom logger for the SDK logs. + * @param logger - Custom logger. + */ + setLogger(logger?: ILogger) { + log.setLogger(logger); + }, /** * Disables all the log levels. */ diff --git a/src/logger/types.ts b/src/logger/types.ts index 2f05b3ba..867cd841 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -1,12 +1,13 @@ import SplitIO from '../../types/splitio'; export interface ILoggerOptions { - prefix?: string, - logLevel?: SplitIO.LogLevel, - showLevel?: boolean, // @TODO remove this param eventually since it is not being set `false` anymore + prefix?: string; + logLevel?: SplitIO.LogLevel; } export interface ILogger extends SplitIO.ILogger { + setLogger(logger?: SplitIO.Logger): void; + debug(msg: any): void; debug(msg: string | number, args?: any[]): void; diff --git a/src/utils/settingsValidation/logger/__tests__/index.spec.ts b/src/utils/settingsValidation/logger/__tests__/index.spec.ts index 3f91c2db..9b7e765a 100644 --- a/src/utils/settingsValidation/logger/__tests__/index.spec.ts +++ b/src/utils/settingsValidation/logger/__tests__/index.spec.ts @@ -3,6 +3,13 @@ import { loggerMock, getLoggerLogLevel } from '../../../../logger/__tests__/sdkL import { validateLogger as pluggableValidateLogger } from '../pluggableLogger'; import { validateLogger as builtinValidateLogger } from '../builtinLogger'; +const customLogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn() +}; + const testTargets = [ [pluggableValidateLogger], [builtinValidateLogger] @@ -11,7 +18,13 @@ const testTargets = [ describe('logger validators', () => { const consoleLogSpy = jest.spyOn(global.console, 'log'); - afterEach(() => { consoleLogSpy.mockClear(); }); + afterEach(() => { + consoleLogSpy.mockClear(); + customLogger.debug.mockClear(); + customLogger.info.mockClear(); + customLogger.warn.mockClear(); + customLogger.error.mockClear(); + }); test.each(testTargets)('returns a NONE logger if `debug` property is not defined or false', (validateLogger) => { // @ts-ignore expect(getLoggerLogLevel(validateLogger({}))).toBe('NONE'); @@ -23,8 +36,8 @@ describe('logger validators', () => { test.each(testTargets)('returns a NONE logger if `debug` property is invalid and logs the error', (validateLogger) => { expect(getLoggerLogLevel(validateLogger({ debug: null }))).toBe('NONE'); - expect(getLoggerLogLevel(validateLogger({ debug: 10 }))).toBe('NONE'); - expect(getLoggerLogLevel(validateLogger({ debug: {} }))).toBe('NONE'); + expect(getLoggerLogLevel(validateLogger({ debug: 10, logger: undefined }))).toBe('NONE'); // @ts-expect-error invalid `logger`, ignored because it's falsy + expect(getLoggerLogLevel(validateLogger({ debug: {}, logger: false }))).toBe('NONE'); if (validateLogger === builtinValidateLogger) { // for builtinValidateLogger, a logger cannot be passed as `debug` property @@ -43,6 +56,13 @@ describe('logger validators', () => { expect(getLoggerLogLevel(validateLogger({ debug: 'ERROR' }))).toBe('ERROR'); expect(getLoggerLogLevel(validateLogger({ debug: 'NONE' }))).toBe('NONE'); + // When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true) + expect(getLoggerLogLevel(validateLogger({ debug: 'DEBUG', logger: loggerMock }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'INFO', logger: loggerMock }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'WARN', logger: loggerMock }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'ERROR', logger: loggerMock }))).toBe('DEBUG'); + expect(getLoggerLogLevel(validateLogger({ debug: 'NONE', logger: loggerMock }))).toBe('NONE'); + expect(consoleLogSpy).not.toBeCalled(); }); @@ -52,4 +72,71 @@ describe('logger validators', () => { expect(consoleLogSpy).not.toBeCalled(); }); + test.each(testTargets)('uses the provided custom logger if it is valid', (validateLogger) => { + const logger = validateLogger({ debug: true, logger: customLogger }); + + logger.debug('test debug'); + expect(customLogger.debug).toBeCalledWith('splitio => test debug'); + + logger.info('test info'); + expect(customLogger.info).toBeCalledWith('splitio => test info'); + + logger.warn('test warn'); + expect(customLogger.warn).toBeCalledWith('splitio => test warn'); + + logger.error('test error'); + expect(customLogger.error).toBeCalledWith('splitio => test error'); + + expect(consoleLogSpy).not.toBeCalled(); + }); + + test.each(testTargets)('uses the default console.log method if the provided custom logger is not valid', (validateLogger) => { + // @ts-expect-error `logger` property is not valid + const logger = validateLogger({ debug: true, logger: {} }); + expect(consoleLogSpy).toBeCalledWith('[ERROR] splitio => Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`'); + + logger.debug('test debug'); + expect(consoleLogSpy).toBeCalledWith('[DEBUG] splitio => test debug'); + + logger.info('test info'); + expect(consoleLogSpy).toBeCalledWith('[INFO] splitio => test info'); + + logger.warn('test warn'); + expect(consoleLogSpy).toBeCalledWith('[WARN] splitio => test warn'); + + logger.error('test error'); + expect(consoleLogSpy).toBeCalledWith('[ERROR] splitio => test error'); + + expect(consoleLogSpy).toBeCalledTimes(5); + }); + + test.each(testTargets)('uses the default console.log method if the provided custom logger throws an error', (validateLogger) => { + const customLoggerWithErrors = { + debug: jest.fn(() => { throw new Error('debug error'); }), + info: jest.fn(() => { throw new Error('info error'); }), + warn: jest.fn(() => { throw new Error('warn error'); }), + error: jest.fn(() => { throw new Error('error error'); }) + }; + + const logger = validateLogger({ debug: true, logger: customLoggerWithErrors }); + + logger.debug('test debug'); + expect(customLoggerWithErrors.debug).toBeCalledWith('splitio => test debug'); + expect(consoleLogSpy).toBeCalledWith('[DEBUG] splitio => test debug'); + + logger.info('test info'); + expect(customLoggerWithErrors.info).toBeCalledWith('splitio => test info'); + expect(consoleLogSpy).toBeCalledWith('[INFO] splitio => test info'); + + logger.warn('test warn'); + expect(customLoggerWithErrors.warn).toBeCalledWith('splitio => test warn'); + expect(consoleLogSpy).toBeCalledWith('[WARN] splitio => test warn'); + + logger.error('test error'); + expect(customLoggerWithErrors.error).toBeCalledWith('splitio => test error'); + expect(consoleLogSpy).toBeCalledWith('[ERROR] splitio => test error'); + + expect(consoleLogSpy).toBeCalledTimes(4); + }); + }); diff --git a/src/utils/settingsValidation/logger/builtinLogger.ts b/src/utils/settingsValidation/logger/builtinLogger.ts index 4f099c7c..7fce3078 100644 --- a/src/utils/settingsValidation/logger/builtinLogger.ts +++ b/src/utils/settingsValidation/logger/builtinLogger.ts @@ -40,15 +40,16 @@ if (/^(enabled?|on)/i.test(initialState)) { * @param settings - user config object, with an optional `debug` property of type boolean or string log level. * @returns a logger instance with the log level at `settings.debug`. If `settings.debug` is invalid or not provided, `initialLogLevel` is used. */ -export function validateLogger(settings: { debug: unknown }): ILogger { - const { debug } = settings; +export function validateLogger(settings: { debug: unknown, logger?: SplitIO.Logger }): ILogger { + const { debug, logger } = settings; const logLevel: SplitIO.LogLevel | undefined = debug !== undefined ? getLogLevel(debug) : initialLogLevel; const log = new Logger({ logLevel: logLevel || initialLogLevel }, allCodes); + log.setLogger(logger); - // @ts-ignore // if logLevel is undefined at this point, it means that settings `debug` value is invalid - if (!logLevel) log._log(LogLevels.ERROR, 'Invalid Log Level - No changes to the logs will be applied.'); + // if logLevel is undefined at this point, it means that settings `debug` value is invalid + if (!logLevel) log._log('error', 'Invalid Log Level - No changes to the logs will be applied.'); return log; } diff --git a/src/utils/settingsValidation/logger/commons.ts b/src/utils/settingsValidation/logger/commons.ts index 8c11cbbb..fe27151b 100644 --- a/src/utils/settingsValidation/logger/commons.ts +++ b/src/utils/settingsValidation/logger/commons.ts @@ -10,15 +10,15 @@ import SplitIO from '../../../../types/splitio'; * @returns LogLevel of the given debugValue or undefined if the provided value is invalid */ export function getLogLevel(debugValue: unknown): SplitIO.LogLevel | undefined { - if (typeof debugValue === 'boolean') { - if (debugValue) { - return LogLevels.DEBUG; - } else { - return LogLevels.NONE; - } - } else if (typeof debugValue === 'string' && isLogLevelString(debugValue)) { - return debugValue; - } else { - return undefined; - } + return typeof debugValue === 'boolean' ? + debugValue ? + LogLevels.DEBUG : + LogLevels.NONE : + typeof debugValue === 'string' && isLogLevelString(debugValue) ? + debugValue : + undefined; +} + +export function isLogger(log: any): log is SplitIO.Logger { + return log !== null && typeof log === 'object' && typeof log.debug === 'function' && typeof log.info === 'function' && typeof log.warn === 'function' && typeof log.error === 'function'; } diff --git a/src/utils/settingsValidation/logger/pluggableLogger.ts b/src/utils/settingsValidation/logger/pluggableLogger.ts index 063134c9..22deaf92 100644 --- a/src/utils/settingsValidation/logger/pluggableLogger.ts +++ b/src/utils/settingsValidation/logger/pluggableLogger.ts @@ -1,10 +1,10 @@ import { Logger, LogLevels } from '../../../logger'; import { ILogger } from '../../../logger/types'; import SplitIO from '../../../../types/splitio'; -import { getLogLevel } from './commons'; +import { getLogLevel, isLogger } from './commons'; -function isLogger(log: any): log is ILogger { - return log !== null && typeof log === 'object' && typeof log.debug === 'function' && typeof log.info === 'function' && typeof log.warn === 'function' && typeof log.error === 'function' && typeof log.setLogLevel === 'function'; +function isILogger(log: any): log is ILogger { + return isLogger(log) && typeof (log as any).setLogLevel === 'function'; } // By default it starts disabled. @@ -17,19 +17,23 @@ let initialLogLevel = LogLevels.NONE; * @returns a logger instance, that might be: the provided logger at `settings.debug`, or one with the given `debug` log level, * or one with NONE log level if `debug` is not defined or invalid. */ -export function validateLogger(settings: { debug: unknown }): ILogger { - const { debug } = settings; +export function validateLogger(settings: { debug: unknown, logger?: SplitIO.Logger }): ILogger { + const { debug, logger } = settings; let logLevel: SplitIO.LogLevel | undefined = initialLogLevel; if (debug !== undefined) { - if (isLogger(debug)) return debug; + if (isILogger(debug)) { + debug.setLogger(logger); + return debug; + } logLevel = getLogLevel(settings.debug); } const log = new Logger({ logLevel: logLevel || initialLogLevel }); + log.setLogger(logger); - // @ts-ignore // `debug` value is invalid if logLevel is undefined at this point - if (!logLevel) log._log(LogLevels.ERROR, 'Invalid `debug` value at config. Logs will be disabled.'); + // `debug` value is invalid if logLevel is undefined at this point + if (!logLevel) log._log('error', 'Invalid `debug` value at config. Logs will be disabled.'); return log; } diff --git a/types/splitio.d.ts b/types/splitio.d.ts index eaa490f3..ebeba4df 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -91,6 +91,10 @@ interface ISharedSettings { * Do not change these settings unless you're working an advanced use case, like connecting to the Split proxy. */ urls?: SplitIO.UrlSettings; + /** + * Custom logger object. If not provided, the SDK will use the default `console.log` method for all log levels. + */ + logger?: SplitIO.Logger; } /** * Common settings properties for SDKs with synchronous API (standalone and localhost modes). @@ -141,6 +145,8 @@ interface IPluggableSharedSettings { * config.debug = ErrorLogger() * ``` * + * When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true), delegating log level control to the custom logger. + * * @defaultValue `false` */ debug?: boolean | SplitIO.LogLevel | SplitIO.ILogger; @@ -164,6 +170,8 @@ interface INonPluggableSharedSettings { * config.debug = 'WARN' * ``` * + * When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true), delegating log level control to the custom logger. + * * @defaultValue `false` */ debug?: boolean | SplitIO.LogLevel; @@ -587,6 +595,7 @@ declare namespace SplitIO { telemetry: string; }; readonly integrations?: IntegrationFactory[]; + readonly logger?: Logger; readonly debug: boolean | LogLevel | ILogger; readonly version: string; /** @@ -617,6 +626,15 @@ declare namespace SplitIO { * Log levels. */ type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'NONE'; + /** + * Custom logger interface. + */ + interface Logger { + debug(message: string): any; + info(message: string): any; + warn(message: string): any; + error(message: string): any; + } /** * Logger API */ @@ -631,8 +649,16 @@ declare namespace SplitIO { disable(): void; /** * Sets a log level for the SDK logs. + * + * @param logLevel - The log level to set. */ setLogLevel(logLevel: LogLevel): void; + /** + * Sets a custom logger for the SDK logs. + * + * @param logger - The custom logger to set, or `undefined` to remove the custom logger and fall back to the default `console.log` method. + */ + setLogger(logger?: Logger): void; /** * Log level constants. Use this to pass them to setLogLevel function. */