1import * as ApplicationSignals from "@/AWS/ApplicationSignals";
2import * as Lambda from "@/AWS/Lambda";
3import type * as appsignals from "@distilled.cloud/aws/application-signals";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "handler.ts");
11
12// KeyAttributes of a service that does NOT exist — the discovery reads are
13// account-scoped time-range queries that return well-formed empty results
14// for unknown services, which is exactly what a fresh test account has.
15const PROBE_KEY_ATTRIBUTES = {
16 Type: "Service",
17 Name: "alchemy-test-appsignals-bindings-probe",
18 Environment: "alchemy-test-env",
19};
20
21// A period-based SLI over an arbitrary CloudWatch metric (the metric does
22// not need to exist for the SLO to be valid).
23const SLI_CONFIG: appsignals.ServiceLevelIndicatorConfig = {
24 SliMetricConfig: {
25 MetricDataQueries: [
26 {
27 Id: "m1",
28 MetricStat: {
29 Metric: {
30 Namespace: "Alchemy/Test",
31 MetricName: "AppSignalsBindingsLatency",
32 Dimensions: [{ Name: "Fixture", Value: "bindings" }],
33 },
34 Period: 60,
35 Stat: "Average",
36 },
37 ReturnData: true,
38 },
39 ],
40 },
41 MetricThreshold: 2000,
42 ComparisonOperator: "LessThanOrEqualTo",
43};
44
45export class ApplicationSignalsTestFunction extends Lambda.Function<Lambda.Function>()(
46 "ApplicationSignalsTestFunction",
47) {}
48
49export default ApplicationSignalsTestFunction.make(
50 {
51 main,
52 functionUrl: true,
53 },
54 Effect.gen(function* () {
55 const slo = yield* ApplicationSignals.ServiceLevelObjective("BindingsSlo", {
56 description: "alchemy application-signals bindings fixture slo",
57 sliConfig: SLI_CONFIG,
58 goal: {
59 Interval: { RollingInterval: { DurationUnit: "DAY", Duration: 7 } },
60 AttainmentGoal: 99,
61 WarningThreshold: 50,
62 },
63 });
64 const probe = yield* ApplicationSignals.InstrumentationConfiguration(
65 "BindingsProbe",
66 {
67 instrumentationType: "PROBE",
68 service: "alchemy-test-appsignals-bindings-ic",
69 environment: "alchemy-test-env",
70 signalType: "SNAPSHOT",
71 location: {
72 Language: "Python",
73 CodeUnit: "app.main",
74 MethodName: "handler",
75 FilePath: "app/main.py",
76 LineNumber: 10,
77 },
78 captureConfiguration: {
79 CaptureLocals: ["x"],
80 CaptureLimits: { MaxHits: 1 },
81 },
82 },
83 );
84
85 // --- account-level discovery bindings ---
86 const listServices = yield* ApplicationSignals.ListServices();
87 const getService = yield* ApplicationSignals.GetService();
88 const listServiceDependencies =
89 yield* ApplicationSignals.ListServiceDependencies();
90 const listServiceDependents =
91 yield* ApplicationSignals.ListServiceDependents();
92 const listServiceOperations =
93 yield* ApplicationSignals.ListServiceOperations();
94 const listServiceStates = yield* ApplicationSignals.ListServiceStates();
95 const listEntityEvents = yield* ApplicationSignals.ListEntityEvents();
96 const listAuditFindings = yield* ApplicationSignals.ListAuditFindings();
97
98 // --- SLO-scoped bindings ---
99 const listSlos = yield* ApplicationSignals.ListServiceLevelObjectives();
100 const getSlo = yield* ApplicationSignals.GetServiceLevelObjective(slo);
101 const getBudgetReport =
102 yield* ApplicationSignals.BatchGetServiceLevelObjectiveBudgetReport(slo);
103 const listExclusionWindows =
104 yield* ApplicationSignals.ListServiceLevelObjectiveExclusionWindows(slo);
105 const updateExclusionWindows =
106 yield* ApplicationSignals.BatchUpdateExclusionWindows(slo);
107
108 // --- instrumentation-configuration-scoped binding ---
109 const getInstrumentationStatus =
110 yield* ApplicationSignals.GetInstrumentationConfigurationStatus(probe);
111
112 const bound = {
113 listServices,
114 getService,
115 listServiceDependencies,
116 listServiceDependents,
117 listServiceOperations,
118 listServiceStates,
119 listEntityEvents,
120 listAuditFindings,
121 listSlos,
122 getSlo,
123 getBudgetReport,
124 listExclusionWindows,
125 updateExclusionWindows,
126 getInstrumentationStatus,
127 };
128
129 const window = () => {
130 const EndTime = new Date();
131 const StartTime = new Date(EndTime.getTime() - 3600_000);
132 return { StartTime, EndTime };
133 };
134
135 return {
136 fetch: Effect.gen(function* () {
137 const request = yield* HttpServerRequest;
138 const url = new URL(request.originalUrl);
139 const pathname = url.pathname;
140
141 if (request.method === "GET" && pathname === "/bindings") {
142 return yield* HttpServerResponse.json({
143 bound: Object.keys(bound),
144 });
145 }
146
147 if (request.method === "GET" && pathname === "/services") {
148 const result = yield* listServices(window());
149 return yield* HttpServerResponse.json({
150 count: result.ServiceSummaries.length,
151 });
152 }
153
154 if (request.method === "GET" && pathname === "/service") {
155 const result = yield* getService({
156 ...window(),
157 KeyAttributes: PROBE_KEY_ATTRIBUTES,
158 });
159 // An unknown service returns an empty Service object.
160 return yield* HttpServerResponse.json({
161 discovered: result.Service.KeyAttributes !== undefined,
162 });
163 }
164
165 if (request.method === "GET" && pathname === "/dependencies") {
166 const result = yield* listServiceDependencies({
167 ...window(),
168 KeyAttributes: PROBE_KEY_ATTRIBUTES,
169 });
170 return yield* HttpServerResponse.json({
171 count: result.ServiceDependencies.length,
172 });
173 }
174
175 if (request.method === "GET" && pathname === "/dependents") {
176 const result = yield* listServiceDependents({
177 ...window(),
178 KeyAttributes: PROBE_KEY_ATTRIBUTES,
179 });
180 return yield* HttpServerResponse.json({
181 count: result.ServiceDependents.length,
182 });
183 }
184
185 if (request.method === "GET" && pathname === "/operations") {
186 const result = yield* listServiceOperations({
187 ...window(),
188 KeyAttributes: PROBE_KEY_ATTRIBUTES,
189 });
190 return yield* HttpServerResponse.json({
191 count: result.ServiceOperations.length,
192 });
193 }
194
195 if (request.method === "GET" && pathname === "/states") {
196 const result = yield* listServiceStates(window());
197 return yield* HttpServerResponse.json({
198 count: result.ServiceStates.length,
199 });
200 }
201
202 if (request.method === "GET" && pathname === "/entity-events") {
203 const result = yield* listEntityEvents({
204 ...window(),
205 Entity: PROBE_KEY_ATTRIBUTES,
206 });
207 return yield* HttpServerResponse.json({
208 count: result.ChangeEvents.length,
209 });
210 }
211
212 if (request.method === "GET" && pathname === "/audit-findings") {
213 const result = yield* listAuditFindings({
214 ...window(),
215 AuditTargets: [
216 { Type: "service", Data: { Service: PROBE_KEY_ATTRIBUTES } },
217 ],
218 });
219 return yield* HttpServerResponse.json({
220 count: result.AuditFindings.length,
221 });
222 }
223
224 if (request.method === "GET" && pathname === "/slos") {
225 // The fixture SLO must be among the account's SLOs.
226 const result = yield* listSlos({});
227 const summaries = result.SloSummaries ?? [];
228 return yield* HttpServerResponse.json({
229 count: summaries.length,
230 names: summaries.map((summary) => summary.Name),
231 });
232 }
233
234 if (request.method === "GET" && pathname === "/slo") {
235 const result = yield* getSlo();
236 return yield* HttpServerResponse.json({
237 name: result.Slo.Name,
238 arn: result.Slo.Arn,
239 attainmentGoal: result.Slo.Goal.AttainmentGoal,
240 });
241 }
242
243 if (request.method === "GET" && pathname === "/budget-report") {
244 const result = yield* getBudgetReport({ Timestamp: new Date() });
245 return yield* HttpServerResponse.json({
246 reports: result.Reports.length,
247 errors: result.Errors.length,
248 budgetStatus: result.Reports[0]?.BudgetStatus ?? null,
249 });
250 }
251
252 if (request.method === "GET" && pathname === "/exclusion-windows") {
253 const result = yield* listExclusionWindows();
254 return yield* HttpServerResponse.json({
255 count: result.ExclusionWindows.length,
256 });
257 }
258
259 if (request.method === "POST" && pathname === "/exclusion-windows") {
260 // Add a maintenance window, observe it, then remove it again so
261 // the fixture SLO stays clean between requests.
262 const exclusionWindow: appsignals.ExclusionWindow = {
263 StartTime: new Date(Date.now() + 3600_000),
264 Window: { DurationUnit: "HOUR", Duration: 1 },
265 Reason: "alchemy bindings test",
266 };
267 const added = yield* updateExclusionWindows({
268 AddExclusionWindows: [exclusionWindow],
269 });
270 const afterAdd = yield* listExclusionWindows();
271 const removed = yield* updateExclusionWindows({
272 RemoveExclusionWindows: [exclusionWindow],
273 });
274 const afterRemove = yield* listExclusionWindows();
275 return yield* HttpServerResponse.json({
276 addErrors: added.Errors.length,
277 afterAdd: afterAdd.ExclusionWindows.length,
278 removeErrors: removed.Errors.length,
279 afterRemove: afterRemove.ExclusionWindows.length,
280 });
281 }
282
283 if (request.method === "GET" && pathname === "/ic-status") {
284 // No SDK agent is attached to the synthetic service, so the
285 // status history is empty — the call itself proves the identity
286 // injection and the IAM grant.
287 const result = yield* getInstrumentationStatus();
288 return yield* HttpServerResponse.json({
289 events: result.Events.length,
290 });
291 }
292
293 return yield* HttpServerResponse.json(
294 { error: "Not found", method: request.method, pathname },
295 { status: 404 },
296 );
297 }).pipe(Effect.orDie),
298 };
299 }).pipe(
300 Effect.provide(
301 Layer.mergeAll(
302 ApplicationSignals.ListServicesHttp,
303 ApplicationSignals.GetServiceHttp,
304 ApplicationSignals.ListServiceDependenciesHttp,
305 ApplicationSignals.ListServiceDependentsHttp,
306 ApplicationSignals.ListServiceOperationsHttp,
307 ApplicationSignals.ListServiceStatesHttp,
308 ApplicationSignals.ListEntityEventsHttp,
309 ApplicationSignals.ListAuditFindingsHttp,
310 ApplicationSignals.ListServiceLevelObjectivesHttp,
311 ApplicationSignals.GetServiceLevelObjectiveHttp,
312 ApplicationSignals.BatchGetServiceLevelObjectiveBudgetReportHttp,
313 ApplicationSignals.ListServiceLevelObjectiveExclusionWindowsHttp,
314 ApplicationSignals.BatchUpdateExclusionWindowsHttp,
315 ApplicationSignals.GetInstrumentationConfigurationStatusHttp,
316 ),
317 ),
318 ),
319);