weelog
Version:
Next-generation JavaScript logging library with performance tracking, memory monitoring, analytics, and advanced debugging features.
730 lines (639 loc) • 20.9 kB
text/typescript
/**
* WeeLog - Tiny Logging Library for JavaScript
* Zero dependencies, browser and Node.js compatible
*/
export interface LoggerOptions {
level?: LogLevel;
enabled?: boolean;
useTimestamp?: boolean;
useHumanReadableTimestamp?: boolean;
enablePerformanceTracking?: boolean;
enableMemoryTracking?: boolean;
logMemoryInline?: boolean;
maxLogHistory?: number;
enableLogAnalytics?: boolean;
// Environment-aware configuration
autoDetectEnvironment?: boolean;
environment?: 'development' | 'production' | 'staging' | 'test';
developmentConfig?: Partial<LoggerOptions>;
productionConfig?: Partial<LoggerOptions>;
stagingConfig?: Partial<LoggerOptions>;
testConfig?: Partial<LoggerOptions>;
}
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
export type LogInterceptor = (level: LogLevel, message: string, context?: string, data?: any) => void;
export interface LogEntry {
level: LogLevel;
message: string;
context?: string;
data?: any;
timestamp: Date;
formatted: string;
performance?: PerformanceMetrics;
memory?: MemoryInfo;
stackTrace?: string;
sessionId?: string;
}
export interface PerformanceMetrics {
duration?: number;
timestamp: number;
memoryUsage?: number;
}
export interface MemoryInfo {
used: number;
total: number;
percentage: number;
}
export interface LogAnalytics {
totalLogs: number;
logsByLevel: Record<LogLevel, number>;
averageLogRate: number;
errorRate: number;
topContexts: Array<{ context: string; count: number }>;
}
export class Logger {
private level: LogLevel;
private enabled: boolean;
private useTimestamp: boolean;
private useHumanReadableTimestamp: boolean;
private context?: string;
private interceptors: LogInterceptor[];
private enablePerformanceTracking: boolean;
private enableMemoryTracking: boolean;
private logMemoryInline: boolean;
private maxLogHistory: number;
private enableLogAnalytics: boolean;
private logHistory: LogEntry[];
private sessionId: string;
private performanceMarks: Map<string, number>;
private analytics: LogAnalytics;
private detectedEnvironment: 'development' | 'production' | 'staging' | 'test';
private readonly levels: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3
};
private readonly colors: Record<LogLevel, string> = {
debug: '#6b7280',
info: '#2563eb',
warn: '#f59e0b',
error: '#ef4444'
};
constructor(options: LoggerOptions = {}) {
// Detect environment first
this.detectedEnvironment = this.detectEnvironment(options);
// Apply environment-specific configuration
const finalOptions = this.applyEnvironmentConfig(options);
this.level = finalOptions.level || 'info';
this.enabled = finalOptions.enabled !== false;
this.useTimestamp = finalOptions.useTimestamp || false;
this.useHumanReadableTimestamp = finalOptions.useHumanReadableTimestamp || false;
this.enablePerformanceTracking = finalOptions.enablePerformanceTracking || false;
this.enableMemoryTracking = finalOptions.enableMemoryTracking || false;
this.logMemoryInline = finalOptions.logMemoryInline || false;
this.maxLogHistory = finalOptions.maxLogHistory || 1000;
this.enableLogAnalytics = finalOptions.enableLogAnalytics || false;
this.interceptors = [];
this.logHistory = [];
this.sessionId = this.generateSessionId();
this.performanceMarks = new Map();
this.analytics = {
totalLogs: 0,
logsByLevel: { debug: 0, info: 0, warn: 0, error: 0 },
averageLogRate: 0,
errorRate: 0,
topContexts: []
};
}
/**
* Detect the current environment
*/
private detectEnvironment(options: LoggerOptions): 'development' | 'production' | 'staging' | 'test' {
// If explicitly set, use that
if (options.environment) {
return options.environment;
}
// If auto-detection is disabled, default to development
if (options.autoDetectEnvironment === false) {
return 'development';
}
// Try to detect from WEELOG_ENV first (higher priority)
if (typeof process !== 'undefined' && process.env && process.env.WEELOG_ENV) {
const weelogEnv = process.env.WEELOG_ENV.toLowerCase();
if (weelogEnv === 'production' || weelogEnv === 'prod') return 'production';
if (weelogEnv === 'staging' || weelogEnv === 'stage') return 'staging';
if (weelogEnv === 'test' || weelogEnv === 'testing') return 'test';
if (weelogEnv === 'development' || weelogEnv === 'dev') return 'development';
}
// Try to detect from NODE_ENV (Node.js)
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV) {
const nodeEnv = process.env.NODE_ENV.toLowerCase();
if (nodeEnv === 'production' || nodeEnv === 'prod') return 'production';
if (nodeEnv === 'staging' || nodeEnv === 'stage') return 'staging';
if (nodeEnv === 'test' || nodeEnv === 'testing') return 'test';
if (nodeEnv === 'development' || nodeEnv === 'dev') return 'development';
}
// Browser environment detection
if (typeof window !== 'undefined') {
// Check for common development indicators
if (window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.hostname.includes('dev') ||
window.location.hostname.includes('test') ||
window.location.port !== '') {
return 'development';
}
// Check for staging indicators
if (window.location.hostname.includes('staging') ||
window.location.hostname.includes('stage')) {
return 'staging';
}
// Otherwise assume production in browser
return 'production';
}
// Default to development if nothing else detected
return 'development';
}
/**
* Apply environment-specific configuration
*/
private applyEnvironmentConfig(options: LoggerOptions): LoggerOptions {
const env = this.detectedEnvironment;
// Get environment-specific config
let envConfig: Partial<LoggerOptions> = {};
switch (env) {
case 'development':
envConfig = options.developmentConfig || {
level: 'debug',
useTimestamp: true,
enablePerformanceTracking: true,
enableMemoryTracking: true,
enableLogAnalytics: true
};
break;
case 'production':
envConfig = options.productionConfig || {
level: 'warn',
useTimestamp: true,
enablePerformanceTracking: false,
enableMemoryTracking: false,
logMemoryInline: false,
enableLogAnalytics: false
};
break;
case 'staging':
envConfig = options.stagingConfig || {
level: 'info',
useTimestamp: true,
enablePerformanceTracking: true,
enableMemoryTracking: true,
enableLogAnalytics: true
};
break;
case 'test':
envConfig = options.testConfig || {
level: 'error',
enabled: false,
useTimestamp: false,
enablePerformanceTracking: false,
enableMemoryTracking: false,
enableLogAnalytics: false
};
break;
}
// Merge configurations with explicit options taking precedence
const finalConfig = { ...envConfig };
// Only override environment config if explicitly set in options
Object.keys(options).forEach(key => {
if (key !== 'autoDetectEnvironment' &&
key !== 'environment' &&
key !== 'developmentConfig' &&
key !== 'productionConfig' &&
key !== 'stagingConfig' &&
key !== 'testConfig' &&
options[key as keyof LoggerOptions] !== undefined) {
(finalConfig as any)[key] = options[key as keyof LoggerOptions];
}
});
return finalConfig;
}
/**
* Get the detected environment
*/
getEnvironment(): 'development' | 'production' | 'staging' | 'test' {
return this.detectedEnvironment;
}
/**
* Set the minimum log level
*/
setLevel(level: LogLevel): Logger {
this.level = level;
return this;
}
/**
* Enable or disable logging
*/
enable(enabled: boolean): Logger {
this.enabled = enabled;
return this;
}
/**
* Create a logger with a specific context
*/
withContext(context: string): Logger {
const newLogger = Object.create(this);
newLogger.context = context;
return newLogger;
}
/**
* Add a log interceptor callback
*/
onLog(callback: LogInterceptor): Logger {
this.interceptors.push(callback);
return this;
}
/**
* Generate a unique session ID
*/
private generateSessionId(): string {
return 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
}
/**
* Get memory usage information
*/
private getMemoryInfo(): MemoryInfo | undefined {
if (!this.enableMemoryTracking) return undefined;
// Browser environment
if (typeof window !== 'undefined' && (performance as any).memory) {
const memory = (performance as any).memory;
return {
used: memory.usedJSHeapSize,
total: memory.totalJSHeapSize,
percentage: Math.round((memory.usedJSHeapSize / memory.totalJSHeapSize) * 100)
};
}
// Node.js environment
if (typeof process !== 'undefined' && process.memoryUsage) {
const memory = process.memoryUsage();
return {
used: memory.heapUsed,
total: memory.heapTotal,
percentage: Math.round((memory.heapUsed / memory.heapTotal) * 100)
};
}
return undefined;
}
/**
* Format memory usage for inline display
*/
private formatMemoryUsage(): string {
if (!this.logMemoryInline) {
return '';
}
const memoryInfo = this.getMemoryInfo();
if (!memoryInfo) {
return '';
}
const memoryMB = (memoryInfo.used / 1024 / 1024).toFixed(2);
return ` (Memory: ${memoryMB} MB)`;
}
/**
* Start performance tracking for a specific operation
*/
startPerformanceTimer(label: string): Logger {
if (this.enablePerformanceTracking) {
this.performanceMarks.set(label, Date.now());
}
return this;
}
/**
* End performance tracking and log the duration
*/
endPerformanceTimer(label: string, message?: string): LogEntry | null {
if (this.enablePerformanceTracking && this.performanceMarks.has(label)) {
const startTime = this.performanceMarks.get(label)!;
const duration = Date.now() - startTime;
this.performanceMarks.delete(label);
const perfMessage = message || `Performance: ${label} completed`;
return this.info(perfMessage, {
performanceTimer: label,
duration: `${duration}ms`,
timestamp: Date.now()
});
}
return null;
}
/**
* Log with automatic stack trace capture
*/
trace(message: string, data?: any): LogEntry | null {
const stackTrace = new Error().stack;
return this.log('debug', message, data, stackTrace);
}
/**
* Get current analytics data
*/
getAnalytics(): LogAnalytics {
return { ...this.analytics };
}
/**
* Get log history
*/
getLogHistory(): LogEntry[] {
return [...this.logHistory];
}
/**
* Clear log history
*/
clearHistory(): Logger {
this.logHistory = [];
return this;
}
/**
* Export logs as JSON
*/
exportLogs(): string {
return JSON.stringify({
sessionId: this.sessionId,
exportedAt: new Date().toISOString(),
analytics: this.analytics,
logs: this.logHistory
}, null, 2);
}
/**
* Search logs by criteria
*/
searchLogs(criteria: {
level?: LogLevel;
context?: string;
message?: string;
timeRange?: { start: Date; end: Date };
}): LogEntry[] {
return this.logHistory.filter(entry => {
if (criteria.level && entry.level !== criteria.level) return false;
if (criteria.context && entry.context !== criteria.context) return false;
if (criteria.message && !entry.message.includes(criteria.message)) return false;
if (criteria.timeRange) {
if (entry.timestamp < criteria.timeRange.start || entry.timestamp > criteria.timeRange.end) {
return false;
}
}
return true;
});
}
/**
* Check if a log level should be output
*/
private shouldLog(level: LogLevel): boolean {
return this.enabled && this.levels[level] >= this.levels[this.level];
}
/**
* Format a log message
*/
private formatMessage(level: LogLevel, message: string, data?: any): string {
let formatted = '';
if (this.useTimestamp) {
const timestamp = new Date();
if (this.useHumanReadableTimestamp) {
// Human readable format: "Dec 16, 2024 at 9:45:23 PM"
formatted += `[${timestamp.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true
})}] `;
} else {
formatted += `[${timestamp.toISOString()}] `;
}
}
formatted += `[${level.toUpperCase()}]`;
if (this.context) {
formatted += ` [${this.context}]`;
}
formatted += ` ${message}`;
// Add inline memory usage if enabled
if (this.logMemoryInline) {
formatted += this.formatMemoryUsage();
}
if (data !== undefined && data !== null) {
if (typeof data === 'object') {
try {
// Process the data to apply human readable timestamps if enabled
const processedData = this.processDataTimestamps(data);
formatted += ` ${JSON.stringify(processedData)}`;
} catch (e) {
formatted += ` [Object (circular)]`;
}
} else {
formatted += ` ${data}`;
}
}
return formatted;
}
/**
* Process data object to apply human readable timestamps
*/
private processDataTimestamps(data: any): any {
if (!this.useHumanReadableTimestamp) {
return data;
}
if (data === null || data === undefined) {
return data;
}
if (typeof data !== 'object') {
return data;
}
if (Array.isArray(data)) {
return data.map(item => this.processDataTimestamps(item));
}
const processed: any = {};
for (const [key, value] of Object.entries(data)) {
if (key === 'timestamp' && (typeof value === 'number' || value instanceof Date)) {
// Convert timestamp to human readable format
const date = typeof value === 'number' ? new Date(value) : value;
processed[key] = date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true
});
} else if (typeof value === 'object') {
processed[key] = this.processDataTimestamps(value);
} else {
processed[key] = value;
}
}
return processed;
}
/**
* Internal log method
*/
private log(level: LogLevel, message: string, data?: any, stackTrace?: string): LogEntry | null {
if (!this.shouldLog(level)) {
return null;
}
const timestamp = new Date();
const formatted = this.formatMessage(level, message, data);
const logEntry: LogEntry = {
level,
message,
context: this.context,
data,
timestamp,
formatted,
sessionId: this.sessionId,
stackTrace: stackTrace
};
// Add performance and memory tracking
if (this.enablePerformanceTracking) {
logEntry.performance = {
timestamp: Date.now(),
memoryUsage: this.enableMemoryTracking ? this.getMemoryInfo()?.used : undefined
};
}
if (this.enableMemoryTracking) {
logEntry.memory = this.getMemoryInfo();
}
// Update analytics
if (this.enableLogAnalytics) {
this.updateAnalytics(level, this.context);
}
// Add to history
this.logHistory.push(logEntry);
if (this.logHistory.length > this.maxLogHistory) {
this.logHistory.shift();
}
// Call interceptors
this.interceptors.forEach(interceptor => {
try {
interceptor(level, message, this.context, data);
} catch (e) {
// Avoid infinite loops by using plain console.error
if (typeof console !== 'undefined' && console.error) {
console.error('Logger interceptor error:', e);
}
}
});
// Output to console
this.outputToConsole(level, formatted);
return logEntry;
}
/**
* Update analytics data
*/
private updateAnalytics(level: LogLevel, context?: string): void {
this.analytics.totalLogs++;
this.analytics.logsByLevel[level]++;
if (level === 'error') {
this.analytics.errorRate = (this.analytics.logsByLevel.error / this.analytics.totalLogs) * 100;
}
if (context) {
const existingContext = this.analytics.topContexts.find(c => c.context === context);
if (existingContext) {
existingContext.count++;
} else {
this.analytics.topContexts.push({ context, count: 1 });
}
// Keep only top 10 contexts
this.analytics.topContexts.sort((a, b) => b.count - a.count);
this.analytics.topContexts = this.analytics.topContexts.slice(0, 10);
}
}
/**
* Output formatted message to console with colors (browser only)
*/
private outputToConsole(level: LogLevel, formatted: string): void {
if (typeof console === 'undefined') {
return;
}
const color = this.colors[level];
// Detect browser environment more reliably
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
// Browser environment - use colored output with CSS styling
if (isBrowser && console.log) {
const weight = level === 'error' ? 'bold' : 'normal';
const styles = `color: ${color}; font-weight: ${weight}; font-family: monospace;`;
try {
console.log(`%c${formatted}`, styles);
} catch (e) {
// Fallback if styling fails
console.log(formatted);
}
}
// Node.js environment - use appropriate console method with ANSI colors
else if (isNode) {
const ansiColors: Record<LogLevel, string> = {
debug: '\x1b[90m', // gray
info: '\x1b[36m', // cyan
warn: '\x1b[33m', // yellow
error: '\x1b[31m' // red
};
const reset = '\x1b[0m';
const coloredMessage = `${ansiColors[level]}${formatted}${reset}`;
switch (level) {
case 'debug':
console.debug ? console.debug(coloredMessage) : console.log(coloredMessage);
break;
case 'info':
console.info(coloredMessage);
break;
case 'warn':
console.warn(coloredMessage);
break;
case 'error':
console.error(coloredMessage);
break;
default:
console.log(coloredMessage);
}
}
// Fallback for other environments
else {
console.log(formatted);
}
}
/**
* Log a debug message
*/
debug(message: string, data?: any): LogEntry | null {
return this.log('debug', message, data);
}
/**
* Log an info message
*/
info(message: string, data?: any): LogEntry | null {
return this.log('info', message, data);
}
/**
* Log a warning message
*/
warn(message: string, data?: any): LogEntry | null {
return this.log('warn', message, data);
}
/**
* Log an error message
*/
error(message: string, data?: any): LogEntry | null {
return this.log('error', message, data);
}
}
// Create a default logger instance for convenience functions
const defaultLogger = new Logger();
// Named exports for individual logging functions
export const log = (message: string, data?: any) => defaultLogger.info(message, data);
export const info = (message: string, data?: any) => defaultLogger.info(message, data);
export const warn = (message: string, data?: any) => defaultLogger.warn(message, data);
export const error = (message: string, data?: any) => defaultLogger.error(message, data);
export const debug = (message: string, data?: any) => defaultLogger.debug(message, data);
export const success = (message: string, data?: any) => defaultLogger.info(`✅ ${message}`, data);
// Types are already exported inline above
// Default export for easy importing
export default Logger;