Files
openclaw/test/e2e/qa-lab/runtime/otel-generation-config-watcher-runtime.test.ts
SunnyShu 3bc188efe5 fix(diagnostics-otel): keep telemetry exporting across in-process restarts (#120131)
* [AI] fix(diagnostics-otel): keep telemetry exporting across in-process restarts

Owned diagnostics-otel generations previously registered their OpenTelemetry
providers globally and only shut the providers down on stop. The pinned
sdk-node registers globals without override and never unregisters them, so a
second in-process generation (config-watcher reload, OPENCLAW_NO_RESPAWN=1,
containers) kept exporting through the first, already-shutdown providers and
telemetry silently stopped.

Switch owned mode to private BasicTracerProvider + MeterProvider instances
whose handles are injected directly into the existing recorder runtime, and
keep the preloaded (OPENCLAW_OTEL_PRELOADED=1) mode on the host's global
providers without ever registering or replacing globals. Resource detection
honors the pinned NodeSDK OTEL_NODE_RESOURCE_DETECTORS contract (unset
defaults to env+process+host; none/subset/all are respected), and the unused
@opentelemetry/sdk-node dependency is removed with a minimal lockfile update.

Adds a real-SDK two-generation restart regression, real-SDK resource-detector
selection coverage, migrates the unit mocks to the provider lifecycle, and
updates the OpenTelemetry docs.

Fixes #119997

Co-Authored-By: glm-5.2 <noreply@anthropic.com>

* [AI] test(diagnostics-otel): restore OpenTelemetry globals after restart test

The restart regression disables global context, metrics, propagation, and
trace APIs and forces OPENCLAW_OTEL_PRELOADED=0, but its cleanup only reset
diagnostic events, so a later test in the same worker could inherit no-op
providers. Snapshot the prior global registrations and preloaded env at
module load, and re-register/restore them in afterEach, matching the
existing exporter-health integration test pattern.

Co-Authored-By: glm-5.2 <noreply@anthropic.com>

* [AI] fix(diagnostics-otel): reconcile private providers with OTEL_SDK_DISABLED

Rebase onto main after #119961 (honor OTEL_SDK_DISABLED) changed the
disabled admission path to return before SDK construction. Restore the
removed getBooleanFromEnv import, drop the stale ownedNodeSdkDisabled
guard, and update disabled-mode tests to the new semantics (all routes
off, disabled runtime registered) plus the private-provider signal
path for the integration suite.

Related to #119997

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(diagnostics-otel): isolate restartable provider generations

* test(diagnostics-otel): prove same-pid generation routing

* test(diagnostics-otel): use supported watcher reload mode

* test(diagnostics-otel): satisfy ownership proof gates

* test(diagnostics-otel): use managed proof cleanup

* test(diagnostics-otel): verify injected trace ancestry

---------

Co-authored-by: glm-5.2 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-08-07 22:24:18 +08:00

129 lines
4.7 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../../helpers/temp-dir.js";
import {
runOtelGenerationConfigWatcherRuntime,
testing,
} from "./otel-generation-config-watcher-runtime.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
describe("OTEL generation config watcher runtime", () => {
it(
"keeps all three signals on the active same-PID provider generation",
{ timeout: 300_000 },
async () => {
const artifactBase = tempDirs.make("otel-generation-config-watcher-");
const { evidence, summary } = await runOtelGenerationConfigWatcherRuntime({
artifactBase,
repoRoot: process.cwd(),
});
expect(evidence.schemaVersion).toBe(2);
expect(evidence.entries[0]?.result.status).toBe("pass");
expect(summary).toMatchObject({
collectorAPostReadyRequestCount: 0,
failures: [],
noRespawn: true,
passed: true,
pid: { same: true },
readyAfterMutation: true,
restartLogObserved: true,
});
for (const [collector, parentSpanId] of [
[summary.collectorA, "1111111111111111"],
[summary.collectorB, "2222222222222222"],
] as const) {
expect(collector).toMatchObject({
externalParentSpanIds: [parentSpanId],
failedRequestCount: 0,
logCorrelationValid: true,
parentGraphValid: true,
requiredSpanNames: ["openclaw.model.call", "openclaw.run"],
traceparentAccepted: true,
});
expect(collector?.signalRequestCounts).toMatchObject({
logs: expect.any(Number),
metrics: expect.any(Number),
traces: expect.any(Number),
});
expect(collector?.signalRequestCounts.logs).toBeGreaterThan(0);
expect(collector?.signalRequestCounts.metrics).toBeGreaterThan(0);
expect(collector?.signalRequestCounts.traces).toBeGreaterThan(0);
}
const summaryText = await fs.readFile(
path.join(artifactBase, "otel-generation-config-watcher-summary.json"),
"utf8",
);
expect(summaryText).not.toContain(artifactBase);
expect(summaryText).not.toContain("http://127.0.0.1");
const evidenceText = await fs.readFile(path.join(artifactBase, "qa-evidence.json"), "utf8");
expect(evidenceText).not.toContain(artifactBase);
},
);
it("rejects missing output-dir values", () => {
expect(() => testing.parseOptions(["--output-dir"])).toThrow("--output-dir requires a value");
});
it("redacts local failure details before writing artifacts", () => {
const localEndpoint = `http://${["127", "0", "0", "1"].join(".")}:4318`;
const gatewayToken = "qa-suite-12345678-1234-1234-1234-123456789abc";
const failure = testing.sanitizeProofFailure(
new Error(
`failed at /workspace/repo/test.ts via ${localEndpoint} in /tmp/openclaw-qa-suite-private with ${gatewayToken}`,
),
"/workspace/repo",
);
expect(failure).toContain("<repo>");
expect(failure).toContain("<local-endpoint>");
expect(failure).toContain("<temp-path>");
expect(failure).toContain("<gateway-token>");
expect(failure).not.toContain("/workspace/repo");
expect(failure).not.toContain(localEndpoint);
});
it("requires every exported span chain to terminate at the injected parent", () => {
const externalParentSpanId = "1111111111111111";
const unrelatedParentSpanId = "2222222222222222";
const rootSpanId = "aaaaaaaaaaaaaaaa";
const childSpanId = "bbbbbbbbbbbbbbbb";
const base = {
attributes: {},
name: "span",
parent: true,
traceId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
};
const inspect = (spans: Parameters<typeof testing.inspectParentGraph>[0]) =>
testing.inspectParentGraph(spans, externalParentSpanId);
expect(
inspect([
{ ...base, parentSpanId: externalParentSpanId, spanId: rootSpanId },
{ ...base, parentSpanId: rootSpanId, spanId: childSpanId },
]),
).toEqual({
externalParentSpanIds: [externalParentSpanId],
valid: true,
});
for (const spans of [
[
{ ...base, parentSpanId: unrelatedParentSpanId, spanId: rootSpanId },
{ ...base, parentSpanId: rootSpanId, spanId: childSpanId },
],
[
{ ...base, parentSpanId: externalParentSpanId, spanId: rootSpanId },
{ ...base, parentSpanId: undefined, spanId: childSpanId },
],
[
{ ...base, parentSpanId: childSpanId, spanId: rootSpanId },
{ ...base, parentSpanId: rootSpanId, spanId: childSpanId },
],
]) {
expect(inspect(spans)).toMatchObject({ valid: false });
}
});
});