Files
skywatch/server/metrics.js
nmemmert db85573986 feat: Add real-time notification center with WebSocket integration
- Add in-app notification system with database storage
- Implement bell icon component with unread count badge in dashboard header
- Add full Notifications page tab with pagination and filtering (All/Unread/Read)
- Implement real-time WebSocket push notifications to all connected clients
- Add notification API endpoints: GET, PUT /read, DELETE, POST /clear
- Fix critical bug in notificationService where 'status' variable was undefined
- Add Notifications route to App.js routing
- Enhanced UI with notification popup in header and dedicated full-page view
- Auto-reconnect WebSocket with 5s timeout on disconnect
- Support rich notification content with resource details and timestamps
2026-01-14 14:51:29 -05:00

129 lines
3.4 KiB
JavaScript

const db = require('./database');
class MetricsCollector {
constructor() {
this.apiMetrics = new Map();
this.errorCounts = new Map();
this.startTime = Date.now();
}
// Track API request metrics
trackApiRequest(endpoint, method, statusCode, duration) {
const key = `${method}:${endpoint}`;
if (!this.apiMetrics.has(key)) {
this.apiMetrics.set(key, {
endpoint,
method,
totalRequests: 0,
successCount: 0,
errorCount: 0,
totalDuration: 0,
minDuration: Infinity,
maxDuration: 0,
statusCodes: {}
});
}
const metrics = this.apiMetrics.get(key);
metrics.totalRequests++;
metrics.totalDuration += duration;
metrics.minDuration = Math.min(metrics.minDuration, duration);
metrics.maxDuration = Math.max(metrics.maxDuration, duration);
if (statusCode >= 200 && statusCode < 400) {
metrics.successCount++;
} else {
metrics.errorCount++;
}
metrics.statusCodes[statusCode] = (metrics.statusCodes[statusCode] || 0) + 1;
}
// Track application errors
trackError(error, context = {}) {
const errorKey = error.message || 'Unknown Error';
const errorData = {
message: error.message,
stack: error.stack,
context,
timestamp: new Date().toISOString(),
count: (this.errorCounts.get(errorKey) || 0) + 1
};
this.errorCounts.set(errorKey, errorData.count);
// Log to database
try {
db.prepare(`
INSERT INTO error_logs (error_message, stack_trace, context, occurred_at)
VALUES (?, ?, ?, datetime('now'))
`).run(
error.message,
error.stack || '',
JSON.stringify(context)
);
} catch (err) {
// Silent failure - don't crash on logging errors
}
return errorData;
}
// Get current metrics snapshot
getMetrics() {
const apiStats = [];
this.apiMetrics.forEach((metrics, key) => {
const avgDuration = metrics.totalRequests > 0
? Math.round(metrics.totalDuration / metrics.totalRequests)
: 0;
apiStats.push({
endpoint: metrics.endpoint,
method: metrics.method,
totalRequests: metrics.totalRequests,
successRate: metrics.totalRequests > 0
? ((metrics.successCount / metrics.totalRequests) * 100).toFixed(2)
: 0,
avgResponseTime: avgDuration,
minResponseTime: metrics.minDuration === Infinity ? 0 : metrics.minDuration,
maxResponseTime: metrics.maxDuration,
statusCodes: metrics.statusCodes
});
});
return {
uptime: Math.floor((Date.now() - this.startTime) / 1000),
apiStats: apiStats.sort((a, b) => b.totalRequests - a.totalRequests),
errorCount: Array.from(this.errorCounts.values()).reduce((sum, count) => sum + count, 0),
uniqueErrors: this.errorCounts.size
};
}
// Get recent errors from database
getRecentErrors(limit = 50) {
try {
return db.prepare(`
SELECT
id,
error_message,
stack_trace,
context,
REPLACE(occurred_at, ' ', 'T') || 'Z' as occurred_at
FROM error_logs
ORDER BY occurred_at DESC
LIMIT ?
`).all(limit);
} catch (err) {
return [];
}
}
// Clear in-memory metrics (for testing/reset)
reset() {
this.apiMetrics.clear();
this.errorCounts.clear();
}
}
module.exports = new MetricsCollector();