|
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' |
|
| 'api' |
|
| 'auth' |
|
| 'database' |
|
| 'network' |
|
| 'performance' |
|
| 'settings' |
|
| 'task' |
|
| 'update' |
|
| 'feature'; |
|
subCategory?: string; |
|
duration?: number; |
|
statusCode?: number; |
|
source?: string; |
|
stack?: string; |
|
metadata?: { |
|
component?: string; |
|
action?: string; |
|
userId?: string; |
|
sessionId?: string; |
|
previousValue?: any; |
|
newValue?: any; |
|
}; |
|
} |
|
|
|
interface LogDetails extends Record<string, any> { |
|
type: string; |
|
message: string; |
|
} |
|
|
|
const MAX_LOGS = 1000; |
|
|
|
class LogStore { |
|
private _logs = map<Record<string, LogEntry>>({}); |
|
showLogs = atom(true); |
|
private _readLogs = new Set<string>(); |
|
|
|
constructor() { |
|
|
|
this._loadLogs(); |
|
|
|
|
|
if (typeof window !== 'undefined') { |
|
this._loadReadLogs(); |
|
} |
|
} |
|
|
|
|
|
get logs() { |
|
return this._logs; |
|
} |
|
|
|
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 _loadReadLogs() { |
|
if (typeof window === 'undefined') { |
|
return; |
|
} |
|
|
|
const savedReadLogs = localStorage.getItem('bolt_read_logs'); |
|
|
|
if (savedReadLogs) { |
|
try { |
|
const parsedReadLogs = JSON.parse(savedReadLogs); |
|
this._readLogs = new Set(parsedReadLogs); |
|
} catch (error) { |
|
logger.error('Failed to parse read logs:', error); |
|
} |
|
} |
|
} |
|
|
|
private _saveLogs() { |
|
const currentLogs = this._logs.get(); |
|
Cookies.set('eventLogs', JSON.stringify(currentLogs)); |
|
} |
|
|
|
private _saveReadLogs() { |
|
if (typeof window === 'undefined') { |
|
return; |
|
} |
|
|
|
localStorage.setItem('bolt_read_logs', JSON.stringify(Array.from(this._readLogs))); |
|
} |
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
private _addLog( |
|
message: string, |
|
level: LogEntry['level'], |
|
category: LogEntry['category'], |
|
details?: Record<string, any>, |
|
metadata?: LogEntry['metadata'], |
|
) { |
|
const id = this._generateId(); |
|
const entry: LogEntry = { |
|
id, |
|
timestamp: new Date().toISOString(), |
|
level, |
|
message, |
|
details, |
|
category, |
|
metadata, |
|
}; |
|
|
|
this._logs.setKey(id, entry); |
|
this._trimLogs(); |
|
this._saveLogs(); |
|
|
|
return id; |
|
} |
|
|
|
|
|
private _addApiLog( |
|
message: string, |
|
method: string, |
|
url: string, |
|
details: { |
|
method: string; |
|
url: string; |
|
statusCode: number; |
|
duration: number; |
|
request: any; |
|
response: any; |
|
}, |
|
) { |
|
const statusCode = details.statusCode; |
|
return this._addLog(message, statusCode >= 400 ? 'error' : 'info', 'api', details, { |
|
component: 'api', |
|
action: method, |
|
}); |
|
} |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
logAPIRequest(endpoint: string, method: string, duration: number, statusCode: number, details?: Record<string, any>) { |
|
const message = `${method} ${endpoint} - ${statusCode} (${duration}ms)`; |
|
const level = statusCode >= 400 ? 'error' : statusCode >= 300 ? 'warning' : 'info'; |
|
|
|
return this._addLog(message, level, 'api', { |
|
...details, |
|
endpoint, |
|
method, |
|
duration, |
|
statusCode, |
|
timestamp: new Date().toISOString(), |
|
}); |
|
} |
|
|
|
|
|
logAuth( |
|
action: 'login' | 'logout' | 'token_refresh' | 'key_validation', |
|
success: boolean, |
|
details?: Record<string, any>, |
|
) { |
|
const message = `Auth ${action} - ${success ? 'Success' : 'Failed'}`; |
|
const level = success ? 'info' : 'error'; |
|
|
|
return this._addLog(message, level, 'auth', { |
|
...details, |
|
action, |
|
success, |
|
timestamp: new Date().toISOString(), |
|
}); |
|
} |
|
|
|
|
|
logNetworkStatus(status: 'online' | 'offline' | 'reconnecting' | 'connected', details?: Record<string, any>) { |
|
const message = `Network ${status}`; |
|
const level = status === 'offline' ? 'error' : status === 'reconnecting' ? 'warning' : 'info'; |
|
|
|
return this._addLog(message, level, 'network', { |
|
...details, |
|
status, |
|
timestamp: new Date().toISOString(), |
|
}); |
|
} |
|
|
|
|
|
logDatabase(operation: string, success: boolean, duration: number, details?: Record<string, any>) { |
|
const message = `DB ${operation} - ${success ? 'Success' : 'Failed'} (${duration}ms)`; |
|
const level = success ? 'info' : 'error'; |
|
|
|
return this._addLog(message, level, 'database', { |
|
...details, |
|
operation, |
|
success, |
|
duration, |
|
timestamp: new Date().toISOString(), |
|
}); |
|
} |
|
|
|
|
|
logError(message: string, error?: Error | unknown, details?: Record<string, any>) { |
|
const errorDetails = |
|
error instanceof Error |
|
? { |
|
name: error.name, |
|
message: error.message, |
|
stack: error.stack, |
|
...details, |
|
} |
|
: { error, ...details }; |
|
|
|
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; |
|
}); |
|
} |
|
|
|
markAsRead(logId: string) { |
|
this._readLogs.add(logId); |
|
this._saveReadLogs(); |
|
} |
|
|
|
isRead(logId: string): boolean { |
|
return this._readLogs.has(logId); |
|
} |
|
|
|
clearReadLogs() { |
|
this._readLogs.clear(); |
|
this._saveReadLogs(); |
|
} |
|
|
|
|
|
logApiCall( |
|
method: string, |
|
endpoint: string, |
|
statusCode: number, |
|
duration: number, |
|
requestData?: any, |
|
responseData?: any, |
|
) { |
|
return this._addLog( |
|
`API ${method} ${endpoint}`, |
|
statusCode >= 400 ? 'error' : 'info', |
|
'api', |
|
{ |
|
method, |
|
endpoint, |
|
statusCode, |
|
duration, |
|
request: requestData, |
|
response: responseData, |
|
}, |
|
{ |
|
component: 'api', |
|
action: method, |
|
}, |
|
); |
|
} |
|
|
|
|
|
logNetworkRequest( |
|
method: string, |
|
url: string, |
|
statusCode: number, |
|
duration: number, |
|
requestData?: any, |
|
responseData?: any, |
|
) { |
|
return this._addLog( |
|
`${method} ${url}`, |
|
statusCode >= 400 ? 'error' : 'info', |
|
'network', |
|
{ |
|
method, |
|
url, |
|
statusCode, |
|
duration, |
|
request: requestData, |
|
response: responseData, |
|
}, |
|
{ |
|
component: 'network', |
|
action: method, |
|
}, |
|
); |
|
} |
|
|
|
|
|
logAuthEvent(event: string, success: boolean, details?: Record<string, any>) { |
|
return this._addLog( |
|
`Auth ${event} ${success ? 'succeeded' : 'failed'}`, |
|
success ? 'info' : 'error', |
|
'auth', |
|
details, |
|
{ |
|
component: 'auth', |
|
action: event, |
|
}, |
|
); |
|
} |
|
|
|
|
|
logPerformance(operation: string, duration: number, details?: Record<string, any>) { |
|
return this._addLog( |
|
`Performance: ${operation}`, |
|
duration > 1000 ? 'warning' : 'info', |
|
'performance', |
|
{ |
|
operation, |
|
duration, |
|
...details, |
|
}, |
|
{ |
|
component: 'performance', |
|
action: 'metric', |
|
}, |
|
); |
|
} |
|
|
|
|
|
logErrorWithStack(error: Error, category: LogEntry['category'] = 'error', details?: Record<string, any>) { |
|
return this._addLog( |
|
error.message, |
|
'error', |
|
category, |
|
{ |
|
...details, |
|
name: error.name, |
|
stack: error.stack, |
|
}, |
|
{ |
|
component: category, |
|
action: 'error', |
|
}, |
|
); |
|
} |
|
|
|
|
|
refreshLogs() { |
|
const currentLogs = this._logs.get(); |
|
this._logs.set({ ...currentLogs }); |
|
} |
|
|
|
|
|
logInfo(message: string, details: LogDetails) { |
|
return this._addLog(message, 'info', 'system', details); |
|
} |
|
|
|
logSuccess(message: string, details: LogDetails) { |
|
return this._addLog(message, 'info', 'system', { ...details, success: true }); |
|
} |
|
|
|
logApiRequest( |
|
method: string, |
|
url: string, |
|
details: { |
|
method: string; |
|
url: string; |
|
statusCode: number; |
|
duration: number; |
|
request: any; |
|
response: any; |
|
}, |
|
) { |
|
return this._addApiLog(`API ${method} ${url}`, method, url, details); |
|
} |
|
|
|
logSettingsChange(component: string, setting: string, oldValue: any, newValue: any) { |
|
return this._addLog( |
|
`Settings changed in ${component}: ${setting}`, |
|
'info', |
|
'settings', |
|
{ |
|
setting, |
|
previousValue: oldValue, |
|
newValue, |
|
}, |
|
{ |
|
component, |
|
action: 'settings_change', |
|
previousValue: oldValue, |
|
newValue, |
|
}, |
|
); |
|
} |
|
|
|
logFeatureToggle(featureId: string, enabled: boolean) { |
|
return this._addLog( |
|
`Feature ${featureId} ${enabled ? 'enabled' : 'disabled'}`, |
|
'info', |
|
'feature', |
|
{ featureId, enabled }, |
|
{ |
|
component: 'features', |
|
action: 'feature_toggle', |
|
}, |
|
); |
|
} |
|
|
|
logTaskOperation(taskId: string, operation: string, status: string, details?: any) { |
|
return this._addLog( |
|
`Task ${taskId}: ${operation} - ${status}`, |
|
'info', |
|
'task', |
|
{ taskId, operation, status, ...details }, |
|
{ |
|
component: 'task-manager', |
|
action: 'task_operation', |
|
}, |
|
); |
|
} |
|
|
|
logProviderAction(provider: string, action: string, success: boolean, details?: any) { |
|
return this._addLog( |
|
`Provider ${provider}: ${action} - ${success ? 'Success' : 'Failed'}`, |
|
success ? 'info' : 'error', |
|
'provider', |
|
{ provider, action, success, ...details }, |
|
{ |
|
component: 'providers', |
|
action: 'provider_action', |
|
}, |
|
); |
|
} |
|
|
|
logPerformanceMetric(component: string, operation: string, duration: number, details?: any) { |
|
return this._addLog( |
|
`Performance: ${component} - ${operation} took ${duration}ms`, |
|
duration > 1000 ? 'warning' : 'info', |
|
'performance', |
|
{ component, operation, duration, ...details }, |
|
{ |
|
component, |
|
action: 'performance_metric', |
|
}, |
|
); |
|
} |
|
} |
|
|
|
export const logStore = new LogStore(); |
|
|