Open History to see the last commit
1import { assert, describe, it } from "@effect/vitest";
2import * as ConfigProvider from "effect/ConfigProvider";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as OtlpResource from "effect/observability/OtlpResource";
6
7import * as OtelEnvironment from "./otelEnvironment.ts";
8
9const load = (env: Record<string, string>) =>
10 OtelEnvironment.load.pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env }))));
11
12const SPEC_OFF =
13 "OTEL_SDK_DISABLED is set, so no telemetry is exported, whatever configured it; set T3CODE_OTEL_SDK_DISABLED=false to export anyway";
14const T3_OFF =
15 "T3CODE_OTEL_SDK_DISABLED is set, so no telemetry is exported, whatever configured it";
16const specIgnored = (value: string) =>
17 `OTEL_SDK_DISABLED=${value} was read as false; the OpenTelemetry specification recognizes only the string true, so use OTEL_SDK_DISABLED=true or T3CODE_OTEL_SDK_DISABLED to say it any other way`;
18
19describe("OtelEnvironment", () => {
20 it.effect.each([
21 { name: "nothing set", env: {}, disabled: false, warnings: [] },
22 { name: "spec true", env: { OTEL_SDK_DISABLED: "true" }, disabled: true, warnings: [SPEC_OFF] },
23 {
24 name: "spec 1",
25 env: { OTEL_SDK_DISABLED: "1" },
26 disabled: false,
27 warnings: [specIgnored("1")],
28 },
29 { name: "t3 1", env: { T3CODE_OTEL_SDK_DISABLED: "1" }, disabled: true, warnings: [T3_OFF] },
30 {
31 name: "t3 false overrides spec true",
32 env: { T3CODE_OTEL_SDK_DISABLED: "false", OTEL_SDK_DISABLED: "true" },
33 disabled: false,
34 warnings: [],
35 },
36 {
37 name: "unreadable t3 warns and falls through",
38 env: { T3CODE_OTEL_SDK_DISABLED: "maybe", OTEL_SDK_DISABLED: "true" },
39 disabled: true,
40 warnings: ["T3CODE_OTEL_SDK_DISABLED=maybe is not a yes or a no and was ignored", SPEC_OFF],
41 },
42 ])("$name", ({ env, disabled, warnings }) =>
43 Effect.gen(function* () {
44 const resolved = yield* load(env);
45 assert.strictEqual(resolved.disabled, disabled);
46 assert.deepStrictEqual(resolved.warnings, warnings);
47 }),
48 );
49
50 it.effect.each([
51 {
52 name: "a percent-encoded list",
53 env: { OTEL_RESOURCE_ATTRIBUTES: "team=core,message=hello%20world" },
54 resourceAttributes: { team: "core", message: "hello world" },
55 warnings: [],
56 },
57 {
58 name: "a list that does not decode",
59 env: { OTEL_RESOURCE_ATTRIBUTES: "team=core,broken=%zz" },
60 resourceAttributes: {},
61 warnings: [
62 "OTEL_RESOURCE_ATTRIBUTES is not a list of percent-encoded key=value pairs and was ignored",
63 ],
64 },
65 ])("resource attributes: $name", ({ env, resourceAttributes, warnings }) =>
66 Effect.gen(function* () {
67 const resolved = yield* load(env);
68 assert.deepStrictEqual(resolved.resourceAttributes, resourceAttributes);
69 assert.deepStrictEqual(resolved.warnings, warnings);
70 }),
71 );
72
73 describe("endpoints", () => {
74 const urlOf = (signal: OtelEnvironment.OtelSignal) =>
75 OtelEnvironment.OtelSignal.$match(signal, {
76 Export: ({ url }) => url,
77 Off: () => "Off",
78 Unset: () => "Unset",
79 });
80 it.effect.each([
81 {
82 name: "generic endpoint appends each signal's path, keeping the query",
83 env: { OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318/base?api_key=secret" },
84 traces: "https://collector:4318/base/v1/traces?api_key=secret",
85 metrics: "https://collector:4318/base/v1/metrics?api_key=secret",
86 logs: "https://collector:4318/base/v1/logs?api_key=secret",
87 warnings: [],
88 },
89 {
90 name: "a per-signal endpoint is used verbatim",
91 env: { OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://tempo:4318/custom" },
92 traces: "https://tempo:4318/custom",
93 metrics: "Unset",
94 logs: "Unset",
95 warnings: [],
96 },
97 {
98 name: "a per-signal endpoint beats the generic one",
99 env: {
100 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://tempo:4318/custom",
101 OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318/base",
102 },
103 traces: "https://tempo:4318/custom",
104 metrics: "https://collector:4318/base/v1/metrics",
105 logs: "https://collector:4318/base/v1/logs",
106 warnings: [],
107 },
108 {
109 name: "a blank per-signal endpoint falls through to the generic one",
110 env: {
111 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: " ",
112 OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318/base",
113 },
114 traces: "https://collector:4318/base/v1/traces",
115 metrics: "https://collector:4318/base/v1/metrics",
116 logs: "https://collector:4318/base/v1/logs",
117 warnings: [],
118 },
119 {
120 name: "an invalid per-signal endpoint warns and does not fall through",
121 env: {
122 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "not-a-url",
123 OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318/base",
124 },
125 traces: "Off",
126 metrics: "https://collector:4318/base/v1/metrics",
127 logs: "https://collector:4318/base/v1/logs",
128 warnings: [
129 "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is not an http or https URL, so the signals it configures are not exported",
130 ],
131 },
132 {
133 name: "an invalid generic endpoint warns without leaking its query",
134 env: { OTEL_EXPORTER_OTLP_ENDPOINT: "not-a-url?api_key=secret" },
135 traces: "Off",
136 metrics: "Off",
137 logs: "Off",
138 warnings: [
139 "OTEL_EXPORTER_OTLP_ENDPOINT is not an http or https URL, so the signals it configures are not exported",
140 ],
141 },
142 {
143 name: "the kill switch wins outright over a valid endpoint",
144 env: {
145 T3CODE_OTEL_SDK_DISABLED: "true",
146 OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318/base",
147 },
148 traces: "Unset",
149 metrics: "Unset",
150 logs: "Unset",
151 warnings: [T3_OFF],
152 },
153 {
154 name: "an exporter of none turns off only its signal",
155 env: {
156 OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318",
157 OTEL_LOGS_EXPORTER: "none",
158 },
159 traces: "https://collector:4318/v1/traces",
160 metrics: "https://collector:4318/v1/metrics",
161 logs: "Off",
162 warnings: [],
163 },
164 ])("$name", ({ env, traces, metrics, logs, warnings }) =>
165 Effect.gen(function* () {
166 const resolved = yield* load(env);
167 assert.strictEqual(urlOf(resolved.traces), traces);
168 assert.strictEqual(urlOf(resolved.metrics), metrics);
169 assert.strictEqual(urlOf(resolved.logs), logs);
170 assert.deepStrictEqual(resolved.warnings, warnings);
171 }),
172 );
173
174 const ENDPOINT = { OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318" };
175 const exportOf = (signal: OtelEnvironment.OtelSignal): unknown =>
176 OtelEnvironment.OtelSignal.$match(signal, {
177 Export: ({ protocol, headers }) => ({ protocol, headers }),
178 Off: () => "Off",
179 Unset: () => "Unset",
180 });
181 it.effect.each([
182 {
183 name: "a per-signal protocol and headers beat the generic ones",
184 env: {
185 ...ENDPOINT,
186 OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
187 OTEL_EXPORTER_OTLP_HEADERS: "api-key=shared",
188 OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "http/protobuf",
189 OTEL_EXPORTER_OTLP_TRACES_HEADERS: "api-key=traces%20only",
190 },
191 traces: { protocol: "http/protobuf", headers: { "api-key": "traces only" } },
192 logs: { protocol: "http/json", headers: { "api-key": "shared" } },
193 warnings: [],
194 },
195 {
196 name: "an unsupported protocol turns off the signals it configures",
197 env: { ...ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: "grpc" },
198 traces: { protocol: "http/protobuf", headers: undefined },
199 logs: "Off",
200 warnings: [
201 "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL is not http/protobuf or http/json, so the signals it configures are not exported",
202 ],
203 },
204 {
205 name: "undecodable headers turn off every signal once, without leaking them",
206 env: { ...ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS: "api-key=%zz" },
207 traces: "Off",
208 logs: "Off",
209 warnings: [
210 "OTEL_EXPORTER_OTLP_HEADERS is not a list of key=value pairs with percent-encoded values, so the signals it configures are not exported",
211 ],
212 },
213 ])("$name", ({ env, traces, logs, warnings }) =>
214 Effect.gen(function* () {
215 const resolved = yield* load(env);
216 assert.deepStrictEqual(exportOf(resolved.traces), traces);
217 assert.deepStrictEqual(exportOf(resolved.logs), logs);
218 assert.deepStrictEqual(resolved.warnings, warnings);
219 }),
220 );
221 });
222
223 it.effect("names the variable each signal's endpoint came from", () =>
224 Effect.gen(function* () {
225 const otel = yield* load({
226 OTEL_EXPORTER_OTLP_ENDPOINT: "https://collector:4318",
227 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://tempo:4318/v1/traces",
228 });
229 const variableOf = (signal: OtelEnvironment.OtelSignal) =>
230 OtelEnvironment.OtelSignal.$match(signal, {
231 Export: ({ variable }) => variable,
232 Off: () => undefined,
233 Unset: () => undefined,
234 });
235 assert.strictEqual(variableOf(otel.traces), "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT");
236 assert.strictEqual(variableOf(otel.metrics), "OTEL_EXPORTER_OTLP_ENDPOINT");
237 assert.strictEqual(variableOf(otel.logs), "OTEL_EXPORTER_OTLP_ENDPOINT");
238 }),
239 );
240
241 describe("resolveSignalEndpoint", () => {
242 const t3Export = {
243 protocol: "http/json",
244 headers: { "x-key": "t3" },
245 exportIntervalMs: 5_000,
246 } as const;
247 const withLogs = (logs: OtelEnvironment.OtelSignal, disabled = false) => ({
248 ...OtelEnvironment.none,
249 disabled,
250 logs,
251 });
252 const otelExport = OtelEnvironment.OtelSignal.Export({
253 url: "http://otel:4318/v1/logs",
254 variable: "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
255 protocol: "http/protobuf",
256 headers: { "x-key": "otel" },
257 });
258 it.each([
259 {
260 name: "T3CODE_OTLP_*_URL wins over a signal the OTEL variables turned off",
261 otel: withLogs(OtelEnvironment.OtelSignal.Off()),
262 t3Url: "http://t3:4318/v1/logs",
263 expected: {
264 url: "http://t3:4318/v1/logs",
265 export: t3Export,
266 source: "environment" as const,
267 variable: "T3CODE_OTLP_LOGS_URL",
268 },
269 },
270 {
271 name: "an OTEL endpoint brings its headers and protocol over the fallback",
272 otel: withLogs(otelExport),
273 t3Url: " ",
274 expected: {
275 url: "http://otel:4318/v1/logs",
276 export: {
277 protocol: "http/protobuf" as const,
278 headers: { "x-key": "otel" },
279 exportIntervalMs: 5_000,
280 },
281 source: "environment" as const,
282 variable: "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
283 },
284 },
285 {
286 name: "a signal the OTEL variables turned off does not fall through",
287 otel: withLogs(OtelEnvironment.OtelSignal.Off()),
288 t3Url: undefined,
289 expected: undefined,
290 },
291 {
292 name: "an unset signal takes the first non-blank fallback",
293 otel: withLogs(OtelEnvironment.OtelSignal.Unset()),
294 t3Url: undefined,
295 expected: {
296 url: "http://settings:4318/v1/logs",
297 export: t3Export,
298 source: "settings" as const,
299 },
300 },
301 {
302 name: "a desktop bootstrap URL that differs from the setting wins as the desktop's own",
303 otel: withLogs(OtelEnvironment.OtelSignal.Unset()),
304 t3Url: undefined,
305 bootstrapUrl: "http://desktop:4318/v1/logs",
306 expected: {
307 url: "http://desktop:4318/v1/logs",
308 export: t3Export,
309 source: "bootstrap" as const,
310 },
311 },
312 {
313 name: "a desktop bootstrap URL that forwards the setting counts as the setting",
314 otel: withLogs(OtelEnvironment.OtelSignal.Unset()),
315 t3Url: undefined,
316 bootstrapUrl: " http://settings:4318/v1/logs ",
317 expected: {
318 url: "http://settings:4318/v1/logs",
319 export: t3Export,
320 source: "settings" as const,
321 },
322 },
323 ])("$name", ({ otel, t3Url, bootstrapUrl = "", expected }) => {
324 assert.deepStrictEqual(
325 OtelEnvironment.resolveSignalEndpoint(
326 otel,
327 "logs",
328 { url: t3Url, export: t3Export },
329 { bootstrapUrl, settingsUrl: "http://settings:4318/v1/logs" },
330 ),
331 expected,
332 );
333 });
334 });
335
336 it.effect("an exporter of none keeps the Settings endpoint from re-enabling its signal", () =>
337 Effect.gen(function* () {
338 const otel = yield* load({ OTEL_LOGS_EXPORTER: "none" });
339 const t3 = {
340 url: undefined,
341 export: { protocol: "http/json", headers: undefined, exportIntervalMs: 10_000 },
342 } as const;
343 assert.strictEqual(
344 OtelEnvironment.resolveSignalEndpoint(otel, "logs", t3, {
345 bootstrapUrl: undefined,
346 settingsUrl: "http://settings:4318/v1/logs",
347 }),
348 undefined,
349 );
350 assert.strictEqual(
351 OtelEnvironment.resolveSignalEndpoint(otel, "traces", t3, {
352 bootstrapUrl: undefined,
353 settingsUrl: "http://settings:4318/v1/traces",
354 })?.url,
355 "http://settings:4318/v1/traces",
356 );
357 }),
358 );
359
360 describe("layerResourceAttributes", () => {
361 it.effect.each([
362 { name: "a list that does not decode", raw: "team=%zz", attributes: [] },
363 { name: "encoded separators", raw: "a%2Cb=x%3Dy", attributes: ["a,b"] },
364 ])("lets the exporters' own read succeed with $name", ({ raw, attributes }) =>
365 Effect.gen(function* () {
366 const layerEnv = ConfigProvider.layer(
367 ConfigProvider.fromEnv({ env: { OTEL_RESOURCE_ATTRIBUTES: raw } }),
368 );
369 const otel = yield* OtelEnvironment.load.pipe(Effect.provide(layerEnv));
370 const resource = yield* OtlpResource.fromConfig({ serviceName: "t3" }).pipe(
371 Effect.provide(
372 Layer.provide(
373 OtelEnvironment.layerResourceAttributes(otel.resourceAttributes),
374 layerEnv,
375 ),
376 ),
377 );
378 assert.deepStrictEqual(
379 resource.attributes
380 .map((attribute) => attribute.key)
381 .filter((key) => !key.startsWith("telemetry.sdk.")),
382 [...attributes, "service.name"],
383 );
384 }),
385 );
386 });
387});
388
389describe("isRelayTracingEnabled", () => {
390 it("defaults on and respects the T3CODE SDK override of the standard kill switch", () => {
391 assert.isTrue(OtelEnvironment.isRelayTracingEnabled({}));
392 assert.isFalse(OtelEnvironment.isRelayTracingEnabled({ OTEL_SDK_DISABLED: "true" }));
393 assert.isTrue(
394 OtelEnvironment.isRelayTracingEnabled({
395 OTEL_SDK_DISABLED: "true",
396 T3CODE_OTEL_SDK_DISABLED: "false",
397 }),
398 );
399 assert.isFalse(
400 OtelEnvironment.isRelayTracingEnabled({
401 OTEL_SDK_DISABLED: "false",
402 T3CODE_OTEL_SDK_DISABLED: "true",
403 }),
404 );
405 });
406
407 it.effect("product opt-out beats SDK enable without disabling user-configured tracing", () =>
408 Effect.gen(function* () {
409 const env = {
410 T3CODE_TELEMETRY_ENABLED: "false",
411 T3CODE_OTEL_SDK_DISABLED: "false",
412 OTEL_SDK_DISABLED: "true",
413 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "https://user.example.test/v1/traces",
414 };
415 assert.isFalse(OtelEnvironment.isRelayTracingEnabled(env));
416
417 const otel = yield* load(env);
418 assert.isFalse(otel.disabled);
419 assert.deepStrictEqual(
420 otel.traces,
421 OtelEnvironment.OtelSignal.Export({
422 url: env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
423 variable: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
424 protocol: "http/protobuf",
425 headers: undefined,
426 }),
427 );
428 }),
429 );
430});