import globalLog, {type Logger, markSensitive as _markSensitive} from '@appium/logger'; import type {AppiumLogger, AppiumLoggerContext, AppiumLoggerLevel, AppiumLoggerPrefix} from '@appium/types'; export const LEVELS: readonly AppiumLoggerLevel[] = ['silly', 'verbose', 'debug', 'info', 'http', 'warn', 'error']; const MAX_LOG_RECORDS_COUNT = 3000; interface GlobalWithNpmlog { _global_npmlog?: Logger; } const globalWithNpmlog = globalThis as typeof globalThis & GlobalWithNpmlog; const noop = () => {}; // mock log object is used in testing mode to silence the output const MOCK_LOG = { unwrap: () => ({ loadSecureValuesPreprocessingRules: () => Promise.resolve({issues: [], rules: []}), level: 'verbose', prefix: '', log: noop, }), ...(Object.fromEntries(LEVELS.map((l) => [l, noop])) as Record void>), } as unknown as Logger; export const log = getLogger(); /** * @param prefix - Optional log prefix * @returns A wrapped Appium logger instance */ export function getLogger(prefix: AppiumLoggerPrefix | null = null): AppiumLogger { const {logger, defaultToVerbose} = _getLogger(); const wrappedLogger = { unwrap: () => logger, levels: [...LEVELS], prefix: prefix ?? undefined, errorWithException(...args: any[]) { this.error(...args); return args[0] instanceof Error ? args[0] : new Error(args.join('\n')); }, errorAndThrow(...args: any[]) { throw this.errorWithException(...args); }, updateAsyncContext(contextInfo: AppiumLoggerContext, replace = false) { this.unwrap().updateAsyncStorage?.(contextInfo, replace); }, } as AppiumLogger; Object.defineProperty(wrappedLogger, 'level', { get() { return logger.level; }, set(newValue: string) { logger.level = newValue; }, enumerable: true, configurable: true, }); const isDebugTimestampLoggingEnabled = process.env._LOG_TIMESTAMP === '1'; for (const level of LEVELS) { wrappedLogger[level] = function (this: typeof wrappedLogger, ...args: any[]) { const finalPrefix = getFinalPrefix(this.prefix, isDebugTimestampLoggingEnabled); if (args.length) { (logger as Record void>)[level](finalPrefix, ...args); } else { (logger as Record void>)[level](finalPrefix, ''); } }; } // Default to verbose when the global was not already set (first use, or standalone); // main server will override later. if (defaultToVerbose) { wrappedLogger.level = 'verbose'; } return wrappedLogger; } /** * Marks arbitrary log message as sensitive. * This message will then be replaced with the default replacer * while being logged by any `info`, `debug`, etc. methods if the * asyncStorage has `isSensitive` flag enabled in its async context. * The latter is enabled by the corresponding HTTP middleware * in response to the `X-Appium-Is-Sensitive` request header * being set to 'true'. */ export function markSensitive(logMessage: T): {[k: string]: T} { return _markSensitive(logMessage); } function _getLogger(): {logger: Logger; defaultToVerbose: boolean} { const testingMode = process.env._TESTING === '1'; const forceLogMode = process.env._FORCE_LOGS === '1'; const defaultToVerbose = !globalWithNpmlog._global_npmlog; const logger: Logger = testingMode && !forceLogMode ? MOCK_LOG : (globalWithNpmlog._global_npmlog ?? globalLog); if (!testingMode && !globalWithNpmlog._global_npmlog && logger === globalLog) { globalWithNpmlog._global_npmlog = globalLog; logger.maxRecordSize = MAX_LOG_RECORDS_COUNT; } return {logger, defaultToVerbose}; } function getFinalPrefix(prefix: AppiumLoggerPrefix | null | undefined, shouldLogTimestamp = false): string { const result = (typeof prefix === 'function' ? prefix() : prefix) ?? ''; if (!shouldLogTimestamp) { return result; } const now = new Date(); const pad = (n: number, z = 2) => String(n).padStart(z, '0'); const formattedTimestamp = `[${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}:${pad( now.getMilliseconds(), 3, )}]`; return result ? `${formattedTimestamp} ${result}` : formattedTimestamp; } export default log;