Files
openclaw/test/e2e/qa-lab/runtime/otel-generation-config-watcher-runtime.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

753 lines
24 KiB
TypeScript

import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import { pathToFileURL } from "node:url";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { WebSocket, type RawData } from "ws";
import {
QA_EVIDENCE_FILENAME,
startQaGatewayChild,
startQaMockOpenAiServer,
} from "../../../../extensions/qa-lab/api.js";
import {
GATEWAY_CLIENT_IDS,
GATEWAY_CLIENT_MODES,
} from "../../../../packages/gateway-protocol/src/client-info.js";
import {
MIN_CLIENT_PROTOCOL_VERSION,
PROTOCOL_VERSION,
} from "../../../../packages/gateway-protocol/src/version.js";
import type { OpenClawConfig } from "../../../../src/config/types.openclaw.js";
import { formatErrorMessage } from "../../../../src/infra/errors.js";
import {
type CapturedLogRecord,
type CapturedRequest,
type CapturedSpan,
startLocalOtlpReceiver,
} from "./otel-test-support.js";
import { createQaScriptEvidenceWriter } from "./script-evidence.js";
const SCENARIO_ID = "otel-generation-config-watcher";
const SOURCE_PATH = "test/e2e/qa-lab/runtime/otel-generation-config-watcher-runtime.ts";
const FRAME_TIMEOUT_MS = 90_000;
const SIGNAL_TIMEOUT_MS = 60_000;
const RESTART_TIMEOUT_MS = 120_000;
const POST_STOP_SETTLE_MS = 500;
type RuntimeOptions = {
artifactBase: string;
repoRoot: string;
};
type GenerationTarget = {
marker: string;
parentSpanId: string;
traceId: string;
};
type SignalRequestCounts = Record<"logs" | "metrics" | "traces", number>;
type GenerationEvidence = {
externalParentSpanIds: string[];
failedRequestCount: number;
logCorrelationValid: boolean;
logRecordCount: number;
metricNames: string[];
parentGraphValid: boolean;
requiredSpanNames: string[];
signalRequestCounts: SignalRequestCounts;
spanCount: number;
spanNames: string[];
traceId: string;
traceparentAccepted: boolean;
};
export type OtelGenerationConfigWatcherSummary = {
collectorA?: GenerationEvidence;
collectorB?: GenerationEvidence;
collectorAPostReadyRequestCount: number | null;
failures: string[];
noRespawn: boolean;
passed: boolean;
pid: {
after: number | null;
before: number | null;
same: boolean;
};
readyAfterMutation: boolean;
restartLogObserved: boolean;
};
type RawGatewayClient = {
frames: unknown[];
socket: WebSocket;
};
type LocalReceiver = ReturnType<typeof startLocalOtlpReceiver> & {
baseUrl: string;
};
const GENERATION_A: GenerationTarget = {
marker: "OTEL-GENERATION-A-OK",
parentSpanId: "1111111111111111",
traceId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
};
const GENERATION_B: GenerationTarget = {
marker: "OTEL-GENERATION-B-OK",
parentSpanId: "2222222222222222",
traceId: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
};
function assertContract(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function parseOptions(argv: readonly string[], repoRoot = process.cwd()): RuntimeOptions {
let artifactBase = path.join(repoRoot, ".artifacts", "qa-e2e", "otel-generation-config-watcher");
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--output-dir") {
const value = argv[++index];
if (!value) {
throw new Error("--output-dir requires a value");
}
artifactBase = path.resolve(repoRoot, value);
continue;
}
if (arg === "--") {
continue;
}
throw new Error(`Unknown argument: ${arg}`);
}
return { artifactBase, repoRoot };
}
function rawDataText(data: RawData): string {
if (Array.isArray(data)) {
return Buffer.concat(data.map((chunk) => Buffer.from(chunk))).toString("utf8");
}
if (Buffer.isBuffer(data)) {
return data.toString("utf8");
}
return Buffer.from(data).toString("utf8");
}
async function openRawGatewayClient(wsUrl: string): Promise<RawGatewayClient> {
const socket = new WebSocket(wsUrl);
const frames: unknown[] = [];
socket.on("message", (data) => {
frames.push(JSON.parse(rawDataText(data)));
});
await new Promise<void>((resolve, reject) => {
socket.once("open", resolve);
socket.once("error", reject);
});
return { frames, socket };
}
async function waitFor<T>(params: {
label: string;
read: () => Promise<T | undefined> | T | undefined;
timeoutMs: number;
timeoutContext?: () => unknown;
}): Promise<T> {
const deadline = Date.now() + params.timeoutMs;
while (Date.now() < deadline) {
const value = await params.read();
if (value !== undefined) {
return value;
}
await sleep(50);
}
const context = params.timeoutContext?.();
throw new Error(
`timed out waiting for ${params.label}${
context === undefined ? "" : `: ${JSON.stringify(context)}`
}`,
);
}
function responseFor(id: string) {
return (frame: unknown) => isRecord(frame) && frame.type === "res" && frame.id === id;
}
async function waitForFrame(
client: RawGatewayClient,
predicate: (frame: unknown) => boolean,
startIndex = 0,
timeoutMs = FRAME_TIMEOUT_MS,
): Promise<unknown> {
return await waitFor({
label: "Gateway response frame",
timeoutMs,
read: () => client.frames.slice(startIndex).find(predicate),
timeoutContext: () => ({
capturedFrameCount: client.frames.length,
socketState: client.socket.readyState,
}),
});
}
async function closeRawGatewayClient(client: RawGatewayClient): Promise<void> {
const isClosed = () => client.socket.readyState === WebSocket.CLOSED;
if (isClosed()) {
return;
}
const closed = new Promise<void>((resolve) => {
client.socket.once("close", () => resolve());
});
if (client.socket.readyState === WebSocket.OPEN) {
client.socket.close();
}
await Promise.race([closed, sleep(1_000)]);
if (!isClosed()) {
client.socket.terminate();
}
}
function sendFrame(client: RawGatewayClient, frame: unknown): void {
client.socket.send(JSON.stringify(frame));
}
async function requestRawFrame(
client: RawGatewayClient,
params: {
method: string;
requestParams: unknown;
timeoutMs?: number;
traceparent?: string;
},
): Promise<Record<string, unknown>> {
const id = randomUUID();
const startIndex = client.frames.length;
sendFrame(client, {
type: "req",
id,
method: params.method,
params: params.requestParams,
...(params.traceparent ? { traceparent: params.traceparent } : {}),
});
const frame = await waitForFrame(
client,
responseFor(id),
startIndex,
params.timeoutMs ?? FRAME_TIMEOUT_MS,
);
assertContract(isRecord(frame), `${params.method} response was not an object`);
return frame;
}
async function requestRaw(
client: RawGatewayClient,
params: {
method: string;
requestParams: unknown;
timeoutMs?: number;
traceparent?: string;
},
): Promise<Record<string, unknown>> {
const frame = await requestRawFrame(client, params);
assertContract(
frame.ok === true,
`${params.method} failed: ${JSON.stringify(frame.error ?? frame)}`,
);
assertContract(isRecord(frame.payload), `${params.method} response omitted payload`);
return frame.payload;
}
async function connectRawGateway(params: {
token: string;
wsUrl: string;
}): Promise<RawGatewayClient> {
const client = await openRawGatewayClient(params.wsUrl);
await waitForFrame(
client,
(frame) => isRecord(frame) && frame.type === "event" && frame.event === "connect.challenge",
);
const payload = await requestRaw(client, {
method: "connect",
requestParams: {
minProtocol: MIN_CLIENT_PROTOCOL_VERSION,
maxProtocol: PROTOCOL_VERSION,
client: {
id: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT,
version: SCENARIO_ID,
platform: process.platform,
mode: GATEWAY_CLIENT_MODES.BACKEND,
},
role: "operator",
scopes: ["operator.admin"],
auth: { token: params.token },
},
});
assertContract(payload.type === "hello-ok", "Gateway connect did not return hello-ok");
return client;
}
function traceparent(target: GenerationTarget): string {
return `00-${target.traceId}-${target.parentSpanId}-01`;
}
async function runTracedTurn(
gateway: Awaited<ReturnType<typeof startQaGatewayChild>>,
target: GenerationTarget,
): Promise<void> {
const client = await connectRawGateway({ token: gateway.token, wsUrl: gateway.wsUrl });
try {
const started = await requestRaw(client, {
method: "chat.send",
requestParams: {
sessionKey: `agent:qa:${SCENARIO_ID}-${target.marker.toLowerCase()}-${randomUUID()}`,
message: `Reply exactly: ${target.marker}`,
deliver: false,
idempotencyKey: randomUUID(),
},
traceparent: traceparent(target),
timeoutMs: 30_000,
});
assertContract(started.status === "started", "chat.send did not start");
assertContract(typeof started.runId === "string" && started.runId, "chat.send omitted runId");
const completed = await requestRaw(client, {
method: "agent.wait",
requestParams: {
runId: started.runId,
timeoutMs: 60_000,
},
traceparent: traceparent(target),
timeoutMs: 70_000,
});
assertContract(completed.status === "ok", "agent.wait did not complete successfully");
const logProbe = await requestRawFrame(client, {
method: "qa.otel.generation.log-probe",
requestParams: {},
traceparent: traceparent(target),
timeoutMs: 10_000,
});
assertContract(logProbe.ok === false, "unknown-method log probe unexpectedly succeeded");
} finally {
await closeRawGatewayClient(client);
}
}
async function startReceiver(): Promise<LocalReceiver> {
const receiver = startLocalOtlpReceiver();
const port = await receiver.listen();
return { ...receiver, baseUrl: `http://127.0.0.1:${port}` };
}
function signalRequestCounts(requests: readonly CapturedRequest[]): SignalRequestCounts {
return {
logs: requests.filter((request) => request.status === 200 && request.signal === "logs").length,
metrics: requests.filter((request) => request.status === 200 && request.signal === "metrics")
.length,
traces: requests.filter((request) => request.status === 200 && request.signal === "traces")
.length,
};
}
function isTraceId(value: string | undefined): value is string {
return typeof value === "string" && /^[0-9a-f]{32}$/u.test(value);
}
function isSpanId(value: string | undefined): value is string {
return typeof value === "string" && /^[0-9a-f]{16}$/u.test(value);
}
function inspectParentGraph(
spans: readonly CapturedSpan[],
expectedExternalParentSpanId: string,
): {
externalParentSpanIds: string[];
valid: boolean;
} {
const spansById = new Map(
spans.flatMap((span) => (isSpanId(span.spanId) ? ([[span.spanId, span]] as const) : [])),
);
if (spansById.size !== spans.length) {
return { externalParentSpanIds: [], valid: false };
}
const externalParentSpanIds = new Set<string>();
for (const span of spans) {
const visited = new Set<string>();
let current: CapturedSpan | undefined = span;
while (current) {
if (!isSpanId(current.parentSpanId)) {
return { externalParentSpanIds: [...externalParentSpanIds].toSorted(), valid: false };
}
if (visited.has(current.parentSpanId)) {
return { externalParentSpanIds: [...externalParentSpanIds].toSorted(), valid: false };
}
visited.add(current.parentSpanId);
const parent = spansById.get(current.parentSpanId);
if (!parent) {
externalParentSpanIds.add(current.parentSpanId);
if (current.parentSpanId !== expectedExternalParentSpanId) {
return { externalParentSpanIds: [...externalParentSpanIds].toSorted(), valid: false };
}
}
current = parent;
}
}
return {
externalParentSpanIds: [...externalParentSpanIds].toSorted(),
valid:
externalParentSpanIds.size === 1 && externalParentSpanIds.has(expectedExternalParentSpanId),
};
}
function inspectGeneration(receiver: LocalReceiver, target: GenerationTarget): GenerationEvidence {
const spans = receiver.capturedSpans.filter((span) => span.traceId === target.traceId);
const logs = receiver.capturedLogRecords.filter((record) => record.traceId === target.traceId);
const graph = inspectParentGraph(spans, target.parentSpanId);
const spanNames = [...new Set(spans.map((span) => span.name))].toSorted();
const metricNames = [
...new Set(
receiver.capturedMetrics
.map((metric) => metric.name)
.filter((name) => name.startsWith("openclaw.")),
),
].toSorted();
const requiredSpanNames = ["openclaw.model.call", "openclaw.run"].filter((name) =>
spanNames.includes(name),
);
const logCorrelationValid = logs.some(
(record: CapturedLogRecord) =>
isTraceId(record.traceId) && isSpanId(record.spanId) && record.traceId === target.traceId,
);
return {
externalParentSpanIds: graph.externalParentSpanIds,
failedRequestCount: receiver.capturedRequests.filter((request) => request.status !== 200)
.length,
logCorrelationValid,
logRecordCount: logs.length,
metricNames,
parentGraphValid: graph.valid,
requiredSpanNames,
signalRequestCounts: signalRequestCounts(receiver.capturedRequests),
spanCount: spans.length,
spanNames,
traceId: target.traceId,
traceparentAccepted: spans.length > 0 && graph.valid,
};
}
function generationReady(evidence: GenerationEvidence): boolean {
return (
evidence.failedRequestCount === 0 &&
evidence.signalRequestCounts.logs > 0 &&
evidence.signalRequestCounts.metrics > 0 &&
evidence.signalRequestCounts.traces > 0 &&
evidence.requiredSpanNames.length === 2 &&
evidence.metricNames.length > 0 &&
evidence.logCorrelationValid &&
evidence.parentGraphValid &&
evidence.traceparentAccepted
);
}
async function waitForGeneration(
receiver: LocalReceiver,
target: GenerationTarget,
): Promise<GenerationEvidence> {
return await waitFor({
label: `${target.marker} OTLP signals`,
timeoutMs: SIGNAL_TIMEOUT_MS,
read: () => {
const evidence = inspectGeneration(receiver, target);
return generationReady(evidence) ? evidence : undefined;
},
timeoutContext: () => inspectGeneration(receiver, target),
});
}
function withOtelEndpoint(config: OpenClawConfig, endpoint: string): OpenClawConfig {
return {
...config,
gateway: {
...config.gateway,
reload: { mode: "hybrid" },
},
logging: {
...config.logging,
level: "info",
consoleLevel: "info",
},
diagnostics: {
...config.diagnostics,
enabled: true,
otel: {
...config.diagnostics?.otel,
enabled: true,
endpoint,
tracesEndpoint: `${endpoint}/v1/traces`,
metricsEndpoint: `${endpoint}/v1/metrics`,
logsEndpoint: `${endpoint}/v1/logs`,
protocol: "http/protobuf",
traces: true,
metrics: true,
logs: true,
logsExporter: "otlp",
sampleRate: 1,
flushIntervalMs: 250,
captureContent: false,
},
},
};
}
async function updateWatchedEndpoint(configPath: string, endpoint: string): Promise<void> {
const parsed = JSON.parse(await fs.readFile(configPath, "utf8")) as OpenClawConfig;
const next = withOtelEndpoint(parsed, endpoint);
await fs.writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
}
async function stopResources(params: {
gateway?: Awaited<ReturnType<typeof startQaGatewayChild>>;
gatewayStopped: boolean;
mock?: Awaited<ReturnType<typeof startQaMockOpenAiServer>>;
receiverA?: LocalReceiver;
receiverB?: LocalReceiver;
}): Promise<void> {
const failures: unknown[] = [];
if (params.gateway && !params.gatewayStopped) {
await params.gateway.stop().catch((error: unknown) => failures.push(error));
}
await params.mock?.stop().catch((error: unknown) => failures.push(error));
await params.receiverA?.close().catch((error: unknown) => failures.push(error));
await params.receiverB?.close().catch((error: unknown) => failures.push(error));
if (failures.length > 0) {
throw new AggregateError(failures, "OTEL generation watcher cleanup failed");
}
}
async function probeOtelGenerationConfigWatcher(
options: RuntimeOptions,
): Promise<OtelGenerationConfigWatcherSummary> {
let receiverA: LocalReceiver | undefined;
let receiverB: LocalReceiver | undefined;
let mock: Awaited<ReturnType<typeof startQaMockOpenAiServer>> | undefined;
let gateway: Awaited<ReturnType<typeof startQaGatewayChild>> | undefined;
let gatewayStopped = false;
try {
receiverA = await startReceiver();
receiverB = await startReceiver();
mock = await startQaMockOpenAiServer();
gateway = await startQaGatewayChild({
repoRoot: options.repoRoot,
useRepoCli: true,
providerBaseUrl: `${mock.baseUrl}/v1`,
providerMode: "mock-openai",
primaryModel: "mock-openai/gpt-5.6-luna",
alternateModel: "mock-openai/gpt-5.6-luna-alt",
transportBaseUrl: "http://127.0.0.1",
enabledPluginIds: ["diagnostics-otel"],
controlUiEnabled: false,
runtimeEnvPatch: {
OPENCLAW_NO_RESPAWN: "1",
OPENCLAW_OTEL_PRELOADED: "0",
OTEL_SDK_DISABLED: "false",
},
mutateConfig: (config) => withOtelEndpoint(config, receiverA!.baseUrl),
});
const noRespawn = gateway.runtimeEnv.OPENCLAW_NO_RESPAWN === "1";
const pidBefore = gateway.pid;
assertContract(noRespawn, "QA Gateway did not set OPENCLAW_NO_RESPAWN=1");
assertContract(typeof pidBefore === "number", "QA Gateway did not expose its PID");
await runTracedTurn(gateway, GENERATION_A);
await waitForGeneration(receiverA, GENERATION_A);
const restartLogOffset = gateway.logs().length;
await updateWatchedEndpoint(gateway.configPath, receiverB.baseUrl);
const restartLogObserved = await waitFor({
label: "in-process config watcher restart",
timeoutMs: RESTART_TIMEOUT_MS,
read: () =>
gateway!
.logs()
.slice(restartLogOffset)
.includes("restart mode: in-process restart (OPENCLAW_NO_RESPAWN)")
? true
: undefined,
});
await waitFor({
label: "post-restart Gateway readiness",
timeoutMs: RESTART_TIMEOUT_MS,
read: async () => {
try {
const response = await fetch(`${gateway!.baseUrl}/readyz`);
return response.ok ? true : undefined;
} catch {
return undefined;
}
},
});
await gateway.call("config.get", {}, { timeoutMs: RESTART_TIMEOUT_MS });
const readyAfterMutation = true;
const readyAtMs = Date.now();
const collectorARequestCountAtReady = receiverA.capturedRequests.length;
const pidAfter = gateway.pid;
await runTracedTurn(gateway, GENERATION_B);
await waitForGeneration(receiverB, GENERATION_B);
await sleep(1_000);
await gateway.stop();
gatewayStopped = true;
await sleep(POST_STOP_SETTLE_MS);
const collectorA = inspectGeneration(receiverA, GENERATION_A);
const collectorB = inspectGeneration(receiverB, GENERATION_B);
const postReadyByCursor = receiverA.capturedRequests.slice(collectorARequestCountAtReady);
const postReadyByTimestamp = receiverA.capturedRequests.filter(
(request) => (request.receivedAtMs ?? 0) > readyAtMs,
);
const collectorAPostReadyRequestCount = Math.max(
postReadyByCursor.length,
postReadyByTimestamp.length,
);
const failures: string[] = [];
if (!generationReady(collectorA)) {
failures.push("collector A final OTLP evidence failed the generation contract");
}
if (!generationReady(collectorB)) {
failures.push("collector B final OTLP evidence failed the generation contract");
}
if (pidAfter !== pidBefore) {
failures.push(`Gateway PID changed across in-process restart: ${pidBefore} -> ${pidAfter}`);
}
if (collectorAPostReadyRequestCount !== 0) {
failures.push(
`collector A received ${collectorAPostReadyRequestCount} request(s) after readiness`,
);
}
return {
collectorA,
collectorB,
collectorAPostReadyRequestCount,
failures,
noRespawn,
passed: failures.length === 0,
pid: {
after: pidAfter,
before: pidBefore,
same: pidAfter === pidBefore,
},
readyAfterMutation,
restartLogObserved,
};
} finally {
await stopResources({ gateway, gatewayStopped, mock, receiverA, receiverB });
}
}
function createWriter(options: RuntimeOptions) {
return createQaScriptEvidenceWriter({
artifactBase: options.artifactBase,
logFileName: `${SCENARIO_ID}.log`,
primaryModel: "mock-openai/gpt-5.6-luna",
providerMode: "mock-openai",
repoRoot: options.repoRoot,
target: {
id: SCENARIO_ID,
title: "OTEL generation config watcher",
sourcePath: SOURCE_PATH,
docsRefs: ["docs/gateway/opentelemetry.md", "docs/concepts/qa-e2e-automation.md"],
codeRefs: [
SOURCE_PATH,
"extensions/diagnostics-otel/src/service.ts",
"extensions/diagnostics-otel/src/service-propagation.ts",
"extensions/qa-lab/src/gateway-child.ts",
"test/e2e/qa-lab/runtime/otel-test-support.ts",
],
},
});
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
function sanitizeProofFailure(error: unknown, repoRoot: string): string {
return formatErrorMessage(error)
.replace(new RegExp(escapeRegExp(repoRoot), "gu"), "<repo>")
.replace(/https?:\/\/(?:127\.0\.0\.1|localhost):\d+/giu, "<local-endpoint>")
.replace(/qa-suite-[0-9a-f-]{20,}/giu, "<gateway-token>")
.replace(/(?:\/private)?\/var\/folders\/[^\s'"]+/gu, "<temp-path>")
.replace(/\/tmp\/[^\s'"]+/gu, "<temp-path>")
.replace(/[a-z]:\\[^\s'"]+/giu, "<absolute-path>")
.slice(0, 2_000);
}
function failedSummary(error: unknown, repoRoot: string): OtelGenerationConfigWatcherSummary {
return {
collectorAPostReadyRequestCount: null,
failures: [sanitizeProofFailure(error, repoRoot)],
noRespawn: false,
passed: false,
pid: {
after: null,
before: null,
same: false,
},
readyAfterMutation: false,
restartLogObserved: false,
};
}
export async function runOtelGenerationConfigWatcherRuntime(options: RuntimeOptions) {
const writer = createWriter(options);
const startedAt = Date.now();
let summary: OtelGenerationConfigWatcherSummary;
try {
summary = await probeOtelGenerationConfigWatcher(options);
} catch (error) {
summary = failedSummary(error, options.repoRoot);
}
const summaryPath = path.join(options.artifactBase, `${SCENARIO_ID}-summary.json`);
await fs.mkdir(options.artifactBase, { recursive: true });
await fs.writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
writer.appendLog(
`${SCENARIO_ID}: ${summary.passed ? "passed" : "failed"} ` +
`samePid=${summary.pid.same} collectorAPostReady=${summary.collectorAPostReadyRequestCount}\n`,
);
const evidence = await writer.write({
artifacts: [{ kind: "summary", filePath: summaryPath }],
details: summary.passed
? `same PID=${summary.pid.same}; collector A post-ready requests=0; collector B signals=trace,metric,log`
: summary.failures.join("\n"),
durationMs: Math.max(1, Date.now() - startedAt),
status: summary.passed ? "pass" : "fail",
});
return { evidence, summary };
}
async function main(): Promise<void> {
const result = await runOtelGenerationConfigWatcherRuntime(parseOptions(process.argv.slice(2)));
process.stdout.write(
`${SCENARIO_ID}: ${result.summary.passed ? "passed" : "failed"}; evidence=${QA_EVIDENCE_FILENAME}\n`,
);
if (!result.summary.passed) {
process.exitCode = 1;
}
}
export const testing = {
inspectGeneration,
inspectParentGraph,
parseOptions,
sanitizeProofFailure,
};
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
main().catch((error: unknown) => {
process.stderr.write(`${SCENARIO_ID}: ${formatErrorMessage(error)}\n`);
process.exitCode = 1;
});
}