UNPKG

serverstruct

Version:

Type safe and modular servers with H3

121 lines (118 loc) 4.59 kB
import { defineMiddleware, getRequestURL, toResponse } from "h3"; import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api"; import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_SERVER_ADDRESS, ATTR_URL_FULL, ATTR_URL_PATH, ATTR_URL_QUERY, ATTR_URL_SCHEME, ATTR_USER_AGENT_ORIGINAL } from "@opentelemetry/semantic-conventions"; //#region package.json var name = "serverstruct"; var version = "2.3.0"; //#endregion //#region src/otel.ts /** * Creates an H3 middleware for OpenTelemetry distributed tracing. * * Automatically instruments HTTP requests with OpenTelemetry spans, capturing * the following semantic convention attributes: * - `http.request.method` - HTTP method * - `url.full` - Full request URL * - `url.path` - URL path * - `url.query` - Query string * - `url.scheme` - URL scheme * - `server.address` - Server host * - `user_agent.original` - User agent header * - `http.response.status_code` - Response status code * - `http.request.header.<name>` - Custom request headers * - `http.response.header.<name>` - Custom response headers * * Exceptions are recorded with full details when errors occur. * * Status codes are mapped to span statuses: * - 1xx-4xx: SpanStatusCode.OK * - 5xx: SpanStatusCode.ERROR * * The middleware supports trace context propagation for distributed tracing across * microservices using OpenTelemetry propagators. * * @param options - Configuration options for tracing behavior * @returns H3 middleware function * * @example * ```ts * import { traceMiddleware } from "serverstruct/otel"; * * // Default usage * app.use(traceMiddleware()); * * // With options * app.use(traceMiddleware({ * headers: { * request: ["authorization"], * response: ["x-request-id"] * }, * propagation: { * disabled: true * } * })); * ``` */ function traceMiddleware(options) { const tracer = options?.tracer ?? trace.getTracer(name, version); const requestHeaderAttrs = options?.headers?.request ?? []; const responseHeaderAttrs = options?.headers?.response ?? []; const propagator = options?.propagation?.propagator ?? propagation; const propagationDisabled = options?.propagation?.disabled ?? false; const getter = { keys: (headers) => Array.from(headers.keys()), get: (headers, key) => headers.get(key) ?? void 0 }; return defineMiddleware(async (event, next) => { const extractedCtx = propagationDisabled ? context.active() : propagator.extract(context.active(), event.req.headers, getter); const url = getRequestURL(event); if (url.username) url.username = "REDACTED"; if (url.password) url.password = "REDACTED"; const span = tracer.startSpan(options?.spanName?.(event) ?? `${event.req.method} ${url.pathname}`, { kind: SpanKind.SERVER }, extractedCtx); const spanCtx = trace.setSpan(extractedCtx, span); const recording = span.isRecording(); if (recording) { span.setAttribute(ATTR_HTTP_REQUEST_METHOD, event.req.method); span.setAttribute(ATTR_URL_FULL, event.req.url); span.setAttribute(ATTR_URL_PATH, url.pathname); if (url.search) span.setAttribute(ATTR_URL_QUERY, url.search.slice(1)); span.setAttribute(ATTR_URL_SCHEME, url.protocol.replace(":", "")); span.setAttribute(ATTR_SERVER_ADDRESS, url.host); const userAgent = event.req.headers.get("user-agent"); if (userAgent) span.setAttribute(ATTR_USER_AGENT_ORIGINAL, userAgent); for (const header of requestHeaderAttrs) { const value = event.req.headers.get(header); if (value != null) span.setAttribute(ATTR_HTTP_REQUEST_HEADER(header.toLowerCase()), [value]); } if (options?.spanAttributes) span.setAttributes(options.spanAttributes(event)); } try { const response = await context.with(spanCtx, async () => { return toResponse(await next(), event); }); if (recording) { span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status); span.setStatus({ code: response.status < 500 ? SpanStatusCode.OK : SpanStatusCode.ERROR }); for (const header of responseHeaderAttrs) { const value = response.headers.get(header); if (value != null) span.setAttribute(ATTR_HTTP_RESPONSE_HEADER(header.toLowerCase()), [value]); } } return response; } catch (err) { if (recording) { const error = err instanceof Error ? err : new Error(String(err)); span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message }); } throw err; } finally { span.end(); } }); } //#endregion export { traceMiddleware };