Initial commit - Resource Monitor with Ubuntu installer
This commit is contained in:
commit
17c366dcae
16 files changed
+3865
No files matched your search
@@ -0,0 +1,44 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const db = new Database(path.join(__dirname, '../data/monitor.db'));
|
||||
|
||||
// Create tables
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'http',
|
||||
check_interval INTEGER DEFAULT 60000,
|
||||
timeout INTEGER DEFAULT 5000,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS checks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
resource_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
response_time INTEGER,
|
||||
status_code INTEGER,
|
||||
error_message TEXT,
|
||||
checked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS incidents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
resource_id INTEGER NOT NULL,
|
||||
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
resolved_at DATETIME,
|
||||
notified INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_resource_id ON checks(resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_checked_at ON checks(checked_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_resource_id ON incidents(resource_id);
|
||||
`);
|
||||
|
||||
module.exports = db;
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
const db = require('./database');
|
||||
const scheduler = require('./scheduler');
|
||||
const monitorService = require('./monitorService');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// API Routes
|
||||
|
||||
// Get all resources
|
||||
app.get('/api/resources', (req, res) => {
|
||||
const resources = db.prepare('SELECT * FROM resources ORDER BY name').all();
|
||||
res.json(resources);
|
||||
});
|
||||
|
||||
// Get single resource with stats
|
||||
app.get('/api/resources/:id', (req, res) => {
|
||||
const resource = db.prepare('SELECT * FROM resources WHERE id = ?').get(req.params.id);
|
||||
if (!resource) {
|
||||
return res.status(404).json({ error: 'Resource not found' });
|
||||
}
|
||||
|
||||
const stats = monitorService.getResourceStats(resource.id, 24);
|
||||
const lastCheck = monitorService.getLastCheck(resource.id);
|
||||
const activeIncident = db.prepare(`
|
||||
SELECT * FROM incidents
|
||||
WHERE resource_id = ? AND resolved_at IS NULL
|
||||
`).get(resource.id);
|
||||
|
||||
res.json({
|
||||
...resource,
|
||||
stats,
|
||||
lastCheck,
|
||||
hasActiveIncident: !!activeIncident,
|
||||
});
|
||||
});
|
||||
|
||||
// Create resource
|
||||
app.post('/api/resources', (req, res) => {
|
||||
const { name, url, type, check_interval, timeout } = req.body;
|
||||
|
||||
if (!name || !url) {
|
||||
return res.status(400).json({ error: 'Name and URL are required' });
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO resources (name, url, type, check_interval, timeout)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
name,
|
||||
url,
|
||||
type || 'http',
|
||||
check_interval || 60000,
|
||||
timeout || 5000
|
||||
);
|
||||
|
||||
res.json({ id: result.lastInsertRowid, message: 'Resource created' });
|
||||
});
|
||||
|
||||
// Update resource
|
||||
app.put('/api/resources/:id', (req, res) => {
|
||||
const { name, url, type, check_interval, timeout, enabled } = req.body;
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE resources
|
||||
SET name = ?, url = ?, type = ?, check_interval = ?, timeout = ?, enabled = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(name, url, type, check_interval, timeout, enabled ? 1 : 0, req.params.id);
|
||||
res.json({ message: 'Resource updated' });
|
||||
});
|
||||
|
||||
// Delete resource
|
||||
app.delete('/api/resources/:id', (req, res) => {
|
||||
db.prepare('DELETE FROM resources WHERE id = ?').run(req.params.id);
|
||||
res.json({ message: 'Resource deleted' });
|
||||
});
|
||||
|
||||
// Get dashboard overview
|
||||
app.get('/api/dashboard', (req, res) => {
|
||||
const resources = db.prepare('SELECT * FROM resources').all();
|
||||
const overview = resources.map(resource => {
|
||||
const lastCheck = monitorService.getLastCheck(resource.id);
|
||||
const stats = monitorService.getResourceStats(resource.id, 24);
|
||||
const activeIncident = db.prepare(`
|
||||
SELECT * FROM incidents
|
||||
WHERE resource_id = ? AND resolved_at IS NULL
|
||||
`).get(resource.id);
|
||||
|
||||
return {
|
||||
id: resource.id,
|
||||
name: resource.name,
|
||||
url: resource.url,
|
||||
enabled: resource.enabled,
|
||||
status: lastCheck?.status || 'unknown',
|
||||
uptime: stats.uptime,
|
||||
avgResponseTime: stats.avgResponseTime,
|
||||
lastCheck: lastCheck?.checked_at,
|
||||
hasActiveIncident: !!activeIncident,
|
||||
};
|
||||
});
|
||||
|
||||
res.json(overview);
|
||||
});
|
||||
|
||||
// Get incidents
|
||||
app.get('/api/incidents', (req, res) => {
|
||||
const incidents = db.prepare(`
|
||||
SELECT i.*, r.name as resource_name, r.url as resource_url
|
||||
FROM incidents i
|
||||
JOIN resources r ON i.resource_id = r.id
|
||||
ORDER BY i.started_at DESC
|
||||
LIMIT 50
|
||||
`).all();
|
||||
|
||||
res.json(incidents);
|
||||
});
|
||||
|
||||
// Get settings
|
||||
app.get('/api/settings', (req, res) => {
|
||||
const settings = {
|
||||
email_enabled: process.env.EMAIL_ENABLED === 'true',
|
||||
email_host: process.env.EMAIL_HOST || 'smtp.gmail.com',
|
||||
email_port: parseInt(process.env.EMAIL_PORT) || 587,
|
||||
email_user: process.env.EMAIL_USER || '',
|
||||
email_pass: '', // Don't send password to client
|
||||
email_from: process.env.EMAIL_FROM || '',
|
||||
email_to: process.env.EMAIL_TO || '',
|
||||
webhook_enabled: process.env.WEBHOOK_ENABLED === 'true',
|
||||
webhook_url: process.env.WEBHOOK_URL || '',
|
||||
check_interval: parseInt(process.env.CHECK_INTERVAL) || 60000,
|
||||
timeout: parseInt(process.env.TIMEOUT) || 5000,
|
||||
};
|
||||
res.json(settings);
|
||||
});
|
||||
|
||||
// Save settings
|
||||
app.post('/api/settings', (req, res) => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const envPath = path.join(__dirname, '../.env');
|
||||
|
||||
const {
|
||||
email_enabled,
|
||||
email_host,
|
||||
email_port,
|
||||
email_user,
|
||||
email_pass,
|
||||
email_from,
|
||||
email_to,
|
||||
webhook_enabled,
|
||||
webhook_url,
|
||||
check_interval,
|
||||
timeout,
|
||||
} = req.body;
|
||||
|
||||
const envContent = `PORT=${process.env.PORT || 3001}
|
||||
EMAIL_ENABLED=${email_enabled}
|
||||
EMAIL_HOST=${email_host}
|
||||
EMAIL_PORT=${email_port}
|
||||
EMAIL_USER=${email_user}
|
||||
EMAIL_PASS=${email_pass || process.env.EMAIL_PASS || ''}
|
||||
EMAIL_FROM=${email_from}
|
||||
EMAIL_TO=${email_to}
|
||||
WEBHOOK_ENABLED=${webhook_enabled}
|
||||
WEBHOOK_URL=${webhook_url}
|
||||
CHECK_INTERVAL=${check_interval}
|
||||
TIMEOUT=${timeout}
|
||||
`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(envPath, envContent);
|
||||
res.json({ message: 'Settings saved successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
res.status(500).json({ error: 'Failed to save settings' });
|
||||
}
|
||||
});
|
||||
|
||||
// Test email
|
||||
app.post('/api/test-email', async (req, res) => {
|
||||
const nodemailer = require('nodemailer');
|
||||
const { email_host, email_port, email_user, email_pass, email_from, email_to } = req.body;
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: email_host,
|
||||
port: parseInt(email_port),
|
||||
secure: false,
|
||||
auth: {
|
||||
user: email_user,
|
||||
pass: email_pass,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: email_from,
|
||||
to: email_to,
|
||||
subject: 'Test Email from Resource Monitor',
|
||||
text: `This is a test email from your Resource Monitor system.\n\nTime: ${new Date().toLocaleString()}\n\nIf you received this, your email notifications are working correctly!`,
|
||||
});
|
||||
|
||||
res.json({ message: 'Test email sent successfully! Check your inbox.' });
|
||||
} catch (error) {
|
||||
console.error('Test email error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Test webhook
|
||||
app.post('/api/test-webhook', async (req, res) => {
|
||||
const axios = require('axios');
|
||||
const { webhook_url } = req.body;
|
||||
|
||||
try {
|
||||
await axios.post(webhook_url, {
|
||||
resource: 'Test Resource',
|
||||
url: 'https://example.com',
|
||||
status: 'test',
|
||||
message: 'This is a test webhook from Resource Monitor',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
res.json({ message: 'Test webhook sent successfully!' });
|
||||
} catch (error) {
|
||||
console.error('Test webhook error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve React app in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(express.static(path.join(__dirname, '../client/build')));
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../client/build/index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on http://localhost:${PORT}`);
|
||||
scheduler.start();
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
const axios = require('axios');
|
||||
const db = require('./database');
|
||||
|
||||
class MonitorService {
|
||||
async checkResource(resource) {
|
||||
const startTime = Date.now();
|
||||
let result = {
|
||||
resource_id: resource.id,
|
||||
status: 'down',
|
||||
response_time: null,
|
||||
status_code: null,
|
||||
error_message: null,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.get(resource.url, {
|
||||
timeout: resource.timeout,
|
||||
validateStatus: () => true, // Don't throw on any status
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
const isUp = response.status >= 200 && response.status < 400;
|
||||
|
||||
result = {
|
||||
...result,
|
||||
status: isUp ? 'up' : 'down',
|
||||
response_time: responseTime,
|
||||
status_code: response.status,
|
||||
error_message: isUp ? null : `HTTP ${response.status}`,
|
||||
};
|
||||
} catch (error) {
|
||||
result.error_message = error.message;
|
||||
result.response_time = Date.now() - startTime;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
saveCheck(check) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO checks (resource_id, status, response_time, status_code, error_message)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
stmt.run(
|
||||
check.resource_id,
|
||||
check.status,
|
||||
check.response_time,
|
||||
check.status_code,
|
||||
check.error_message
|
||||
);
|
||||
}
|
||||
|
||||
getLastCheck(resourceId) {
|
||||
return db.prepare(`
|
||||
SELECT * FROM checks
|
||||
WHERE resource_id = ?
|
||||
ORDER BY checked_at DESC
|
||||
LIMIT 1
|
||||
`).get(resourceId);
|
||||
}
|
||||
|
||||
handleIncident(resourceId, isDown) {
|
||||
const activeIncident = db.prepare(`
|
||||
SELECT * FROM incidents
|
||||
WHERE resource_id = ? AND resolved_at IS NULL
|
||||
`).get(resourceId);
|
||||
|
||||
if (isDown && !activeIncident) {
|
||||
// Start new incident
|
||||
db.prepare(`
|
||||
INSERT INTO incidents (resource_id) VALUES (?)
|
||||
`).run(resourceId);
|
||||
return { type: 'started', incident: true };
|
||||
} else if (!isDown && activeIncident) {
|
||||
// Resolve incident
|
||||
db.prepare(`
|
||||
UPDATE incidents
|
||||
SET resolved_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(activeIncident.id);
|
||||
return { type: 'resolved', incident: activeIncident };
|
||||
}
|
||||
|
||||
return { type: 'none' };
|
||||
}
|
||||
|
||||
getResourceStats(resourceId, hours = 24) {
|
||||
const checks = db.prepare(`
|
||||
SELECT * FROM checks
|
||||
WHERE resource_id = ?
|
||||
AND checked_at >= datetime('now', '-' || ? || ' hours')
|
||||
ORDER BY checked_at ASC
|
||||
`).all(resourceId, hours);
|
||||
|
||||
const upChecks = checks.filter(c => c.status === 'up');
|
||||
const totalChecks = checks.length;
|
||||
const uptime = totalChecks > 0 ? (upChecks.length / totalChecks) * 100 : 0;
|
||||
|
||||
const responseTimes = checks
|
||||
.filter(c => c.response_time !== null)
|
||||
.map(c => c.response_time);
|
||||
|
||||
const avgResponseTime = responseTimes.length > 0
|
||||
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
uptime: uptime.toFixed(2),
|
||||
avgResponseTime: avgResponseTime.toFixed(0),
|
||||
totalChecks,
|
||||
checks: checks.slice(-50), // Last 50 checks
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MonitorService();
|
||||
@@ -0,0 +1,71 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const axios = require('axios');
|
||||
require('dotenv').config();
|
||||
|
||||
class NotificationService {
|
||||
constructor() {
|
||||
this.emailEnabled = process.env.EMAIL_ENABLED === 'true';
|
||||
this.webhookEnabled = process.env.WEBHOOK_ENABLED === 'true';
|
||||
|
||||
if (this.emailEnabled) {
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: parseInt(process.env.EMAIL_PORT),
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async sendAlert(resource, incident) {
|
||||
const message = incident.type === 'started'
|
||||
? `🔴 ${resource.name} is DOWN!\n\nURL: ${resource.url}\nTime: ${new Date().toLocaleString()}`
|
||||
: `🟢 ${resource.name} is back UP!\n\nURL: ${resource.url}\nTime: ${new Date().toLocaleString()}`;
|
||||
|
||||
const promises = [];
|
||||
|
||||
if (this.emailEnabled) {
|
||||
promises.push(this.sendEmail(resource, message, incident.type));
|
||||
}
|
||||
|
||||
if (this.webhookEnabled) {
|
||||
promises.push(this.sendWebhook(resource, message, incident.type));
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
async sendEmail(resource, message, type) {
|
||||
try {
|
||||
await this.transporter.sendMail({
|
||||
from: process.env.EMAIL_FROM,
|
||||
to: process.env.EMAIL_TO,
|
||||
subject: `Alert: ${resource.name} is ${type === 'started' ? 'DOWN' : 'UP'}`,
|
||||
text: message,
|
||||
});
|
||||
console.log(`Email sent for ${resource.name}`);
|
||||
} catch (error) {
|
||||
console.error('Email error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async sendWebhook(resource, message, type) {
|
||||
try {
|
||||
await axios.post(process.env.WEBHOOK_URL, {
|
||||
resource: resource.name,
|
||||
url: resource.url,
|
||||
status: type === 'started' ? 'down' : 'up',
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
console.log(`Webhook sent for ${resource.name}`);
|
||||
} catch (error) {
|
||||
console.error('Webhook error:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new NotificationService();
|
||||
@@ -0,0 +1,55 @@
|
||||
const cron = require('node-cron');
|
||||
const db = require('./database');
|
||||
const monitorService = require('./monitorService');
|
||||
const notificationService = require('./notificationService');
|
||||
|
||||
class Scheduler {
|
||||
constructor() {
|
||||
this.jobs = new Map();
|
||||
}
|
||||
|
||||
start() {
|
||||
// Run checks every minute
|
||||
cron.schedule('* * * * *', async () => {
|
||||
await this.runChecks();
|
||||
});
|
||||
|
||||
// Clean old checks every hour (keep last 7 days)
|
||||
cron.schedule('0 * * * *', () => {
|
||||
db.prepare(`
|
||||
DELETE FROM checks
|
||||
WHERE checked_at < datetime('now', '-7 days')
|
||||
`).run();
|
||||
});
|
||||
|
||||
console.log('Scheduler started');
|
||||
}
|
||||
|
||||
async runChecks() {
|
||||
const resources = db.prepare(`
|
||||
SELECT * FROM resources WHERE enabled = 1
|
||||
`).all();
|
||||
|
||||
for (const resource of resources) {
|
||||
try {
|
||||
const result = await monitorService.checkResource(resource);
|
||||
monitorService.saveCheck(result);
|
||||
|
||||
const incident = monitorService.handleIncident(
|
||||
resource.id,
|
||||
result.status === 'down'
|
||||
);
|
||||
|
||||
if (incident.type !== 'none') {
|
||||
await notificationService.sendAlert(resource, incident);
|
||||
}
|
||||
|
||||
console.log(`Checked ${resource.name}: ${result.status} (${result.response_time}ms)`);
|
||||
} catch (error) {
|
||||
console.error(`Error checking ${resource.name}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Scheduler();
|
||||
Reference in new issue
Block a user