Add heartbeat monitoring, alert rules, cert badges, webhook templates, and status page visibility
- Heartbeat type: cron jobs POST to /api/heartbeat/:token to stay alive; scheduler marks
them down if no ping is received within the configured timeout window
- Alert rules: per-resource consecutive_failures_threshold now respected in handleIncident,
overriding the global setting; GET /api/resources/:id/alert-rules endpoint added
- SSL cert expiry badge: dashboard rows surface days_remaining for TLS monitors, turning
orange at ≤30 days and red at ≤7 days
- Webhook payload templates: {{name}}/{{status}}/{{url}}/{{type}}/{{message}}/{{timestamp}}
variables interpolated before POST; Slack-compatible text wrapping as fallback
- Status page visibility: is_public column per resource; GET /api/status-page returns only
public resources; Status.js updated to use the new endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
34986b5760
commit
a6cf6a9762
16 files changed
+554
-422
No files matched your search
@@ -98,12 +98,36 @@ class MonitorService {
|
||||
case 'icmp':
|
||||
case 'ping':
|
||||
return this.checkIcmp(resource, startTime, base);
|
||||
case 'heartbeat':
|
||||
return this.checkHeartbeat(resource, startTime, base);
|
||||
default:
|
||||
// Fallback to HTTP if unknown
|
||||
return this.checkHttp(resource, startTime, base);
|
||||
}
|
||||
}
|
||||
|
||||
checkHeartbeat(resource, startTime, base) {
|
||||
const now = Date.now();
|
||||
const lastPing = resource.last_heartbeat_at
|
||||
? new Date(resource.last_heartbeat_at).getTime()
|
||||
: null;
|
||||
const timeout = resource.heartbeat_timeout || 300000;
|
||||
|
||||
if (!lastPing) {
|
||||
return { ...base, status: 'down', response_time: null, error_message: 'No heartbeat received yet' };
|
||||
}
|
||||
const elapsed = now - lastPing;
|
||||
if (elapsed > timeout) {
|
||||
return {
|
||||
...base,
|
||||
status: 'down',
|
||||
response_time: elapsed,
|
||||
error_message: `Missed heartbeat — ${Math.round(elapsed / 1000)}s since last ping (timeout: ${timeout / 1000}s)`,
|
||||
};
|
||||
}
|
||||
return { ...base, status: 'up', response_time: elapsed };
|
||||
}
|
||||
|
||||
async checkHttp(resource, startTime, base) {
|
||||
let result = { ...base };
|
||||
const circuitBreaker = this.getResourceCircuitBreaker(resource.id);
|
||||
@@ -353,9 +377,12 @@ class MonitorService {
|
||||
handleIncident(resourceId, isDown) {
|
||||
const resource = db.prepare('SELECT * FROM resources WHERE id = ?').get(resourceId);
|
||||
|
||||
// Get incident failure threshold from settings (default 10)
|
||||
// Per-resource threshold overrides global setting
|
||||
const setting = db.prepare("SELECT value FROM settings WHERE key = 'incident_failure_threshold'").get();
|
||||
const threshold = parseInt(setting?.value || '10');
|
||||
const globalThreshold = parseInt(setting?.value || '10');
|
||||
const threshold = (resource?.consecutive_failures_threshold > 0)
|
||||
? resource.consecutive_failures_threshold
|
||||
: globalThreshold;
|
||||
|
||||
const activeIncident = db.prepare(`
|
||||
SELECT * FROM incidents
|
||||
@@ -371,8 +398,8 @@ class MonitorService {
|
||||
LIMIT ?
|
||||
`).all(resourceId, threshold);
|
||||
|
||||
const consecutiveFailures = recentChecks.every(c => c.status === 'down') ? recentChecks.length :
|
||||
recentChecks.findIndex(c => c.status === 'up') + 1;
|
||||
const firstUpIdx = recentChecks.findIndex(c => c.status === 'up');
|
||||
const consecutiveFailures = firstUpIdx === -1 ? recentChecks.length : firstUpIdx;
|
||||
|
||||
// Only trigger incident if we've hit the threshold
|
||||
if (consecutiveFailures >= threshold && !activeIncident) {
|
||||
|
||||
Reference in new issue
Block a user