bolt.diy/app/lib/stores/logs.ts
2025-01-28 22:57:06 +01:00

520 lines
12 KiB
TypeScript

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; // Maximum number of logs to keep in memory
class LogStore {
private _logs = map<Record<string, LogEntry>>({});
showLogs = atom(true);
private _readLogs = new Set<string>();
constructor() {
// Load saved logs from cookies on initialization
this._loadLogs();
// Only load read logs in browser environment
if (typeof window !== 'undefined') {
this._loadReadLogs();
}
}
// Expose the logs store for subscription
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);
}
}
// Base log method for general logging
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;
}
// Specialized method for API logging
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,
});
}
// System events
logSystem(message: string, details?: Record<string, any>) {
return this._addLog(message, 'info', 'system', details);
}
// Provider events
logProvider(message: string, details?: Record<string, any>) {
return this._addLog(message, 'info', 'provider', details);
}
// User actions
logUserAction(message: string, details?: Record<string, any>) {
return this._addLog(message, 'info', 'user', details);
}
// API Connection Logging
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(),
});
}
// Authentication Logging
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(),
});
}
// Network Status Logging
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(),
});
}
// Database Operations Logging
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(),
});
}
// Error events
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);
}
// Warning events
logWarning(message: string, details?: Record<string, any>) {
return this._addLog(message, 'warning', 'system', details);
}
// Debug events
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();
}
// API interactions
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,
},
);
}
// Network operations
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,
},
);
}
// Authentication events
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,
},
);
}
// Performance tracking
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',
},
);
}
// Error handling
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',
},
);
}
// Refresh logs (useful for real-time updates)
refreshLogs() {
const currentLogs = this._logs.get();
this._logs.set({ ...currentLogs });
}
// Enhanced logging methods
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();