UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

315 lines 11.9 kB
import { BEIGNET_ERROR_OWNER_HEADER, } from "../contracts/index.js"; import { redactValue } from "../ports/redaction.js"; import { resolveProviderInstrumentationPort, } from "../providers/instrumentation.js"; import { createTraceContext, parseTraceparent, resolveTracingPort, } from "../tracing/index.js"; import { clearActiveRequestContext, enterActiveRequestContext, readContextActor, readContextTenant, } from "./request-context.js"; function getContextRequestId(ctx) { if (!ctx || typeof ctx !== "object") return undefined; const requestId = ctx.requestId; return typeof requestId === "string" ? requestId : undefined; } function getContextTraceContext(ctx) { if (!ctx || typeof ctx !== "object") return undefined; const context = ctx; if (!context.traceId && !context.spanId && !context.parentSpanId && !context.traceparent && !context.tracestate) { return undefined; } return { traceId: context.traceId, spanId: context.spanId, parentSpanId: context.parentSpanId, traceparent: context.traceparent, tracestate: context.tracestate, }; } function getErrorMessage(error) { if (error instanceof Error) return error.message; if (typeof error === "string") return error; return "Unknown error"; } function getErrorStack(error) { return error instanceof Error ? error.stack : undefined; } function createRequestId() { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); } return `${Date.now()}-${Math.random().toString(16).slice(2)}`; } function getPathname(req) { try { return new URL(req.url).pathname; } catch { return req.url; } } function isIgnoredPath(pathname, ignorePaths) { return ignorePaths.some((ignorePath) => { const normalized = ignorePath.replace(/\/+$/, ""); return pathname === normalized || pathname.startsWith(`${normalized}/`); }); } function requestHeadersToRecord(headers) { const record = {}; headers.forEach((value, key) => { record[key] = value; }); return record; } function getResponseHeader(headers, name) { if (!headers) return undefined; const direct = headers[name]; if (direct !== undefined) { return typeof direct === "string" ? direct : direct[0]; } const normalized = name.toLowerCase(); const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === normalized); const value = entry?.[1]; return typeof value === "string" ? value : value?.[0]; } function getResponseOwner(response) { return getResponseHeader(response.headers, BEIGNET_ERROR_OWNER_HEADER) === "framework" ? "framework" : "route"; } function isWatcherEnabled(port, name) { return port.isWatcherEnabled?.(name) ?? true; } /** * Create the server-owned instrumentation runtime for `createServer(...)`. * * Correlation values (request ID and trace context) are always resolved so * context factories receive stable `requestId`/`trace` arguments, even when * instrumentation is disabled. Headers and event recording only run when * instrumentation is enabled. */ export function createServerInstrumentation(options) { const enabled = options !== false; const resolvedOptions = options === false || options === undefined ? {} : options; const requestIdHeader = resolvedOptions.requestIdHeader ?? "x-request-id"; const traceContextHeader = resolvedOptions.traceContextHeader ?? "traceparent"; const ignorePaths = resolvedOptions.ignorePaths ?? ["/api/devtools"]; let port; let tracing; const correlations = new WeakMap(); const prepareRequest = (req) => { const cached = correlations.get(req); if (cached) return cached; const headerRequestId = requestIdHeader === false ? undefined : (req.headers.get(requestIdHeader) ?? undefined); const headerTraceparent = traceContextHeader === false ? undefined : (req.headers.get(traceContextHeader) ?? undefined); const parsedTraceparent = parseTraceparent(headerTraceparent); const activeTrace = tracing?.current(); const correlation = { requestId: headerRequestId ?? createRequestId(), trace: activeTrace ?? createTraceContext({ traceparent: parsedTraceparent?.traceparent, tracestate: parsedTraceparent ? (req.headers.get("tracestate") ?? undefined) : undefined, }), }; correlations.set(req, correlation); return correlation; }; const resolveRequestId = (args) => getContextRequestId(args.ctx) ?? prepareRequest(args.req).requestId; const resolveTraceContext = (args) => { const activeTrace = tracing?.current(); if (activeTrace) return activeTrace; const contextTrace = getContextTraceContext(args.ctx); if (contextTrace) { return createTraceContext(contextTrace); } return prepareRequest(args.req).trace; }; const record = (event) => { if (!port) return; let prepared = redactValue(event); if (resolvedOptions.redact) { try { prepared = resolvedOptions.redact(prepared); } catch (error) { try { port.record({ type: "error", message: "Server instrumentation redactor failed", owner: "framework", details: { message: getErrorMessage(error), }, }); } catch { // Instrumentation sinks must never affect responses. } return; } } try { port.record(prepared); } catch { // Instrumentation sinks must never affect responses. } }; const enterAmbientContext = (args) => { const trace = resolveTraceContext(args); enterActiveRequestContext({ requestId: resolveRequestId(args), traceId: trace.traceId, spanId: trace.spanId, parentSpanId: trace.parentSpanId, traceparent: trace.traceparent, tracestate: trace.tracestate, actor: readContextActor(args.ctx), tenant: readContextTenant(args.ctx), }); }; const hook = { name: "beignet.instrumentation", onRequest: ({ req }) => { enterAmbientContext({ req }); return undefined; }, beforeHandle: ({ req, ctx }) => { // Re-enter with context values so app-owned overrides win for ambient // correlation inheritance. enterAmbientContext({ req, ctx }); return undefined; }, beforeSend: ({ req, ctx, response }) => { if (!enabled || (requestIdHeader === false && traceContextHeader === false)) { return undefined; } const requestId = resolveRequestId({ req, ctx }); const trace = resolveTraceContext({ req, ctx }); return { ...response, headers: { ...response.headers, ...(requestIdHeader === false ? {} : { [requestIdHeader]: requestId }), ...(traceContextHeader === false ? {} : { [traceContextHeader]: trace.traceparent }), }, }; }, afterSend: ({ req, ctx, contract, response, error, durationMs, stages, }) => { try { if (!port) return; const path = getPathname(req); if (isIgnoredPath(path, ignorePaths)) return; const shouldCaptureRequest = isWatcherEnabled(port, "requests"); const shouldCaptureError = Boolean(error) && isWatcherEnabled(port, "errors"); if (!shouldCaptureRequest && !shouldCaptureError) return; if (resolvedOptions.shouldCapture && !resolvedOptions.shouldCapture({ req, ctx: ctx, contract, response, error, })) { return; } const requestId = resolveRequestId({ req, ctx }); const trace = resolveTraceContext({ req, ctx }); const responseOwner = getResponseOwner(response); if (shouldCaptureRequest) { record({ type: "request", requestId, traceId: trace.traceId, spanId: trace.spanId, parentSpanId: trace.parentSpanId, traceparent: trace.traceparent, tracestate: trace.tracestate, method: req.method, path, contractName: contract.name, responseOwner, status: response.status, durationMs, stages, details: { headers: requestHeadersToRecord(req.headers), route: { contractName: contract.name, method: req.method, path, }, response: { owner: responseOwner, status: response.status, }, hookPhases: [ "onRequest", "beforeHandle", "beforeSend", "afterSend", ], }, }); } if (error && shouldCaptureError) { record({ type: "error", requestId, traceId: trace.traceId, spanId: trace.spanId, parentSpanId: trace.parentSpanId, traceparent: trace.traceparent, tracestate: trace.tracestate, message: getErrorMessage(error), stack: getErrorStack(error), contractName: contract.name, owner: responseOwner === "framework" ? "framework" : "route", }); } } finally { clearActiveRequestContext(); } }, }; return { attachPorts(ports) { tracing = resolveTracingPort(ports); if (!enabled) return; port = resolveProviderInstrumentationPort(ports); }, prepareRequest, createServiceCorrelation: () => ({ requestId: createRequestId(), trace: tracing?.current() ?? createTraceContext(), }), hook, }; } //# sourceMappingURL=instrumentation.js.map