1import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, it } from "@effect/vitest";
4import { DEFAULT_SIGNAL_EXPORT } from "@t3tools/shared/observability";
5import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Tracer from "effect/Tracer";
9import * as NetAddress from "effect/net/NetAddress";
10import {
11 FetchHttpClient,
12 HttpClient,
13 HttpClientError,
14 HttpClientResponse,
15 HttpServer,
16 HttpServerRequest,
17 HttpServerResponse,
18} from "effect/http";
19import { describe } from "vite-plus/test";
20
21import * as ServerConfig from "../config.ts";
22import * as OtlpEndpointCheck from "./OtlpEndpointCheck.ts";
23
24interface PostedRequest {
25 readonly url: string;
26 readonly headers: Readonly<Record<string, string>>;
27 readonly body: string;
28 readonly signal: AbortSignal;
29}
30
31/** Answers every post with `status`, or fails like a refused connection when it is null. */
32const checkWith = (
33 status: number | null,
34 overrides: Partial<ServerConfig.ServerConfig["Service"]> = {},
35 httpClientLayer?: Layer.Layer<HttpClient.HttpClient>,
36) => {
37 const requests: Array<PostedRequest> = [];
38 const httpClient = HttpClient.make((request, _url, signal) => {
39 requests.push({
40 url: request.url,
41 headers: request.headers,
42 body: request.body._tag === "Uint8Array" ? new TextDecoder().decode(request.body.body) : "",
43 signal,
44 });
45 return status === null
46 ? Effect.fail(
47 new HttpClientError.HttpClientError({
48 reason: new HttpClientError.TransportError({ request }),
49 }),
50 )
51 : Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status })));
52 });
53 const configLayer = Layer.effect(
54 ServerConfig.ServerConfig,
55 Effect.gen(function* () {
56 const config = yield* ServerConfig.ServerConfig;
57 return ServerConfig.make({ ...config, ...overrides });
58 }),
59 ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-otlp-check-" })));
60 const layer = OtlpEndpointCheck.layer.pipe(
61 Layer.provide(httpClientLayer ?? Layer.succeed(HttpClient.HttpClient, httpClient)),
62 Layer.provide(configLayer),
63 Layer.provide(NodeServices.layer),
64 );
65 return { requests, layer };
66};
67
68const check = (input: Parameters<OtlpEndpointCheck.OtlpEndpointCheck["Service"]["check"]>[0]) =>
69 Effect.gen(function* () {
70 const service = yield* OtlpEndpointCheck.OtlpEndpointCheck;
71 return yield* service.check(input);
72 });
73
74describe("OtlpEndpointCheck", () => {
75 it.effect("posts an empty export for the signal with the T3 headers", () => {
76 const { requests, layer } = checkWith(200, {
77 otlpMetricsUrl: "http://collector:4318/v1/metrics",
78 otlpMetricsExport: { ...DEFAULT_SIGNAL_EXPORT, headers: { authorization: "Bearer t3" } },
79 });
80 return Effect.gen(function* () {
81 const result = yield* check({ signal: "metrics", url: "http://collector:4318/v1/metrics" });
82
83 assert.strictEqual(result._tag, "Accepted");
84 assert.lengthOf(requests, 1);
85 assert.strictEqual(requests[0]?.url, "http://collector:4318/v1/metrics");
86 assert.strictEqual(requests[0]?.headers.authorization, "Bearer t3");
87 assert.strictEqual(requests[0]?.body, `{"resourceMetrics":[]}`);
88 }).pipe(Effect.provide(layer));
89 });
90
91 it.effect("keeps T3 credentials paired with each running signal endpoint", () => {
92 const signalExport = { ...DEFAULT_SIGNAL_EXPORT, headers: { "x-api-key": "secret" } };
93 const { requests, layer } = checkWith(200, {
94 otlpTracesUrl: "https://collector.example.com/v1/traces",
95 otlpMetricsUrl: "https://collector.example.com/v1/metrics",
96 otlpLogsUrl: "https://collector.example.com/v1/logs",
97 otlpTracesExport: signalExport,
98 otlpMetricsExport: signalExport,
99 otlpLogsExport: signalExport,
100 otelEnvironment: OtelEnvironment.none,
101 });
102 return Effect.gen(function* () {
103 for (const signal of ["traces", "metrics", "logs"] as const) {
104 const runningUrl = `https://collector.example.com/v1/${signal}`;
105 yield* check({ signal, url: runningUrl });
106 yield* check({ signal, url: "https://elsewhere.example.com/collect" });
107 yield* check({ signal, url: `${runningUrl}/other` });
108 yield* check({ signal, url: `${runningUrl}?tenant=other` });
109 }
110
111 assert.lengthOf(requests, 12);
112 requests.forEach((request, index) => {
113 if (index % 4 === 0) assert.strictEqual(request.headers["x-api-key"], "secret");
114 else assert.isUndefined(request.headers["x-api-key"]);
115 });
116 }).pipe(Effect.provide(layer));
117 });
118
119 it.effect("does not send export credentials when the signal has no running endpoint", () => {
120 const { requests, layer } = checkWith(200, {
121 otlpLogsUrl: undefined,
122 otlpLogsExport: { ...DEFAULT_SIGNAL_EXPORT, headers: { authorization: "Bearer t3" } },
123 otelEnvironment: OtelEnvironment.none,
124 });
125 return Effect.gen(function* () {
126 const result = yield* check({ signal: "logs", url: "https://new.example.com/v1/logs" });
127
128 assert.strictEqual(result._tag, "Accepted");
129 assert.lengthOf(requests, 1);
130 assert.isUndefined(requests[0]?.headers.authorization);
131 }).pipe(Effect.provide(layer));
132 });
133
134 it.effect("releases the request once the status is known", () => {
135 const { requests, layer } = checkWith(200);
136 return Effect.gen(function* () {
137 yield* check({ signal: "traces", url: "http://collector:4318/v1/traces" });
138
139 assert.isTrue(requests[0]?.signal.aborted);
140 }).pipe(Effect.provide(layer));
141 });
142
143 it.effect("keeps endpoint URLs and credentials out of traces", () => {
144 const url = "https://collector.example.com/v1/logs?api_key=query-secret";
145 const { requests, layer } = checkWith(200, {
146 otlpLogsUrl: url,
147 otlpLogsExport: {
148 ...DEFAULT_SIGNAL_EXPORT,
149 headers: { "x-honeycomb-team": "header-secret" },
150 },
151 otelEnvironment: OtelEnvironment.none,
152 });
153 const recorded: Array<unknown> = [];
154 const tracer = Tracer.make({
155 span: (options) => {
156 const span = new Tracer.NativeSpan(options);
157 const end = span.end.bind(span);
158 span.end = (endTime, exit) => {
159 end(endTime, exit);
160 recorded.push({ name: span.name, attributes: Object.fromEntries(span.attributes) });
161 };
162 return span;
163 },
164 });
165 return Effect.gen(function* () {
166 yield* check({ signal: "logs", url }).pipe(Effect.withTracer(tracer));
167
168 assert.strictEqual(requests[0]?.headers["x-honeycomb-team"], "header-secret");
169 assert.notInclude(JSON.stringify(recorded), "secret");
170 }).pipe(Effect.provide(layer));
171 });
172
173 it.effect("does not forward credentials through a receiver redirect", () =>
174 Effect.scoped(
175 Effect.gen(function* () {
176 const requests: Array<{ url: string; apiKey: string | undefined }> = [];
177 const server = yield* HttpServer.HttpServer;
178 yield* server.serve(
179 Effect.gen(function* () {
180 const request = yield* HttpServerRequest.HttpServerRequest;
181 yield* request.text;
182 requests.push({ url: request.url, apiKey: request.headers["x-api-key"] });
183 return request.url === "/v1/traces"
184 ? HttpServerResponse.redirect("/other", { status: 307 })
185 : HttpServerResponse.empty({ status: 200 });
186 }),
187 );
188 if (!NetAddress.isInetAddress(server.address)) {
189 return yield* Effect.die(new Error("Expected a TCP address"));
190 }
191 const url = `http://127.0.0.1:${server.address.port}/v1/traces`;
192 const { layer } = checkWith(
193 200,
194 {
195 otlpTracesUrl: url,
196 otlpTracesExport: { ...DEFAULT_SIGNAL_EXPORT, headers: { "x-api-key": "secret" } },
197 },
198 FetchHttpClient.layer,
199 );
200 const result = yield* check({ signal: "traces", url }).pipe(Effect.provide(layer));
201
202 assert.deepStrictEqual(result, { _tag: "Rejected", status: 307 });
203 assert.deepStrictEqual(requests, [{ url: "/v1/traces", apiKey: "secret" }]);
204 }),
205 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
206 );
207
208 it.effect("reports the status a receiver rejects the export with", () => {
209 const { layer } = checkWith(401);
210 return Effect.gen(function* () {
211 const result = yield* check({ signal: "traces", url: "http://collector:4318/v1/traces" });
212
213 assert.deepStrictEqual(result, { _tag: "Rejected", status: 401 });
214 }).pipe(Effect.provide(layer));
215 });
216
217 it.effect("reports an endpoint that cannot be reached", () => {
218 const { layer } = checkWith(null);
219 return Effect.gen(function* () {
220 const result = yield* check({ signal: "logs", url: "http://localhost:1/v1/logs" });
221
222 assert.deepStrictEqual(result, { _tag: "Unreachable", timedOut: false });
223 }).pipe(Effect.provide(layer));
224 });
225
226 it.effect("keeps OTEL endpoint credentials off other endpoints", () => {
227 const otelUrl = "https://otel.example.com/v1/traces";
228 const otelExport = { ...DEFAULT_SIGNAL_EXPORT, headers: { "x-api-key": "secret" } };
229 const { requests, layer } = checkWith(200, {
230 otlpTracesUrl: otelUrl,
231 otlpTracesExport: otelExport,
232 otelEnvironment: {
233 ...OtelEnvironment.none,
234 traces: OtelEnvironment.OtelSignal.Export({
235 url: otelUrl,
236 protocol: otelExport.protocol,
237 headers: otelExport.headers,
238 }),
239 },
240 });
241 return Effect.gen(function* () {
242 yield* check({ signal: "traces", url: otelUrl });
243 yield* check({ signal: "traces", url: "https://elsewhere.example.com/v1/traces" });
244
245 assert.strictEqual(requests[0]?.headers["x-api-key"], "secret");
246 assert.isUndefined(requests[1]?.headers["x-api-key"]);
247 }).pipe(Effect.provide(layer));
248 });
249});