Skip to content

Commit 964db89

Browse files
rcourtmanclaude
andcommitted
feat: implement webhook notifications for alerts
- Add comprehensive webhook support for Discord, Slack, Teams - Rich embeds with color-coded severity and inline fields - Webhook configuration UI with test functionality - Dual payload format (Discord embeds + Slack attachments) - Error handling with timeout and proper HTTP responses - Test webhook endpoint with sample alert data 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2c61e09 commit 964db89

File tree

3 files changed

+375
-5
lines changed

3 files changed

+375
-5
lines changed

server/alertManager.js

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const EventEmitter = require('events');
22
const fs = require('fs').promises;
33
const path = require('path');
44
const nodemailer = require('nodemailer');
5+
const axios = require('axios');
56
const customThresholdManager = require('./customThresholds');
67

78
class AlertManager extends EventEmitter {
@@ -1134,15 +1135,135 @@ This alert was generated by Pulse monitoring system.
11341135
}
11351136

11361137
/**
1137-
* Send webhook notification (placeholder for future implementation)
1138+
* Send webhook notification
11381139
*/
11391140
async sendWebhookNotification(channel, alert) {
11401141
if (!channel.config.url) {
11411142
throw new Error('Webhook URL not configured');
11421143
}
1143-
1144-
// Placeholder for webhook implementation
1145-
console.log(`[WEBHOOK] Would send to: ${channel.config.url}`);
1144+
1145+
const severityEmoji = {
1146+
'info': '💙',
1147+
'warning': '⚠️',
1148+
'critical': '🚨'
1149+
};
1150+
1151+
const payload = {
1152+
timestamp: new Date(alert.timestamp).toISOString(),
1153+
alert: {
1154+
id: alert.id,
1155+
rule: {
1156+
name: alert.rule.name,
1157+
description: alert.rule.description,
1158+
severity: alert.rule.severity,
1159+
metric: alert.rule.metric
1160+
},
1161+
guest: {
1162+
name: alert.guest.name,
1163+
id: alert.guest.id,
1164+
type: alert.guest.type,
1165+
node: alert.guest.node,
1166+
status: alert.guest.status
1167+
},
1168+
value: alert.value,
1169+
threshold: alert.threshold,
1170+
emoji: severityEmoji[alert.rule.severity] || '📢'
1171+
},
1172+
// Discord/Slack compatible format
1173+
embeds: [{
1174+
title: `${severityEmoji[alert.rule.severity] || '📢'} ${alert.rule.name}`,
1175+
description: alert.rule.description,
1176+
color: alert.rule.severity === 'critical' ? 15158332 : // Red
1177+
alert.rule.severity === 'warning' ? 15844367 : // Orange
1178+
3447003, // Blue
1179+
fields: [
1180+
{
1181+
name: 'VM/LXC',
1182+
value: `${alert.guest.name} (${alert.guest.type} ${alert.guest.id})`,
1183+
inline: true
1184+
},
1185+
{
1186+
name: 'Node',
1187+
value: alert.guest.node,
1188+
inline: true
1189+
},
1190+
{
1191+
name: 'Status',
1192+
value: alert.guest.status,
1193+
inline: true
1194+
},
1195+
{
1196+
name: 'Metric',
1197+
value: alert.rule.metric.toUpperCase(),
1198+
inline: true
1199+
},
1200+
{
1201+
name: 'Current Value',
1202+
value: `${alert.value}%`,
1203+
inline: true
1204+
},
1205+
{
1206+
name: 'Threshold',
1207+
value: `${alert.threshold}%`,
1208+
inline: true
1209+
}
1210+
],
1211+
footer: {
1212+
text: 'Pulse Monitoring System'
1213+
},
1214+
timestamp: new Date(alert.timestamp).toISOString()
1215+
}],
1216+
// Slack compatible format
1217+
text: `${severityEmoji[alert.rule.severity] || '📢'} *${alert.rule.name}*`,
1218+
attachments: [{
1219+
color: alert.rule.severity === 'critical' ? 'danger' :
1220+
alert.rule.severity === 'warning' ? 'warning' : 'good',
1221+
fields: [
1222+
{
1223+
title: 'VM/LXC',
1224+
value: `${alert.guest.name} (${alert.guest.type} ${alert.guest.id})`,
1225+
short: true
1226+
},
1227+
{
1228+
title: 'Node',
1229+
value: alert.guest.node,
1230+
short: true
1231+
},
1232+
{
1233+
title: 'Metric',
1234+
value: `${alert.rule.metric.toUpperCase()}: ${alert.value}% (threshold: ${alert.threshold}%)`,
1235+
short: false
1236+
}
1237+
],
1238+
footer: 'Pulse Monitoring',
1239+
ts: Math.floor(alert.timestamp / 1000)
1240+
}]
1241+
};
1242+
1243+
// Set appropriate headers
1244+
const headers = {
1245+
'Content-Type': 'application/json',
1246+
'User-Agent': 'Pulse-Monitoring/1.0',
1247+
...channel.config.headers
1248+
};
1249+
1250+
try {
1251+
const response = await axios.post(channel.config.url, payload, {
1252+
headers,
1253+
timeout: 10000, // 10 second timeout
1254+
maxRedirects: 3
1255+
});
1256+
1257+
console.log(`[WEBHOOK] Alert sent to: ${channel.config.url} (${response.status})`);
1258+
} catch (error) {
1259+
if (error.response) {
1260+
throw new Error(`Webhook failed: ${error.response.status} ${error.response.statusText}`);
1261+
} else if (error.request) {
1262+
throw new Error(`Webhook failed: No response from ${channel.config.url}`);
1263+
} else {
1264+
throw new Error(`Webhook failed: ${error.message}`);
1265+
}
1266+
}
11461267
}
11471268

