1import * as ce from "@distilled.cloud/aws/cost-explorer";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11import { fetchCeTags, pinCe, syncCeTags, toResourceTags } from "./common.ts";
12
13export interface AnomalyMonitorProps {
14 /**
15 * Name of the anomaly monitor. If omitted, a unique name is generated from
16 * the app, stage, and logical ID. Renaming updates the monitor in place.
17 */
18 monitorName?: string;
19 /**
20 * The type of monitor.
21 *
22 * - `DIMENSIONAL` — monitors spend grouped by a built-in dimension
23 * (`monitorDimension`). AWS allows only one `SERVICE`-dimension monitor
24 * per account.
25 * - `CUSTOM` — monitors spend matching a custom `monitorSpecification`
26 * expression (linked accounts, cost allocation tags, or cost categories).
27 *
28 * Changing the type replaces the monitor.
29 */
30 monitorType: "DIMENSIONAL" | "CUSTOM" | (string & {});
31 /**
32 * The dimension to group spend by for `DIMENSIONAL` monitors. Required when
33 * `monitorType` is `DIMENSIONAL`. Changing it replaces the monitor.
34 */
35 monitorDimension?: "SERVICE" | (string & {});
36 /**
37 * The expression selecting the spend a `CUSTOM` monitor evaluates —
38 * linked accounts, cost allocation tags, or cost categories (raw
39 * Cost Explorer `Expression` shape). Required when `monitorType` is
40 * `CUSTOM`. Changing it replaces the monitor.
41 */
42 monitorSpecification?: ce.Expression;
43 /**
44 * User-defined tags to apply to the monitor.
45 */
46 tags?: Record<string, string>;
47}
48
49export interface AnomalyMonitor extends Resource<
50 "AWS.CostExplorer.AnomalyMonitor",
51 AnomalyMonitorProps,
52 {
53 /** ARN of the anomaly monitor. */
54 monitorArn: string;
55 /** Name of the anomaly monitor. */
56 monitorName: string;
57 /** Type of the monitor (`DIMENSIONAL` or `CUSTOM`). */
58 monitorType: string;
59 /** Current tags on the monitor. */
60 tags: Record<string, string>;
61 },
62 never,
63 Providers
64> {}
65
66/**
67 * A Cost Explorer anomaly detection monitor. Monitors evaluate your spend for
68 * unusual patterns; pair with an
69 * {@link ../AnomalySubscription | AnomalySubscription} to receive alerts.
70 *
71 * Cost Explorer is a global service — all calls are pinned to `us-east-1`
72 * regardless of the stack region. Monitors are free and take effect
73 * immediately.
74 *
75 * ### Creating Anomaly Monitors
76 * **Example:** Custom monitor scoped by a cost allocation tag
77 * ```typescript
78 * import * as CostExplorer from "alchemy/AWS/CostExplorer";
79 *
80 * const monitor = yield* CostExplorer.AnomalyMonitor("TeamSpend", {
81 * monitorType: "CUSTOM",
82 * monitorSpecification: {
83 * Tags: { Key: "CostCenter", Values: ["10000"] },
84 * },
85 * });
86 * ```
87 *
88 * **Example:** Dimensional monitor across all AWS services
89 * ```typescript
90 * const monitor = yield* CostExplorer.AnomalyMonitor("ServiceSpend", {
91 * monitorType: "DIMENSIONAL",
92 * monitorDimension: "SERVICE",
93 * });
94 * ```
95 *
96 * @resource
97 */
98export const AnomalyMonitor = Resource<AnomalyMonitor>(
99 "AWS.CostExplorer.AnomalyMonitor",
100);
101
102export const AnomalyMonitorProvider = () =>
103 Provider.effect(
104 AnomalyMonitor,
105 Effect.gen(function* () {
106 const createName = Effect.fn(function* (
107 id: string,
108 props: { monitorName?: string | undefined },
109 ) {
110 return (
111 props.monitorName ??
112 (yield* createPhysicalName({ id, maxLength: 100 }))
113 );
114 });
115
116 // Observe by ARN — the typed miss tag is UnknownMonitorException.
117 const getByArn = (monitorArn: string) =>
118 pinCe(ce.getAnomalyMonitors({ MonitorArnList: [monitorArn] })).pipe(
119 Effect.map((r) => r.AnomalyMonitors[0]),
120 Effect.catchTag("UnknownMonitorException", () =>
121 Effect.succeed(undefined),
122 ),
123 );
124
125 // Fallback observation when no ARN is cached: scan all monitors for the
126 // deterministic physical name.
127 const findByName = (monitorName: string) =>
128 pinCe(
129 ce.getAnomalyMonitors.items({}).pipe(
130 Stream.filter((m) => m.MonitorName === monitorName),
131 Stream.take(1),
132 Stream.runCollect,
133 ),
134 ).pipe(Effect.map((chunk) => Array.from(chunk)[0]));
135
136 const toAttrs = Effect.fn(function* (live: ce.AnomalyMonitor) {
137 const monitorArn = live.MonitorArn!;
138 return {
139 monitorArn,
140 monitorName: live.MonitorName,
141 monitorType: live.MonitorType,
142 tags: yield* fetchCeTags(monitorArn),
143 };
144 });
145
146 return AnomalyMonitor.Provider.of({
147 stables: ["monitorArn"],
148 list: () =>
149 Effect.gen(function* () {
150 const monitors = yield* pinCe(
151 ce.getAnomalyMonitors.items({}).pipe(Stream.runCollect),
152 ).pipe(Effect.map((chunk) => Array.from(chunk)));
153 return yield* Effect.forEach(
154 monitors.filter((m) => m.MonitorArn !== undefined),
155 toAttrs,
156 { concurrency: 10 },
157 );
158 }),
159 read: Effect.fn(function* ({ id, olds, output }) {
160 const live = output?.monitorArn
161 ? yield* getByArn(output.monitorArn)
162 : yield* findByName(yield* createName(id, olds ?? {}));
163 if (live?.MonitorArn === undefined) return undefined;
164 const attrs = yield* toAttrs(live);
165 return (yield* hasAlchemyTags(id, attrs.tags))
166 ? attrs
167 : Unowned(attrs);
168 }),
169 diff: Effect.fn(function* ({ olds, news }) {
170 if (!isResolved(news)) return;
171 const prior: Partial<AnomalyMonitorProps> = olds ?? {};
172 if (prior.monitorType === undefined) return;
173 // Only the name is mutable via UpdateAnomalyMonitor — the type,
174 // dimension, and specification are create-only.
175 if (
176 prior.monitorType !== news.monitorType ||
177 prior.monitorDimension !== news.monitorDimension ||
178 JSON.stringify(prior.monitorSpecification) !==
179 JSON.stringify(news.monitorSpecification)
180 ) {
181 return { action: "replace" } as const;
182 }
183 }),
184 reconcile: Effect.fn(function* ({ id, news, output, session }) {
185 const name = yield* createName(id, news);
186 const internalTags = yield* createInternalTags(id);
187 const desiredTags = { ...news.tags, ...internalTags };
188
189 // OBSERVE — cloud state is authoritative; output is only an ARN
190 // cache. Monitor names are not unique identity (the ARN is), so the
191 // find-by-name fallback only adopts a monitor whose create-only
192 // props already match the desired state. This keeps replacement
193 // correct: while the engine replaces a monitor (spec change), the
194 // old same-name monitor is still live and must NOT be adopted.
195 const matchesDesired = (m: ce.AnomalyMonitor) =>
196 m.MonitorType === news.monitorType &&
197 m.MonitorDimension === news.monitorDimension &&
198 JSON.stringify(m.MonitorSpecification ?? null) ===
199 JSON.stringify(news.monitorSpecification ?? null);
200 const live = output?.monitorArn
201 ? yield* getByArn(output.monitorArn)
202 : yield* findByName(name).pipe(
203 Effect.map((m) =>
204 m !== undefined && matchesDesired(m) ? m : undefined,
205 ),
206 );
207
208 let monitorArn = live?.MonitorArn;
209 if (monitorArn === undefined) {
210 // ENSURE — create if missing. Monitor names are unique per
211 // account; tolerate the AlreadyExists race by adopting the
212 // same-name monitor only when its create-only props match the
213 // desired state (a mismatch means a genuine conflict — e.g. a
214 // replacement targeting an explicit name — and must surface).
215 const created = yield* pinCe(
216 ce.createAnomalyMonitor({
217 AnomalyMonitor: {
218 MonitorName: name,
219 MonitorType: news.monitorType,
220 MonitorDimension: news.monitorDimension,
221 MonitorSpecification: news.monitorSpecification,
222 },
223 ResourceTags: toResourceTags(desiredTags),
224 }),
225 ).pipe(
226 Effect.catchTag("AnomalyMonitorAlreadyExists", (error) =>
227 findByName(name).pipe(
228 Effect.flatMap((m) =>
229 m?.MonitorArn !== undefined && matchesDesired(m)
230 ? Effect.succeed({ MonitorArn: m.MonitorArn })
231 : Effect.fail(error),
232 ),
233 ),
234 ),
235 );
236 monitorArn = created.MonitorArn;
237 } else if (live!.MonitorName !== name) {
238 // SYNC — the name is the only mutable aspect of a monitor.
239 yield* pinCe(
240 ce.updateAnomalyMonitor({
241 MonitorArn: monitorArn,
242 MonitorName: name,
243 }),
244 );
245 }
246
247 // SYNC TAGS — diff against observed cloud tags.
248 yield* syncCeTags(monitorArn, desiredTags);
249
250 yield* session.note(monitorArn);
251 return {
252 monitorArn,
253 monitorName: name,
254 monitorType: news.monitorType,
255 tags: desiredTags,
256 };
257 }),
258 delete: Effect.fn(function* ({ output }) {
259 yield* pinCe(
260 ce.deleteAnomalyMonitor({ MonitorArn: output.monitorArn }),
261 ).pipe(Effect.catchTag("UnknownMonitorException", () => Effect.void));
262 }),
263 });
264 }),
265 );