This commit is contained in:
Alex Holliday committed 2026-02-16 21:48:03 +00:00
1 parent e18681606c
commit 0e1dc3e3fe
3 files changed
+173 -5

No files matched your search

@@ -62,8 +62,8 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
return "threshold_breach";
}
// Recovery from threshold breach
if (decision.notificationReason === "status_change" && monitor.status === "up") {
// Recovery from threshold breach (only for hardware monitors)
if (decision.notificationReason === "status_change" && monitor.status === "up" && monitor.type === "hardware") {
return "threshold_resolved";
}
@@ -156,7 +156,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
clientHost: string
): NotificationContent {
const title = `Threshold Breach: ${monitor.name}`;
const summary = `Hardware monitor "${monitor.name}" has breached one or more thresholds.`;
const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`;
const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse);
@@ -172,7 +172,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
private buildThresholdResolvedContent(monitor: Monitor, clientHost: string): NotificationContent {
const title = `Thresholds Resolved: ${monitor.name}`;
const summary = `Hardware monitor "${monitor.name}" thresholds have returned to normal.`;
const summary = `Monitor "${monitor.name}" thresholds have returned to normal.`;
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
return {
@@ -2,6 +2,7 @@ const SERVICE_NAME = "SlackProvider";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import {
buildHardwareAlerts,
buildHardwareNotificationMessage,
@@ -99,4 +100,162 @@ export class SlackProvider implements INotificationProvider {
return false;
}
}
/**
* New unified message format - builds Slack Block Kit payload from NotificationMessage
*/
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
if (!notification.address) {
return false;
}
const payload = this.buildSlackPayload(message);
try {
await got.post(notification.address, {
json: payload,
headers: {
"Content-Type": "application/json",
},
});
this.logger.info({
message: "[NEW] Slack notification sent via sendMessage",
service: SERVICE_NAME,
method: "sendMessage",
});
return true;
} catch (error) {
const err = error as Error;
this.logger.warn({
message: "[NEW] Slack alert failed via sendMessage",
service: SERVICE_NAME,
method: "sendMessage",
stack: err?.stack,
});
return false;
}
}
/**
* Build Slack Block Kit payload from NotificationMessage
* Uses Slack's rich formatting with sections, fields, and context blocks
*/
private buildSlackPayload(message: NotificationMessage): object {
const blocks: any[] = [];
// Determine color based on severity
const colorMap = {
critical: "#FF0000", // Red
warning: "#FFA500", // Orange
success: "#00FF00", // Green
info: "#0000FF", // Blue
};
const color = colorMap[message.severity] || "#808080";
// Header block with title
blocks.push({
type: "header",
text: {
type: "plain_text",
text: message.content.title,
emoji: true,
},
});
// Summary section
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: message.content.summary,
},
});
// Monitor details as fields
const monitorFields = [
{ type: "mrkdwn", text: `*Name:*\n${message.monitor.name}` },
{ type: "mrkdwn", text: `*Type:*\n${message.monitor.type}` },
{ type: "mrkdwn", text: `*Status:*\n${message.monitor.status}` },
{ type: "mrkdwn", text: `*URL:*\n${message.monitor.url}` },
];
blocks.push({
type: "section",
fields: monitorFields,
});
// Divider
blocks.push({ type: "divider" });
// Threshold breaches (if any)
if (message.content.thresholds && message.content.thresholds.length > 0) {
const thresholdText = message.content.thresholds
.map((breach) => `• *${breach.metric.toUpperCase()}:* ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`)
.join("\n");
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: `*Threshold Breaches:*\n${thresholdText}`,
},
});
blocks.push({ type: "divider" });
}
// Additional details (if any)
if (message.content.details && message.content.details.length > 0) {
const detailsText = message.content.details.map((detail) => `• ${detail}`).join("\n");
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: `*Additional Information:*\n${detailsText}`,
},
});
}
// Incident link button (if incident exists)
if (message.content.incident) {
blocks.push({
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "View Incident",
emoji: true,
},
url: `${message.clientHost}/infrastructure/${message.monitor.id}`,
style: "primary",
},
],
});
}
// Context footer with timestamp
blocks.push({
type: "context",
elements: [
{
type: "mrkdwn",
text: `Checkmate | ${new Date(message.content.timestamp).toUTCString()}`,
},
],
});
// Return Slack payload with blocks and attachment color
return {
blocks,
attachments: [
{
color,
blocks: [], // Empty blocks in attachment for color bar
},
],
};
}
}
@@ -71,7 +71,7 @@ export class NotificationsService implements INotificationsService {
const settings = this.settingsService.getSettings();
const clientHost = settings.clientHost || "Host not defined";
// For webhook notifications, try new sendMessage method if available
// Route to new sendMessage method if provider supports it
if (notification.type === "webhook" && this.webhookProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for webhook",
@@ -81,6 +81,15 @@ export class NotificationsService implements INotificationsService {
return await this.webhookProvider.sendMessage(notification, notificationMessage);
}
if (notification.type === "slack" && this.slackProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for slack",
service: SERVICE_NAME,
method: "send",
});
return await this.slackProvider.sendMessage(notification, notificationMessage);
}
// Fallback to existing sendAlert for all providers
switch (notification.type) {
case "email":