| 1 | import * as appsignals from "@distilled.cloud/aws/application-signals"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import { Unowned } from "../../AdoptPolicy.ts"; |
| 4 | import * as Provider from "../../Provider.ts"; |
| 5 | import { Resource } from "../../Resource.ts"; |
| 6 | import type { Providers } from "../Providers.ts"; |
| 7 | |
| 8 | export 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 | |
| 19 | export 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 | */ |
| 84 | export const GroupingConfiguration = Resource<GroupingConfiguration>( |
| 85 | "AWS.ApplicationSignals.GroupingConfiguration", |
| 86 | ); |
| 87 | |
| 88 | /** Normalize a definition for comparison (drop undefined members). */ |
| 89 | const normalizeDefinition = ( |
| 90 | definition: appsignals.GroupingAttributeDefinition, |
| 91 | ) => ({ |
| 92 | GroupingName: definition.GroupingName, |
| 93 | GroupingSourceKeys: definition.GroupingSourceKeys ?? [], |
| 94 | DefaultGroupingValue: definition.DefaultGroupingValue, |
| 95 | }); |
| 96 | |
| 97 | const 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 | |
| 108 | export 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 | ); |