UNPKG

cross-log

Version:

A universal logging package that works in both browser and Node.js environments with environment variable configuration

860 lines (852 loc) 29.6 kB
/** * Core types and interfaces for the universal logger */ // Log level definitions var LogLevel; (function (LogLevel) { LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG"; LogLevel[LogLevel["INFO"] = 1] = "INFO"; LogLevel[LogLevel["WARN"] = 2] = "WARN"; LogLevel[LogLevel["ERROR"] = 3] = "ERROR"; LogLevel[LogLevel["SILENT"] = 4] = "SILENT"; // No logging })(LogLevel || (LogLevel = {})); /** * Utility functions for the universal logger */ /** * Detect the current environment */ function detectEnvironment() { const isBrowser = typeof window !== 'undefined'; const isNode = !isBrowser && typeof process !== 'undefined'; // Check for production environment let isProduction = false; if (typeof process !== 'undefined' && process.env && typeof process.env === 'object') { isProduction = process.env.NODE_ENV === 'production'; } const isDevelopment = !isProduction; return { isBrowser, isNode, isDevelopment, isProduction }; } /** * Parse log level from string */ function parseLogLevel(level) { if (!level) return null; const upperLevel = level.toUpperCase(); switch (upperLevel) { case 'DEBUG': return LogLevel.DEBUG; case 'INFO': return LogLevel.INFO; case 'WARN': return LogLevel.WARN; case 'ERROR': return LogLevel.ERROR; case 'SILENT': return LogLevel.SILENT; default: return null; } } /** * Parse boolean from environment variable */ function parseEnvBoolean(value, defaultValue = false) { if (value === undefined) return defaultValue; return value.toLowerCase() === 'true'; } /** * Parse integer from environment variable */ function parseEnvInt(value, defaultValue) { if (value === undefined) return defaultValue; const parsed = parseInt(value, 10); return isNaN(parsed) ? defaultValue : parsed; } /** * Get environment variable with fallback */ function getEnvVar(key, defaultValue) { if (typeof process !== 'undefined' && process.env && typeof process.env === 'object') { return process.env[key] || defaultValue; } return defaultValue; } /** * Format timestamp for logging */ function formatTimestamp(date = new Date()) { return date.toISOString(); } /** * Check if logging should be enabled for the specified level */ function isLoggingEnabled(currentLevel, messageLevel, globalEnabled) { return globalEnabled && messageLevel >= currentLevel && currentLevel < LogLevel.SILENT && messageLevel < LogLevel.SILENT; } /** * Format message with category and timestamp */ function formatMessage(message, category, showTimestamp = true) { const parts = []; if (showTimestamp) { parts.push(`[${formatTimestamp()}]`); } if (category) { parts.push(`[${category}]`); } parts.push(message); return parts.join(' '); } /** * Create ANSI color code for terminal output */ function createAnsiColor(colorCode) { return `\x1b[${colorCode}m`; } /** * Reset ANSI color */ function resetAnsiColor() { return '\x1b[0m'; } /** * Configuration manager with environment-based defaults */ class ConfigManager { constructor(initialConfig) { this.environment = detectEnvironment(); this.config = this.mergeWithDefaults(initialConfig); } /** * Get the current configuration */ getConfig() { return { ...this.config }; } /** * Update configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Merge user config with smart defaults */ mergeWithDefaults(userConfig) { const envConfig = this.getEnvConfig(); // Base defaults without environment variables const baseDefaults = { enabled: true, minLevel: this.environment.isDevelopment ? LogLevel.DEBUG : LogLevel.WARN, showTimestamp: true, includeStackTrace: true, categories: {}, colors: { enabled: this.getDefaultColorsEnabled(), browser: { debug: '#6EC1E4', info: '#4A9FCA', warn: '#FBC02D', error: '#D67C2A' }, ansi: { debug: 36, info: 36, warn: 33, error: 31 } }, storage: { enabled: this.environment.isBrowser, keyPrefix: 'universal_logger' }, browserControls: { enabled: this.environment.isBrowser && this.environment.isDevelopment, windowNamespace: '__universalLogger' } }; // Environment variable overrides const envOverrides = {}; if (envConfig.LOGGER_ENABLED !== undefined) { envOverrides.enabled = parseEnvBoolean(envConfig.LOGGER_ENABLED, true); } if (envConfig.LOG_LEVEL !== undefined) { const envLevel = parseLogLevel(envConfig.LOG_LEVEL); if (envLevel !== null) { envOverrides.minLevel = envLevel; } } if (envConfig.LOGGER_TIMESTAMPS !== undefined) { envOverrides.showTimestamp = parseEnvBoolean(envConfig.LOGGER_TIMESTAMPS, true); } if (envConfig.LOGGER_STACK_TRACES !== undefined) { envOverrides.includeStackTrace = parseEnvBoolean(envConfig.LOGGER_STACK_TRACES, true); } // Handle colors configuration const colorsOverride = {}; let hasColorOverrides = false; if (envConfig.LOGGER_COLORS !== undefined) { colorsOverride.enabled = parseEnvBoolean(envConfig.LOGGER_COLORS, this.getDefaultColorsEnabled()); hasColorOverrides = true; } // Browser color overrides const browserColors = {}; let hasBrowserColorOverrides = false; if (envConfig.LOGGER_COLOR_DEBUG) { browserColors.debug = envConfig.LOGGER_COLOR_DEBUG; hasBrowserColorOverrides = true; } if (envConfig.LOGGER_COLOR_INFO) { browserColors.info = envConfig.LOGGER_COLOR_INFO; hasBrowserColorOverrides = true; } if (envConfig.LOGGER_COLOR_WARN) { browserColors.warn = envConfig.LOGGER_COLOR_WARN; hasBrowserColorOverrides = true; } if (envConfig.LOGGER_COLOR_ERROR) { browserColors.error = envConfig.LOGGER_COLOR_ERROR; hasBrowserColorOverrides = true; } // ANSI color overrides const ansiColors = {}; let hasAnsiColorOverrides = false; if (envConfig.LOGGER_ANSI_DEBUG) { const parsed = parseInt(envConfig.LOGGER_ANSI_DEBUG, 10); if (!isNaN(parsed)) { ansiColors.debug = parsed; hasAnsiColorOverrides = true; } } if (envConfig.LOGGER_ANSI_INFO) { const parsed = parseInt(envConfig.LOGGER_ANSI_INFO, 10); if (!isNaN(parsed)) { ansiColors.info = parsed; hasAnsiColorOverrides = true; } } if (envConfig.LOGGER_ANSI_WARN) { const parsed = parseInt(envConfig.LOGGER_ANSI_WARN, 10); if (!isNaN(parsed)) { ansiColors.warn = parsed; hasAnsiColorOverrides = true; } } if (envConfig.LOGGER_ANSI_ERROR) { const parsed = parseInt(envConfig.LOGGER_ANSI_ERROR, 10); if (!isNaN(parsed)) { ansiColors.error = parsed; hasAnsiColorOverrides = true; } } if (hasColorOverrides || hasBrowserColorOverrides || hasAnsiColorOverrides) { envOverrides.colors = { ...baseDefaults.colors, ...colorsOverride, browser: hasBrowserColorOverrides ? { ...baseDefaults.colors.browser, ...browserColors } : baseDefaults.colors.browser, ansi: hasAnsiColorOverrides ? { ...baseDefaults.colors.ansi, ...ansiColors } : baseDefaults.colors.ansi }; } // Storage overrides const storageOverride = {}; let hasStorageOverrides = false; if (envConfig.LOGGER_STORAGE_ENABLED !== undefined) { storageOverride.enabled = parseEnvBoolean(envConfig.LOGGER_STORAGE_ENABLED, this.environment.isBrowser); hasStorageOverrides = true; } if (envConfig.LOGGER_STORAGE_KEY_PREFIX) { storageOverride.keyPrefix = envConfig.LOGGER_STORAGE_KEY_PREFIX; hasStorageOverrides = true; } if (hasStorageOverrides) { envOverrides.storage = { ...baseDefaults.storage, ...storageOverride }; } // Browser controls overrides const browserControlsOverride = {}; let hasBrowserControlsOverrides = false; if (envConfig.LOGGER_BROWSER_CONTROLS !== undefined) { browserControlsOverride.enabled = parseEnvBoolean(envConfig.LOGGER_BROWSER_CONTROLS, this.environment.isBrowser && this.environment.isDevelopment); hasBrowserControlsOverrides = true; } if (envConfig.LOGGER_WINDOW_NAMESPACE) { browserControlsOverride.windowNamespace = envConfig.LOGGER_WINDOW_NAMESPACE; hasBrowserControlsOverrides = true; } if (hasBrowserControlsOverrides) { envOverrides.browserControls = { ...baseDefaults.browserControls, ...browserControlsOverride }; } // Merge: base defaults < user config < environment variables return { ...baseDefaults, ...userConfig, ...envOverrides }; } /** * Get environment configuration */ getEnvConfig() { return { LOG_LEVEL: getEnvVar('LOG_LEVEL'), LOGGER_ENABLED: getEnvVar('LOGGER_ENABLED'), LOGGER_TIMESTAMPS: getEnvVar('LOGGER_TIMESTAMPS'), LOGGER_STACK_TRACES: getEnvVar('LOGGER_STACK_TRACES'), LOGGER_COLORS: getEnvVar('LOGGER_COLORS'), LOGGER_STORAGE_ENABLED: getEnvVar('LOGGER_STORAGE_ENABLED'), LOGGER_STORAGE_KEY_PREFIX: getEnvVar('LOGGER_STORAGE_KEY_PREFIX'), LOGGER_BROWSER_CONTROLS: getEnvVar('LOGGER_BROWSER_CONTROLS'), LOGGER_WINDOW_NAMESPACE: getEnvVar('LOGGER_WINDOW_NAMESPACE'), LOGGER_COLOR_DEBUG: getEnvVar('LOGGER_COLOR_DEBUG'), LOGGER_COLOR_INFO: getEnvVar('LOGGER_COLOR_INFO'), LOGGER_COLOR_WARN: getEnvVar('LOGGER_COLOR_WARN'), LOGGER_COLOR_ERROR: getEnvVar('LOGGER_COLOR_ERROR'), LOGGER_ANSI_DEBUG: getEnvVar('LOGGER_ANSI_DEBUG'), LOGGER_ANSI_INFO: getEnvVar('LOGGER_ANSI_INFO'), LOGGER_ANSI_WARN: getEnvVar('LOGGER_ANSI_WARN'), LOGGER_ANSI_ERROR: getEnvVar('LOGGER_ANSI_ERROR') }; } /** * Get default colors enabled setting */ getDefaultColorsEnabled() { // Colors enabled in browser, or Node.js development return this.environment.isBrowser || this.environment.isDevelopment; } /** * Get environment information */ getEnvironment() { return { ...this.environment }; } } /** * Base logger implementation with shared functionality */ class BaseLogger { constructor(initialConfig) { this.recentLogs = new Map(); this.duplicateThreshold = 1000; // ms this.configManager = new ConfigManager(initialConfig); } /** * Log debug message */ debug(message, category, ...args) { this.logWithLevel(LogLevel.DEBUG, message, category, ...args); } /** * Log informational message */ info(message, category, ...args) { this.logWithLevel(LogLevel.INFO, message, category, ...args); } /** * Log warning message */ warn(message, category, ...args) { this.logWithLevel(LogLevel.WARN, message, category, ...args); } /** * Log error message or Error object */ error(message, category, ...args) { this.logWithLevel(LogLevel.ERROR, message, category, ...args); } /** * Set the minimum log level */ setLevel(level) { const config = this.configManager.getConfig(); this.configManager.updateConfig({ ...config, minLevel: level }); } /** * Configure the logger */ configure(newConfig) { this.configManager.updateConfig(newConfig); } /** * Enable logging for a specific category */ enableCategory(category, minLevel = LogLevel.DEBUG) { const config = this.configManager.getConfig(); config.categories[category] = { enabled: true, minLevel }; this.configManager.updateConfig(config); } /** * Disable logging for a specific category */ disableCategory(category) { const config = this.configManager.getConfig(); if (config.categories[category]) { config.categories[category].enabled = false; } else { config.categories[category] = { enabled: false, minLevel: LogLevel.DEBUG }; } this.configManager.updateConfig(config); } /** * Enable all logging */ enableAll() { const config = this.configManager.getConfig(); config.enabled = true; config.minLevel = LogLevel.DEBUG; // Enable all existing categories Object.keys(config.categories).forEach(category => { config.categories[category].enabled = true; }); this.configManager.updateConfig(config); } /** * Disable all logging */ disableAll() { const config = this.configManager.getConfig(); config.enabled = false; this.configManager.updateConfig(config); } /** * Get the current logger configuration */ getConfig() { return this.configManager.getConfig(); } /** * Check if logging is currently enabled */ isEnabled() { return this.configManager.getConfig().enabled; } /** * Log level constants */ get Level() { return LogLevel; } /** * Core logging method with level checking */ logWithLevel(level, message, category, ...args) { const config = this.configManager.getConfig(); // Check if logging is globally enabled if (!config.enabled) return; // Check category-specific settings first if (category && config.categories[category]) { const categoryConfig = config.categories[category]; if (!categoryConfig.enabled) return; if (!isLoggingEnabled(categoryConfig.minLevel, level, true)) return; } else { // Check global log level only if no category-specific settings if (!isLoggingEnabled(config.minLevel, level, true)) return; } // Check for duplicate logs if (this.isDuplicateLog(message, category)) return; // Create log entry const logEntry = { level, message: typeof message === 'string' ? message : message.message, category, timestamp: new Date(), args }; // Format message const formattedMessage = formatMessage(logEntry.message, category, config.showTimestamp); // Output the log this.outputLog(level, formattedMessage, logEntry, ...args); // Handle error stack traces if (level === LogLevel.ERROR && config.includeStackTrace) { // Check if message is an Error if (message instanceof Error) { this.outputStackTrace(message); } else { // Check if any of the arguments is an Error for (const arg of args) { if (arg instanceof Error) { this.outputStackTrace(arg); break; // Only output the first error's stack trace } } } } } /** * Check if this is a duplicate log message */ isDuplicateLog(message, category) { const key = `${typeof message === 'string' ? message : message.message}:${category || ''}`; const now = Date.now(); const lastTime = this.recentLogs.get(key); if (lastTime && (now - lastTime) < this.duplicateThreshold) { return true; } this.recentLogs.set(key, now); // Clean up old entries if (this.recentLogs.size > 100) { const cutoff = now - this.duplicateThreshold * 2; for (const [k, time] of this.recentLogs.entries()) { if (time < cutoff) { this.recentLogs.delete(k); } } } return false; } } /** * Browser-specific logger implementation */ class BrowserLogger extends BaseLogger { constructor(initialConfig) { super(initialConfig); // Store references to original console methods to prevent infinite recursion this.originalConsole = { debug: console.debug.bind(console), info: console.info.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), log: console.log.bind(console) }; this.storageAvailable = this.checkStorageAvailability(); this.loadConfigFromStorage(); this.setupBrowserControls(); } /** * Output log with browser-specific styling */ outputLog(level, formattedMessage, _logEntry, ...args) { const config = this.configManager.getConfig(); // Get the original console method to avoid infinite recursion // when logger methods are used to replace console methods const originalConsole = this.getOriginalConsoleMethod(level); if (config.colors.enabled) { const color = this.getColorForLevel(level); const style = `color: ${color}; font-weight: bold`; originalConsole(`%c${formattedMessage}`, style, ...args); } else { originalConsole(formattedMessage, ...args); } } /** * Output stack trace for errors */ outputStackTrace(error) { if (error.stack) { // Use original console.error to avoid infinite recursion const originalError = this.getOriginalConsoleMethod(LogLevel.ERROR); originalError(error.stack); } } /** * Override configure to save to storage */ configure(newConfig) { super.configure(newConfig); this.saveConfigToStorage(); } /** * Override setLevel to save to storage */ setLevel(level) { super.setLevel(level); this.saveConfigToStorage(); } /** * Override enableCategory to save to storage */ enableCategory(category, minLevel = LogLevel.DEBUG) { super.enableCategory(category, minLevel); this.saveConfigToStorage(); } /** * Override disableCategory to save to storage */ disableCategory(category) { super.disableCategory(category); this.saveConfigToStorage(); } /** * Override enableAll to save to storage */ enableAll() { super.enableAll(); this.saveConfigToStorage(); } /** * Override disableAll to save to storage */ disableAll() { super.disableAll(); this.saveConfigToStorage(); } /** * Get original console method to avoid infinite recursion */ getOriginalConsoleMethod(level) { switch (level) { case LogLevel.DEBUG: return this.originalConsole.debug; case LogLevel.INFO: return this.originalConsole.info; case LogLevel.WARN: return this.originalConsole.warn; case LogLevel.ERROR: return this.originalConsole.error; default: return this.originalConsole.log; } } /** * Get color for log level */ getColorForLevel(level) { const config = this.configManager.getConfig(); const colors = config.colors.browser; switch (level) { case LogLevel.DEBUG: return colors.debug; case LogLevel.INFO: return colors.info; case LogLevel.WARN: return colors.warn; case LogLevel.ERROR: return colors.error; default: return colors.info; } } /** * Check if localStorage is available */ checkStorageAvailability() { try { if (typeof window === 'undefined' || !window.localStorage) { return false; } const testKey = '__logger_test__'; window.localStorage.setItem(testKey, 'test'); window.localStorage.removeItem(testKey); return true; } catch (_a) { return false; } } /** * Load configuration from localStorage */ loadConfigFromStorage() { if (!this.storageAvailable) return; const config = this.configManager.getConfig(); if (!config.storage.enabled) return; try { const configKey = `${config.storage.keyPrefix}_config`; const enabledKey = `${config.storage.keyPrefix}_enabled`; const savedConfig = localStorage.getItem(configKey); const savedEnabled = localStorage.getItem(enabledKey); if (savedConfig) { const parsedConfig = JSON.parse(savedConfig); this.configManager.updateConfig(parsedConfig); } if (savedEnabled !== null) { const enabled = savedEnabled === 'true'; this.configManager.updateConfig({ enabled }); } } catch (error) { console.error('Error loading logger config from localStorage:', error); } } /** * Save configuration to localStorage */ saveConfigToStorage() { if (!this.storageAvailable) return; const config = this.configManager.getConfig(); if (!config.storage.enabled) return; try { const configKey = `${config.storage.keyPrefix}_config`; const enabledKey = `${config.storage.keyPrefix}_enabled`; localStorage.setItem(configKey, JSON.stringify(config)); localStorage.setItem(enabledKey, config.enabled.toString()); } catch (error) { console.error('Error saving logger config to localStorage:', error); } } /** * Setup browser console controls */ setupBrowserControls() { const config = this.configManager.getConfig(); if (!config.browserControls.enabled || typeof window === 'undefined') { return; } const win = window; const namespace = config.browserControls.windowNamespace; // Expose the logger instance win[namespace] = this; // Helper functions const enableFuncName = `enable${this.capitalizeFirst(namespace.replace('__', ''))}Logging`; const disableFuncName = `disable${this.capitalizeFirst(namespace.replace('__', ''))}Logging`; const statusFuncName = `${namespace.replace('__', '')}LoggingStatus`; win[enableFuncName] = () => { this.enableAll(); console.log(`%cLogging enabled!`, `color: ${config.colors.browser.info}; font-size: 14px; font-weight: bold`); return `Logging enabled. Use ${namespace} to access the logger API.`; }; win[disableFuncName] = () => { this.disableAll(); console.log(`%cLogging disabled!`, `color: ${config.colors.browser.warn}; font-size: 14px; font-weight: bold`); return 'Logging disabled.'; }; win[statusFuncName] = () => { const currentConfig = this.getConfig(); const status = currentConfig.enabled ? 'enabled' : 'disabled'; console.log(`%cLogging is currently ${status}`, `color: ${config.colors.browser.info}; font-size: 14px;`); if (currentConfig.enabled) { console.log('Current configuration:', currentConfig); } return { enabled: currentConfig.enabled, config: currentConfig }; }; } /** * Capitalize first letter of string */ capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } } /** * Node.js-specific logger implementation */ class NodeLogger extends BaseLogger { constructor(initialConfig) { super(initialConfig); // Store references to original console methods to prevent infinite recursion this.originalConsole = { debug: console.debug.bind(console), info: console.info.bind(console), warn: console.warn.bind(console), error: console.error.bind(console), log: console.log.bind(console) }; } /** * Output log with Node.js-specific ANSI coloring */ outputLog(level, formattedMessage, _logEntry, ...args) { const config = this.configManager.getConfig(); // Get the original console method to avoid infinite recursion // when logger methods are used to replace console methods const originalConsole = this.getOriginalConsoleMethod(level); if (config.colors.enabled) { const colorCode = this.getAnsiColorForLevel(level); const colorStart = createAnsiColor(colorCode); const colorEnd = resetAnsiColor(); originalConsole(`${colorStart}${formattedMessage}${colorEnd}`, ...args); } else { originalConsole(formattedMessage, ...args); } } /** * Output stack trace for errors */ outputStackTrace(error) { if (error.stack) { // Use original console.error to avoid infinite recursion const originalError = this.getOriginalConsoleMethod(LogLevel.ERROR); originalError(error.stack); } } /** * Get original console method to avoid infinite recursion */ getOriginalConsoleMethod(level) { switch (level) { case LogLevel.DEBUG: return this.originalConsole.debug; case LogLevel.INFO: return this.originalConsole.info; case LogLevel.WARN: return this.originalConsole.warn; case LogLevel.ERROR: return this.originalConsole.error; default: return this.originalConsole.log; } } /** * Get ANSI color code for log level */ getAnsiColorForLevel(level) { const config = this.configManager.getConfig(); const colors = config.colors.ansi; switch (level) { case LogLevel.DEBUG: return colors.debug; case LogLevel.INFO: return colors.info; case LogLevel.WARN: return colors.warn; case LogLevel.ERROR: return colors.error; default: return colors.info; } } } /** * Universal Logger - Main entry point * * Automatically detects environment and provides appropriate logger implementation */ /** * Create a logger instance with automatic environment detection */ function createLogger(config) { const env = detectEnvironment(); if (env.isBrowser) { return new BrowserLogger(config); } else { return new NodeLogger(config); } } /** * Default logger instance */ const defaultLogger = createLogger(); // Export the default logger methods for convenience const debug = defaultLogger.debug.bind(defaultLogger); const info = defaultLogger.info.bind(defaultLogger); const warn = defaultLogger.warn.bind(defaultLogger); const error = defaultLogger.error.bind(defaultLogger); const setLevel = defaultLogger.setLevel.bind(defaultLogger); const configure = defaultLogger.configure.bind(defaultLogger); const enableCategory = defaultLogger.enableCategory.bind(defaultLogger); const disableCategory = defaultLogger.disableCategory.bind(defaultLogger); const enableAll = defaultLogger.enableAll.bind(defaultLogger); const disableAll = defaultLogger.disableAll.bind(defaultLogger); const getConfig = defaultLogger.getConfig.bind(defaultLogger); const isEnabled = defaultLogger.isEnabled.bind(defaultLogger); const Level = defaultLogger.Level; export { BaseLogger, BrowserLogger, ConfigManager, Level, LogLevel, NodeLogger, configure, createLogger, debug, defaultLogger as default, detectEnvironment, disableAll, disableCategory, enableAll, enableCategory, error, formatMessage, formatTimestamp, getConfig, info, isEnabled, isLoggingEnabled, parseEnvBoolean, parseEnvInt, parseLogLevel, setLevel, warn }; //# sourceMappingURL=index.esm.js.map