cross-log
Version:
A universal logging package that works in both browser and Node.js environments with environment variable configuration
1,481 lines (1,469 loc) • 87 kB
JavaScript
/**
* 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 = {}));
/**
* Type guard to check if a value is a valid LogLevel
*/
function isLogLevel(value) {
return typeof value === 'number' && value >= LogLevel.DEBUG && value <= LogLevel.SILENT;
}
/**
* Type guard to check if a value is a valid LogLevel string
*/
function isLogLevelString(value) {
return typeof value === 'string' && value in LogLevel;
}
/**
* Deep merge configuration objects
*/
function mergeConfig(base, partial) {
var _a, _b, _c, _d, _e, _f, _g;
const result = { ...base };
if (partial.enabled !== undefined)
result.enabled = partial.enabled;
if (partial.minLevel !== undefined)
result.minLevel = partial.minLevel;
if (partial.showTimestamp !== undefined)
result.showTimestamp = partial.showTimestamp;
if (partial.includeStackTrace !== undefined)
result.includeStackTrace = partial.includeStackTrace;
if (partial.categories) {
result.categories = { ...base.categories };
// Merge partial category configs
for (const [key, value] of Object.entries(partial.categories)) {
if (value) {
result.categories[key] = {
enabled: (_c = (_a = value.enabled) !== null && _a !== void 0 ? _a : (_b = base.categories[key]) === null || _b === void 0 ? void 0 : _b.enabled) !== null && _c !== void 0 ? _c : true,
minLevel: (_f = (_d = value.minLevel) !== null && _d !== void 0 ? _d : (_e = base.categories[key]) === null || _e === void 0 ? void 0 : _e.minLevel) !== null && _f !== void 0 ? _f : LogLevel.DEBUG
};
}
}
}
if (partial.colors) {
result.colors = {
enabled: (_g = partial.colors.enabled) !== null && _g !== void 0 ? _g : base.colors.enabled,
browser: partial.colors.browser ? { ...base.colors.browser, ...partial.colors.browser } : base.colors.browser,
ansi: partial.colors.ansi ? { ...base.colors.ansi, ...partial.colors.ansi } : base.colors.ansi
};
}
if (partial.storage) {
result.storage = { ...base.storage, ...partial.storage };
}
if (partial.browserControls) {
result.browserControls = { ...base.browserControls, ...partial.browserControls };
}
return result;
}
/**
* Environment abstraction layer for cross-platform compatibility
* Handles differences between Node.js, Browser, and Edge Runtime environments
*/
/**
* Runtime type detection
*/
const RuntimeType = {
BROWSER: 'browser',
NODE: 'node',
EDGE: 'edge',
DENO: 'deno',
BUN: 'bun',
UNKNOWN: 'unknown'
};
/**
* Detect the current runtime type
*/
function detectRuntimeType() {
// Browser detection
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
return RuntimeType.BROWSER;
}
// Deno detection
if (typeof globalThis.Deno !== 'undefined') {
return RuntimeType.DENO;
}
// Bun detection
if (typeof globalThis.Bun !== 'undefined') {
return RuntimeType.BUN;
}
// Node.js detection (check this before Edge Runtime)
if (typeof process !== 'undefined' &&
process.versions &&
process.versions.node) {
return RuntimeType.NODE;
}
// Edge Runtime detection (Vercel Edge, Cloudflare Workers, etc.)
// Edge runtimes have specific globals and no process.versions
// Check for Edge-specific APIs or runtime indicators
if (typeof globalThis.process === 'undefined') {
// Vercel Edge Runtime
if (typeof globalThis.EdgeRuntime !== 'undefined') {
return RuntimeType.EDGE;
}
// Generic Edge detection - has fetch but no process
if (typeof globalThis.fetch !== 'undefined') {
return RuntimeType.EDGE;
}
}
// Additional Edge Runtime detection for Cloudflare Workers
if (typeof globalThis.caches !== 'undefined' &&
typeof globalThis.process === 'undefined') {
return RuntimeType.EDGE;
}
return RuntimeType.UNKNOWN;
}
/**
* Environment variable abstraction
* Works across different runtime environments
*/
function getEnvironmentVariable(key, defaultValue) {
var _a;
const runtime = detectRuntimeType();
switch (runtime) {
case RuntimeType.NODE:
// Node.js environment
if (typeof process !== 'undefined' && process.env) {
return process.env[key] || defaultValue;
}
break;
case RuntimeType.DENO:
// Deno environment
try {
const denoEnv = (_a = globalThis.Deno) === null || _a === void 0 ? void 0 : _a.env;
if (denoEnv && typeof denoEnv.get === 'function') {
return denoEnv.get(key) || defaultValue;
}
}
catch (_b) {
// Deno may require permissions
}
break;
case RuntimeType.BUN:
// Bun environment
if (typeof process !== 'undefined' && process.env) {
return process.env[key] || defaultValue;
}
break;
case RuntimeType.EDGE:
// Edge Runtime (Vercel/Cloudflare Workers)
// Try to access environment variables through globalThis
if (typeof globalThis[key] !== 'undefined') {
return String(globalThis[key]);
}
break;
case RuntimeType.BROWSER:
// Browser environment - no direct env var access
// Could potentially read from window object if exposed
if (typeof window.__ENV__ === 'object' &&
window.__ENV__[key]) {
return window.__ENV__[key];
}
break;
}
return defaultValue;
}
/**
* Detect if running in production mode
* Works across different environments
*/
function isProductionEnvironment() {
// Check various production indicators
const nodeEnv = getEnvironmentVariable('NODE_ENV');
if (nodeEnv === 'production')
return true;
const environment = getEnvironmentVariable('ENVIRONMENT');
if (environment === 'production' || environment === 'prod')
return true;
const vercelEnv = getEnvironmentVariable('VERCEL_ENV');
if (vercelEnv === 'production')
return true;
const cfPages = getEnvironmentVariable('CF_PAGES');
const cfPagesBranch = getEnvironmentVariable('CF_PAGES_BRANCH');
if (cfPages === '1' && cfPagesBranch === 'main')
return true;
return false;
}
/**
* Enhanced environment detection that works across all runtimes
*/
function detectEnvironment$1() {
const runtime = detectRuntimeType();
const isProduction = isProductionEnvironment();
return {
isBrowser: runtime === RuntimeType.BROWSER,
isNode: runtime === RuntimeType.NODE,
isDevelopment: !isProduction,
isProduction,
// Additional runtime info
runtime
};
}
/**
* Utility functions for the universal logger
*/
/**
* Detect the current environment
* Re-exported from environment module for backward compatibility
*/
function detectEnvironment() {
return detectEnvironment$1();
}
/**
* 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
* Now uses the environment abstraction for cross-platform compatibility
*/
function getEnvVar(key, defaultValue) {
return getEnvironmentVariable(key, 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(' ');
}
/**
* Get console method for log level
*/
function getConsoleMethod(level) {
switch (level) {
case LogLevel.DEBUG:
return console.debug;
case LogLevel.INFO:
return console.info;
case LogLevel.WARN:
return console.warn;
case LogLevel.ERROR:
return console.error;
default:
return console.log;
}
}
/**
* 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 = mergeConfig(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
let result = baseDefaults;
if (userConfig) {
result = mergeConfig(result, userConfig);
}
if (Object.keys(envOverrides).length > 0) {
result = mergeConfig(result, envOverrides);
}
return result;
}
/**
* 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 };
}
}
class PluginManager {
constructor(logger) {
this.plugins = new Map();
this.logger = logger;
}
use(plugin) {
if (this.plugins.has(plugin.name)) {
this.logger.warn(`Plugin "${plugin.name}" is already registered`, 'PluginManager');
return;
}
const context = {
logger: this.logger,
config: plugin.config || { enabled: true }
};
const instance = {
plugin,
context
};
try {
plugin.init(context);
// Get methods from context after init
if (context.methods) {
instance.methods = context.methods;
}
this.plugins.set(plugin.name, instance);
this.attachPluginMethods(plugin.name, instance);
this.logger.debug(`Plugin "${plugin.name}" initialized successfully`, 'PluginManager');
}
catch (error) {
this.logger.error(`Failed to initialize plugin "${plugin.name}"`, 'PluginManager', error);
throw error;
}
}
attachPluginMethods(name, instance) {
const loggerWithPlugins = this.logger;
switch (name) {
case 'api':
loggerWithPlugins.api = instance.methods;
break;
case 'database':
loggerWithPlugins.database = instance.methods;
break;
case 'analytics':
loggerWithPlugins.analytics = instance.methods;
break;
case 'performance':
loggerWithPlugins.performance = instance.methods;
break;
case 'security':
loggerWithPlugins.security = instance.methods;
break;
}
}
remove(name) {
const instance = this.plugins.get(name);
if (!instance) {
return false;
}
try {
if (instance.plugin.destroy) {
instance.plugin.destroy();
}
this.detachPluginMethods(name);
this.plugins.delete(name);
this.logger.debug(`Plugin "${name}" removed successfully`, 'PluginManager');
return true;
}
catch (error) {
this.logger.error(`Failed to remove plugin "${name}"`, 'PluginManager', error);
return false;
}
}
detachPluginMethods(name) {
const loggerWithPlugins = this.logger;
switch (name) {
case 'api':
delete loggerWithPlugins.api;
break;
case 'database':
delete loggerWithPlugins.database;
break;
case 'analytics':
delete loggerWithPlugins.analytics;
break;
case 'performance':
delete loggerWithPlugins.performance;
break;
case 'security':
delete loggerWithPlugins.security;
break;
}
}
getPlugin(name) {
return this.plugins.get(name);
}
getAllPlugins() {
return Array.from(this.plugins.values());
}
hasPlugin(name) {
return this.plugins.has(name);
}
clear() {
for (const [name] of this.plugins) {
this.remove(name);
}
}
}
/**
* Base logger implementation with shared functionality
*/
class BaseLogger {
constructor(initialConfig) {
this.recentLogs = new Map();
this.duplicateThreshold = 1000; // ms
this.configManager = new ConfigManager(initialConfig);
this.pluginManager = new PluginManager(this);
}
/**
* 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;
}
/**
* Use a plugin with the logger
*/
use(plugin) {
this.pluginManager.use(plugin);
const instance = this.pluginManager.getPlugin(plugin.name);
if (instance === null || instance === void 0 ? void 0 : instance.methods) {
this[plugin.name] = instance.methods;
}
return this;
}
/**
* Get a plugin instance by name
*/
getPlugin(name) {
const instance = this.pluginManager.getPlugin(name);
return instance === null || instance === void 0 ? void 0 : instance.plugin;
}
/**
* Check if a plugin is loaded
*/
hasPlugin(name) {
return this.pluginManager.hasPlugin(name);
}
/**
* Remove a plugin
*/
removePlugin(name) {
this.pluginManager.remove(name);
delete this[name];
}
/**
* 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;
}
}
}
/**
* Edge Runtime logger implementation
* Compatible with Vercel Edge Functions, Cloudflare Workers, and other Edge Runtimes
*/
/**
* Edge-specific logger that works in Edge Runtime environments
* Uses only Web APIs and avoids Node.js-specific features
*/
class EdgeLogger extends BaseLogger {
constructor(config) {
var _a, _b;
// Apply Edge-specific defaults before passing to parent
const edgeConfig = config ? {
...config,
storage: {
enabled: false,
keyPrefix: ((_a = config.storage) === null || _a === void 0 ? void 0 : _a.keyPrefix) || 'logger_'
},
browserControls: {
enabled: false,
windowNamespace: ((_b = config.browserControls) === null || _b === void 0 ? void 0 : _b.windowNamespace) || 'logger'
}
} : undefined;
super(edgeConfig);
}
/**
* Output log implementation for Edge Runtime
* Uses console methods which are available in Edge environments
*/
outputLog(level, formattedMessage, _logEntry, ...args) {
const consoleMethod = getConsoleMethod(level);
// Edge runtimes support console methods
if (args.length > 0) {
consoleMethod(formattedMessage, ...args);
}
else {
consoleMethod(formattedMessage);
}
}
/**
* Output stack trace for Edge Runtime
* Limited stack trace support in Edge environments
*/
outputStackTrace(error) {
if (error.stack) {
console.error(error.stack);
}
}
/**
* Edge-specific configuration
* Disables features not available in Edge Runtime
*/
configure(config) {
var _a, _b;
// Disable features not available in Edge Runtime
const edgeConfig = {
...config,
// Storage is not available in Edge Runtime
storage: {
enabled: false,
keyPrefix: ((_a = config.storage) === null || _a === void 0 ? void 0 : _a.keyPrefix) || 'logger_'
},
// Browser controls are not applicable
browserControls: {
enabled: false,
windowNamespace: ((_b = config.browserControls) === null || _b === void 0 ? void 0 : _b.windowNamespace) || 'logger'
}
};
super.configure(edgeConfig);
}
}
/**
* Builder pattern implementation for creating loggers with type-safe plugin support
*/
/**
* Implementation of the LoggerBuilder with type-safe plugin chaining
*/
class LoggerBuilderImpl {
constructor(config) {
this.loadedPlugins = [];
this.logger = createLogger(config);
this.pluginManager = new PluginManager(this.logger);
}
withPlugin(plugin) {
this.pluginManager.use(plugin);
this.loadedPlugins.push(plugin.name);
// Attach methods to logger
const loggerWithPlugins = this.logger;
const instance = this.pluginManager.getPlugin(plugin.name);
if (instance === null || instance === void 0 ? void 0 : instance.methods) {
loggerWithPlugins[plugin.name] = instance.methods;
}
return this;
}
build() {
const loggerWithPlugins = this.logger;
// Add plugin management methods
loggerWithPlugins.use = (plugin) => {
this.pluginManager.use(plugin);
const instance = this.pluginManager.getPlugin(plugin.name);
if (instance === null || instance === void 0 ? void 0 : instance.methods) {
loggerWithPlugins[plugin.name] = instance.methods;
}
return loggerWithPlugins;
};
loggerWithPlugins.getPlugin = (name) => {
const instance = this.pluginManager.getPlugin(name);
return instance ? instance.plugin : undefined;
};
loggerWithPlugins.hasPlugin = (name) => {
return this.pluginManager.hasPlugin(name);
};
loggerWithPlugins.removePlugin = (name) => {
this.pluginManager.remove(name);
delete loggerWithPlugins[name];
};
return loggerWithPlugins;
}
}
/**
* Create a new logger builder with optional configuration
*/
function createLoggerBuilder(config) {
return new LoggerBuilderImpl(config);
}
/**
* Create a logger with plugins using a configuration object
* Provides full type safety for all configured plugins
*/
function createLoggerWithPlugins(loggerConfig, pluginConfigs) {
const builder = createLoggerBuilder(loggerConfig);
let currentBuilder = builder;
if (pluginConfigs) {
// Import plugins dynamically to avoid circular dependencies
const { plugins } = require('./index');
// Add each configured plugin
for (const [pluginName, config] of Object.entries(pluginConfigs)) {
if (config && pluginName in plugins) {
const pluginFactory = plugins[pluginName];
if (pluginFactory) {
currentBuilder = currentBuilder.withPlugin(pluginFactory(config));
}
}
}
}
return currentBuilder.build();
}
/**
* Type-safe wrapper for existing logger instance
* Adds type information for already loaded plugins
*/
function wrapLogger(logger,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_pluginNames) {
return logger;
}
/**
* Type guard to check if logger has a specific plugin
*/
function hasPlugin(logger, pluginName) {
return pluginName in logger && typeof logger[pluginName] === 'object';
}
/**
* Type guard to check if logger has multiple plugins
*/
function hasPlugins(logger, pluginNames) {
return pluginNames.every(name => hasPlugin(logger, name));
}
class ApiRequestPlugin {
constructor(config) {
this.name = 'api';
this.version = '1.0.0';
this.config = {
enabled: true,
includeSessionId: true,
includeHeaders: false,
truncateUrl: 100,
...config
};
}
init(context) {
this.context = context;
if (this.config.includeSessionId) {
this.sessionId = this.generateSessionId();
}
const methods = {
request: this.request.bind(this),
response: this.response.bind(this),
error: this.error.bind(this)
};
this.methods = methods;
context.methods = methods;
const instance = context;
instance.methods = methods;
}
generateSessionId() {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
truncateUrl(url) {
if (!this.config.truncateUrl || url.length <= this.config.truncateUrl) {
return url;
}
return url.substring(0, this.config.truncateUrl) + '...';
}
formatDuration(duration) {
if (duration === undefined)
return '';
if (duration < 1000)
return `${duration}ms`;
return `${(duration / 1000).toFixed(2)}s`;
}
request(method, url, duration, status, data) {
if (!this.context || !this.config.enabled)
return;
const logData = {
type: 'api_request',
method: method.toUpperCase(),
url: this.truncateUrl(url),
timestamp: new Date().toISOString()
};
if (this.config.includeSessionId && this.sessionId) {
logData.sessionId = this.sessionId;
}
if (duration !== undefined) {
logData.duration = this.formatDuration(duration);
logData.durationMs = duration;
}
if (status !== undefined) {
logData.status = status;
}
if (data !== undefined) {
logData.data = data;
}
const message = `API Request: ${method.toUpperCase()} ${this.truncateUrl(url)}${status ? ` - ${status}` : ''}${duration !== undefined ? ` (${this.formatDuration(duration)})` : ''}`;
if (status && status >= 400) {
this.context.logger.error(message, 'API', logData);
}
else if (status && status >= 300) {
this.context.logger.warn(message, 'API', logData);
}
else {
this.context.logger.info(message, 'API', logData);
}
}
response(url, status, duration, data) {
if (!this.context || !this.config.enabled)
return;
const logData = {
type: 'api_response',
url: this.truncateUrl(url),
status,
timestamp: new Date().toISOString()
};
if (this.config.includeSessionId && this.sessionId) {
logData.sessionId = this.sessionId;
}
if (duration !== undefined) {
logData.duration = this.formatDuration(duration);
logData.durationMs = duration;
}
if (data !== undefined) {
logData.data = data;
}
const message = `API Response: ${this.truncateUrl(url)} - ${status}${duration !== undefined ? ` (${this.formatDuration(duration)})` : ''}`;
if (status >= 500) {
this.context.logger.error(message, 'API', logData);
}
else if (status >= 400) {
this.context.logger.warn(message, 'API', logData);
}
else {
this.context.logger.info(message, 'API', logData);
}
}
error(method, url, error, status) {
if (!this.context || !this.config.enabled)
return;
const logData = {
type: 'api_error',
method: method.toUpperCase(),
url: this.truncateUrl(url),
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
name: error.name
} : error,
timestamp: new Date().toISOString()
};
if (this.config.includeSessionId && this.sessionId) {
logData.sessionId = this.sessionId;
}
if (status !== undefined) {
logData.status = status;
}
const message = `API Error: ${method.toUpperCase()} ${this.truncateUrl(url)}${status ? ` - ${status}` : ''} - ${error instanceof Error ? error.message : error}`;
this.context.logger.error(message, 'API', logData);
}
destroy() {
this.context = undefined;
this.sessionId = undefined;
}
}
const createApiPlugin = (config) => {
return new ApiRequestPlugin(config);
};
class DatabaseQueryPlugin {
constructor(config) {
this.name = 'database';
this.version = '1.0.0';