1import type * as Effect from "effect/Effect";
2import type * as Stream from "effect/Stream";
3import type { Input } from "../../Input.ts";
4import {
5 consumeBusEvents,
6 type EventRecord,
7 type EventRouteProps,
8} from "../EventBridge/EventSource.ts";
9
10/**
11 * The detail-types CloudWatch Synthetics publishes to EventBridge:
12 * canary lifecycle transitions and per-run pass/fail results.
13 */
14export type CanaryEventDetailType =
15 | "Synthetics Canary Status Change"
16 | "Synthetics Canary TestRun Successful"
17 | "Synthetics Canary TestRun Failure";
18
19/**
20 * The `detail` payload CloudWatch Synthetics delivers to EventBridge for
21 * canary status changes and test-run results. Keys are hyphenated on the
22 * wire; fields not shared by every detail-type are optional (the schema
23 * grows over time).
24 */
25export interface CanaryEventDetail {
26 /** The account the canary lives in. */
27 "account-id"?: string;
28 /** Service-assigned unique ID of the canary. */
29 "canary-id"?: string;
30 /** Name of the canary the event is about. */
31 "canary-name"?: string;
32 /** Status-change events: the canary's new state (e.g. `RUNNING`). */
33 "current-state"?: string;
34 /** Status-change events: the canary's previous state. */
35 "previous-state"?: string;
36 /** Test-run events: the run result (`PASSED`, `FAILED`). */
37 "test-run-status"?: string;
38 /** Test-run events: the id of the canary run. */
39 "canary-run-id"?: string;
40 /** S3 location of the run's artifacts. */
41 "artifact-location"?: string;
42 /** Human-readable reason for the state, when present. */
43 "state-reason"?: string;
44 /** Additional event fields (the schema grows over time). */
45 [key: string]: unknown;
46}
47
48/** A Synthetics canary event delivered to the handler. */
49export type CanaryEventRecord = EventRecord<CanaryEventDetail>;
50
51export interface CanaryEventSourceProps extends EventRouteProps {
52 /**
53 * Logical id for the backing EventBridge rule.
54 * @default "SyntheticsCanaryEvents"
55 */
56 id?: string;
57 /**
58 * Restrict to events emitted by the given canaries (matched against the
59 * event detail's `canary-name`). Pass `canary.canaryName`.
60 * @default all canaries in the account
61 */
62 canaryNames?: readonly Input<string>[];
63 /**
64 * Restrict to the given detail-types (status changes and/or test-run
65 * results).
66 * @default all three Synthetics detail-types
67 */
68 detailTypes?: readonly CanaryEventDetailType[];
69}
70
71/**
72 * Event source connecting CloudWatch Synthetics canary events to the
73 * hosting compute. Synthetics publishes canary status changes and per-run
74 * pass/fail results to the account's default EventBridge bus (source
75 * `aws.synthetics`); this subscribes the host Function to those events so
76 * it can page on failed runs, annotate deploy dashboards, or kick off
77 * remediation.
78 *
79 * Synthetics publishes to EventBridge automatically — no additional
80 * resource is created besides the EventBridge rule targeting the host.
81 * Provide the host-specific implementation layer (e.g.
82 * `AWS.Lambda.EventSource`) on the Function effect.
83 *
84 * ### Consuming Canary Events
85 * **Example:** Page on Failed Canary Runs
86 * ```typescript
87 * import * as AWS from "alchemy/AWS";
88 *
89 * export default AlertFunction.make(
90 * { main: import.meta.url },
91 * Effect.gen(function* () {
92 * const canary = yield* AWS.Synthetics.Canary("ApiMonitor", {
93 * script: canaryScript,
94 * artifactS3Location: artifactLocation,
95 * start: true,
96 * });
97 *
98 * yield* AWS.Synthetics.consumeCanaryEvents(
99 * {
100 * canaryNames: [canary.canaryName],
101 * detailTypes: ["Synthetics Canary TestRun Failure"],
102 * },
103 * (events) =>
104 * Stream.runForEach(events, (event) =>
105 * Effect.logError(
106 * `canary ${event.detail["canary-name"]} run failed`,
107 * ),
108 * ),
109 * );
110 * return {};
111 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
112 * );
113 * ```
114 */
115export const consumeCanaryEvents = <StreamReq = never, Req = never>(
116 props: CanaryEventSourceProps,
117 process: (
118 events: Stream.Stream<CanaryEventRecord, never, StreamReq>,
119 ) => Effect.Effect<void, never, Req>,
120) =>
121 consumeBusEvents(
122 props.id ?? "SyntheticsCanaryEvents",
123 {
124 source: ["aws.synthetics"],
125 "detail-type": [
126 ...(props.detailTypes ?? [
127 "Synthetics Canary Status Change",
128 "Synthetics Canary TestRun Successful",
129 "Synthetics Canary TestRun Failure",
130 ]),
131 ],
132 ...(props.canaryNames !== undefined
133 ? { detail: { "canary-name": [...props.canaryNames] } }
134 : {}),
135 },
136 { description: props.description, state: props.state },
137 process,
138 );