Skip to content

Commit

Permalink
Merge pull request #17 from checkly/feature/alert-format
Browse files Browse the repository at this point in the history
update alert format to use block kit + refactor context model
  • Loading branch information
schobele authored Nov 22, 2024
2 parents f5fd136 + 7c3e712 commit acc39c4
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 86 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
Warnings:
- You are about to drop the column `context` on the `Alert` table. All the data in the column will be lost.
*/
-- CreateEnum
CREATE TYPE "Source" AS ENUM ('custom', 'checkly', 'github');

-- AlterTable
ALTER TABLE "Alert" DROP COLUMN "context";

-- CreateTable
CREATE TABLE "AlertContext" (
"id" TEXT NOT NULL,
"alertId" TEXT NOT NULL,
"source" "Source" NOT NULL DEFAULT 'custom',
"key" TEXT NOT NULL,
"value" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "AlertContext_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "AlertContext" ADD CONSTRAINT "AlertContext_alertId_fkey" FOREIGN KEY ("alertId") REFERENCES "Alert"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
22 changes: 20 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,28 @@ datasource db {
}

model Alert {
id String @id @default(cuid())
id String @id @default(cuid())
data Json
context String
context AlertContext[]
summary String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

enum Source {
custom
checkly
github
}

model AlertContext {
id String @id @default(cuid())
alertId String
source Source @default(custom)
key String
value Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
alert Alert @relation(fields: [alertId], references: [id])
}
2 changes: 2 additions & 0 deletions src/aggregator/ContextAggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { checklyAggregator } from "./checkly-aggregator";
import { WebhookAlertDto } from "../checkly/alertDTO";

export enum ContextKey {
ChecklyScript = "checkly.script",
ChecklyAlert = "checkly.alert",
ChecklyCheck = "checkly.check",
ChecklyResults = "checkly.results",
ChecklyPrometheusStatus = "checkly.prometheusStatus",
ChecklyLogs = "checkly.logs",
}

export interface CheckContext {
Expand Down
22 changes: 21 additions & 1 deletion src/aggregator/checkly-aggregator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,32 @@ describe("ChecklyService", () => {
plainToInstance(
WebhookAlertDto,
{
CHECK_ID: checks[0].id,
CHECK_NAME: "fail50",
CHECK_ID: "b68422ae-6528-45a5-85a6-e85e1be9de2e",
CHECK_TYPE: "MULTI_STEP",
GROUP_NAME: "",
ALERT_TITLE: "fail50 has failed",
ALERT_TYPE: "ALERT_FAILURE",
CHECK_RESULT_ID: "64f3fe90-db20-4817-abce-c3fb9dd4228a",
RESPONSE_TIME: 1715,
API_CHECK_RESPONSE_STATUS_CODE: 0,
"API_CHECK_RESPONSE_STATUS_T∑EXT": "",
RUN_LOCATION: "Frankfurt",
RESULT_LINK:
"https://app.checklyhq.com/checks/b68422ae-6528-45a5-85a6-e85e1be9de2e/results/multi_step/64f3fe90-db20-4817-abce-c3fb9dd4228a",
SSL_DAYS_REMAINING: 0,
SSL_CHECK_DOMAIN: "",
STARTED_AT: "2024-11-15T13:39:26.259Z",
TAGS: [],
$RANDOM_NUMBER: 3022,
$UUID: "cbe2286f-a353-400c-a797-87f4cda1d6d8",
moment: "November 15, 2024",
},
{ enableImplicitConversion: true }
)
);

expect(context).toBeDefined();
expect(context.length).toBeGreaterThan(0);
});
});
56 changes: 52 additions & 4 deletions src/aggregator/checkly-aggregator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,53 @@
import { CheckContext, ContextKey } from "./ContextAggregator";
import { checkly } from "../checkly/client";
import { WebhookAlertDto } from "../checkly/alertDTO";
import { Check, CheckResult } from "../checkly/models";

const getCheckLogs = async (checkId: string, checkResultId: string) => {
const logs = await checkly.getCheckResult(checkId, checkResultId);
console.log("logs");
console.log(logs);

return logs;
};

const mapCheckToContextValue = (check: Check) => {
return {
checkId: check.id,
type: check.checkType,
frequency: check.frequency,
frequencyOffset: check.frequencyOffset,
shouldFail: check.shouldFail,
locations: check.locations,
tags: check.tags,
maxResponseTime: check.maxResponseTime,
sslCheckDomain: check.sslCheckDomain,
retryStrategy: check.retryStrategy,
};
};

const mapCheckResultToContextValue = (result: CheckResult) => {
return {
resultId: result.id,
hasErrors: result.hasErrors,
hasFailures: result.hasFailures,
runLocation: result.runLocation,
startedAt: result.startedAt,
stoppedAt: result.stoppedAt,
responseTime: result.responseTime,
checkId: result.checkId,
attempts: result.attempts,
isDegraded: result.isDegraded,
overMaxResponseTime: result.overMaxResponseTime,
resultType: result.resultType,
};
};

