feat: attach cloud api otel trace headers
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 dispatcher。Workbench 新资源路由应委托 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");
|
||||
|
||||
Reference in New Issue
Block a user