diff --git a/crates/bindings-typescript/src/sdk/logger.ts b/crates/bindings-typescript/src/sdk/logger.ts index 7374e8b3510..053a7b2ebd2 100644 --- a/crates/bindings-typescript/src/sdk/logger.ts +++ b/crates/bindings-typescript/src/sdk/logger.ts @@ -115,6 +115,39 @@ export const stringify = (value: unknown): string | undefined => return current; }); +/** + * Receives the SDK's log output once it has passed the global log level. + * Lazy messages and arguments are already resolved. + */ +export type Logger = ( + level: LogLevel, + message: unknown, + ...args: unknown[] +) => void; + +/** The default {@link Logger}: styled output through `console.log`. */ +export const consoleLogger: Logger = (level, message, ...args) => { + console.log( + `%c${LogLevelIdentifierIcon[level]} ${level.toUpperCase()}%c ${message}`, + LogStyle[level], + LogTextStyle[level], + ...args + ); +}; + +let globalLogger: Logger = consoleLogger; + +/** + * Replaces where the SDK's log output goes, e.g. to forward it to an app's + * own logger, or to environments where `%c` styling isn't supported (such as + * React Native). Pass {@link consoleLogger} to restore the default. + */ +export const setGlobalLogger = (logger: Logger): void => { + globalLogger = logger; +}; + +export const getGlobalLogger = (): Logger => globalLogger; + export const stdbLogger = ( level: LogLevel, message: Lazy, @@ -123,12 +156,5 @@ export const stdbLogger = ( if (!shouldLog(level)) { return; } - const resolvedMessage = resolveLazy(message); - const resolvedArgs = args.map(resolveLazy); - console.log( - `%c${LogLevelIdentifierIcon[level]} ${level.toUpperCase()}%c ${resolvedMessage}`, - LogStyle[level], - LogTextStyle[level], - ...resolvedArgs - ); + globalLogger(level, resolveLazy(message), ...args.map(resolveLazy)); }; diff --git a/crates/bindings-typescript/tests/logger.test.ts b/crates/bindings-typescript/tests/logger.test.ts index 54da6a9ce80..2d21328ce42 100644 --- a/crates/bindings-typescript/tests/logger.test.ts +++ b/crates/bindings-typescript/tests/logger.test.ts @@ -1,7 +1,10 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import { ConnectionId, Identity } from '../src'; import { + consoleLogger, + getGlobalLogger, getGlobalLogLevel, + setGlobalLogger, setGlobalLogLevel, stringify, stdbLogger, @@ -10,6 +13,7 @@ import { describe('logger', () => { beforeEach(() => { setGlobalLogLevel('debug'); + setGlobalLogger(consoleLogger); vi.restoreAllMocks(); }); @@ -54,6 +58,49 @@ describe('logger', () => { expect(getGlobalLogLevel()).toBe('info'); }); + test('setGlobalLogger receives resolved logs instead of the console', () => { + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const logger = vi.fn(); + + setGlobalLogger(logger); + stdbLogger( + 'warn', + () => 'lazy message', + () => 'lazy arg', + 42 + ); + + expect(logger).toHaveBeenCalledWith('warn', 'lazy message', 'lazy arg', 42); + expect(spy).not.toHaveBeenCalled(); + }); + + test('setGlobalLogger still respects the global log level', () => { + const logger = vi.fn(); + const lazyMessage = vi.fn(() => 'computed'); + + setGlobalLogger(logger); + setGlobalLogLevel('error'); + stdbLogger('info', lazyMessage); + + expect(logger).not.toHaveBeenCalled(); + expect(lazyMessage).not.toHaveBeenCalled(); + }); + + test('consoleLogger restores the default output', () => { + const spy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + setGlobalLogger(vi.fn()); + setGlobalLogger(consoleLogger); + stdbLogger('error', 'error message'); + + expect(getGlobalLogger()).toBe(consoleLogger); + expect(spy).toHaveBeenCalledWith( + expect.stringContaining('ERROR%c error message'), + expect.any(String), + expect.any(String) + ); + }); + test('stringify renders short Uint8Array as full hex', () => { const payload = new Uint8Array([0, 1, 15, 16, 255]); expect(stringify({ payload })).toBe('{"payload":"0x00010f10ff"}');