mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-10 06:00:19 +00:00
520 lines
12 KiB
TypeScript
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();
|