mono/packages/ui/src/Logger.ts
babayaga 8ec419b87e ui
2026-01-29 17:57:27 +01:00

181 lines
4.7 KiB
TypeScript

import { toast } from 'sonner';
import React from 'react';
export type LogLevel = 'info' | 'warn' | 'error' | 'success' | 'debug';
export interface LogEntry {
level: LogLevel;
message: string;
timestamp: number;
data?: any;
}
export interface LoggerConfig {
enableConsole: boolean;
enableToaster: boolean;
deduplicationWindow: number; // ms
}
class Logger {
private static instance: Logger;
private config: LoggerConfig;
private recentLogs: Map<string, number> = new Map();
private constructor() {
this.config = {
enableConsole: true,
enableToaster: true,
deduplicationWindow: 5000, // 5 seconds
};
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public configure(config: Partial<LoggerConfig>): void {
this.config = { ...this.config, ...config };
}
private isDuplicate(message: string): boolean {
const now = Date.now();
const lastLogTime = this.recentLogs.get(message);
if (lastLogTime && (now - lastLogTime) < this.config.deduplicationWindow) {
return true;
}
this.recentLogs.set(message, now);
// Clean up old entries
for (const [msg, timestamp] of this.recentLogs.entries()) {
if (now - timestamp > this.config.deduplicationWindow) {
this.recentLogs.delete(msg);
}
}
return false;
}
private logToConsole(level: LogLevel, message: string, data?: any): void {
if (!this.config.enableConsole) return;
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
switch (level) {
case 'error':
console.error(prefix, message, data || '');
break;
case 'warn':
console.warn(prefix, message, data || '');
break;
case 'debug':
console.debug(prefix, message, data || '');
break;
case 'info':
case 'success':
default:
console.log(prefix, message, data || '');
break;
}
}
private logToToaster(level: LogLevel, message: string): void {
if (!this.config.enableToaster) return;
const toastId = toast(
React.createElement(
'div',
{
onClick: () => toast.dismiss(toastId),
style: { cursor: 'pointer', width: '100%' },
},
message
)
);
switch (level) {
case 'error':
toast.error(message, { id: toastId });
break;
case 'warn':
toast.warning(message, { id: toastId });
break;
case 'success':
toast.success(message, { id: toastId });
break;
case 'info':
toast.info(message, { id: toastId });
break;
case 'debug':
// Don't show debug messages in toaster by default
toast.dismiss(toastId);
break;
}
}
public log(level: LogLevel, message: string, data?: any): void {
if (this.isDuplicate(message)) {
return;
}
this.logToConsole(level, message, data);
this.logToToaster(level, message);
}
public info(message: string, data?: any): void {
this.log('info', message, data);
}
public warn(message: string, data?: any): void {
this.log('warn', message, data);
}
public error(message: string, data?: any): void {
this.log('error', message, data);
}
public success(message: string, data?: any): void {
this.log('success', message, data);
}
public debug(message: string, data?: any): void {
this.log('debug', message, data);
}
// Convenience methods for common patterns
public logError(error: unknown, context?: string): void {
const message = context
? `${context}: ${error instanceof Error ? error.message : String(error)}`
: error instanceof Error ? error.message : String(error);
this.error(message, error instanceof Error ? error.stack : undefined);
}
public logApiError(operation: string, error: unknown): void {
this.logError(error, `Error during ${operation}`);
}
public logWebSocketError(operation: string, error: unknown): void {
this.logError(error, `WebSocket error during ${operation}`);
}
// Method to temporarily disable toaster (useful for batch operations)
public withoutToaster<T>(fn: () => T): T {
const originalToasterState = this.config.enableToaster;
this.config.enableToaster = false;
try {
return fn();
} finally {
this.config.enableToaster = originalToasterState;
}
}
}
// Export singleton instance
export const logger = Logger.getInstance();
export default logger;