1import * as Lambda from "@/AWS/Lambda";
2import * as Telemetry from "@/Telemetry.ts";
3import * as Config from "effect/Config";
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";
8
9/**
10 * Lambda fixture for Telemetry.test.ts: exercises the `Telemetry.layerOtlp`
11 * binding layer. Building it at deploy time binds the collector url
12 * (resolved from the deployer's `COLLECTOR_URL` config, provided by the
13 * test's ConfigProvider override) and the service name onto the Function;
14 * at runtime the exporter reads them back per invocation.
15 *
16 * `GET /work` runs a child span and a log so the test can assert traces AND
17 * logs arrive at the collector after the invocation scope flushes.
18 */
19export class OtelTestFunction extends Lambda.Function<Lambda.Function>()(
20 "OtelTelemetryFunction",
21) {}
22
23export const OtelTestFunctionLive = OtelTestFunction.make(
24 {
25 main: import.meta.url,
26 functionUrl: true,
27 },
28 Effect.gen(function* () {
29 const doWork = Effect.fn("lambda.child-span")(function* () {
30 yield* Effect.log("lambda-work-log");
31 return "lambda-did-work";
32 });
33
34 return {
35 fetch: Effect.gen(function* () {
36 const request = yield* HttpServerRequest;
37 const url = new URL(request.url, "http://x");
38 if (url.pathname === "/work") {
39 const marker = yield* doWork();
40 return yield* HttpServerResponse.json({ marker });
41 }
42 // Readiness gate for the test: the collector's workers.dev URL
43 // propagates per-PoP, so it can serve placeholder 404s to the
44 // Lambda's region long after the test machine sees it. The test
45 // polls this route until it reports 200 before asserting on
46 // exported telemetry.
47 if (url.pathname === "/probe") {
48 const endpoint = yield* Config.String("COLLECTOR_URL").pipe(
49 Effect.orDie,
50 );
51 const result = yield* Effect.tryPromise(() =>
52 fetch(`${endpoint}/v1/probe`, {
53 method: "POST",
54 body: JSON.stringify({ probe: true }),
55 }).then(async (r) => ({
56 status: r.status,
57 body: (await r.text()).slice(0, 200),
58 })),
59 ).pipe(
60 Effect.catchCause((cause) =>
61 Effect.succeed({ status: -1, body: String(cause) }),
62 ),
63 );
64 return yield* HttpServerResponse.json(result);
65 }
66 return HttpServerResponse.text("otel-lambda-ok");
67 }),
68 };
69 }).pipe(
70 // The telemetry binding layer, composed like any other binding: the
71 // collector url resolves once at deploy time and binds onto the
72 // Function; nothing telemetry-specific appears in handler code.
73 Effect.provide(
74 Layer.unwrap(
75 Effect.gen(function* () {
76 const url = yield* Config.String("COLLECTOR_URL");
77 return Telemetry.layerOtlp({ url, serviceName: "otel-lambda-test" });
78 }),
79 ),
80 ),
81 ),
82);
83
84export default OtelTestFunctionLive;