1import * as AWS from "@/AWS";
2import * as Cloudflare from "@/Cloudflare/index.ts";
3import * as Test from "@/Test/Alchemy";
4import { describe } from "alchemy-test";
5import * as ConfigProvider from "effect/ConfigProvider";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as pathe from "pathe";
9import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
10import type { OtelSink } from "./fixtures/otel-collector-worker.ts";
11import {
12 OtelTestFunction,
13 OtelTestFunctionLive,
14} from "./fixtures/otel-handler.ts";
15
16const { test } = Test.make({
17 providers: Layer.mergeAll(AWS.providers(), Cloudflare.providers()),
18});
19
20const collectorMain = pathe.resolve(
21 import.meta.dirname,
22 "fixtures/otel-collector-worker.ts",
23);
24
25// The OTLP sink the Lambda exports to. A workers.dev URL is fine here —
26// the 1042 worker-to-worker restriction doesn't apply to requests coming
27// from Lambda. Declared identically in both deploy steps so the second
28// deploy keeps it.
29const collectorWorker = () =>
30 Cloudflare.Worker("OtelCollector", {
31 main: collectorMain,
32 env: {
33 SINK: Cloudflare.DurableObject<OtelSink>("OtelSink"),
34 },
35 compatibility: { date: "2024-09-23" },
36 });
37
38describe(
39 "AWS.Lambda Telemetry",
40 {
41 tags: [
42 "provider:aws",
43 "provider:aws:lambda",
44 "provider:cloudflare",
45 "provider:cloudflare:worker",
46 "live",
47 ],
48 },
49 () => {
50 test.provider(
51 "Lambda exports OTLP telemetry per invocation",
52 (stack) =>
53 Effect.gen(function* () {
54 yield* stack.destroy();
55
56 // Deploy the collector first: the Lambda's OTLP endpoint is
57 // resolved from the deployer's environment at deploy time.
58 const collector = yield* stack.deploy(
59 Effect.gen(function* () {
60 return yield* collectorWorker();
61 }),
62 );
63 const collected = `${collector.url}/collected`;
64 yield* expectUrlContains(collected, "otel-collector-ok", {
65 timeout: "180 seconds",
66 });
67
68 // The fixture's telemetry binding layer reads `COLLECTOR_URL` via
69 // `Config` at Init. The test harness snapshots its ConfigProvider
70 // before the test body runs, so layer the just-learned collector
71 // URL on top of the current provider for this deploy only.
72 const currentConfig = yield* ConfigProvider.ConfigProvider;
73 const { fn } = yield* stack
74 .deploy(
75 Effect.gen(function* () {
76 yield* collectorWorker();
77 const fn = yield* OtelTestFunction.pipe(
78 Effect.provide(OtelTestFunctionLive),
79 );
80 return { fn };
81 }),
82 )
83 .pipe(
84 Effect.provideService(
85 ConfigProvider.ConfigProvider,
86 ConfigProvider.orElse(
87 ConfigProvider.fromUnknown({
88 COLLECTOR_URL: collector.url,
89 }),
90 currentConfig,
91 ),
92 ),
93 );
94
95 // Wait for the function URL to serve AND for the collector to be
96 // reachable from the Lambda's region — workers.dev propagates
97 // per-PoP, so the Lambda can see placeholder 404s long after the
98 // test machine gets 200s.
99 const fnUrl = (fn.functionUrl as string).replace(/\/$/, "");
100 yield* expectUrlContains(`${fnUrl}/probe`, '"status":200', {
101 timeout: "240 seconds",
102 label: "collector reachable from Lambda",
103 });
104
105 // Drive one traced invocation.
106 yield* expectUrlContains(`${fnUrl}/work`, "lambda-did-work", {
107 timeout: "120 seconds",
108 });
109
110 // The invocation scope's flush ships everything the invocation
111 // produced before the response is returned.
112 yield* expectUrlContains(collected, "otel-lambda-test", {
113 timeout: "120 seconds",
114 label: "lambda service.name",
115 });
116 // http.server root span from the HttpMiddleware tracer.
117 yield* expectUrlContains(collected, "http.server GET");
118 // Child span from Effect.fn instrumentation.
119 yield* expectUrlContains(collected, "lambda.child-span");
120 // Log record shipped by the OTLP logger.
121 yield* expectUrlContains(collected, "lambda-work-log");
122 }),
123 { timeout: 600_000 },
124 );
125 },
126);