mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
3bc188efe5
* [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>
129 lines
4.7 KiB
TypeScript
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 });
|
|
}
|
|
});
|
|
});
|