feat: premium tier features — auto-backup scheduling, resource alerting, bundled workflows, one-click revert, notification manager
This commit is contained in:
@@ -16,6 +16,7 @@ const STATS_FILE = process.env.STATS_FILE || path.join(__dirname, 'container-sta
|
||||
const STATS_HOURLY_FILE = process.env.STATS_HOURLY_FILE || path.join(__dirname, 'container-stats-hourly.json');
|
||||
const STATS_DAILY_FILE = process.env.STATS_DAILY_FILE || path.join(__dirname, 'container-stats-daily.json');
|
||||
const ALERT_CONFIG_FILE = process.env.ALERT_CONFIG_FILE || path.join(__dirname, 'alert-config.json');
|
||||
const ALERT_HISTORY_FILE = process.env.ALERT_HISTORY_FILE || path.join(__dirname, 'alert-history.json');
|
||||
const STATS_RETENTION_HOURS = parseInt(process.env.STATS_RETENTION_HOURS || '168', 10); // 7 days raw
|
||||
const STATS_HOURLY_RETENTION_DAYS = parseInt(process.env.STATS_HOURLY_RETENTION_DAYS || '30', 10); // 30 days hourly
|
||||
const STATS_DAILY_RETENTION_DAYS = parseInt(process.env.STATS_DAILY_RETENTION_DAYS || '365', 10); // 365 days daily
|
||||
@@ -35,11 +36,21 @@ class ResourceMonitor extends EventEmitter {
|
||||
this.dailyHistory = new Map(); // containerId -> { name, samples: [...] } (daily avg, 365d)
|
||||
this.alerts = new Map(); // containerId -> alert config
|
||||
this.lastAlerts = new Map(); // containerId -> last alert timestamp
|
||||
this.alertHistory = []; // alert history entries
|
||||
this.notificationManager = null;
|
||||
|
||||
this.loadStats();
|
||||
this.loadHourlyStats();
|
||||
this.loadDailyStats();
|
||||
this.loadAlertConfig();
|
||||
this.loadAlertHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notification manager for sending alerts
|
||||
*/
|
||||
setNotificationManager(nm) {
|
||||
this.notificationManager = nm;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,20 +296,58 @@ class ResourceMonitor extends EventEmitter {
|
||||
|
||||
if (alerts.length > 0) {
|
||||
this.lastAlerts.set(containerId, now);
|
||||
|
||||
this.emit('alert', {
|
||||
|
||||
// Add alert history entries
|
||||
for (const alert of alerts) {
|
||||
this.addAlertHistoryEntry({
|
||||
id: `${containerId}-${Date.now()}-${alert.type}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
containerId,
|
||||
containerName,
|
||||
type: alert.type,
|
||||
metric: alert.type,
|
||||
value: alert.value,
|
||||
threshold: alert.threshold,
|
||||
severity: alert.severity,
|
||||
notified: !!this.notificationManager,
|
||||
autoRestartTriggered: !!alertConfig.autoRestart
|
||||
});
|
||||
}
|
||||
|
||||
const alertPayload = {
|
||||
containerId,
|
||||
containerName,
|
||||
timestamp: new Date().toISOString(),
|
||||
alerts,
|
||||
stats,
|
||||
config: alertConfig
|
||||
});
|
||||
};
|
||||
|
||||
this.emit('alert', alertPayload);
|
||||
|
||||
// Send notification if manager is configured
|
||||
if (this.notificationManager) {
|
||||
this.notificationManager.sendAlert(alertPayload).catch(err => {
|
||||
console.error('[ResourceMonitor] Failed to send alert notification:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-restart if configured
|
||||
if (alertConfig.autoRestart) {
|
||||
this.restartContainer(containerId, containerName, alerts);
|
||||
}
|
||||
|
||||
// Trigger bundled workflows for resource-alert
|
||||
this.triggerWorkflows('resource-alert', {
|
||||
containerId,
|
||||
containerName,
|
||||
alerts,
|
||||
stats,
|
||||
diskPercent: (stats.disk?.readBytes + stats.disk?.writeBytes) > 0
|
||||
? Math.round((stats.disk.readBytes / (stats.disk.readBytes + stats.disk.writeBytes)) * 100)
|
||||
: 0,
|
||||
host: require('os').hostname()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,11 +367,55 @@ class ResourceMonitor extends EventEmitter {
|
||||
timestamp: new Date().toISOString(),
|
||||
reason: alerts
|
||||
});
|
||||
|
||||
// Send notification if manager is configured
|
||||
if (this.notificationManager) {
|
||||
this.notificationManager.send('auto-restart', {
|
||||
containerId,
|
||||
containerName,
|
||||
timestamp: new Date().toISOString(),
|
||||
reason: alerts
|
||||
}).catch(err => {
|
||||
console.error('[ResourceMonitor] Failed to send auto-restart notification:', err.message);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[ResourceMonitor] Failed to restart ${containerName}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger bundled workflows for an event
|
||||
*/
|
||||
triggerWorkflows(eventType, eventData) {
|
||||
if (!this.workflowEngine) {
|
||||
console.log('[ResourceMonitor] Workflow engine not set, skipping workflow trigger');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.workflowEngine.triggerForEvent(eventType, eventData)
|
||||
.then(results => {
|
||||
if (results && results.length > 0) {
|
||||
console.log(`[ResourceMonitor] Triggered ${results.length} workflow(s) for ${eventType}`);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[ResourceMonitor] Workflow trigger error:', err.message);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ResourceMonitor] Error triggering workflows:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the workflow engine for triggering workflows
|
||||
*/
|
||||
setWorkflowEngine(workflowEngine) {
|
||||
this.workflowEngine = workflowEngine;
|
||||
console.log('[ResourceMonitor] Workflow engine configured');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current stats for a container
|
||||
*/
|
||||
@@ -430,6 +523,62 @@ class ResourceMonitor extends EventEmitter {
|
||||
this.saveAlertConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all alert configurations
|
||||
*/
|
||||
getAllAlertConfigs() {
|
||||
const configs = {};
|
||||
for (const [containerId, config] of this.alerts.entries()) {
|
||||
configs[containerId] = config;
|
||||
}
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add entry to alert history
|
||||
*/
|
||||
addAlertHistoryEntry(entry) {
|
||||
this.alertHistory.unshift(entry);
|
||||
// Keep only last 1000 entries
|
||||
if (this.alertHistory.length > 1000) {
|
||||
this.alertHistory = this.alertHistory.slice(0, 1000);
|
||||
}
|
||||
this.saveAlertHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alert history
|
||||
*/
|
||||
getAlertHistory(limit = 50) {
|
||||
return this.alertHistory.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load alert history from disk
|
||||
*/
|
||||
loadAlertHistory() {
|
||||
try {
|
||||
if (fs.existsSync(ALERT_HISTORY_FILE)) {
|
||||
const data = JSON.parse(fs.readFileSync(ALERT_HISTORY_FILE, 'utf8'));
|
||||
this.alertHistory = Array.isArray(data) ? data : [];
|
||||
console.log(`[ResourceMonitor] Loaded ${this.alertHistory.length} alert history entries`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ResourceMonitor] Error loading alert history:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save alert history to disk
|
||||
*/
|
||||
saveAlertHistory() {
|
||||
try {
|
||||
fs.writeFileSync(ALERT_HISTORY_FILE, JSON.stringify(this.alertHistory, null, 2));
|
||||
} catch (error) {
|
||||
console.error('[ResourceMonitor] Error saving alert history:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old stats beyond retention period
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user