Skip to content

Commit

Permalink
📟 Pagerduty integration (#890)
Browse files Browse the repository at this point in the history
* 🚧 wip

* 🔥 pagerduty integration

* 🚀 fix build

* 🚀 fix build

* 🚀 fix build

* chore: small improvements

* 🧹

---------

Co-authored-by: Maximilian Kaske <[email protected]>
  • Loading branch information
thibaultleouay and mxkaske authored Jun 27, 2024
1 parent 70cf416 commit efffb8d
Show file tree
Hide file tree
Showing 44 changed files with 671 additions and 175 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/docs/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"group": "Notification Channels",
"pages": [
"synthetic/features/notification/discord",
"synthetic/features/notification/pagerduty",
"synthetic/features/notification/phone-call",
"synthetic/features/notification/slack",
"synthetic/features/notification/sms",
Expand Down
41 changes: 41 additions & 0 deletions apps/docs/synthetic/features/notification/pagerduty.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: PagerDuty
---

Get Notified on PagerDuty when we create an incident.

## How to connect PagerDuty

Go to the Alerts Page . Select `PagerDuty` from the list of available integrations.



<Frame caption="Connect to PagerDuty">
<img
src="/images/notification/pagerduty/pagerduty-1.png"
alt="Connect to PagerDuty"
/>
</Frame>

You will be redirected to the PagerDuty website to authorize OpenStatus to send notifications to your account.

<Frame caption="Connect to PagerDuty">
<img
src="/images/notification/pagerduty/pagerduty-3.png"
alt="Connect to PagerDuty"
/>
</Frame>


Select the service you want to use to send notifications. You can create a new service if you don't have one.

<Frame caption="Connect to PagerDuty">
<img
src="/images/notification/pagerduty/pagerduty-2.png"
alt="Connect to PagerDuty"
/>
</Frame>

You are now connected to PagerDuty. Give your integration a name and save it.

You will receive some notifications if we detect an incident
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@openstatus/error": "workspace:*",
"@openstatus/notification-discord": "workspace:*",
"@openstatus/notification-emails": "workspace:*",
"@openstatus/notification-pagerduty": "workspace:*",
"@openstatus/notification-slack": "workspace:*",
"@openstatus/notification-twillio-sms": "workspace:*",
"@openstatus/plans": "workspace:*",
Expand Down
10 changes: 7 additions & 3 deletions apps/server/src/checker/alerting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,31 @@ export const triggerNotifications = async ({
statusCode,
message,
notifType,
incidentId,
}: {
monitorId: string;
statusCode?: number;
message?: string;
notifType: "alert" | "recovery";
incidentId?: string;
}) => {
console.log(`💌 triggerAlerting for ${monitorId}`);
const notifications = await db
.select()
.from(schema.notificationsToMonitors)
.innerJoin(
schema.notification,
eq(schema.notification.id, schema.notificationsToMonitors.notificationId),
eq(schema.notification.id, schema.notificationsToMonitors.notificationId)
)
.innerJoin(
schema.monitor,
eq(schema.monitor.id, schema.notificationsToMonitors.monitorId),
eq(schema.monitor.id, schema.notificationsToMonitors.monitorId)
)
.where(eq(schema.monitor.id, Number(monitorId)))
.all();
for (const notif of notifications) {
console.log(
`💌 sending notification for ${monitorId} and chanel ${notif.notification.provider} for ${notifType}`,
`💌 sending notification for ${monitorId} and chanel ${notif.notification.provider} for ${notifType}`
);
const monitor = selectMonitorSchema.parse(notif.monitor);
switch (notifType) {
Expand All @@ -48,6 +50,7 @@ export const triggerNotifications = async ({
notification: selectNotificationSchema.parse(notif.notification),
statusCode,
message,
incidentId,
});
break;
case "recovery":
Expand All @@ -56,6 +59,7 @@ export const triggerNotifications = async ({
notification: selectNotificationSchema.parse(notif.notification),
statusCode,
message,
incidentId,
});
break;
}
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/checker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,23 @@ import {
sendRecovery as sendSmsRecovery,
} from "@openstatus/notification-twillio-sms";

import {
sendAlert as sendPagerdutyAlert,
sendRecovery as sendPagerDutyRecovery,
} from "@openstatus/notification-pagerduty";

type SendNotification = ({
monitor,
notification,
statusCode,
message,
incidentId,
}: {
monitor: Monitor;
notification: Notification;
statusCode?: number;
message?: string;
incidentId?: string;
}) => Promise<void>;

type Notif = {
Expand All @@ -47,4 +54,8 @@ export const providerToFunction = {
},
discord: { sendAlert: sendDiscordAlert, sendRecovery: sendDiscordRecovery },
sms: { sendAlert: sendSmsAlert, sendRecovery: sendSmsRecovery },
pagerduty: {
sendAlert: sendPagerdutyAlert,
sendRecovery: sendPagerDutyRecovery,
},
} satisfies Record<NotificationProvider, Notif>;
8 changes: 5 additions & 3 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ DATABASE_URL=file:./../../openstatus-dev.db
DATABASE_AUTH_TOKEN=any-token

# JITSU - no need to touch on local development
JITSU_HOST="https://your-jitsu-domain.com"
JITSU_WRITE_KEY="jitsu-key:jitsu-secret"
# JITSU_HOST="https://your-jitsu-domain.com"
# JITSU_WRITE_KEY="jitsu-key:jitsu-secret"

# Solves 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', see https://github.com/nextauthjs/next-auth/issues/3580
# NODE_TLS_REJECT_UNAUTHORIZED="0"
Expand Down Expand Up @@ -72,4 +72,6 @@ AUTH_GITHUB_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=

PLAIN_API_KEY=
PLAIN_API_KEY=

PAGERDUTY_APP_ID=
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@openstatus/notification-discord": "workspace:*",
"@openstatus/notification-emails": "workspace:*",
"@openstatus/notification-slack": "workspace:*",
"@openstatus/notification-pagerduty": "workspace:*",
"@openstatus/plans": "workspace:*",
"@openstatus/react": "workspace:*",
"@openstatus/rum": "workspace:*",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Workspace } from "@openstatus/db/src/schema";
import { getLimit } from "@openstatus/plans";
import { Button, Separator } from "@openstatus/ui";
import Link from "next/link";

// FIXME: create a Channel Component within the file to avoid code duplication

interface ChannelTable {
workspace: Workspace;
disabled?: boolean;
}

export default function ChannelTable({ workspace, disabled }: ChannelTable) {
const isPagerDutyAllowed = getLimit(workspace.plan, "pagerduty");
const isSMSAllowed = getLimit(workspace.plan, "sms");
return (
<div className="col-span-full w-full rounded-lg border border-border border-dashed bg-background p-8">
<h2 className="font-cal text-2xl">Channels</h2>
<h3 className="text-muted-foreground">Connect all your channels</h3>
<div className="mt-4 rounded-md border">
<Channel
title="Discord"
description="Send notifications to discord."
href="./notifications/new/discord"
disabled={disabled}
/>
<Separator />
<Channel
title="Email"
description="Send notifications by email."
href="./notifications/new/email"
disabled={disabled}
/>
<Separator />
<Channel
title="PagerDuty"
description="Send notifications to PagerDuty."
href={`https://app.pagerduty.com/install/integration?app_id=PN76M56&redirect_url=${
process.env.NODE_ENV === "development" // FIXME: This sucks
? "http://localhost:3000"
: "https://www.openstatus.dev"
}/app/${workspace.slug}/notifications/new/pagerduty&version=2`}
disabled={disabled || isPagerDutyAllowed}
/>
<Separator />
<Channel
title="Slack"
description="Send notifications to Slack."
href="./notifications/new/slack"
disabled={disabled}
/>
<Separator />
<Channel
title="SMS"
description="Send notifications to your phones."
href="./notifications/new/sms"
disabled={disabled || isSMSAllowed}
/>
</div>
</div>
);
}

interface ChannelProps {
title: string;
description: string;
href: string;
disabled?: boolean;
}

function Channel({ title, description, href, disabled }: ChannelProps) {
return (
<div className="flex items-center gap-4 px-4 py-3">
<div className="flex-1 space-y-1">
<p className="font-medium text-sm leading-none">{title}</p>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
<div>
<Button disabled={disabled} asChild={!disabled}>
{disabled ? "Create" : <Link href={href}>Create</Link>}
</Button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,11 @@ export default async function Layout({
}: {
children: React.ReactNode;
}) {
const isLimitReached =
await api.notification.isNotificationLimitReached.query();
return (
<AppPageLayout>
<Header
title="Notifications"
description="Overview of all your notification channels."
actions={
<ButtonWithDisableTooltip
tooltip="You reached the limits"
asChild={!isLimitReached}
disabled={isLimitReached}
>
<Link href="./notifications/new">Create</Link>
</ButtonWithDisableTooltip>
}
/>
{children}
</AppPageLayout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,33 @@ import { Limit } from "@/components/dashboard/limit";
import { columns } from "@/components/data-table/notification/columns";
import { DataTable } from "@/components/data-table/notification/data-table";
import { api } from "@/trpc/server";
import ChannelTable from "./_components/channel-table";

export default async function NotificationPage() {
const workspace = await api.workspace.getWorkspace.query();
const notifications =
await api.notification.getNotificationsByWorkspace.query();
const isLimitReached =
await api.notification.isNotificationLimitReached.query();

if (notifications.length === 0) {
return (
<EmptyState
icon="bell"
title="No notifications"
description="Create your first notification channel"
action={
<Button asChild>
<Link href="./notifications/new">Create</Link>
</Button>
}
/>
<>
<EmptyState
icon="bell"
title="No notifications"
description="Create your first notification channel"
/>
<ChannelTable workspace={workspace} disabled={isLimitReached} />
</>
);
}

return (
<>
<DataTable columns={columns} data={notifications} />
{isLimitReached ? <Limit /> : null}
<ChannelTable workspace={workspace} disabled={isLimitReached} />
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NotificationForm } from "@/components/forms/notification/form";
import { NotificationForm } from "@/components/forms/notification-form";
import { api } from "@/trpc/server";

export default async function EditPage({
Expand All @@ -16,6 +16,7 @@ export default async function EditPage({
<NotificationForm
defaultValues={notification}
workspacePlan={workspace.plan}
provider={notification.provider}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ProFeatureAlert } from "@/components/billing/pro-feature-alert";
import { NotificationForm } from "@/components/forms/notification-form";
import { api } from "@/trpc/server";
import { notificationProviderSchema } from "@openstatus/db/src/schema";
import { getLimit } from "@openstatus/plans";
import { notFound } from "next/navigation";

export default async function ChannelPage({
params,
}: {
params: { channel: string };
}) {
const validation = notificationProviderSchema
.exclude(["pagerduty"])
.safeParse(params.channel);

if (!validation.success) notFound();

const workspace = await api.workspace.getWorkspace.query();

const provider = validation.data;

const allowed =
provider === "sms" ? getLimit(workspace.plan, provider) : true;

if (!allowed) return <ProFeatureAlert feature="SMS channel notification" />;

const isLimitReached =
await api.notification.isNotificationLimitReached.query();

if (isLimitReached)
return <ProFeatureAlert feature="More notification channel" />;

return (
<NotificationForm
workspacePlan={workspace.plan}
nextUrl="../"
provider={provider}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export default async function Layout({
}) {
return (
<AppPageLayout>
<Header title="Notifications" description="Create your notification" />
<Header
title="Notifications"
description="Add your a new notification channel "
/>
{children}
</AppPageLayout>
);
Expand Down
Loading

0 comments on commit efffb8d

Please sign in to comment.