|
import { atom, map } from 'nanostores'; |
|
import Cookies from 'js-cookie'; |
|
import { createScopedLogger } from '~/utils/logger'; |
|
|
|
const logger = createScopedLogger('LogStore'); |
|
|
|
export interface LogEntry { |
|
id: string; |
|
timestamp: string; |
|
level: 'info' | 'warning' | 'error' | 'debug'; |
|
message: string; |
|
details?: Record<string, any>; |
|
category: 'system' | 'provider' | 'user' | 'error'; |
|
} |
|
|
|
const MAX_LOGS = 1000; |
|
|
|
class LogStore { |
|
private _logs = map<Record<string, LogEntry>>({}); |
|
showLogs = atom(true); |
|
|
|
constructor() { |
|
|
|
this._loadLogs(); |
|
} |
|
|
|
private _loadLogs() { |
|
const savedLogs = Cookies.get('eventLogs'); |
|
|
|
if (savedLogs) { |
|
try { |
|
const parsedLogs = JSON.parse(savedLogs); |
|
this._logs.set(parsedLogs); |
|
} catch (error) { |
|
logger.error('Failed to parse logs from cookies:', error); |
|
} |
|
} |
|
} |
|
|
|
private _saveLogs() { |
|
const currentLogs = this._logs.get(); |
|
Cookies.set('eventLogs', JSON.stringify(currentLogs)); |
|
} |
|
|
|
private _generateId(): string { |
|
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; |
|
} |
|
|
|
private _trimLogs() { |
|
const currentLogs = Object.entries(this._logs.get()); |
|
|
|
if (currentLogs.length > MAX_LOGS) { |
|
const sortedLogs = currentLogs.sort( |
|
([, a], [, b]) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), |
|
); |
|
const newLogs = Object.fromEntries(sortedLogs.slice(0, MAX_LOGS)); |
|
this._logs.set(newLogs); |
|
} |
|
} |
|
|
|
addLog( |
|
message: string, |
|
level: LogEntry['level'] = 'info', |
|
category: LogEntry['category'] = 'system', |
|
details?: Record<string, any>, |
|
) { |
|
const id = this._generateId(); |
|
const entry: LogEntry = { |
|
id, |
|
timestamp: new Date().toISOString(), |
|
level, |
|
message, |
|
details, |
|
category, |
|
}; |
|
|
|
this._logs.setKey(id, entry); |
|
this._trimLogs(); |
|
this._saveLogs(); |
|
|
|
return id; |
|
} |
|
|
|
|
|
logSystem(message: string, details?: Record<string, any>) { |
|
return this.addLog(message, 'info', 'system', details); |
|
} |
|
|
|
|
|
logProvider(message: string, details?: Record<string, any>) { |
|
return this.addLog(message, 'info', 'provider', details); |
|
} |
|
|
|
|
|
logUserAction(message: string, details?: Record<string, any>) { |
|
return this.addLog(message, 'info', 'user', details); |
|
} |
|
|
|
|
|
logError(message: string, error?: Error | unknown, details?: Record<string, any>) { |
|
const errorDetails = { |
|
...(details || {}), |
|
error: |
|
error instanceof Error |
|
? { |
|
message: error.message, |
|
stack: error.stack, |
|
} |
|
: error, |
|
}; |
|
return this.addLog(message, 'error', 'error', errorDetails); |
|
} |
|
|
|
|
|
logWarning(message: string, details?: Record<string, any>) { |
|
return this.addLog(message, 'warning', 'system', details); |
|
} |
|
|
|
|
|
logDebug(message: string, details?: Record<string, any>) { |
|
return this.addLog(message, 'debug', 'system', details); |
|
} |
|
|
|
clearLogs() { |
|
this._logs.set({}); |
|
this._saveLogs(); |
|
} |
|
|
|
getLogs() { |
|
return Object.values(this._logs.get()).sort( |
|
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), |
|
); |
|
} |
|
|
|
getFilteredLogs(level?: LogEntry['level'], category?: LogEntry['category'], searchQuery?: string) { |
|
return this.getLogs().filter((log) => { |
|
const matchesLevel = !level || level === 'debug' || log.level === level; |
|
const matchesCategory = !category || log.category === category; |
|
const matchesSearch = |
|
!searchQuery || |
|
log.message.toLowerCase().includes(searchQuery.toLowerCase()) || |
|
JSON.stringify(log.details).toLowerCase().includes(searchQuery.toLowerCase()); |
|
|
|
return matchesLevel && matchesCategory && matchesSearch; |
|
}); |
|
} |
|
} |
|
|
|
export const logStore = new LogStore(); |
|
|