1import * as cloudwatch from "@distilled.cloud/aws/cloudwatch";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { createPhysicalName } from "../../PhysicalName.ts";
5import { createInternalTags, createTagsList, diffTags } from "../../Tags.ts";
6
7export type CloudWatchTags = Record<string, string>;
8
9export const createName = (
10 id: string,
11 providedName: string | undefined,
12 maxLength: number,
13) =>
14 providedName
15 ? Effect.succeed(providedName)
16 : createPhysicalName({
17 id,
18 maxLength,
19 });
20
21export const toTagRecord = (
22 tags: cloudwatch.Tag[] | undefined,
23): CloudWatchTags =>
24 Object.fromEntries(
25 (tags ?? [])
26 .filter(
27 (tag): tag is { Key: string; Value: string } =>
28 typeof tag.Key === "string" && typeof tag.Value === "string",
29 )
30 .map((tag) => [tag.Key, tag.Value]),
31 );
32
33export const createManagedTags = Effect.fn(function* (
34 id: string,
35 tags: Record<string, string> | undefined,
36) {
37 return {
38 ...(yield* createInternalTags(id)),
39 ...tags,
40 };
41});
42
43export const updateResourceTags = Effect.fn(function* ({
44 id,
45 resourceArn,
46 olds,
47 news,
48}: {
49 id: string;
50 resourceArn: string;
51 olds: Record<string, string> | undefined;
52 news: Record<string, string> | undefined;
53}) {
54 const oldTags = olds ? yield* createManagedTags(id, olds) : {};
55 const newTags = yield* createManagedTags(id, news);
56 const { removed, upsert } = diffTags(oldTags, newTags);
57
58 if (removed.length > 0) {
59 yield* cloudwatch.untagResource({
60 ResourceARN: resourceArn,
61 TagKeys: removed,
62 });
63 }
64
65 if (upsert.length > 0) {
66 yield* cloudwatch.tagResource({
67 ResourceARN: resourceArn,
68 Tags: upsert,
69 });
70 }
71
72 return newTags;
73});
74
75export const readResourceTags = (resourceArn: string) =>
76 cloudwatch
77 .listTagsForResource({
78 ResourceARN: resourceArn,
79 })
80 .pipe(Effect.map((response) => toTagRecord(response.Tags)));
81
82export const createTagList = (tags: Record<string, string>) =>
83 createTagsList(tags);
84
85export const retryConcurrent = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
86 effect.pipe(
87 Effect.retry({
88 while: (error: any) =>
89 error?._tag === "ConcurrentModificationException" ||
90 error?._tag === "ConflictException" ||
91 error?._tag === "LimitExceededException",
92 schedule: Schedule.max([Schedule.exponential(200), Schedule.recurs(8)]),
93 }),
94 );
95
96const normalizeDimensions = (dimensions: cloudwatch.Dimension[] | undefined) =>
97 [...(dimensions ?? [])].sort((a, b) =>
98 `${a.Name ?? ""}:${a.Value ?? ""}`.localeCompare(
99 `${b.Name ?? ""}:${b.Value ?? ""}`,
100 ),
101 );
102
103const normalizeSingleMetricDetector = (
104 input: Pick<
105 cloudwatch.PutAnomalyDetectorInput,
106 | "Namespace"
107 | "MetricName"
108 | "Dimensions"
109 | "Stat"
110 | "SingleMetricAnomalyDetector"
111 >,
112) => {
113 const singleMetric = input.SingleMetricAnomalyDetector;
114 return {
115 Namespace: singleMetric?.Namespace ?? input.Namespace,
116 MetricName: singleMetric?.MetricName ?? input.MetricName,
117 Dimensions: normalizeDimensions(
118 singleMetric?.Dimensions ?? input.Dimensions,
119 ),
120 Stat: singleMetric?.Stat ?? input.Stat,
121 };
122};
123
124export const detectorIdentity = (
125 input: Pick<
126 cloudwatch.PutAnomalyDetectorInput,
127 | "Namespace"
128 | "MetricName"
129 | "Dimensions"
130 | "Stat"
131 | "SingleMetricAnomalyDetector"
132 | "MetricMathAnomalyDetector"
133 >,
134) =>
135 JSON.stringify({
136 SingleMetric: normalizeSingleMetricDetector(input),
137 MetricMathAnomalyDetector: input.MetricMathAnomalyDetector,
138 });
139
140export const matchesDetectorIdentity = (
141 detector: cloudwatch.AnomalyDetector,
142 input: cloudwatch.PutAnomalyDetectorInput,
143) => detectorIdentity(detector) === detectorIdentity(input);
144
145export const sortByLogicalId = <T extends { LogicalId: string }>(items: T[]) =>
146 [...items].sort((a, b) => a.LogicalId.localeCompare(b.LogicalId));