// eslint-disable-next-line prefer-const let selfLog: Logger | undefined export type Level = 1 | 2 | 3 | 4 | 5 | 6 export interface Logger { trace(...args: Array): void debug(...args: Array): void info(...args: Array): void warn(...args: Array): void error(...args: Array): void fatal(...args: Array): void logAtLevel(level: Level, ...args: Array): void levelEnabled(level: Level): boolean inputLogProvider: LogProvider } export type LogProvider = ( loggerPath: string, level: Level, ...args: Array ) => void export type LogFunctionProvider = (level: Level) => (...args: any[]) => void export const LOG_LEVEL_TRACE = 1 export const LOG_LEVEL_DEBUG = 2 export const LOG_LEVEL_INFO = 3 export const LOG_LEVEL_WARN = 4 export const LOG_LEVEL_ERROR = 5 export const LOG_LEVEL_FATAL = 6 // Keep these sorted from most severe to least severe, since logic below // depends on that ordering. const LOG_LEVELS_MAX_TO_MIN: Level[] = [ LOG_LEVEL_FATAL, LOG_LEVEL_ERROR, LOG_LEVEL_WARN, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG, LOG_LEVEL_TRACE, ] const LOG_LEVELS_MIN_TO_MAX = [...LOG_LEVELS_MAX_TO_MIN].reverse() const DEFAULT_LOG_LEVEL = 'DEFAULT_LOG_LEVEL' const TRACE = 'TRACE' const DEBUG = 'DEBUG' const INFO = 'INFO' const WARN = 'WARN' const ERROR = 'ERROR' const FATAL = 'FATAL' const LOG_NO_DATE = 'LOG_NO_DATE' const ALL_ENV_VARS = new Set([ DEFAULT_LOG_LEVEL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, LOG_NO_DATE, ]) export const logLevelToName: { 1: 'TRACE' 2: 'DEBUG' 3: 'INFO' 4: 'WARN' 5: 'ERROR' 6: 'FATAL' } = { [LOG_LEVEL_TRACE]: TRACE, [LOG_LEVEL_DEBUG]: DEBUG, [LOG_LEVEL_INFO]: INFO, [LOG_LEVEL_WARN]: WARN, [LOG_LEVEL_ERROR]: ERROR, [LOG_LEVEL_FATAL]: FATAL, } function assertValidLogLevel(level: Level): void { switch (level) { case LOG_LEVEL_TRACE: case LOG_LEVEL_DEBUG: case LOG_LEVEL_INFO: case LOG_LEVEL_WARN: case LOG_LEVEL_ERROR: case LOG_LEVEL_FATAL: return default: throw new Error(`invalid log level: ${level}`) } } /** log levels explicitly configured by the user. Highest priority. */ let configuredLogLevels: { [path in string]?: Level } = {} /** log level patterns configured by the user. Highest priority. */ let configuredPatterns = { byPattern: new Map(), byLevel: new Map>(), regexes: new Map(), } /** log levels configured by environment variables. Lowest priority. */ let envLogLevels: { [path in string]?: Level } = {} /** log level patterns configured by environment variables. Lowest priority. */ let envLogLevelPatterns = new Map() /** calculated levels based on configuredLogLevels and envLogLevels */ let logLevelsCache: { [path in string]?: Level } = {} const logLevelAtPath = (path: string): Level | undefined => configuredLogLevels[path] || envLogLevels[path] const envVar = (varName: string): string | undefined => // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition typeof process !== 'undefined' && process.env ? process.env[varName] : undefined const calcHasDate = (): boolean => !parseInt(envVar(LOG_NO_DATE) || '') let hasDate = calcHasDate() function normalizePath(path: string) { return path.replace(/[:/]/g, '.') } function makeRegExp(patterns: string[]) { return new RegExp( `^(${patterns.map((pattern) => pattern.replace(/[/\-\\^$+?.()|[\]{}]/g, '\\$&').replace(/\*/g, '.*')).join('|')})$` ) } let calcedEnvLogLevels = false function calcEnvLogLevels(): void { if (calcedEnvLogLevels) return // walk log levels from least to most verbose, so that the most verbose setting wins if // the user sets DEBUG=foo and TRACE=foo, foo will be set to TRACE for (const logLevel of LOG_LEVELS_MAX_TO_MIN) { const envForLevel = envVar((logLevelToName as any)[logLevel]) const patterns: string[] = [] if (envForLevel && typeof envForLevel === 'string') { const targetsForLevel = envForLevel.split(',').filter(Boolean) targetsForLevel.forEach((target: string) => { if (target.includes('*')) { patterns.push(target) } else { envLogLevels[normalizePath(target)] = logLevel } }) } if (patterns.length) { envLogLevelPatterns.set(logLevel, makeRegExp(patterns)) } } calcedEnvLogLevels = true if (selfLog?.levelEnabled(LOG_LEVEL_TRACE)) { selfLog.trace('calcEnvLogLevels():') for (const key of Object.keys(envLogLevels).sort()) { if (envLogLevels[key] == null) continue selfLog.trace( ` ${JSON.stringify(key)}: ${logLevelToName[envLogLevels[key]]}` ) } for (const [logLevel, pattern] of envLogLevelPatterns.entries()) { selfLog.trace(` ${logLevelToName[logLevel]}: ${pattern}`) } } } function calcDefaultLogLevel(): Level { const envDefaultLogLevel = envVar(DEFAULT_LOG_LEVEL) if (envDefaultLogLevel) { for (const logLevel of LOG_LEVELS_MAX_TO_MIN) { if (envDefaultLogLevel === (logLevelToName as any)[logLevel]) { return logLevel } } } return LOG_LEVEL_INFO } let defaultLogLevel = calcDefaultLogLevel() export function envVarChanged( varName: string | undefined = undefined, newValue: string | null | undefined = undefined ): void { selfLog?.trace( () => `envVarChanged(${JSON.stringify(varName)}, ${JSON.stringify(newValue)})` ) if (!varName || ALL_ENV_VARS.has(varName)) { calcedEnvLogLevels = false envLogLevels = {} envLogLevelPatterns = new Map() logLevelsCache = {} hasDate = calcHasDate() defaultLogLevel = calcDefaultLogLevel() } } export function resetLogLevels(): void { selfLog?.trace('resetLogLevels()') logLevelsCache = {} calcedEnvLogLevels = false envLogLevels = {} envLogLevelPatterns = new Map() configuredLogLevels = {} configuredPatterns = { byPattern: new Map(), byLevel: new Map>(), regexes: new Map(), } } export function setLogLevel(path: string, level: Level): void { selfLog?.trace( () => `setLogLevel(${JSON.stringify(path)}, ${level} (${logLevelToName[level]}))` ) assertValidLogLevel(level) if (path.includes('*')) { const oldLevel = configuredPatterns.byPattern.get(path) if (oldLevel !== level) { const oldLevelPatterns = oldLevel != null ? configuredPatterns.byLevel.get(oldLevel) : undefined if (oldLevel != null && oldLevel !== level && oldLevelPatterns) { oldLevelPatterns.delete(path) if (oldLevelPatterns.size) { configuredPatterns.regexes.set( oldLevel, makeRegExp([...oldLevelPatterns]) ) } else { configuredPatterns.byLevel.delete(oldLevel) configuredPatterns.regexes.delete(oldLevel) } } configuredPatterns.byPattern.set(path, level) let levelPatterns = configuredPatterns.byLevel.get(level) if (!levelPatterns) configuredPatterns.byLevel.set(level, (levelPatterns = new Set())) levelPatterns.add(path) configuredPatterns.regexes.set(level, makeRegExp([...levelPatterns])) // Bust the cache logLevelsCache = {} } } else { const normalized = normalizePath(path) if (level !== configuredLogLevels[normalized]) { configuredLogLevels[normalized] = level // Bust the cache logLevelsCache = {} } } } function calcLogLevel(path: string): Level { calcEnvLogLevels() const log = path === 'log4jcore' ? undefined : selfLog const patternLevel = LOG_LEVELS_MIN_TO_MAX.find( (logLevel) => configuredPatterns.regexes.get(logLevel)?.test(path) || envLogLevelPatterns.get(logLevel)?.test(path) ) if (patternLevel !== LOG_LEVELS_MIN_TO_MAX[0]) { const normPath = normalizePath(path) const levelAtExactPath: Level | undefined = logLevelAtPath(normPath) if (levelAtExactPath != null) { if (patternLevel == null || levelAtExactPath <= patternLevel) { log?.trace( () => `calcLogLevel(${JSON.stringify(path)}): ${logLevelToName[levelAtExactPath]} (exact path, ${configuredLogLevels[path] ? 'configured' : 'env'})` ) return levelAtExactPath } } else { const exactPathSplit = normPath.split('.') for ( let compareLen = exactPathSplit.length - 1; compareLen >= 0; --compareLen ) { const subPath = exactPathSplit.slice(0, compareLen).join('.') const levelAtSubPath: Level | undefined = logLevelAtPath(subPath) if (levelAtSubPath != null) { if (patternLevel == null || levelAtSubPath <= patternLevel) { log?.trace( () => `calcLogLevel(${JSON.stringify(path)}): ${logLevelToName[levelAtSubPath]} (at parent path: ${JSON.stringify(subPath)}, ${configuredLogLevels[subPath] ? 'configured' : 'env'})` ) return levelAtSubPath } } } } } if (patternLevel != null) { log?.trace( () => `calcLogLevel(${JSON.stringify(path)}): ${logLevelToName[patternLevel]} (pattern, ${configuredPatterns.regexes.get(patternLevel) != null ? 'configured' : 'env'})` ) return patternLevel } log?.trace( () => `calcLogLevel(${JSON.stringify(path)}): ${logLevelToName[defaultLogLevel]} (default)` ) return defaultLogLevel } function logLevel(path: string): Level { let levelForPath: Level | undefined = logLevelsCache[path] if (levelForPath == null) { logLevelsCache[path] = levelForPath = calcLogLevel(path) } return levelForPath } export const defaultLogFunctionProvider: LogFunctionProvider = ( level: Level ) => (level >= LOG_LEVEL_ERROR ? console.error : console.log) // eslint-disable-line no-console let _logFunctionProvider: LogFunctionProvider = defaultLogFunctionProvider /** * Simple hook to override the logging function. For example, to always log to console.error, * call setLogFunctionProvider(() => console.error) * @param provider function that returns the log function based on the message's log level */ export function setLogFunctionProvider(provider: LogFunctionProvider): void { selfLog?.trace('setLogFunctionProvider(', provider, ')') _logFunctionProvider = provider } function formatDate(d: Date): string { function part(n: number, width = 2): string { return String(n).padStart(width, '0') } return `${part(d.getFullYear(), 4)}-${part(d.getMonth() + 1)}-${part( d.getDate() )} ${part(d.getHours())}:${part(d.getMinutes())}:${part( d.getSeconds() )}.${part(d.getMilliseconds(), 3)}` } function defaultLogFormat(loggerPath: string, level: Level): string { const date = hasDate ? formatDate(new Date()) + ' ' : '' return `[${date}${loggerPath}] ${(logLevelToName as any)[level]}` } export function createDefaultLogProvider( logFunc: (...args: any[]) => void ): LogProvider { return (loggerPath: string, level: Level, ...args: Array): void => { logFunc(defaultLogFormat(loggerPath, level), ...args) } } export const defaultLogProvider: LogProvider = ( loggerPath: string, level: Level, ...args: Array ) => { const logFunc: (...args: any[]) => void = _logFunctionProvider(level) logFunc(defaultLogFormat(loggerPath, level), ...args) } let _logProvider: LogProvider = defaultLogProvider /** * Hook to provide a complete replacement for the log provider. * @param provider */ export function setLogProvider(provider: LogProvider): void { selfLog?.trace('setLogProvider(', provider, ')') _logProvider = provider } const loggersByPath: { [loggerPath in string]?: Logger } = {} class LoggerImpl implements Logger { loggerPath: string _logProviders: LogProvider[] | undefined constructor({ loggerPath, logProviders }: CreateLoggerOptions) { this.loggerPath = loggerPath this._logProviders = logProviders } logAtLevel = (level: Level, ...args: Array): void => { if (level >= logLevel(this.loggerPath)) { let argsToLogger: Array = args if (args.length === 1 && typeof args[0] === 'function') { // A single function was passed. Execute that function and log the result. // This allows debug text to only be calculated when the relevant debug level is // enabled, e.g. log.trace(() => JSON.stringify(data)) const resolvedArgs = args[0]() argsToLogger = Array.isArray(resolvedArgs) ? resolvedArgs : [resolvedArgs] } for (const provider of this._logProviders || [_logProvider]) { provider(this.loggerPath, level, ...argsToLogger) } } } levelEnabled = (level: number): boolean => { return level >= logLevel(this.loggerPath) } inputLogProvider: LogProvider = ( loggerPath: string, level: Level, ...args: Array ): void => { this.logAtLevel(level, ...args) } trace = (...args: Array): void => { this.logAtLevel(LOG_LEVEL_TRACE, ...args) } debug = (...args: Array): void => { this.logAtLevel(LOG_LEVEL_DEBUG, ...args) } info = (...args: Array): void => { this.logAtLevel(LOG_LEVEL_INFO, ...args) } warn = (...args: Array): void => { this.logAtLevel(LOG_LEVEL_WARN, ...args) } error = (...args: Array): void => { this.logAtLevel(LOG_LEVEL_ERROR, ...args) } fatal = (...args: Array): void => { this.logAtLevel(LOG_LEVEL_FATAL, ...args) } } export type CreateLoggerOptions = { loggerPath: string logProviders?: LogProvider[] } export function createLogger(options: CreateLoggerOptions): Logger { return new LoggerImpl(options) } export function logger(loggerPath = ''): Logger { let logger = loggersByPath[loggerPath] if (!logger) logger = loggersByPath[loggerPath] = createLogger({ loggerPath }) return logger } selfLog = logger('log4jcore')