@beignet/core
Version:
Core framework primitives for Beignet
315 lines • 11.9 kB
JavaScript
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