import { createHash, randomBytes } from "node:crypto"; const OTLP_TIMEOUT_MS = 1500; const MIN_VALID_EPOCH_MS = Date.UTC(2020, 0, 1); const ZERO_TRACE_ID = "00000000000000000000000000000000"; const ZERO_SPAN_ID = "0000000000000000"; export function codeAgentOtelTraceFields(traceId, env = process.env) { const context = codeAgentOtelTraceContext(traceId); return { otelTraceId: context.traceId, traceparent: context.traceparent, otelTrace: { traceId: context.traceId, businessTraceId: context.businessTraceId, tempoPath: `/api/traces/${context.traceId}`, exporterConfigured: Boolean(resolveOtlpTracesEndpoint(env)), valuesPrinted: false } }; } export function codeAgentOtelTraceContext(traceId) { const businessTraceId = String(traceId ?? "").trim() || "trc_unassigned"; const otelTraceId = nonZeroHex(createHash("sha256").update(`hwlab-code-agent:${businessTraceId}`).digest("hex").slice(0, 32), ZERO_TRACE_ID); const parentSpanId = nonZeroHex(createHash("sha256").update(`hwlab-code-agent-parent:${businessTraceId}`).digest("hex").slice(0, 16), ZERO_SPAN_ID); return { businessTraceId, traceId: otelTraceId, parentSpanId, traceparent: `00-${otelTraceId}-${parentSpanId}-01`, valuesPrinted: false }; } export function workbenchUiOtelTraceContext(input = {}) { const businessTraceId = String(input.uiTraceId ?? input.traceId ?? "").trim() || "ui_unassigned"; const explicitTraceId = normalizeOtelTraceId(input.otelTraceId); const otelTraceId = explicitTraceId ?? nonZeroHex(createHash("sha256").update(`hwlab-workbench-ui:${businessTraceId}`).digest("hex").slice(0, 32), ZERO_TRACE_ID); const parentSpanId = normalizeOtelSpanId(input.parentSpanId) ?? nonZeroHex(createHash("sha256").update(`hwlab-workbench-ui-parent:${businessTraceId}:${otelTraceId}`).digest("hex").slice(0, 16), ZERO_SPAN_ID); return { businessTraceId, traceId: otelTraceId, parentSpanId, traceparent: `00-${otelTraceId}-${parentSpanId}-01`, valuesPrinted: false }; } export function authLoginOtelTraceContext(input = {}) { const parsed = parseOtelTraceparent(input.traceparent); const traceId = parsed?.traceId ?? nonZeroHex(randomBytes(16).toString("hex"), ZERO_TRACE_ID); const rootSpanId = normalizeOtelSpanId(input.rootSpanId) ?? newOtelSpanId(); return { traceId, parentSpanId: parsed?.spanId ?? null, rootSpanId, traceparent: `00-${traceId}-${rootSpanId}-01`, valuesPrinted: false }; } 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); } export function traceparentForOtelSpan(context, spanId) { const traceId = normalizeOtelTraceId(context?.traceId); const normalizedSpanId = normalizeOtelSpanId(spanId); return traceId && normalizedSpanId ? `00-${traceId}-${normalizedSpanId}-01` : ""; } export async function emitCodeAgentOtelSpan(name, traceId, env = process.env, options = {}) { const endpoint = resolveOtlpTracesEndpoint(env); if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false }; const context = codeAgentOtelTraceContext(traceId); const now = Date.now(); const startedAtMs = epochUnixMs(options.startTimeMs, now); const endedAtMs = epochUnixMs(options.endTimeMs, now); const spanId = nonZeroHex(String(options.spanId ?? randomBytes(8).toString("hex")).slice(0, 16), ZERO_SPAN_ID); const parentSpanId = typeof options.parentSpanId === "string" && /^[0-9a-f]{16}$/u.test(options.parentSpanId) ? options.parentSpanId : context.parentSpanId; const statusCode = options.status === "error" || options.error ? 2 : 1; const body = { resourceSpans: [{ resource: { attributes: attributesFromRecord(resourceAttributes(env)) }, scopeSpans: [{ scope: { name: "hwlab.code-agent", version: "1" }, spans: [{ traceId: context.traceId, spanId, parentSpanId, name, kind: Number(options.kind ?? 1), startTimeUnixNano: unixNano(startedAtMs), endTimeUnixNano: unixNano(Math.max(startedAtMs, endedAtMs)), attributes: attributesFromRecord({ traceId: context.businessTraceId, "otel.trace_id": context.traceId, "code_agent.stage": name, ...options.attributes }), status: { code: statusCode, ...(options.error ? { message: String(options.error?.message ?? options.error).slice(0, 300) } : {}) } }] }] }] }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS); try { const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: controller.signal }); return { ok: response.ok, status: response.status, valuesPrinted: false }; } catch (error) { return { ok: false, error: error?.name === "AbortError" ? "otlp-timeout" : "otlp-send-failed", valuesPrinted: false }; } finally { clearTimeout(timeout); } } export async function emitWorkbenchUiOtelSpan(name, uiTraceId, env = process.env, options = {}) { const endpoint = resolveOtlpTracesEndpoint(env); if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false }; const context = workbenchUiOtelTraceContext({ uiTraceId, otelTraceId: options.otelTraceId, parentSpanId: options.parentSpanId }); const now = Date.now(); const startedAtMs = epochUnixMs(options.startTimeMs, now); const endedAtMs = epochUnixMs(options.endTimeMs, startedAtMs); const spanId = normalizeOtelSpanId(options.spanId) ?? nonZeroHex(randomBytes(8).toString("hex"), ZERO_SPAN_ID); const parentSpanId = normalizeOtelSpanId(options.parentSpanId) ?? context.parentSpanId; const statusCode = options.status === "error" || options.error ? 2 : 1; const body = { resourceSpans: [{ resource: { attributes: attributesFromRecord(resourceAttributes(env)) }, scopeSpans: [{ scope: { name: "hwlab.workbench-ui", version: "1" }, spans: [{ traceId: context.traceId, spanId, parentSpanId, name, kind: Number(options.kind ?? 1), startTimeUnixNano: unixNano(startedAtMs), endTimeUnixNano: unixNano(Math.max(startedAtMs, endedAtMs)), attributes: attributesFromRecord({ "ui.trace_id": context.businessTraceId, "otel.trace_id": context.traceId, "workbench.ui.stage": name, ...options.attributes }), status: { code: statusCode, ...(options.error ? { message: String(options.error?.message ?? options.error).slice(0, 300) } : {}) } }] }] }] }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS); try { const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: controller.signal }); return { ok: response.ok, status: response.status, traceId: context.traceId, valuesPrinted: false }; } catch (error) { return { ok: false, error: error?.name === "AbortError" ? "otlp-timeout" : "otlp-send-failed", traceId: context.traceId, valuesPrinted: false }; } finally { clearTimeout(timeout); } } export async function emitAuthOtelSpan(name, context, env = process.env, options = {}) { const endpoint = resolveOtlpTracesEndpoint(env); if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false }; const traceContext = normalizeOtelTraceId(context?.traceId) ? context : authLoginOtelTraceContext({ traceparent: context?.traceparent }); const now = Date.now(); const startedAtMs = epochUnixMs(options.startTimeMs, now); const endedAtMs = epochUnixMs(options.endTimeMs, startedAtMs); const spanId = normalizeOtelSpanId(options.spanId) ?? newOtelSpanId(); const parentSpanId = normalizeOtelSpanId(options.parentSpanId) ?? null; const statusCode = options.status === "error" || options.error ? 2 : 1; const span = { traceId: traceContext.traceId, spanId, ...(parentSpanId ? { parentSpanId } : {}), name, kind: Number(options.kind ?? 1), startTimeUnixNano: unixNano(startedAtMs), endTimeUnixNano: unixNano(Math.max(startedAtMs, endedAtMs)), attributes: attributesFromRecord({ "otel.trace_id": traceContext.traceId, "auth.stage": name, ...options.attributes }), status: { code: statusCode, ...(options.error ? { message: String(options.error?.message ?? options.error).slice(0, 300) } : {}) } }; const body = { resourceSpans: [{ resource: { attributes: attributesFromRecord(resourceAttributes(env)) }, scopeSpans: [{ scope: { name: "hwlab.auth", version: "1" }, spans: [span] }] }] }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS); try { const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: controller.signal }); return { ok: response.ok, status: response.status, traceId: traceContext.traceId, valuesPrinted: false }; } catch (error) { return { ok: false, error: error?.name === "AbortError" ? "otlp-timeout" : "otlp-send-failed", traceId: traceContext.traceId, valuesPrinted: false }; } finally { clearTimeout(timeout); } } export async function emitHttpServerRequestSpan(context, env = process.env, options = {}) { const endpoint = resolveOtlpTracesEndpoint(env); if (!endpoint || typeof fetch !== "function") return { ok: false, skipped: true, reason: "otlp-endpoint-missing", valuesPrinted: false }; const traceId = normalizeOtelTraceId(context?.traceId); if (!traceId) return { ok: false, skipped: true, reason: "trace-id-missing", valuesPrinted: false }; const spanId = normalizeOtelSpanId(options.spanId) ?? newOtelSpanId(); const parentSpanId = normalizeOtelSpanId(options.parentSpanId) ?? normalizeOtelSpanId(context?.parentSpanId) ?? null; const now = Date.now(); const startedAtMs = epochUnixMs(options.startTimeMs, now); const endedAtMs = epochUnixMs(options.endTimeMs, startedAtMs); const method = String(options.method ?? context?.method ?? "GET").toUpperCase(); const route = String(options.route ?? context?.route ?? "/").split("?")[0] || "/"; const statusCode = Number.isInteger(Number(options.statusCode)) ? Number(options.statusCode) : 0; const statusClassValue = statusClassFor(statusCode); const errorCode = options.errorCode ? String(options.errorCode) : null; const isError = options.status === "error" || Boolean(options.error) || statusCode >= 500; const name = typeof options.name === "string" && options.name.trim() ? options.name.trim() : `${method} ${route}`; const attributes = { "otel.trace_id": traceId, "request.id": context?.requestId ?? null, "http.request.method": method, "http.route": route, "http.response.status_code": statusCode || null, "http.response.status_class": statusClassValue, "hwlab.http.stage": "http.server.request", ...options.attributes, ...(errorCode ? { "error.code": errorCode, "error.category": options.errorCategory ?? null, "error.layer": options.errorLayer ?? null } : {}) }; const body = { resourceSpans: [{ resource: { attributes: attributesFromRecord(resourceAttributes(env)) }, scopeSpans: [{ scope: { name: "hwlab.http.server", version: "1" }, spans: [{ traceId, spanId, ...(parentSpanId ? { parentSpanId } : {}), name, kind: 2, startTimeUnixNano: unixNano(startedAtMs), endTimeUnixNano: unixNano(Math.max(startedAtMs, endedAtMs)), attributes: attributesFromRecord(attributes), status: { code: isError ? 2 : 1, ...(isError && options.statusMessage ? { message: String(options.statusMessage).slice(0, 300) } : {}) } }] }] }] }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), OTLP_TIMEOUT_MS); try { const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: controller.signal }); return { ok: response.ok, status: response.status, traceId, valuesPrinted: false }; } catch (error) { return { ok: false, error: error?.name === "AbortError" ? "otlp-timeout" : "otlp-send-failed", traceId, valuesPrinted: false }; } finally { clearTimeout(timeout); } } function statusClassFor(statusCode) { if (statusCode >= 500) return "5xx"; if (statusCode >= 400) return "4xx"; if (statusCode >= 300) return "3xx"; if (statusCode >= 200) return "2xx"; if (statusCode >= 100) return "1xx"; return "unknown"; } function resolveOtlpTracesEndpoint(env = process.env) { const explicit = firstNonEmpty(env.HWLAB_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT); if (explicit) return explicit.replace(/\/+$/u, ""); const base = firstNonEmpty(env.HWLAB_OTEL_EXPORTER_OTLP_ENDPOINT, env.OTEL_EXPORTER_OTLP_ENDPOINT); return base ? `${base.replace(/\/+$/u, "")}/v1/traces` : null; } function resourceAttributes(env = process.env) { return { "service.name": firstNonEmpty(env.OTEL_SERVICE_NAME, "hwlab-cloud-api"), "deployment.environment": firstNonEmpty(env.HWLAB_ENVIRONMENT, env.HWLAB_GITOPS_PROFILE, env.HWLAB_RUNTIME_LANE, "unknown"), "unidesk.node": firstNonEmpty(env.HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID, env.HWLAB_NODE_ID, env.UNIDESK_NODE_ID, "unknown"), "hwlab.lane": firstNonEmpty(env.HWLAB_RUNTIME_LANE, env.HWLAB_GITOPS_PROFILE, env.HWLAB_BOOT_REF, "unknown"), "k8s.namespace.name": firstNonEmpty(env.HWLAB_RUNTIME_NAMESPACE, env.POD_NAMESPACE, env.HWLAB_NAMESPACE, "unknown"), "git.commit": firstNonEmpty(env.HWLAB_COMMIT_ID, env.HWLAB_GITOPS_SOURCE_COMMIT, env.HWLAB_REVISION, "unknown") }; } function attributesFromRecord(record = {}) { return Object.entries(record) .filter(([, value]) => value !== undefined && value !== null) .map(([key, value]) => ({ key, value: otlpAnyValue(value) })); } function otlpAnyValue(value) { if (typeof value === "boolean") return { boolValue: value }; if (typeof value === "number" && Number.isInteger(value)) return { intValue: String(value) }; if (typeof value === "number" && Number.isFinite(value)) return { doubleValue: value }; return { stringValue: typeof value === "string" ? value : JSON.stringify(value) }; } function unixNano(ms) { return String(Math.trunc(ms * 1_000_000)); } function epochUnixMs(value, fallback) { const numeric = Number(value); return Number.isFinite(numeric) && numeric >= MIN_VALID_EPOCH_MS ? numeric : fallback; } function firstNonEmpty(...values) { for (const value of values) { const text = String(value ?? "").trim(); if (text) return text; } return null; } function nonZeroHex(value, zero) { return /^[0-9a-f]+$/u.test(value) && value !== zero ? value : zero.replace(/0$/u, "1"); } function normalizeOtelTraceId(value) { const text = String(value ?? "").trim().toLowerCase(); return /^[0-9a-f]{32}$/u.test(text) && text !== ZERO_TRACE_ID ? text : null; } function normalizeOtelSpanId(value) { const text = String(value ?? "").trim().toLowerCase(); return /^[0-9a-f]{16}$/u.test(text) && text !== ZERO_SPAN_ID ? text : null; } function parseOtelTraceparent(value) { const match = String(value ?? "").trim().toLowerCase().match(/^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/u); if (!match) return null; const traceId = normalizeOtelTraceId(match[1]); const spanId = normalizeOtelSpanId(match[2]); return traceId && spanId ? { traceId, spanId } : null; }