Add notification dispatchers (P3 Slack/Email/Webhook, P5 Slack), full YAML parser for P6

- P3 alert: NotificationDispatcher with Slack Block Kit, Resend email, generic webhook; severity-gated dispatch
- P5 cost: CostSlackNotifier with anomaly Block Kit (score, deviation, snooze/expected buttons)
- P6 run: Full YAML runbook parser with serde_yaml, variable substitution ({{var}}), failure actions, 7 tests
- P6 parser: validates non-empty steps, default timeout (300s), default abort on failure
This commit is contained in:
2026-03-01 03:13:06 +00:00
parent f2e0a32cc7
commit 829e408e1e
3 changed files with 484 additions and 14 deletions

View File

@@ -0,0 +1,89 @@
import pino from 'pino';
const logger = pino({ name: 'cost-notifications' });
export interface CostAnomalyNotification {
anomalyId: string;
accountId: string;
resourceType: string;
region: string;
hourlyCost: number;
score: number;
baselineMean: number;
baselineStddev: number;
dashboardUrl: string;
}
// --- Slack Block Kit for Cost Anomalies ---
export class CostSlackNotifier {
private webhookUrl: string;
constructor(webhookUrl: string) {
this.webhookUrl = webhookUrl;
}
async send(anomaly: CostAnomalyNotification): Promise<boolean> {
const scoreEmoji = anomaly.score >= 75 ? '🔴' : anomaly.score >= 50 ? '🟠' : '🟡';
const deviation = anomaly.baselineStddev > 0
? ((anomaly.hourlyCost - anomaly.baselineMean) / anomaly.baselineStddev).toFixed(1)
: '∞';
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: `${scoreEmoji} Cost Anomaly Detected`, emoji: true },
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Resource:*\n\`${anomaly.resourceType}\`` },
{ type: 'mrkdwn', text: `*Account:*\n${anomaly.accountId}` },
{ type: 'mrkdwn', text: `*Region:*\n${anomaly.region}` },
{ type: 'mrkdwn', text: `*Score:*\n${anomaly.score}/100` },
{ type: 'mrkdwn', text: `*Hourly Cost:*\n$${anomaly.hourlyCost.toFixed(4)}` },
{ type: 'mrkdwn', text: `*Baseline:*\n$${anomaly.baselineMean.toFixed(4)} ± ${anomaly.baselineStddev.toFixed(4)}` },
{ type: 'mrkdwn', text: `*Deviation:*\n${deviation}σ` },
],
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: '📊 View Details' },
url: anomaly.dashboardUrl,
action_id: `view_anomaly:${anomaly.anomalyId}`,
},
{
type: 'button',
text: { type: 'plain_text', text: '✅ Expected' },
action_id: `mark_expected:${anomaly.anomalyId}`,
},
{
type: 'button',
text: { type: 'plain_text', text: '😴 Snooze 24h' },
action_id: `snooze_anomaly:${anomaly.anomalyId}`,
},
],
},
];
try {
const res = await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ blocks }),
});
if (!res.ok) {
logger.warn({ status: res.status, anomalyId: anomaly.anomalyId }, 'Slack cost notification failed');
return false;
}
return true;
} catch (err) {
logger.error({ error: (err as Error).message }, 'Slack cost send error');
return false;
}
}
}