11481269
destroy() {

server/index.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,145 @@ Pulse Monitoring System
861861
}
862862
});
863863

864+
// Test webhook endpoint
865+
app.post('/api/test-webhook', async (req, res) => {
866+
try {
867+
const { url, enabled } = req.body;
868+
869+
if (!url) {
870+
return res.status(400).json({
871+
success: false,
872+
error: 'Webhook URL is required for testing'
873+
});
874+
}
875+
876+
// Create test webhook payload
877+
const axios = require('axios');
878+
const testPayload = {
879+
timestamp: new Date().toISOString(),
880+
alert: {
881+
id: 'test-alert-' + Date.now(),
882+
rule: {
883+
name: 'Webhook Test Alert',
884+
description: 'This is a test alert to verify webhook configuration',
885+
severity: 'info',
886+
metric: 'test'
887+
},
888+
guest: {
889+
name: 'Test-VM',
890+
id: '999',
891+
type: 'qemu',
892+
node: 'test-node',
893+
status: 'running'
894+
},
895+
value: 75,
896+
threshold: 80,
897+
emoji: '🧪'
898+
},
899+
// Discord/Slack compatible format
900+
embeds: [{
901+
title: '🧪 Webhook Test Alert',
902+
description: 'This is a test alert to verify webhook configuration',
903+
color: 3447003, // Blue
904+
fields: [
905+
{
906+
name: 'VM/LXC',
907+
value: 'Test-VM (qemu 999)',
908+
inline: true
909+
},
910+
{
911+
name: 'Node',
912+
value: 'test-node',
913+
inline: true
914+
},
915+
{
916+
name: 'Status',
917+
value: 'running',
918+
inline: true
919+
},
920+
{
921+
name: 'Metric',
922+
value: 'TEST',
923+
inline: true
924+
},
925+
{
926+
name: 'Current Value',
927+
value: '75%',
928+
inline: true
929+
},
930+
{
931+
name: 'Threshold',
932+
value: '80%',
933+
inline: true
934+
}
935+
],
936+
footer: {
937+
text: 'Pulse Monitoring System - Test Message'
938+
},
939+
timestamp: new Date().toISOString()
940+
}],
941+
// Slack compatible format
942+
text: '🧪 *Webhook Test Alert*',
943+
attachments: [{
944+
color: 'good',
945+
fields: [
946+
{
947+
title: 'VM/LXC',
948+
value: 'Test-VM (qemu 999)',
949+
short: true
950+
},
951+
{
952+
title: 'Node',
953+
value: 'test-node',
954+
short: true
955+
},
956+
{
957+
title: 'Status',
958+
value: 'Webhook configuration test successful!',
959+
short: false
960+
}
961+
],
962+
footer: 'Pulse Monitoring - Test',
963+
ts: Math.floor(Date.now() / 1000)
964+
}]
965+
};
966+
967+
// Send test webhook
968+
const response = await axios.post(url, testPayload, {
969+
headers: {
970+
'Content-Type': 'application/json',
971+
'User-Agent': 'Pulse-Monitoring/1.0'
972+
},
973+
timeout: 10000, // 10 second timeout
974+
maxRedirects: 3
975+
});
976+
977+
console.log(`[WEBHOOK TEST] Test webhook sent successfully to: ${url} (${response.status})`);
978+
res.json({
979+
success: true,
980+
message: 'Test webhook sent successfully!',
981+
status: response.status
982+
});
983+
984+
} catch (error) {
985+
console.error('[WEBHOOK TEST] Failed to send test webhook:', error);
986+
987+
let errorMessage = 'Failed to send test webhook';
988+
if (error.response) {
989+
errorMessage = `Webhook failed: ${error.response.status} ${error.response.statusText}`;
990+
} else if (error.request) {
991+
errorMessage = `Webhook failed: No response from ${url}`;
992+
} else {
993+
errorMessage = `Webhook failed: ${error.message}`;
994+
}
995+
996+
res.status(400).json({
997+
success: false,
998+
error: errorMessage
999+
});
1000+
}
1001+
});
1002+
8641003
// Global error handler for unhandled API errors
8651004
app.use((err, req, res, next) => {
8661005
console.error('Unhandled API error:', err);

0 commit comments

Comments
 (0)