1import type { Credentials } from "@distilled.cloud/aws/Credentials";
2import type { Region } from "@distilled.cloud/aws/Region";
3import * as obs from "@distilled.cloud/aws/observabilityadmin";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import type * as HttpClient from "effect/http/HttpClient";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import type { Providers } from "../Providers.ts";
11
12export interface TelemetryConfigProps {
13 /**
14 * Whether the CloudWatch telemetry config feature (telemetry evaluation)
15 * is onboarded for this account. Set `false` to keep the resource in the
16 * stack while the feature is off.
17 * @default true
18 */
19 enabled?: boolean;
20}
21
22export interface TelemetryConfig extends Resource<
23 "AWS.ObservabilityAdmin.TelemetryConfig",
24 TelemetryConfigProps,
25 {
26 /**
27 * The onboarding status of the telemetry config feature after
28 * reconciliation (`RUNNING` or `STOPPED`).
29 */
30 status: string;
31 /**
32 * The status observed before this stack first managed the feature
33 * (`NOT_STARTED`, `RUNNING`, `STOPPED`, ...). Destroy restores this —
34 * a feature that was already running when the stack adopted it is left
35 * running.
36 */
37 priorStatus: string;
38 },
39 never,
40 Providers
41> {}
42
43/**
44 * Account-level CloudWatch **telemetry config** (Observability Admin
45 * telemetry evaluation) — an account singleton that audits which AWS
46 * resources (VPCs, Lambda functions, ...) have telemetry such as flow
47 * logs enabled.
48 *
49 * This is an always-present account *setting*, not a discrete resource:
50 * deploying it onboards the account, and destroying it restores whatever
51 * onboarding state the account had before the stack first managed it.
52 *
53 * ### Managing telemetry config
54 * **Example:** Onboard the account
55 * ```typescript
56 * import * as ObservabilityAdmin from "alchemy/AWS/ObservabilityAdmin";
57 *
58 * const telemetry = yield* ObservabilityAdmin.TelemetryConfig("Telemetry");
59 * ```
60 *
61 * **Example:** Keep the resource but switch the feature off
62 * ```typescript
63 * const telemetry = yield* ObservabilityAdmin.TelemetryConfig("Telemetry", {
64 * enabled: false,
65 * });
66 * ```
67 *
68 * @resource
69 */
70export const TelemetryConfig = Resource<TelemetryConfig>(
71 "AWS.ObservabilityAdmin.TelemetryConfig",
72);
73
74/**
75 * Raised when the telemetry config feature reports `FAILED_START` /
76 * `FAILED_STOP`, or fails to converge within the bounded wait.
77 */
78export class TelemetryConfigTransitionFailed extends Data.TaggedError(
79 "TelemetryConfigTransitionFailed",
80)<{ message: string; status: string }> {}
81
82type ObsRequirements = Credentials | Region | HttpClient.HttpClient;
83
84const readStatus: Effect.Effect<
85 string,
86 obs.GetTelemetryEvaluationStatusError,
87 ObsRequirements
88> = obs
89 .getTelemetryEvaluationStatus({})
90 .pipe(Effect.map((r) => r.Status ?? "NOT_STARTED"));
91
92const ON_STATES = ["RUNNING", "STARTING"];
93
94/**
95 * Poll the evaluation status on a bounded schedule until it reaches a
96 * settled state (start/stop completed in ~1s in probing; the bound is
97 * defensive). Explicitly typed at module scope so the conditional types of
98 * `Effect.repeat` never leak into the provider layer's declaration emit.
99 */
100const awaitSettledStatus: Effect.Effect<
101 string,
102 obs.GetTelemetryEvaluationStatusError,
103 ObsRequirements
104> = readStatus.pipe(
105 Effect.repeat({
106 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(10)]),
107 until: (status) => status !== "STARTING" && status !== "STOPPING",
108 }),
109);
110
111const failIfUnsettled = (status: string, transition: "start" | "stop") =>
112 status === "STARTING" || status === "STOPPING" || status.startsWith("FAILED")
113 ? Effect.fail(
114 new TelemetryConfigTransitionFailed({
115 message: `telemetry evaluation did not settle after ${transition} (status: ${status})`,
116 status,
117 }),
118 )
119 : Effect.succeed(status);
120
121/**
122 * One idempotent convergence step shared by reconcile and delete-restore:
123 * observe the live status and start/stop only on an actual delta.
124 */
125const convergeTo = Effect.fn(function* (desiredOn: boolean) {
126 const observed = yield* awaitSettledStatus;
127 const isOn = ON_STATES.includes(observed);
128 if (desiredOn && !isOn) {
129 yield* obs.startTelemetryEvaluation({});
130 return yield* failIfUnsettled(yield* awaitSettledStatus, "start");
131 }
132 if (!desiredOn && isOn) {
133 yield* obs.stopTelemetryEvaluation({});
134 return yield* failIfUnsettled(yield* awaitSettledStatus, "stop");
135 }
136 return observed;
137});
138
139export const TelemetryConfigProvider = () =>
140 Provider.effect(
141 TelemetryConfig,
142 Effect.gen(function* () {
143 return TelemetryConfig.Provider.of({
144 // Account singleton setting — deleting only restores the prior
145 // onboarding state; nuke must not toggle it.
146 nuke: { singleton: true },
147 stables: ["priorStatus"],
148 // No enumeration API — the singleton is always present as account
149 // configuration, never a discrete listable resource.
150 list: () => Effect.succeed([]),
151 read: Effect.fn(function* ({ output }) {
152 const status = yield* readStatus;
153 return {
154 status,
155 priorStatus: output?.priorStatus ?? status,
156 };
157 }),
158 reconcile: Effect.fn(function* ({ news, output, session }) {
159 // Capture the pre-management status exactly once so destroy can
160 // restore it; on later updates the persisted capture wins.
161 const priorStatus = output?.priorStatus ?? (yield* readStatus);
162 const desiredOn = news.enabled !== false;
163 const status = yield* convergeTo(desiredOn);
164 yield* session.note(`telemetry evaluation ${status}`);
165 return { status, priorStatus };
166 }),
167 delete: Effect.fn(function* ({ output, session }) {
168 // Restore the captured pre-management state instead of blindly
169 // stopping: an account that was already onboarded before this
170 // stack managed it stays onboarded.
171 const priorOn = ON_STATES.includes(output.priorStatus);
172 const status = yield* convergeTo(priorOn);
173 yield* session.note(`telemetry evaluation restored to ${status}`);
174 }),
175 });
176 }),
177 );