1import * as amp from "@distilled.cloud/aws/amp";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8import { normalizeAmpLogGroupArn } from "./internal.ts";
9
10export interface LoggingConfigurationProps {
11 /**
12 * Id of the AMP workspace whose rules/alerting logs are shipped. A
13 * workspace has at most one logging configuration. Changing the workspace
14 * replaces the configuration.
15 */
16 workspaceId: string;
17 /**
18 * ARN of the CloudWatch Logs log group that receives the workspace's rule
19 * evaluation and alerting (vended) logs. AMP expects the ARN with a
20 * trailing `:*` — one is appended automatically when missing.
21 */
22 logGroupArn: string;
23}
24
25export interface LoggingConfiguration extends Resource<
26 "AWS.AMP.LoggingConfiguration",
27 LoggingConfigurationProps,
28 {
29 workspaceId: string;
30 logGroupArn: string;
31 status: string;
32 },
33 never,
34 Providers
35> {}
36
37/**
38 * The rules/alerting logging configuration of an Amazon Managed Service for
39 * Prometheus workspace — ships rule evaluation failures and Alertmanager
40 * delivery errors to a CloudWatch Logs log group. A workspace has at most
41 * one.
42 *
43 * ### Creating a Logging Configuration
44 * **Example:** Ship Rule and Alerting Logs to CloudWatch Logs
45 * ```typescript
46 * const workspace = yield* AMP.Workspace("Metrics", {});
47 * const logs = yield* Logs.LogGroup("AmpLogs", {
48 * logGroupName: "/aws/vendedlogs/prometheus/metrics",
49 * });
50 * const logging = yield* AMP.LoggingConfiguration("Logging", {
51 * workspaceId: workspace.workspaceId,
52 * logGroupArn: logs.logGroupArn,
53 * });
54 * ```
55 *
56 * @resource
57 */
58export const LoggingConfiguration = Resource<LoggingConfiguration>(
59 "AWS.AMP.LoggingConfiguration",
60);
61
62export const LoggingConfigurationProvider = () =>
63 Provider.effect(
64 LoggingConfiguration,
65 Effect.gen(function* () {
66 /** Describe the configuration; typed not-found → undefined. */
67 const describe = Effect.fn(function* (workspaceId: string) {
68 const response = yield* amp
69 .describeLoggingConfiguration({ workspaceId })
70 .pipe(
71 Effect.catchTag("ResourceNotFoundException", () =>
72 Effect.succeed(undefined),
73 ),
74 );
75 return response?.loggingConfiguration;
76 });
77
78 /**
79 * Bounded wait toward ACTIVE. Returns the last observed metadata —
80 * a still-transitioning configuration converges on a later reconcile.
81 */
82 const waitActive = Effect.fn(function* (workspaceId: string) {
83 return yield* amp.describeLoggingConfiguration({ workspaceId }).pipe(
84 Effect.map((r) => r.loggingConfiguration),
85 Effect.repeat({
86 schedule: Schedule.max([
87 Schedule.fixed("2 seconds"),
88 Schedule.recurs(20),
89 ]),
90 until: (c) => c.status.statusCode === "ACTIVE",
91 }),
92 );
93 });
94
95 const toAttrs = (meta: amp.LoggingConfigurationMetadata) => ({
96 workspaceId: meta.workspace,
97 logGroupArn: meta.logGroupArn,
98 status: meta.status.statusCode,
99 });
100
101 return {
102 stables: ["workspaceId"],
103
104 diff: Effect.fn(function* ({ olds, news }) {
105 if (!isResolved(news)) return undefined;
106 if (olds?.workspaceId !== news.workspaceId) {
107 return { action: "replace" } as const;
108 }
109 }),
110
111 read: Effect.fn(function* ({ olds, output }) {
112 const workspaceId = output?.workspaceId ?? olds?.workspaceId;
113 if (!workspaceId) return undefined;
114 const meta = yield* describe(workspaceId);
115 if (meta === undefined) return undefined;
116 // Logging configurations are not taggable — ownership is implied
117 // by the owned parent workspace.
118 return toAttrs(meta);
119 }),
120
121 reconcile: Effect.fn(function* ({ news, session }) {
122 const workspaceId = news!.workspaceId;
123 const desiredArn = normalizeAmpLogGroupArn(news!.logGroupArn);
124
125 // 1. Observe — the live configuration is authoritative.
126 const existing = yield* describe(workspaceId);
127
128 // 2/3. Ensure + sync — create when absent (tolerating a create
129 // race), update when the log group drifts.
130 if (existing === undefined) {
131 yield* amp
132 .createLoggingConfiguration({
133 workspaceId,
134 logGroupArn: desiredArn,
135 })
136 .pipe(
137 Effect.catchTag("ConflictException", () =>
138 amp.updateLoggingConfiguration({
139 workspaceId,
140 logGroupArn: desiredArn,
141 }),
142 ),
143 );
144 } else if (existing.logGroupArn !== desiredArn) {
145 yield* amp.updateLoggingConfiguration({
146 workspaceId,
147 logGroupArn: desiredArn,
148 });
149 }
150
151 const fresh = yield* waitActive(workspaceId);
152 yield* session.note(workspaceId);
153 return toAttrs(fresh);
154 }),
155
156 delete: Effect.fn(function* ({ output }) {
157 yield* amp
158 .deleteLoggingConfiguration({ workspaceId: output.workspaceId })
159 .pipe(
160 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
161 Effect.retry({
162 while: (e) => e._tag === "ConflictException",
163 schedule: Schedule.max([
164 Schedule.fixed("3 seconds"),
165 Schedule.recurs(20),
166 ]),
167 }),
168 );
169 }),
170
171 // Singleton sub-resource keyed by its parent workspace.
172 list: () => Effect.succeed([]),
173 };
174 }),
175 );