Files
oh-my-pi/packages/coding-agent/test/telemetry-export.test.ts
T
Chris Greeno d6a0d05fc0 feat(coding-agent): env-driven OTLP trace export for headless hosts
When OTEL_EXPORTER_OTLP_ENDPOINT (or _TRACES_ENDPOINT) is set, register a
global OTLP/proto TracerProvider and switch on the agent loop's telemetry
so its invoke_agent / chat / execute_tool spans reach a collector instead
of a no-op tracer. Honors the standard OTEL_* env contract
(OTEL_SERVICE_NAME, OTEL_SDK_DISABLED, OTEL_TRACES_EXPORTER=none) and the
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT capture toggle; no-op
when no endpoint is configured.

Uses the exporter-trace-otlp-proto 2.x line, which exports cleanly under
Bun (the 1.x line deadlocks). Adds a flushTelemetryExport() helper and a
periodic + signal-driven flush so long-lived processes surface spans.

This makes the existing telemetry usable from hosts that spawn omp as a
child process (e.g. an ACP harness), where an in-process provider
registered by the parent can't reach the child process.
2026-06-03 20:15:29 +01:00

92 lines
3.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { fileURLToPath } from "node:url";
import { initTelemetryExport, isTelemetryExportEnabled } from "../src/telemetry-export";
/**
* Gating contract for the OTLP export bootstrap. These cases all short-circuit
* before a provider is registered, so they never mutate the module singleton
* and are order-independent. The positive export path runs in a subprocess (see
* the "exports spans" test) so the registered global provider can't leak here.
*/
const OTEL_KEYS = [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
"OTEL_SDK_DISABLED",
"OTEL_TRACES_EXPORTER",
] as const;
let saved: Record<string, string | undefined>;
beforeEach(() => {
saved = Object.fromEntries(OTEL_KEYS.map(k => [k, process.env[k]]));
for (const k of OTEL_KEYS) delete process.env[k];
});
afterEach(() => {
for (const k of OTEL_KEYS) {
const v = saved[k];
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
});
describe("initTelemetryExport gating", () => {
it("stays disabled when no OTLP endpoint is configured", () => {
initTelemetryExport();
expect(isTelemetryExportEnabled()).toBe(false);
});
it("stays disabled when OTEL_SDK_DISABLED=true even with an endpoint", () => {
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318";
process.env.OTEL_SDK_DISABLED = "true";
initTelemetryExport();
expect(isTelemetryExportEnabled()).toBe(false);
});
it("stays disabled when OTEL_TRACES_EXPORTER=none even with an endpoint", () => {
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318";
process.env.OTEL_TRACES_EXPORTER = "none";
initTelemetryExport();
expect(isTelemetryExportEnabled()).toBe(false);
});
it("declines unsupported OTLP protocols instead of misrouting spans", () => {
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4317";
process.env.OTEL_EXPORTER_OTLP_PROTOCOL = "grpc";
initTelemetryExport();
expect(isTelemetryExportEnabled()).toBe(false);
process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "http/json";
initTelemetryExport();
expect(isTelemetryExportEnabled()).toBe(false);
});
it("honors the kill-switches case-insensitively per the OTEL env contract", () => {
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318";
process.env.OTEL_SDK_DISABLED = "TRUE";
initTelemetryExport();
expect(isTelemetryExportEnabled()).toBe(false);
delete process.env.OTEL_SDK_DISABLED;
process.env.OTEL_TRACES_EXPORTER = "otlp,None";
initTelemetryExport();
expect(isTelemetryExportEnabled()).toBe(false);
});
});
describe("initTelemetryExport export path", () => {
it("registers a provider and exports spans to an OTLP/proto receiver", async () => {
// Run in a subprocess: initTelemetryExport() registers a process-global
// provider, so exercising the positive path in-process would leak that
// singleton into every later test. The probe stands up its own loopback
// receiver and exits 0 only when a protobuf trace export actually lands.
const probe = fileURLToPath(new URL("./otel-export-probe.ts", import.meta.url));
const proc = Bun.spawn(["bun", probe], { stdout: "pipe", stderr: "pipe" });
const [code, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
expect(stdout).toContain("PROBE: RECEIVED");
expect(code).toBe(0);
}, 20_000);
});