1import * as Cloudflare from "@/Cloudflare/index.ts";
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 * Effect-native Worker exercising the built-in `Telemetry.layerOtlp` binding
11 * layer: building it at deploy time binds the collector url (resolved from
12 * the deployer's `COLLECTOR_URL` config, provided by Telemetry.test.ts
13 * after the collector deploys) and the service name onto the Worker; at
14 * runtime the exporter reads them back per request.
15 *
16 * `GET /work` runs a child span and a log so the test can assert traces
17 * AND logs arrive at the collector after the request scope flushes.
18 */
19export default class OtelTracedWorker extends Cloudflare.Worker<OtelTracedWorker>()(
20 "OtelTracedWorker",
21 {
22 main: import.meta.url,
23 },
24 Effect.gen(function* () {
25 const doWork = Effect.fn("test.child-span")(function* () {
26 yield* Effect.log("did-work-log");
27 return "did-work";
28 });
29 return {
30 fetch: Effect.gen(function* () {
31 const request = yield* HttpServerRequest;
32 const url = new URL(request.url, "http://x");
33 if (url.pathname === "/work") {
34 const result = yield* doWork();
35 return yield* HttpServerResponse.json({ marker: result });
36 }
37 // Readiness gate for the test: OTLP export goes worker->worker, so
38 // the collector's custom domain must be reachable *from inside a
39 // Worker* (workers.dev would be blocked with error 1042). The test
40 // polls this route until it reports 200 before asserting on
41 // exported telemetry.
42 if (url.pathname === "/probe") {
43 const endpoint = yield* Config.String("COLLECTOR_URL").pipe(
44 Effect.orDie,
45 );
46 const result = yield* Effect.tryPromise(() =>
47 fetch(`${endpoint}/v1/traces`, {
48 method: "POST",
49 body: JSON.stringify({ probe: true }),
50 }).then(async (r) => ({
51 status: r.status,
52 body: (await r.text()).slice(0, 200),
53 })),
54 ).pipe(
55 Effect.catchCause((cause) =>
56 Effect.succeed({ status: -1, body: String(cause) }),
57 ),
58 );
59 return yield* HttpServerResponse.json(result);
60 }
61 return HttpServerResponse.text("otel-traced-ok");
62 }),
63 };
64 }).pipe(
65 // The telemetry binding layers, composed like any other binding: the
66 // collector url is resolved once at deploy time and bound onto the
67 // Worker; nothing telemetry-specific appears in handler code. Two
68 // merged otlp layers = two destinations — spans are serialized once
69 // and fanned out to both (the second lands under `/v1/second-traces`,
70 // which the collector records as signal "second-traces").
71 Effect.provide(
72 Layer.unwrap(
73 Effect.gen(function* () {
74 const url = yield* Config.String("COLLECTOR_URL");
75 return Layer.mergeAll(
76 Telemetry.layerOtlp({ url, serviceName: "otel-traced-test" }),
77 Telemetry.layerOtlp({ traces: { url: `${url}/v1/second-traces` } }),
78 );
79 }),
80 ),
81 ),
82 ),
83) {}