/** * Simple, opinionated Logger for use with Node.js and Web Browsers with no * additional dependencies. * * The logger supports and auto-detects the following environments: * - Node.js (with and without JSDOM) * = Browser * - Electron * - React Native - Logs to console or browser window depending on whether a debugger is connected * - Unit Test & CI/CD - Logs are disabled by default * * The default log level is `INFO` for Node.js production environments and `DEBUG` for * anything else. A different default level can be specified using an environment * variable `LOG_LEVEL` (for Node.js like environments) or by setting `window.LOG_LEVEL` * (for Browser like environments). * * The logger will automatically detect if the used terminal supports colored output. * To disable colored terminal output set `FORCE_COLOR` environment variable to `0` or * `false`. To enforce colors regardless of any detected support, set `FORCE_COLOR` to * `1` or `true`. * * @example * import { Logger } from "./logger"; * * const logger = Logger.create(import.meta.url); * * function myFunc() { * try { * logger.info("Hello!"); * logger.info("With payload:", { foo: "bar" }); * // ... * } * catch (error) { * logger.error("Oh no!", { error }); * } * } */ /* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment */ // biome-ignore lint/suspicious/noExplicitAny: Avoid TypeScript errors when trying to access globals not available in our target environment type _any = any; const _global: _any = typeof global !== 'undefined' ? global : typeof globalThis !== 'undefined' ? globalThis : {}; const _console = typeof console !== 'undefined' ? console : undefined; const _process = typeof process !== 'undefined' ? process : undefined; const _window = _global.window; const _document = _global.document; const _navigator = _global.navigator; const _noop = () => {}; /** * ANSI color codes for terminal output */ const colors = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m', // non-standard feature underline: '\x1b[4m', blink: '\x1b[5m', reverse: '\x1b[7m', hidden: '\x1b[8m', fg: { black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', crimson: '\x1b[38m', }, bg: { black: '\x1b[40m', red: '\x1b[41m', green: '\x1b[42m', yellow: '\x1b[43m', blue: '\x1b[44m', magenta: '\x1b[45m', cyan: '\x1b[46m', white: '\x1b[47m', crimson: '\x1b[48m', }, }; /** * Supported log levels. Use `NONE` to disable logging completely. By default * only `DEBUG` and above are logged. To set a different default level set * the `LOG_LEVEL` environment variable or explicitly set the * {@see Logger.defaultLevel} property. Set to `NONE` to fully disable logging. */ export enum LogLevels { /** Lowest debug level; This will not be logged unless explicitly enabled */ SILLY = 0, /** Debug messages for development environments */ DEBUG = 1, /** Info messages will be logged in production environments by default */ INFO = 2, /** Warning about a potential issue or an expected error occured */ WARN = 3, /** An unexpected error occurred */ ERROR = 4, /** Critical application error that cannot be recovered */ CRITICAL = 5, /** Disable logging */ NONE = 99, } /** Names of the log levels used for display */ export const LogLevelNames: { [key in LogLevels]: string } = { [LogLevels.SILLY]: 'SILLY', [LogLevels.DEBUG]: 'DEBUG', [LogLevels.INFO]: 'INFO', [LogLevels.WARN]: 'WARN', [LogLevels.ERROR]: 'ERROR', [LogLevels.CRITICAL]: 'CRITICAL', [LogLevels.NONE]: 'NONE', }; /** * Static mapping of error levels to `console.*` functions. * While technically this can used to customize the log output to use a different * function such as `process.stdout.write`, it is not recommended. Instead create * a new log handler function and register it using {@see Logger.registerHandler}. */ export const ConsoleLogFuncs: { [key in LogLevels]: (...args: _any[]) => void; } = { [LogLevels.SILLY]: _console?.debug.bind(_console) ?? _noop, [LogLevels.DEBUG]: _console?.debug.bind(_console) ?? _noop, [LogLevels.INFO]: _console?.info.bind(_console) ?? _noop, [LogLevels.WARN]: _console?.warn.bind(_console) ?? _noop, [LogLevels.ERROR]: _console?.error.bind(_console) ?? _noop, [LogLevels.CRITICAL]: _console?.error.bind(_console) ?? _noop, [LogLevels.NONE]: () => { /* intentionally left empty */ }, }; /** Any kind of payload data, a plain old JavaScript object */ export type Payload = { [key: string]: _any; error?: Error | unknown }; /** A single log entry */ export interface LogEntry { timestamp: Date; level: LogLevels; message: string; payload?: Payload; meta?: Payload; } /** A custom log handler */ export type LogHandlerFunc = (logEntry: LogEntry) => void; /** * Logger */ export class Logger { /** * Fields that will automatically be scrubbed from logs. Field names will automatically transformed to lower case * and special characters stripped before matching the string, so e.g. "access_token", "access-token" and * "accessToken" will all match "accesstoken". * * To disable scrubbing, set this to an empty array */ static scrubValues = [ 'password', 'newpassword', 'oldpassword', 'secret', 'passwd', 'apikey', 'token', 'accesstoken', 'refreshtoken', 'idtoken', 'authtoken', 'privatekey', 'clientsecret', 'creds', 'credentials', 'mysqlpwd', 'stripetoken', 'cardnumber', ]; /** A list of class names, primarily from network libraries, that will be collapsed in the logs in order to keep it shorter and more readable */ static collapseClasses = [ 'ClientRequest', 'IncomingMessage', 'Buffer', 'TLSSocket', 'Socket', 'WebSocket', 'WebSocketTransport', 'ReadableState', 'WritableState', 'HttpsAgent', 'HttpAgent', 'CDPSession', // Puppeteer ]; /** Regular expression that can be used to check for possible credit card numbers when scrubbing fields */ private static _creditCardRegEx = /^(?:4[0-9]{12}(?:[0-9]{3})?|[25][1-7][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/; /** Metadata shared between all instances of the Logger */ private static _globalMetaData?: Payload; /** List of log handler functions */ private static _handlers: LogHandlerFunc[] = []; /** * Instantiates a new logger * * @param tag - Logger tag, e.g. the name of the current JavaScript file. * For convenience, paths such as `__filename` and `import.meta.url` * will automatically be truncated to the file name only. * @param meta - Logger meta, e.g. the if of the vessel currently logging * * @example * const logger = Logger.create(__filename); * * @example * const logger = Logger.create(import.meta.url); * */ static create(tag?: string, meta?: Payload): Logger { return new Logger(tag, meta); } /** Registers a new log handler */ static registerHandler(handlerFunc: LogHandlerFunc) { Logger._handlers.push(handlerFunc); } /** Removes a log handler */ static unregisterHandler(handlerFunc: LogHandlerFunc) { Logger._handlers = Logger._handlers.filter((func) => func === handlerFunc); } /** Removes all log handlers */ static clearHandlers() { Logger._handlers = []; } /** * Returns `true` if code is running with JSDOM */ static get isJSDOM(): boolean { return ( _navigator?.userAgent?.includes('Node.js') || _navigator?.userAgent?.includes('jsdom') ); } /** * Returns `true` if code is running in a web browser environment */ static get isWebWorker(): boolean { return ( typeof _global.WorkerGlobalScope !== 'undefined' && _global.self instanceof _global.WorkerGlobalScope ); } /** * Returns `true` if code is running in a web browser environment */ static get isBrowser(): boolean { return ( (typeof _window !== 'undefined' || Logger.isWebWorker) && !Logger.isJSDOM ); } /** * Returns `true` if code is running in a web browser environment */ static get isIE(): boolean { return Logger.isBrowser && _document?.documentMode && _window?.StyleMedia; } /** * Returns `true` if code is running in React Native */ static get isReactNative(): boolean { return Logger.isBrowser && _navigator?.product === 'ReactNative'; } /** * Returns `true` if code is running in React Native and the Debugger is attached */ static get isReactNativeDebugger(): boolean { return ( Logger.isReactNative && typeof _global.DedicatedWorkerGlobalScope !== 'undefined' ); } /** * Returns `true` if code is running in a Node.js-like environment (including Electron) */ static get isNode(): boolean { // Note that Webpack et al are often adding process, module, require, etc. return ( typeof _process === 'object' && typeof _process?.env === 'object' && !Logger.isBrowser ); } /** Returns `true` if code is running in Electron */ static get isElectron(): boolean { const userAgent = _navigator?.userAgent?.toLowerCase(); if (userAgent.includes(' electron/')) { // Test for Electron in case no Node.js environment is loaded return true; } return Logger.isNode && typeof _process?.versions?.electron !== 'undefined'; } /** Returns `true` if running in a CI environment, e.g. during an automated build */ static get isCI(): boolean { if (!Logger.isNode || !_process?.env) { return false; } const { CI, CONTINUOUS_INTEGRATION, BUILD_NUMBER, RUN_ID } = _process.env; // Shamelessly stolen from https://github.com/watson/ci-info return !!( CI || // Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari CONTINUOUS_INTEGRATION || // Travis CI, Cirrus CI BUILD_NUMBER || // Jenkins, TeamCity RUN_ID || // TaskCluster, dsari false ); } /** Retruns `true` if running as part of a unit test */ static get isUnitTest(): boolean { if (_window?.__karma__ !== undefined) { return true; } if (Logger.isNode && _process?.env) { const { JEST_WORKER_ID } = _process.env; return !!(JEST_WORKER_ID || typeof _global.it === 'function'); } return false; } /** * Retruns `true` if `--silent` is passed on command line, e.g. when executed via an `npm` script */ static get isSilent(): boolean { if (!Logger.isNode) { return false; } return _process?.argv?.includes('--silent') ?? false; } /** Checking if console exists */ static get hasConsole(): boolean { return !!( _console && 'log' in _console && 'debug' in _console && 'info' in _console && 'warn' in _console && 'error' in _console ); } /** Enable/disable all logging */ static enabled: boolean = !Logger.isCI && !Logger.isSilent && !Logger.isUnitTest && Logger.hasConsole; /** The default log level if no other option is specified */ static defaultLevel: LogLevels = Logger._parseLogLevel(_process?.env?.LOG_LEVEL) ?? Logger._parseLogLevel(_global.LOG_LEVEL) ?? Logger._parseLogLevel(_window?.LOG_LEVEL) ?? (_process?.env?.NODE_ENV === 'production' ? LogLevels.INFO : LogLevels.DEBUG); /** * Set Global Meta Data for Logger. The global meta data will only be included in * Loggers that are instantiated _after_ the meta data was set using this function. * Therefore it is recommended to put all Logger initialization into a `bootstrap.ts` * file and import this at the very beginning of your app or project, making sure * its code is executed before anything else. * * @param meta meta data */ static setGlobalMeta(meta: Payload): void { Logger._globalMetaData = { ...meta }; } /** * Like `JSON.stringify` but handles circular references and serializes error objects. */ static stringify(value: _any, space?: string | number | undefined): string { return JSON.stringify(value, Logger._serializeObj, space); } /** * Returns the URL or file name of the current module. This is used to automatically * set the logger tag to the file name or URL of the module that is using the logger. */ private static getMetaUrl(): string { if (typeof __filename !== 'undefined') { // CommonJS environment (Node.js with CommonJS or transpiled with CommonJS output) return __filename; } if ( // @ts-ignore typeof import.meta !== 'undefined' && // @ts-ignore typeof import.meta.url === 'string' ) { // ES module environment // @ts-ignore return import.meta.url; } return ''; } private static getCwd(): string { if (typeof _process?.cwd === 'function') { return _process?.cwd(); } if (typeof _window?.location?.href === 'string') { return _window.location.href; } return ''; } private static stripCommonPath(basePath: string, targetPath: string): string { // Remove trailing slashes and split paths into segments const baseSegments = basePath .replace(/^\w+:\/\//, '') .replace(/\/+$/, '') .split('/'); const targetSegments = targetPath .replace(/^\w+:\/\//, '') .replace(/\/+$/, '') .split('/'); // Find the index where paths diverge let divergeIndex = 0; while ( divergeIndex < baseSegments.length && divergeIndex < targetSegments.length && baseSegments[divergeIndex] === targetSegments[divergeIndex] ) { divergeIndex++; } // Return the part of the target path that diverges from the base path return targetSegments.slice(divergeIndex).join('/') || '.'; } /** * Destroys circular references for use with JSON serialization * * @param from - Source object or array * @param seen - Array with object already serialized. Set to `[]` (empty array) * when using this function! * @param scrub - If set, passwords and other sensitive data fields will be replaced by a placeholder and hidden from console or log files */ private static _destroyCircular(from: _any, seen: _any[], scrub = false) { let to: _any; if (Array.isArray(from)) { to = []; } else { to = {}; } seen.push(from); for (const key in from) { if (Object.hasOwn(from, key)) { const value = from[key]; if ( typeof value === 'string' && (Logger.scrubValues.includes( key.toLowerCase().replace(/[^a-z0-9]/g, ''), ) || Logger._creditCardRegEx.test(value) || value.startsWith('Bearer ')) ) { // Looks like a sensitive value. Hide it from the logging endpoint to[key] = '[hidden]'; continue; } if (typeof value === 'function') { // No Logging of functions continue; } if (typeof value === 'symbol') { // Use the symbol's name to[key] = value.toString(); continue; } if (!value || typeof value !== 'object') { // Simple data types to[key] = value; continue; } if (typeof value === 'object' && typeof value.toJSON === 'function') { to[key] = value.toJSON(); continue; } if (typeof value === 'object' && value.constructor) { // Superagent/Axios includes a lot of detail information in the error object. // For the sake of readable logs, we remove all of that garbage here. const className = value.constructor.name; if (Logger.collapseClasses.includes(className)) { to[key] = `[${className}]`; continue; } } if (!seen.includes(from[key])) { to[key] = Logger._destroyCircular(from[key], seen.slice(0), scrub); continue; } to[key] = '[Circular]'; } } if (typeof from.name === 'string') { to.name = from.name; } if (typeof from.message === 'string') { to.message = from.message; } if (typeof from.stack === 'string') { to.stack = from.stack; } return to; } /** * Helper function to serialize class instances to plain objects for logging * * @param key - Property key * @param value - Property value * * @example * const myObj = { foo: "bar" }; * const str = JSON.stringify(myObj, serializeObj, "\t"); */ private static _serializeObj(key: _any, value: _any): _any { if (typeof value === 'object' && value !== null) { return Logger._destroyCircular(value, []); } return value; } private static _parseLogLevel( value?: string | number | null, ): LogLevels | undefined { const logLevelMap = { S: LogLevels.SILLY, D: LogLevels.DEBUG, I: LogLevels.INFO, W: LogLevels.WARN, E: LogLevels.ERROR, C: LogLevels.CRITICAL, N: LogLevels.NONE, }; if (typeof value === 'string') { const firstChar = value?.substring(0, 1).toUpperCase(); const entry = Object.entries(logLevelMap).find( ([key]) => firstChar === key, ); return entry?.[1]; } if (typeof value === 'number') { return (value >= LogLevels.SILLY && value <= LogLevels.CRITICAL) || value === LogLevels.NONE ? value : undefined; } return undefined; } /** Metadata passed with each log line */ meta: Payload; /** Current log level; if `undefined` the default level is used */ logLevel?: LogLevels; private constructor(tag?: string, meta?: Payload, logLevel?: LogLevels) { if (logLevel) { this.logLevel = logLevel; } else if (Logger.isNode) { this.logLevel = Logger._parseLogLevel(_process?.env?.LOG_LEVEL); } let sanitizedTag = tag ?? 'default'; if (/\.(?:tsx?|jsx?)(?:\?.*)?$/i.test(sanitizedTag)) { // Strip off unnecessary path information from the logger tag (e.g. if using __filename as tag) const cwd = Logger.getCwd(); sanitizedTag = Logger.stripCommonPath(cwd, sanitizedTag); // Some final cleanup: Remove typical build folders and file extensions sanitizedTag = sanitizedTag.replace( /^(?:[/\\]?(?:src|dist|build|public)[/\\])?[/\\]?(.*?)\.(?:tsx?|jsx?|mjs|cjs)$/, '$1', ); } this.meta = { ...Logger._globalMetaData, ...meta, tag: sanitizedTag, }; } silly(message: string, payload?: Payload): Logger { return this._logInternal(LogLevels.SILLY, message, payload); } trace(message: string, payload?: Payload): Logger { return this._logInternal(LogLevels.SILLY, message, payload); } debug(message: string, payload?: Payload): Logger { return this._logInternal(LogLevels.DEBUG, message, payload); } info(message: string, payload?: Payload): Logger { return this._logInternal(LogLevels.INFO, message, payload); } warn(message: string, payload?: Payload): Logger { return this._logInternal(LogLevels.WARN, message, payload); } error(message: string, payload?: Payload): Logger; error(message: string, error?: Error): Logger; error(error: Error): Logger; error(...args: [string, (Error | Payload)?] | [Error]): Logger { return this._logExceptionInternal(LogLevels.ERROR, ...args); } critical(message: string, payload?: Payload): Logger; critical(message: string, error?: Error): Logger; critical(error: Error): Logger; critical(...args: [string, (Error | Payload)?] | [Error]): Logger { return this._logExceptionInternal(LogLevels.CRITICAL, ...args); } private _dispatchToHandlers(logEntry: LogEntry) { for (const handler of Logger._handlers) { handler(logEntry); } } /** * Write message to logs * @param level * @param logFunc * @param message * @param payloadRaw */ private _logInternal( level: LogLevels, message: string, payloadRaw?: Payload, ): Logger { const logLevel = this.logLevel ?? Logger._parseLogLevel(_global?.LOG_LEVEL) ?? Logger._parseLogLevel(_window?.LOG_LEVEL) ?? Logger.defaultLevel; if (!Logger.enabled || level < logLevel) { return this; } const hasPayload = typeof payloadRaw !== 'undefined' && payloadRaw !== null; let payload: _any = null; try { // The Browser logger seems to have issues with circular references if no // debugger is attached. So we break all circular references here to be safe. payload = hasPayload ? Logger._destroyCircular(payloadRaw, [], true) : undefined; } catch (error) { payload = `[${(error as Error).message}]`; } this._dispatchToHandlers({ timestamp: new Date(), level, message, payload, meta: this.meta, }); return this; } /** * Special handling for error objects * * @param level * @param logFunc * @param messageRaw * @param payloadRaw */ private _logExceptionInternal( level: LogLevels, ...args: [string, (Error | Payload)?] | [Error] ): Logger { const logLevel = this.logLevel ?? Logger._parseLogLevel(_global.LOG_LEVEL) ?? Logger._parseLogLevel(_window?.LOG_LEVEL) ?? Logger.defaultLevel; if (!Logger.enabled || level < logLevel) { return this; } let message = ''; let payload: Payload | undefined; if (typeof args[0] === 'string' || args[0] instanceof String) { message = args.splice(0, 1)[0] as string; } if (args[0] instanceof Error) { const error = args[0]; if (message.length === 0) { message = error.message; } payload = { error }; } else if (typeof args[0] === 'object') { payload = { ...(args[0] as object) }; } return this._logInternal(level, message, payload); } } /** Log output to interactive terminal in a human readable format and in colors (if supported) */ export function createTTYLogger(forceColor = false): LogHandlerFunc { // Colors see https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color const logLevelColors = { [LogLevels.SILLY]: `${colors.underline}${colors.dim}${colors.fg.white}`, [LogLevels.DEBUG]: `${colors.underline}${colors.dim}${colors.fg.white}`, [LogLevels.INFO]: `${colors.underline}${colors.bold}${colors.fg.white}`, [LogLevels.WARN]: `${colors.underline}${colors.fg.yellow}`, [LogLevels.ERROR]: `${colors.underline}${colors.fg.red}`, [LogLevels.CRITICAL]: `${colors.bg.red}${colors.fg.black}`, [LogLevels.NONE]: '', }; const supportsColor = (): boolean => { if (forceColor) { // Colors enforced in any case return true; } if (Logger.isReactNative) { // In React Native we have no process.env but we most likely support colors in Expo console return true; } if (!Logger.isNode) { // No colors if we aren't running in Node (as then we don't have `process` available) return false; } if (!_process?.stdout?.isTTY) { // No colors if we have no interactive terminal return false; } if (_process?.env?.TERM === 'dumb') { // No colors if we are running in a dumb terminal return false; } if (_process?.platform === 'win32') { // A reasonably new Windows version should support colors return true; } if (Logger.isCI) { // Most CI/CD platforms support colors as well return true; } if (_process?.env?.TERM === 'xterm-kitty') { // Kitty terminal supports colors return true; } if (_process?.env?.TERM_PROGRAM) { const version = Number.parseInt( (_process?.env?.TERM_PROGRAM_VERSION || '').split('.')[0], 10, ); switch (_process?.env?.TERM_PROGRAM) { case 'iTerm.app': { return true; } case 'Apple_Terminal': { return true; } // No default } } if (/-256(color)?$/i.test(_process?.env?.TERM ?? '')) { return true; } if ( /^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test( _process?.env?.TERM ?? '', ) ) { return true; } if (_process?.env?.COLORTERM) { return true; } if (_process?.env?.COLOR === '1') { return true; } return false; }; const enableColors = supportsColor(); // The payload is already sanitized so we can simply print it using `utils.inspect()` or `JSON.strigify()`: let stringify: (value: Payload) => string; try { // eslint-disable-next-line @typescript-eslint/no-var-requires const util = require('node:util'); stringify = (value) => typeof util.inspect === 'function' ? util.inspect(value, false, null, enableColors) : JSON.stringify(value, null, 2); } catch (error) { stringify = (value) => JSON.stringify(value, null, 2); } return function log({ level, message, payload, meta }: LogEntry) { // Try to load `util` package. React Native requires you to install `util` manually using `npm i util --save`. const logFunc = ConsoleLogFuncs[level]; if (enableColors) { const log = [ `${colors.fg.magenta}[${meta?.tag}]`, `${logLevelColors[level]}${LogLevelNames[level]}${colors.reset}`, `${colors.fg.white}${message}${colors.reset}`, ].join(' '); if (payload) { logFunc( log, // Dim the output of the payload for better overall readability, stringify(payload) .split('\n') .map((line) => `\n${colors.dim}${line}${colors.reset}`) // dim each line .join(''), ); } else { logFunc(log); } } else { const log = [`[${meta?.tag}]`, LogLevelNames[level], message].join(' '); if (payload) { logFunc(log, `\n${stringify(payload)}`); } else { logFunc(log); } } }; } /** Log to Browser Console; Use colorful logs for Chrome, Safari, etc. */ export function createBrowserLogger(): LogHandlerFunc { return function log({ level, message, payload, meta }: LogEntry) { const logFunc = ConsoleLogFuncs[level]; const darkMode = !!_window?.matchMedia?.('(prefers-color-scheme: dark)') .matches; if ( payload && _console && typeof _console.group === 'function' && typeof _console.groupCollapsed === 'function' ) { // Automatically expand warnings and errors const groupFunc = level >= LogLevels.WARN ? _console.group : _console.groupCollapsed; groupFunc.bind(_console)( `%c[${meta?.tag}]%c %c${message}`, 'color:magenta;', '', // reset color darkMode ? 'color:white;font-weight:normal' : 'color:black;font-weight:normal', ); logFunc(payload); _console.groupEnd(); } else if (payload) { logFunc( `%c[${meta?.tag}] %c${message}`, 'color:magenta;', darkMode ? 'color:white' : 'color:black;', payload, ); } else { logFunc( `%c[${meta?.tag}] %c${message}`, 'color:magenta;', darkMode ? 'color:white' : 'color:black;', ); } }; } export function createJsonConsoleLogger() { return function log(logEntry: LogEntry) { const logFunc = ConsoleLogFuncs[logEntry.level]; logFunc( JSON.stringify({ ...logEntry, timestamp: logEntry.timestamp.toISOString(), level: LogLevelNames[logEntry.level], }), ); // no need to break circular references anymore here }; } /** Automatically chooses the "best" handler depending on whether we're running on a terminal, in a browser or in any other supported environment */ export function createDefaultLogger(forceColor?: boolean): LogHandlerFunc { const logTTY = createTTYLogger(forceColor); const logBrowser = createBrowserLogger(); const logJson = createJsonConsoleLogger(); return function log(logEntry: LogEntry) { if ( Logger.isBrowser && !Logger.isIE && (!Logger.isReactNative || Logger.isReactNativeDebugger) ) { return logBrowser(logEntry); } if (Logger.isBrowser || _process?.stdout?.isTTY || forceColor) { return logTTY(logEntry); } return logJson(logEntry); }; } // Check if FORCE_COLOR environment variable is set const forceColorEnv = ['true', 'on', 'yes', '1'].includes( String(_process?.env?.FORCE_COLOR).toLowerCase(), ); Logger.registerHandler(createDefaultLogger(forceColorEnv));