export const checklyAggregator = {
fetchContext: async (alert: WebhookAlertDto): Promise<CheckContext[]> => {
const [check, results] = await Promise.all([
checkly.getCheck(alert.CHECK_ID),
checkly.getCheckResults(alert.CHECK_ID, undefined, 1),
checkly.getCheckResult(alert.CHECK_ID, alert.CHECK_RESULT_ID),
]);
const makeCheckContext = (key: ContextKey, value: unknown) => {
return {
Expand All @@ -17,10 +58,17 @@ export const checklyAggregator = {
} as CheckContext;
};

const logs = results.getLog();
const script = check.script;

const checklyCheckContext = [
makeCheckContext(ContextKey.ChecklyAlert, { ...alert }),
makeCheckContext(ContextKey.ChecklyCheck, check),
makeCheckContext(ContextKey.ChecklyResults, results),
makeCheckContext(ContextKey.ChecklyScript, script),
makeCheckContext(ContextKey.ChecklyCheck, mapCheckToContextValue(check)),
makeCheckContext(
ContextKey.ChecklyResults,
mapCheckResultToContextValue(results)
),
makeCheckContext(ContextKey.ChecklyLogs, logs),
] as CheckContext[];

return checklyCheckContext;
Expand Down
4 changes: 2 additions & 2 deletions src/ai/Tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ export abstract class Tool<
TAgent extends BaseAssistant = BaseAssistant
> {
protected readonly name: string;
protected readonly description: string;
protected readonly parameters: TParams;
protected readonly agent: TAgent;
protected readonly outputSchema?: TOutput;
protected readonly version: string = "1.0.0";
protected readonly maxRetries: number = 1;
protected readonly timeout?: number;
description: string;
parameters: TParams;

constructor(config: {
name: string;
Expand Down
166 changes: 96 additions & 70 deletions src/routes/checklywebhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,87 +29,113 @@ router.post("/", async (req: Request, res: Response) => {
// Validate the DTO
await validateOrReject(alertDto);

const aggregator = new CheckContextAggregator(alertDto);
const context = await aggregator.aggregate();
const contextAnalysis = await generateContextAnalysis(context);
const summary = await generateContextAnalysisSummary(contextAnalysis);

const alert = await prisma.alert.create({
data: {
data: { ...alertDto } as unknown as Prisma.InputJsonValue,
context: JSON.stringify(contextAnalysis),
summary,
const exisingAlert = await prisma.alert.findFirst({
where: {
AND: [
{
data: {
path: ["CHECK_RESULT_ID"],
equals: alertDto.CHECK_RESULT_ID,
},
},
{
data: {
path: ["CHECK_ID"],
equals: alertDto.CHECK_ID,
},
},
],
},
});

const thread = await getOpenaiClient().beta.threads.create({
messages: [
{
role: "assistant",
content:
"New alert: " + alertDto.CHECK_NAME + "\nSummary: " + summary,
},
],
});
if (exisingAlert && process.env.ALLOW_DUPLICATE_ALERTS !== "true") {
res.status(200).json({ message: "Alert already processed" });
} else {
const aggregator = new CheckContextAggregator(alertDto);
const context = await aggregator.aggregate();
const contextAnalysis = await generateContextAnalysis(context);
const summary = await generateContextAnalysisSummary(contextAnalysis);

const alertMessage = await app.client.chat.postMessage({
channel: "C07V9GNU9L6",
metadata: {
event_type: "alert",
event_payload: {
alertId: alert.id,
threadId: thread.id,
const alert = await prisma.alert.create({
data: {
data: { ...alertDto } as unknown as Prisma.InputJsonValue,
context: {
createMany: {
data: contextAnalysis
.filter((c) => c.key && c.value)
.map((c) => ({
key: c.key,
value: c.value as any,
})),
},
},
summary,
},
},
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "🚨 New Alert: " + alertDto.CHECK_NAME,
emoji: true,
});

const thread = await getOpenaiClient().beta.threads.create({
messages: [
{
role: "assistant",
content:
"New alert: " + alertDto.CHECK_NAME + "\nSummary: " + summary,
},
],
});

await app.client.chat.postMessage({
channel: "C07V9GNU9L6",
metadata: {
event_type: "alert",
event_payload: {
alertId: alert.id,
threadId: thread.id,
},
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "View Check",
emoji: true,
},
url: `https://app.checklyhq.com/checks/${alertDto.CHECK_ID}`,
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "🚨 " + alertDto.CHECK_NAME + " has failed 🚨",
emoji: true,
},
],
},
// {
// type: "section",
// text: {
// type: "mrkdwn",
// text: `*Summary*\n${summary}`,
// },
// },
{
type: "context",
elements: [
{
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `🩺 *Check:* <https://app.checklyhq.com/checks/${alertDto.CHECK_ID}|${alertDto.CHECK_NAME}>`,
},
{
type: "mrkdwn",
text: `🔮 *Result:* <${alertDto.RESULT_LINK}|View>`,
},
{
type: "mrkdwn",
text: `📅 *When:* ${new Date(
alertDto.STARTED_AT
).toLocaleString()}`,
},
{
type: "mrkdwn",
text: `🌍 *Location:* ${alertDto.RUN_LOCATION}`,
},
],
},
{
type: "section",
text: {
type: "mrkdwn",
text: `🕐 Alert created at: ${new Date().toLocaleString()}`,
text: `*Summary*\n${summary}`,
},
],
},
],
});

await app.client.chat.postMessage({
channel: "C07V9GNU9L6",
text: `*Summary*\n${summary}`,
thread_ts: alertMessage.ts,
});
},
],
});

res.json({ message: "OK" });
res.json({ message: "OK" });
}
} catch (error) {
console.error("Error parsing or validating request body:", error);
res.status(400).json({ message: "Invalid request body" });
Expand Down
3 changes: 3 additions & 0 deletions src/sre-assistant/SreAssistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ Format your responses as slack mrkdwn messages and keep the answer concise and r
}

protected async getTools(): Promise<Tool[]> {
const searchContextTool = new SearchContextTool(this);
await searchContextTool.init();

return [new SearchContextTool(this), new GithubAgentInteractionTool(this)];
}
}
Loading

0 comments on commit acc39c4

Please sign in to comment.