triagent 0.1.0-alpha13 → 0.1.0-alpha18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -4
- package/src/cli/config.ts +96 -0
- package/src/index.ts +201 -3
- package/src/integrations/elasticsearch/client.ts +210 -0
- package/src/integrations/grafana/client.ts +186 -0
- package/src/integrations/kubernetes/multi-cluster.ts +199 -0
- package/src/integrations/kubernetes/types.ts +24 -0
- package/src/integrations/loki/client.ts +219 -0
- package/src/integrations/prometheus/client.ts +163 -0
- package/src/integrations/slack/client.ts +265 -0
- package/src/integrations/teams/client.ts +199 -0
- package/src/mastra/agents/debugger.ts +152 -108
- package/src/mastra/tools/approval-store.ts +180 -0
- package/src/mastra/tools/cli.ts +94 -2
- package/src/mastra/tools/cost.ts +389 -0
- package/src/mastra/tools/logs.ts +210 -0
- package/src/mastra/tools/network.ts +253 -0
- package/src/mastra/tools/prometheus.ts +221 -0
- package/src/mastra/tools/remediation.ts +365 -0
- package/src/mastra/tools/runbook.ts +186 -0
- package/src/server/routes/history.ts +207 -0
- package/src/server/routes/notifications.ts +236 -0
- package/src/server/webhook.ts +36 -2
- package/src/storage/index.ts +3 -0
- package/src/storage/investigation-history.ts +277 -0
- package/src/storage/runbook-index.ts +330 -0
- package/src/storage/types.ts +72 -0
- package/src/tui/app.tsx +492 -76
- package/src/tui/components/approval-dialog.tsx +156 -0
- package/src/tui/components/approval-modal.tsx +278 -0
- package/src/tui/components/index.ts +38 -0
- package/src/tui/components/styled-span.tsx +24 -0
- package/src/tui/components/timeline.tsx +223 -0
- package/src/tui/components/toast.tsx +101 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { PrometheusConfig } from "../../cli/config.js";
|
|
2
|
+
|
|
3
|
+
export interface PrometheusQueryResult {
|
|
4
|
+
status: "success" | "error";
|
|
5
|
+
data?: {
|
|
6
|
+
resultType: "vector" | "matrix" | "scalar" | "string";
|
|
7
|
+
result: Array<{
|
|
8
|
+
metric: Record<string, string>;
|
|
9
|
+
value?: [number, string];
|
|
10
|
+
values?: Array<[number, string]>;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
error?: string;
|
|
14
|
+
errorType?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PrometheusAlert {
|
|
18
|
+
labels: Record<string, string>;
|
|
19
|
+
annotations: Record<string, string>;
|
|
20
|
+
state: "firing" | "pending" | "inactive";
|
|
21
|
+
activeAt: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PrometheusTarget {
|
|
26
|
+
labels: Record<string, string>;
|
|
27
|
+
scrapePool: string;
|
|
28
|
+
scrapeUrl: string;
|
|
29
|
+
health: "up" | "down" | "unknown";
|
|
30
|
+
lastScrape: string;
|
|
31
|
+
lastError: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class PrometheusClient {
|
|
35
|
+
private baseUrl: string;
|
|
36
|
+
private authToken?: string;
|
|
37
|
+
|
|
38
|
+
constructor(config: PrometheusConfig) {
|
|
39
|
+
this.baseUrl = config.url.replace(/\/$/, "");
|
|
40
|
+
this.authToken = config.auth?.token;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async request<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
|
|
44
|
+
const url = new URL(`${this.baseUrl}${endpoint}`);
|
|
45
|
+
if (params) {
|
|
46
|
+
for (const [key, value] of Object.entries(params)) {
|
|
47
|
+
url.searchParams.set(key, value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const headers: Record<string, string> = {
|
|
52
|
+
"Accept": "application/json",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (this.authToken) {
|
|
56
|
+
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const response = await fetch(url.toString(), { headers });
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(`Prometheus API error: ${response.status} ${response.statusText}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return response.json() as Promise<T>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async query(query: string, time?: string): Promise<PrometheusQueryResult> {
|
|
68
|
+
const params: Record<string, string> = { query };
|
|
69
|
+
if (time) {
|
|
70
|
+
params.time = time;
|
|
71
|
+
}
|
|
72
|
+
return this.request<PrometheusQueryResult>("/api/v1/query", params);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async queryRange(
|
|
76
|
+
query: string,
|
|
77
|
+
start: string,
|
|
78
|
+
end: string,
|
|
79
|
+
step: string
|
|
80
|
+
): Promise<PrometheusQueryResult> {
|
|
81
|
+
return this.request<PrometheusQueryResult>("/api/v1/query_range", {
|
|
82
|
+
query,
|
|
83
|
+
start,
|
|
84
|
+
end,
|
|
85
|
+
step,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getAlerts(): Promise<{ alerts: PrometheusAlert[] }> {
|
|
90
|
+
const response = await this.request<{
|
|
91
|
+
status: string;
|
|
92
|
+
data: { alerts: PrometheusAlert[] };
|
|
93
|
+
}>("/api/v1/alerts");
|
|
94
|
+
return response.data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getTargets(): Promise<{ activeTargets: PrometheusTarget[] }> {
|
|
98
|
+
const response = await this.request<{
|
|
99
|
+
status: string;
|
|
100
|
+
data: { activeTargets: PrometheusTarget[] };
|
|
101
|
+
}>("/api/v1/targets");
|
|
102
|
+
return response.data;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async getRules(): Promise<{ groups: Array<{ name: string; rules: unknown[] }> }> {
|
|
106
|
+
const response = await this.request<{
|
|
107
|
+
status: string;
|
|
108
|
+
data: { groups: Array<{ name: string; rules: unknown[] }> };
|
|
109
|
+
}>("/api/v1/rules");
|
|
110
|
+
return response.data;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getMetadata(metric?: string): Promise<Record<string, unknown[]>> {
|
|
114
|
+
const params: Record<string, string> = {};
|
|
115
|
+
if (metric) {
|
|
116
|
+
params.metric = metric;
|
|
117
|
+
}
|
|
118
|
+
const response = await this.request<{
|
|
119
|
+
status: string;
|
|
120
|
+
data: Record<string, unknown[]>;
|
|
121
|
+
}>("/api/v1/metadata", params);
|
|
122
|
+
return response.data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
formatQueryResult(result: PrometheusQueryResult): string {
|
|
126
|
+
if (result.status !== "success" || !result.data) {
|
|
127
|
+
return `Error: ${result.error || "Unknown error"}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const lines: string[] = [];
|
|
131
|
+
for (const item of result.data.result) {
|
|
132
|
+
const labels = Object.entries(item.metric)
|
|
133
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
134
|
+
.join(", ");
|
|
135
|
+
|
|
136
|
+
if (item.value) {
|
|
137
|
+
const [timestamp, value] = item.value;
|
|
138
|
+
lines.push(`{${labels}} => ${value} @${new Date(timestamp * 1000).toISOString()}`);
|
|
139
|
+
} else if (item.values) {
|
|
140
|
+
lines.push(`{${labels}}:`);
|
|
141
|
+
for (const [timestamp, value] of item.values.slice(-5)) {
|
|
142
|
+
lines.push(` ${new Date(timestamp * 1000).toISOString()}: ${value}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return lines.join("\n") || "No data";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Singleton instance
|
|
152
|
+
let prometheusClient: PrometheusClient | null = null;
|
|
153
|
+
|
|
154
|
+
export function getPrometheusClient(): PrometheusClient | null {
|
|
155
|
+
return prometheusClient;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function initPrometheusClient(config?: PrometheusConfig): PrometheusClient | null {
|
|
159
|
+
if (config) {
|
|
160
|
+
prometheusClient = new PrometheusClient(config);
|
|
161
|
+
}
|
|
162
|
+
return prometheusClient;
|
|
163
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { SlackConfig } from "../../cli/config.js";
|
|
2
|
+
|
|
3
|
+
export interface SlackMessage {
|
|
4
|
+
channel?: string;
|
|
5
|
+
text: string;
|
|
6
|
+
blocks?: SlackBlock[];
|
|
7
|
+
attachments?: SlackAttachment[];
|
|
8
|
+
thread_ts?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SlackBlock {
|
|
12
|
+
type: "section" | "divider" | "header" | "context" | "actions";
|
|
13
|
+
text?: { type: "mrkdwn" | "plain_text"; text: string };
|
|
14
|
+
fields?: Array<{ type: "mrkdwn" | "plain_text"; text: string }>;
|
|
15
|
+
elements?: SlackBlockElement[];
|
|
16
|
+
accessory?: SlackBlockElement;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SlackBlockElement {
|
|
20
|
+
type: "button" | "static_select" | "image" | "mrkdwn" | "plain_text";
|
|
21
|
+
text?: { type: "plain_text" | "mrkdwn"; text: string; emoji?: boolean };
|
|
22
|
+
action_id?: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
value?: string;
|
|
25
|
+
style?: "primary" | "danger";
|
|
26
|
+
image_url?: string;
|
|
27
|
+
alt_text?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SlackAttachment {
|
|
31
|
+
color?: string;
|
|
32
|
+
fallback?: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
text?: string;
|
|
35
|
+
fields?: Array<{ title: string; value: string; short?: boolean }>;
|
|
36
|
+
footer?: string;
|
|
37
|
+
ts?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SlackResponse {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
error?: string;
|
|
43
|
+
ts?: string;
|
|
44
|
+
channel?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class SlackClient {
|
|
48
|
+
private webhookUrl: string;
|
|
49
|
+
private botToken?: string;
|
|
50
|
+
private defaultChannel?: string;
|
|
51
|
+
|
|
52
|
+
constructor(config: SlackConfig) {
|
|
53
|
+
this.webhookUrl = config.webhookUrl;
|
|
54
|
+
this.botToken = config.botToken;
|
|
55
|
+
this.defaultChannel = config.defaultChannel;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async sendWebhook(message: SlackMessage): Promise<boolean> {
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch(this.webhookUrl, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify(message),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return response.ok;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async sendMessage(message: SlackMessage): Promise<SlackResponse> {
|
|
73
|
+
if (!this.botToken) {
|
|
74
|
+
// Fall back to webhook
|
|
75
|
+
const success = await this.sendWebhook(message);
|
|
76
|
+
return { ok: success };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const channel = message.channel || this.defaultChannel;
|
|
80
|
+
if (!channel) {
|
|
81
|
+
return { ok: false, error: "No channel specified" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch("https://slack.com/api/chat.postMessage", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
"Authorization": `Bearer ${this.botToken}`,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
...message,
|
|
93
|
+
channel,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return await response.json() as SlackResponse;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async updateMessage(channel: string, ts: string, message: SlackMessage): Promise<SlackResponse> {
|
|
104
|
+
if (!this.botToken) {
|
|
105
|
+
return { ok: false, error: "Bot token required for message updates" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch("https://slack.com/api/chat.update", {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
"Authorization": `Bearer ${this.botToken}`,
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
...message,
|
|
117
|
+
channel,
|
|
118
|
+
ts,
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return await response.json() as SlackResponse;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
buildInvestigationMessage(investigation: {
|
|
129
|
+
id: string;
|
|
130
|
+
title: string;
|
|
131
|
+
status: string;
|
|
132
|
+
summary?: string;
|
|
133
|
+
severity?: string;
|
|
134
|
+
rootCause?: string;
|
|
135
|
+
recommendations?: string[];
|
|
136
|
+
url?: string;
|
|
137
|
+
}): SlackMessage {
|
|
138
|
+
const statusEmoji = {
|
|
139
|
+
pending: "⏳",
|
|
140
|
+
running: "🔄",
|
|
141
|
+
completed: "✅",
|
|
142
|
+
failed: "❌",
|
|
143
|
+
}[investigation.status] || "❓";
|
|
144
|
+
|
|
145
|
+
const severityColor = {
|
|
146
|
+
critical: "#dc3545",
|
|
147
|
+
warning: "#ffc107",
|
|
148
|
+
info: "#17a2b8",
|
|
149
|
+
}[investigation.severity || "info"] || "#6c757d";
|
|
150
|
+
|
|
151
|
+
const blocks: SlackBlock[] = [
|
|
152
|
+
{
|
|
153
|
+
type: "header",
|
|
154
|
+
text: { type: "plain_text", text: `${statusEmoji} Investigation: ${investigation.title}` },
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: "section",
|
|
158
|
+
fields: [
|
|
159
|
+
{ type: "mrkdwn", text: `*Status:*\n${investigation.status}` },
|
|
160
|
+
{ type: "mrkdwn", text: `*ID:*\n\`${investigation.id}\`` },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
if (investigation.summary) {
|
|
166
|
+
blocks.push({
|
|
167
|
+
type: "section",
|
|
168
|
+
text: { type: "mrkdwn", text: `*Summary:*\n${investigation.summary}` },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (investigation.rootCause) {
|
|
173
|
+
blocks.push({
|
|
174
|
+
type: "section",
|
|
175
|
+
text: { type: "mrkdwn", text: `*Root Cause:*\n${investigation.rootCause}` },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (investigation.recommendations && investigation.recommendations.length > 0) {
|
|
180
|
+
blocks.push({
|
|
181
|
+
type: "section",
|
|
182
|
+
text: {
|
|
183
|
+
type: "mrkdwn",
|
|
184
|
+
text: `*Recommendations:*\n${investigation.recommendations.map((r, i) => `${i + 1}. ${r}`).join("\n")}`,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (investigation.url) {
|
|
190
|
+
blocks.push({
|
|
191
|
+
type: "actions",
|
|
192
|
+
elements: [
|
|
193
|
+
{
|
|
194
|
+
type: "button",
|
|
195
|
+
text: { type: "plain_text", text: "View Details", emoji: true },
|
|
196
|
+
url: investigation.url,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
text: `Investigation ${investigation.status}: ${investigation.title}`,
|
|
204
|
+
blocks,
|
|
205
|
+
attachments: [
|
|
206
|
+
{
|
|
207
|
+
color: severityColor,
|
|
208
|
+
fallback: investigation.summary || investigation.title,
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
buildAlertMessage(alert: {
|
|
215
|
+
name: string;
|
|
216
|
+
severity: string;
|
|
217
|
+
message: string;
|
|
218
|
+
source?: string;
|
|
219
|
+
timestamp?: Date;
|
|
220
|
+
}): SlackMessage {
|
|
221
|
+
const severityEmoji = {
|
|
222
|
+
critical: "🔴",
|
|
223
|
+
warning: "🟡",
|
|
224
|
+
info: "🔵",
|
|
225
|
+
}[alert.severity] || "⚪";
|
|
226
|
+
|
|
227
|
+
const severityColor = {
|
|
228
|
+
critical: "#dc3545",
|
|
229
|
+
warning: "#ffc107",
|
|
230
|
+
info: "#17a2b8",
|
|
231
|
+
}[alert.severity] || "#6c757d";
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
text: `${severityEmoji} Alert: ${alert.name}`,
|
|
235
|
+
attachments: [
|
|
236
|
+
{
|
|
237
|
+
color: severityColor,
|
|
238
|
+
fallback: alert.message,
|
|
239
|
+
title: `${severityEmoji} ${alert.name}`,
|
|
240
|
+
text: alert.message,
|
|
241
|
+
fields: [
|
|
242
|
+
{ title: "Severity", value: alert.severity.toUpperCase(), short: true },
|
|
243
|
+
{ title: "Source", value: alert.source || "Unknown", short: true },
|
|
244
|
+
],
|
|
245
|
+
footer: "Triagent",
|
|
246
|
+
ts: Math.floor((alert.timestamp || new Date()).getTime() / 1000),
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Singleton instance
|
|
254
|
+
let slackClient: SlackClient | null = null;
|
|
255
|
+
|
|
256
|
+
export function getSlackClient(): SlackClient | null {
|
|
257
|
+
return slackClient;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function initSlackClient(config?: SlackConfig): SlackClient | null {
|
|
261
|
+
if (config) {
|
|
262
|
+
slackClient = new SlackClient(config);
|
|
263
|
+
}
|
|
264
|
+
return slackClient;
|
|
265
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { TeamsConfig } from "../../cli/config.js";
|
|
2
|
+
|
|
3
|
+
export interface TeamsMessage {
|
|
4
|
+
"@type": "MessageCard";
|
|
5
|
+
"@context": "http://schema.org/extensions";
|
|
6
|
+
themeColor?: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
sections?: TeamsSection[];
|
|
9
|
+
potentialAction?: TeamsAction[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TeamsSection {
|
|
13
|
+
activityTitle?: string;
|
|
14
|
+
activitySubtitle?: string;
|
|
15
|
+
activityImage?: string;
|
|
16
|
+
facts?: Array<{ name: string; value: string }>;
|
|
17
|
+
markdown?: boolean;
|
|
18
|
+
text?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TeamsAction {
|
|
22
|
+
"@type": "OpenUri" | "ActionCard" | "HttpPOST";
|
|
23
|
+
name: string;
|
|
24
|
+
targets?: Array<{ os: string; uri: string }>;
|
|
25
|
+
inputs?: TeamsInput[];
|
|
26
|
+
actions?: TeamsAction[];
|
|
27
|
+
target?: string;
|
|
28
|
+
body?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface TeamsInput {
|
|
32
|
+
"@type": "TextInput" | "MultichoiceInput";
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
isMultiline?: boolean;
|
|
36
|
+
isRequired?: boolean;
|
|
37
|
+
choices?: Array<{ display: string; value: string }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class TeamsClient {
|
|
41
|
+
private webhookUrl: string;
|
|
42
|
+
|
|
43
|
+
constructor(config: TeamsConfig) {
|
|
44
|
+
this.webhookUrl = config.webhookUrl;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async sendMessage(message: TeamsMessage): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(this.webhookUrl, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify(message),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return response.ok;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
buildInvestigationMessage(investigation: {
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
status: string;
|
|
65
|
+
summary?: string;
|
|
66
|
+
severity?: string;
|
|
67
|
+
rootCause?: string;
|
|
68
|
+
recommendations?: string[];
|
|
69
|
+
url?: string;
|
|
70
|
+
}): TeamsMessage {
|
|
71
|
+
const statusEmoji = {
|
|
72
|
+
pending: "⏳",
|
|
73
|
+
running: "🔄",
|
|
74
|
+
completed: "✅",
|
|
75
|
+
failed: "❌",
|
|
76
|
+
}[investigation.status] || "❓";
|
|
77
|
+
|
|
78
|
+
const themeColor = {
|
|
79
|
+
critical: "dc3545",
|
|
80
|
+
warning: "ffc107",
|
|
81
|
+
info: "17a2b8",
|
|
82
|
+
completed: "28a745",
|
|
83
|
+
failed: "dc3545",
|
|
84
|
+
}[investigation.severity || investigation.status] || "6c757d";
|
|
85
|
+
|
|
86
|
+
const facts: Array<{ name: string; value: string }> = [
|
|
87
|
+
{ name: "Status", value: `${statusEmoji} ${investigation.status}` },
|
|
88
|
+
{ name: "ID", value: investigation.id },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (investigation.severity) {
|
|
92
|
+
facts.push({ name: "Severity", value: investigation.severity.toUpperCase() });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sections: TeamsSection[] = [
|
|
96
|
+
{
|
|
97
|
+
activityTitle: `Investigation: ${investigation.title}`,
|
|
98
|
+
activitySubtitle: `Status: ${investigation.status}`,
|
|
99
|
+
facts,
|
|
100
|
+
markdown: true,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
if (investigation.summary) {
|
|
105
|
+
sections.push({
|
|
106
|
+
text: `**Summary:** ${investigation.summary}`,
|
|
107
|
+
markdown: true,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (investigation.rootCause) {
|
|
112
|
+
sections.push({
|
|
113
|
+
text: `**Root Cause:** ${investigation.rootCause}`,
|
|
114
|
+
markdown: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (investigation.recommendations && investigation.recommendations.length > 0) {
|
|
119
|
+
const recText = investigation.recommendations
|
|
120
|
+
.map((r, i) => `${i + 1}. ${r}`)
|
|
121
|
+
.join("\n\n");
|
|
122
|
+
sections.push({
|
|
123
|
+
text: `**Recommendations:**\n\n${recText}`,
|
|
124
|
+
markdown: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const actions: TeamsAction[] = [];
|
|
129
|
+
if (investigation.url) {
|
|
130
|
+
actions.push({
|
|
131
|
+
"@type": "OpenUri",
|
|
132
|
+
name: "View Details",
|
|
133
|
+
targets: [{ os: "default", uri: investigation.url }],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"@type": "MessageCard",
|
|
139
|
+
"@context": "http://schema.org/extensions",
|
|
140
|
+
themeColor,
|
|
141
|
+
summary: `Investigation ${investigation.status}: ${investigation.title}`,
|
|
142
|
+
sections,
|
|
143
|
+
potentialAction: actions.length > 0 ? actions : undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
buildAlertMessage(alert: {
|
|
148
|
+
name: string;
|
|
149
|
+
severity: string;
|
|
150
|
+
message: string;
|
|
151
|
+
source?: string;
|
|
152
|
+
timestamp?: Date;
|
|
153
|
+
}): TeamsMessage {
|
|
154
|
+
const severityEmoji = {
|
|
155
|
+
critical: "🔴",
|
|
156
|
+
warning: "🟡",
|
|
157
|
+
info: "🔵",
|
|
158
|
+
}[alert.severity] || "⚪";
|
|
159
|
+
|
|
160
|
+
const themeColor = {
|
|
161
|
+
critical: "dc3545",
|
|
162
|
+
warning: "ffc107",
|
|
163
|
+
info: "17a2b8",
|
|
164
|
+
}[alert.severity] || "6c757d";
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"@type": "MessageCard",
|
|
168
|
+
"@context": "http://schema.org/extensions",
|
|
169
|
+
themeColor,
|
|
170
|
+
summary: `${severityEmoji} Alert: ${alert.name}`,
|
|
171
|
+
sections: [
|
|
172
|
+
{
|
|
173
|
+
activityTitle: `${severityEmoji} ${alert.name}`,
|
|
174
|
+
activitySubtitle: alert.source || "Triagent",
|
|
175
|
+
facts: [
|
|
176
|
+
{ name: "Severity", value: alert.severity.toUpperCase() },
|
|
177
|
+
{ name: "Time", value: (alert.timestamp || new Date()).toISOString() },
|
|
178
|
+
],
|
|
179
|
+
text: alert.message,
|
|
180
|
+
markdown: true,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Singleton instance
|
|
188
|
+
let teamsClient: TeamsClient | null = null;
|
|
189
|
+
|
|
190
|
+
export function getTeamsClient(): TeamsClient | null {
|
|
191
|
+
return teamsClient;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function initTeamsClient(config?: TeamsConfig): TeamsClient | null {
|
|
195
|
+
if (config) {
|
|
196
|
+
teamsClient = new TeamsClient(config);
|
|
197
|
+
}
|
|
198
|
+
return teamsClient;
|
|
199
|
+
}
|