1import * as appsignals from "@distilled.cloud/aws/application-signals";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import type { Providers } from "../Providers.ts";
7
8export interface GroupingConfigurationProps {
9 /**
10 * The custom grouping attribute definitions for this account. Each
11 * definition names a grouping dimension (`GroupingName`), the telemetry
12 * attribute or AWS tag keys it is sourced from (`GroupingSourceKeys`,
13 * e.g. `"Tag.team"` or an OTel resource attribute key), and an optional
14 * `DefaultGroupingValue` used when none of the source keys are present.
15 */
16 groupingAttributeDefinitions: appsignals.GroupingAttributeDefinition[];
17}
18
19export interface GroupingConfiguration extends Resource<
20 "AWS.ApplicationSignals.GroupingConfiguration",
21 GroupingConfigurationProps,
22 {
23 /**
24 * The grouping attribute definitions as returned by the service.
25 */
26 groupingAttributeDefinitions: appsignals.GroupingAttributeDefinition[];
27 /**
28 * When the grouping configuration was last updated (ISO timestamp).
29 */
30 updatedAt: string | undefined;
31 },
32 never,
33 Providers
34> {}
35
36/**
37 * The CloudWatch Application Signals grouping configuration for this
38 * account — an account-level singleton that defines custom grouping
39 * attributes (sourced from telemetry attributes or AWS tags) used to
40 * organize and filter discovered services in the Application Signals
41 * console and APIs.
42 *
43 * There is at most ONE grouping configuration per account/region;
44 * `PutGroupingConfiguration` replaces the whole definition list.
45 *
46 * ### Creating a Grouping Configuration
47 * **Example:** Group Services by Team Tag
48 * ```typescript
49 * import * as AWS from "alchemy/AWS";
50 *
51 * const grouping = yield* AWS.ApplicationSignals.GroupingConfiguration(
52 * "Grouping",
53 * {
54 * groupingAttributeDefinitions: [
55 * {
56 * GroupingName: "Team",
57 * GroupingSourceKeys: ["Tag.team"],
58 * DefaultGroupingValue: "unassigned",
59 * },
60 * ],
61 * },
62 * );
63 * ```
64 *
65 * **Example:** Multiple Grouping Dimensions
66 * ```typescript
67 * const grouping = yield* AWS.ApplicationSignals.GroupingConfiguration(
68 * "Grouping",
69 * {
70 * groupingAttributeDefinitions: [
71 * { GroupingName: "Team", GroupingSourceKeys: ["Tag.team"] },
72 * {
73 * GroupingName: "CostCenter",
74 * GroupingSourceKeys: ["Tag.cost-center", "business_unit"],
75 * DefaultGroupingValue: "shared",
76 * },
77 * ],
78 * },
79 * );
80 * ```
81 *
82 * @resource
83 */
84export const GroupingConfiguration = Resource<GroupingConfiguration>(
85 "AWS.ApplicationSignals.GroupingConfiguration",
86);
87
88/** Normalize a definition for comparison (drop undefined members). */
89const normalizeDefinition = (
90 definition: appsignals.GroupingAttributeDefinition,
91) => ({
92 GroupingName: definition.GroupingName,
93 GroupingSourceKeys: definition.GroupingSourceKeys ?? [],
94 DefaultGroupingValue: definition.DefaultGroupingValue,
95});
96
97const sameDefinitions = (
98 desired: appsignals.GroupingAttributeDefinition[],
99 observed: appsignals.GroupingAttributeDefinition[],
100): boolean =>
101 desired.length === observed.length &&
102 desired.every(
103 (d, i) =>
104 JSON.stringify(normalizeDefinition(d)) ===
105 JSON.stringify(normalizeDefinition(observed[i])),
106 );
107
108export const GroupingConfigurationProvider = () =>
109 Provider.effect(
110 GroupingConfiguration,
111 Effect.gen(function* () {
112 /**
113 * Observe the account's grouping configuration. The API has no
114 * dedicated "get" — an account without a configuration returns an
115 * empty definition list and no `UpdatedAt`.
116 */
117 const observe = appsignals.listGroupingAttributeDefinitions({}).pipe(
118 Effect.map((response) =>
119 response.UpdatedAt === undefined &&
120 response.GroupingAttributeDefinitions.length === 0
121 ? undefined
122 : {
123 groupingAttributeDefinitions: [
124 ...response.GroupingAttributeDefinitions,
125 ],
126 updatedAt: response.UpdatedAt?.toISOString(),
127 },
128 ),
129 );
130
131 return {
132 stables: [],
133
134 read: Effect.fn(function* ({ olds, output }) {
135 const observed = yield* observe;
136 if (observed === undefined) return undefined;
137 // The grouping configuration is not taggable, so ownership cannot
138 // be branded. If we have no record of creating it, surface it as
139 // an existing-but-foreign singleton so takeover requires --adopt.
140 return output !== undefined || olds !== undefined
141 ? observed
142 : Unowned(observed);
143 }),
144
145 reconcile: Effect.fn(function* ({ id, news, session }) {
146 const desired = news.groupingAttributeDefinitions;
147
148 // 1. Observe — the live configuration is authoritative.
149 const observed = yield* observe;
150
151 // 2/3. Ensure + sync — PutGroupingConfiguration is a full-replace
152 // upsert; skip the call when the observed definitions already
153 // match the desired ones.
154 const fresh =
155 observed !== undefined &&
156 sameDefinitions(desired, observed.groupingAttributeDefinitions)
157 ? observed
158 : yield* appsignals
159 .putGroupingConfiguration({
160 GroupingAttributeDefinitions: desired,
161 })
162 .pipe(
163 Effect.map((response) => ({
164 groupingAttributeDefinitions: [
165 ...response.GroupingConfiguration
166 .GroupingAttributeDefinitions,
167 ],
168 updatedAt:
169 response.GroupingConfiguration.UpdatedAt?.toISOString(),
170 })),
171 );
172
173 yield* session.note(id);
174 return fresh;
175 }),
176
177 delete: Effect.fn(function* () {
178 // DeleteGroupingConfiguration is idempotent — deleting an absent
179 // configuration succeeds.
180 yield* appsignals.deleteGroupingConfiguration({});
181 }),
182
183 // Account-level singleton; enumeration happens through read.
184 list: () => Effect.succeed([]),
185 };
186 }),
187 );