| 1 | import * as Axiom from "@distilled.cloud/axiom"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Provider from "../Provider.ts"; |
| 4 | import { Resource } from "../Resource.ts"; |
| 5 | import type { Providers } from "./Providers.ts"; |
| 6 | |
| 7 | export type NotifierProps = Axiom.CreateNotifierRequest; |
| 8 | |
| 9 | export type Notifier = Resource< |
| 10 | "Axiom.Notifier", |
| 11 | NotifierProps, |
| 12 | Axiom.CreateNotifierResponse & { id: string }, |
| 13 | never, |
| 14 | Providers |
| 15 | >; |
| 16 | |
| 17 | /** |
| 18 | * An Axiom notifier — an alert destination (Slack, email, PagerDuty, |
| 19 | * Opsgenie, Discord, Microsoft Teams, generic webhook, or a fully custom |
| 20 | * webhook with templated body/headers) that {@link Monitor monitors} target |
| 21 | * via `notifierIds`. Exactly one channel under `properties` should be set. |
| 22 | * @see https://axiom.co/docs/monitor-data/notifiers |
| 23 | * |
| 24 | * ### Creating a Notifier |
| 25 | * **Example:** Slack incoming webhook |
| 26 | * ```typescript |
| 27 | * const slack = yield* Axiom.Notifier("ops-slack", { |
| 28 | * name: "ops-channel", |
| 29 | * properties: { |
| 30 | * slack: { slackUrl: process.env.SLACK_WEBHOOK_URL! }, |
| 31 | * }, |
| 32 | * }); |
| 33 | * ``` |
| 34 | * |
| 35 | * **Example:** Email distribution list |
| 36 | * ```typescript |
| 37 | * yield* Axiom.Notifier("ops-email", { |
| 38 | * name: "ops-team", |
| 39 | * properties: { email: { emails: ["sre@example.com", "oncall@example.com"] } }, |
| 40 | * }); |
| 41 | * ``` |
| 42 | * |
| 43 | * **Example:** PagerDuty integration |
| 44 | * ```typescript |
| 45 | * yield* Axiom.Notifier("pagerduty", { |
| 46 | * name: "primary-oncall", |
| 47 | * properties: { |
| 48 | * pagerduty: { routingKey: process.env.PAGERDUTY_ROUTING_KEY!, token: "" }, |
| 49 | * }, |
| 50 | * }); |
| 51 | * ``` |
| 52 | * |
| 53 | * **Example:** Custom webhook with templated body |
| 54 | * ```typescript |
| 55 | * yield* Axiom.Notifier("incident-webhook", { |
| 56 | * name: "incident.io", |
| 57 | * properties: { |
| 58 | * customWebhook: { |
| 59 | * url: "https://api.incident.io/v2/alert_events", |
| 60 | * headers: { "Content-Type": "application/json" }, |
| 61 | * secretHeaders: { Authorization: `Bearer ${process.env.INCIDENT_TOKEN}` }, |
| 62 | * body: '{"title": "{{.Monitor.Name}}", "status": "firing"}', |
| 63 | * }, |
| 64 | * }, |
| 65 | * }); |
| 66 | * ``` |
| 67 | * |
| 68 | * @resource |
| 69 | * @product Monitor |
| 70 | */ |
| 71 | export const Notifier = Resource<Notifier>("Axiom.Notifier"); |
| 72 | |
| 73 | export const NotifierProvider = () => |
| 74 | Provider.effect( |
| 75 | Notifier, |
| 76 | Effect.gen(function* () { |
| 77 | const create = yield* Axiom.createNotifier; |
| 78 | const update = yield* Axiom.updateNotifier; |
| 79 | const get = yield* Axiom.getNotifier; |
| 80 | const listNotifiers = yield* Axiom.getNotifiers; |
| 81 | const del = yield* Axiom.deleteNotifier; |
| 82 | |
| 83 | return { |
| 84 | stables: ["id"], |
| 85 | // Enumerate every notifier in the org. Axiom exposes a single |
| 86 | // account-wide `GET /v2/notifiers` collection op (no pagination), so we |
| 87 | // fetch it once and hydrate each row into the exact `read` Attributes |
| 88 | // shape (`CreateNotifierOutput & { id: string }`) — directly usable by |
| 89 | // `delete` with no follow-up get. |
| 90 | list: () => |
| 91 | Effect.gen(function* () { |
| 92 | const notifiers = yield* listNotifiers({}); |
| 93 | return notifiers.map((n) => ({ ...n, id: n.id ?? "" })); |
| 94 | }), |
| 95 | reconcile: Effect.fn(function* ({ news, output }) { |
| 96 | // Observe — Axiom assigns the notifier id server-side, so the |
| 97 | // only handle to a previously-created notifier is the cached |
| 98 | // `output.id`. Probe for live state with that id; treat NotFound |
| 99 | // (deleted out-of-band) as "no observed state" so we converge |
| 100 | // by re-creating. |
| 101 | const observed = output?.id |
| 102 | ? yield* get({ id: output.id }).pipe( |
| 103 | Effect.catchTag("NotFound", () => Effect.succeed(undefined)), |
| 104 | ) |
| 105 | : undefined; |
| 106 | |
| 107 | // Ensure — POST mints a new notifier. The Axiom create response |
| 108 | // sometimes omits `id` on the body; default to "" to keep the |
| 109 | // attribute shape stable until the next read populates it. |
| 110 | if (observed === undefined) { |
| 111 | const result = yield* create(news); |
| 112 | return { ...result, id: result.id ?? "" }; |
| 113 | } |
| 114 | |
| 115 | // Sync — the notifier exists; PATCH against its id with the |
| 116 | // desired props. Preserve the cached id if the API response |
| 117 | // omits it. |
| 118 | const result = yield* update({ ...news, id: observed.id! }); |
| 119 | return { ...result, id: result.id ?? observed.id! }; |
| 120 | }), |
| 121 | delete: Effect.fn(function* ({ output }) { |
| 122 | yield* del({ id: output.id }).pipe( |
| 123 | Effect.catchTag("NotFound", () => Effect.void), |
| 124 | ); |
| 125 | }), |
| 126 | read: Effect.fn(function* ({ output }) { |
| 127 | if (!output?.id) return undefined; |
| 128 | return yield* get({ id: output.id }).pipe( |
| 129 | Effect.map((current) => ({ |
| 130 | ...current, |
| 131 | id: current.id ?? output.id, |
| 132 | })), |
| 133 | Effect.catchTag("NotFound", () => Effect.succeed(undefined)), |
| 134 | ); |
| 135 | }), |
| 136 | }; |
| 137 | }), |
| 138 | ); |