feat: attach cloud api otel trace headers

This commit is contained in:
lyon
2026-06-20 14:54:51 +08:00
parent 0d91fe117f
commit efafeccfb9
4 changed files with 85 additions and 3 deletions
+15
View File
@@ -61,6 +61,21 @@ export function authLoginOtelTraceContext(input = {}) {
};
}
export function httpRequestOtelTraceContext(input = {}) {
const parsed = parseOtelTraceparent(input.traceparent);
const traceId = parsed?.traceId
?? normalizeOtelTraceId(input.traceId)
?? nonZeroHex(randomBytes(16).toString("hex"), ZERO_TRACE_ID);
const spanId = normalizeOtelSpanId(input.spanId) ?? newOtelSpanId();
return {
traceId,
parentSpanId: parsed?.spanId ?? null,
spanId,
traceparent: `00-${traceId}-${spanId}-01`,
valuesPrinted: false
};
}
export function newOtelSpanId() {
return nonZeroHex(randomBytes(8).toString("hex"), ZERO_SPAN_ID);
}
+35
View File
@@ -23,6 +23,41 @@ import {
prepareFakeCodexHome
} from "./server-test-helpers.ts";
test("cloud api attaches OTel trace and request headers to normal and REST error responses", async () => {
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_PROVIDER: "test-disabled"
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const upstreamTraceparent = "00-11111111111111111111111111111111-2222222222222222-01";
const ok = await fetch(`http://127.0.0.1:${port}/live`, {
headers: {
traceparent: upstreamTraceparent,
"x-request-id": "req_trace_context_test"
}
});
assert.equal(ok.status, 200);
const okTraceparent = ok.headers.get("traceparent") ?? "";
assert.match(okTraceparent, /^00-11111111111111111111111111111111-[0-9a-f]{16}-01$/u);
assert.notEqual(okTraceparent.split("-")[2], "2222222222222222");
assert.equal(ok.headers.get("x-hwlab-otel-trace-id"), "11111111111111111111111111111111");
assert.equal(ok.headers.get("x-request-id"), "req_trace_context_test");
const missing = await fetch(`http://127.0.0.1:${port}/route-not-implemented`);
assert.equal(missing.status, 404);
assert.match(missing.headers.get("traceparent") ?? "", /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/u);
assert.match(missing.headers.get("x-hwlab-otel-trace-id") ?? "", /^[0-9a-f]{32}$/u);
assert.match(missing.headers.get("x-request-id") ?? "", /^req_[0-9a-f-]{36}$/u);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api health reports DB env presence and live connection classification without leaking values", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
+33 -2
View File
@@ -1,5 +1,5 @@
/*
* SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p0-projector-resume; PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-010401 Web工作台 draft-2026-06-17-r0; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0
* SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p0-projector-resume; PJ2026-010403 API契约 draft-2026-06-20-p0-error-diagnostics; PJ2026-010401 Web工作台 draft-2026-06-20-p0-error-diagnostics; PJ2026-01060505 Workbench Performance draft-2026-06-20-p0-error-diagnostics
* 职责: Cloud API REST/SSE route dispatcherWorkbench read model / compat wrapper dispatcher
*/
import { createServer } from "node:http";
@@ -73,6 +73,7 @@ import { handleWorkbenchReadModelHttp, handleWorkbenchRealtimeHttp } from "./ser
import { startWorkbenchEmptySessionGc } from "./workbench-empty-session-gc.ts";
import { handleM3IoControlHttp } from "./server-m3-http.ts";
import { handleSkillsHttp } from "./server-skills-http.ts";
import { httpRequestOtelTraceContext } from "./otel-trace.ts";
import { handleProviderProfileCatalogHttp, handleProviderProfilesHttp } from "./provider-profile-management.ts";
import {
configureSkillRuntime,
@@ -126,6 +127,8 @@ export function createCloudApiServer(options = {}) {
logger: console
});
const server = createServer(async (request, response) => {
const requestContext = buildHttpRequestContext(request);
attachHttpRequestContext(request, response, requestContext);
const backendPerformance = backendPerformanceStore.beginHttpRequest(request, response);
await withBackendPerformanceContext(backendPerformance, async () => {
try {
@@ -150,6 +153,29 @@ export function createCloudApiServer(options = {}) {
return server;
}
function buildHttpRequestContext(request) {
const otelContext = httpRequestOtelTraceContext({
traceparent: getHeader(request, "traceparent"),
traceId: getHeader(request, "x-hwlab-otel-trace-id")
});
return {
requestId: safeRequestId(getHeader(request, "x-request-id")) || `req_${randomUUID()}`,
traceId: otelContext.traceId,
parentSpanId: otelContext.parentSpanId,
spanId: otelContext.spanId,
traceparent: otelContext.traceparent,
valuesPrinted: false
};
}
function attachHttpRequestContext(request, response, context) {
request.hwlabHttpRequestContext = context;
if (response.headersSent || response.writableEnded) return;
response.setHeader("traceparent", context.traceparent);
response.setHeader("x-hwlab-otel-trace-id", context.traceId);
response.setHeader("x-request-id", context.requestId);
}
export function ensureCodeAgentRuntimeBase(env = process.env) {
const codeAgentAdapter = String(env.HWLAB_CODE_AGENT_ADAPTER ?? env.HWLAB_CODE_AGENT_PROVIDER ?? "").trim().toLowerCase();
const useCodexStdioRuntime = !codeAgentAdapter || codeAgentAdapter === "codex-stdio";
@@ -1765,7 +1791,7 @@ async function persistGatewayDemoSession({ runtimeStore, session, request, env =
}
function sendRestError(request, response, statusCode, code, message, context = {}) {
const requestId = getHeader(request, "x-request-id") || "req_unassigned";
const requestId = request.hwlabHttpRequestContext?.requestId || getHeader(request, "x-request-id") || "req_unassigned";
const environment = context.environment || runtimeEnvironment(context.env ?? process.env);
sendJson(response, statusCode, {
error: {
@@ -1848,6 +1874,11 @@ function getHeader(request, name) {
return value;
}
function safeRequestId(value) {
const text = String(value ?? "").trim();
return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null;
}
function firstHeaderValue(request, name) {
return getHeader(request, name);
}
@@ -1142,13 +1142,14 @@ test("cloud api exposes auth login OTel trace headers and forwards traceparent",
const upstreamTraceparent = "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01";
const login = await fetch(`http://127.0.0.1:${port}/auth/login`, {
method: "POST",
headers: { "content-type": "application/json", traceparent: upstreamTraceparent },
headers: { "content-type": "application/json", traceparent: upstreamTraceparent, "x-request-id": "req_auth_otel_login" },
body: JSON.stringify({ username: "admin", password: "redacted-password" })
});
assert.equal(login.status, 200);
const responseTraceparent = login.headers.get("traceparent") ?? "";
assert.match(responseTraceparent, /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/u);
assert.equal(login.headers.get("x-hwlab-otel-trace-id"), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
assert.equal(login.headers.get("x-request-id"), "req_auth_otel_login");
assert.equal(responseTraceparent.split("-")[1], "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
const loginCall = calls.find((call) => call.op === "login");