UNPKG

serverstruct

Version:

Type safe and modular servers with H3

121 lines (118 loc) 4.93 kB
let h3 = require("h3"); let _opentelemetry_api = require("@opentelemetry/api"); let _opentelemetry_semantic_conventions = require("@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 ?? _opentelemetry_api.trace.getTracer(name, version); const requestHeaderAttrs = options?.headers?.request ?? []; const responseHeaderAttrs = options?.headers?.response ?? []; const propagator = options?.propagation?.propagator ?? _opentelemetry_api.propagation; const propagationDisabled = options?.propagation?.disabled ?? false; const getter = { keys: (headers) => Array.from(headers.keys()), get: (headers, key) => headers.get(key) ?? void 0 }; return (0, h3.defineMiddleware)(async (event, next) => { const extractedCtx = propagationDisabled ? _opentelemetry_api.context.active() : propagator.extract(_opentelemetry_api.context.active(), event.req.headers, getter); const url = (0, h3.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: _opentelemetry_api.SpanKind.SERVER }, extractedCtx); const spanCtx = _opentelemetry_api.trace.setSpan(extractedCtx, span); const recording = span.isRecording(); if (recording) { span.setAttribute(_opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD, event.req.method); span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_FULL, event.req.url); span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_PATH, url.pathname); if (url.search) span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_QUERY, url.search.slice(1)); span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_SCHEME, url.protocol.replace(":", "")); span.setAttribute(_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS, url.host); const userAgent = event.req.headers.get("user-agent"); if (userAgent) span.setAttribute(_opentelemetry_semantic_conventions.ATTR_USER_AGENT_ORIGINAL, userAgent); for (const header of requestHeaderAttrs) { const value = event.req.headers.get(header); if (value != null) span.setAttribute((0, _opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_HEADER)(header.toLowerCase()), [value]); } if (options?.spanAttributes) span.setAttributes(options.spanAttributes(event)); } try { const response = await _opentelemetry_api.context.with(spanCtx, async () => { return (0, h3.toResponse)(await next(), event); }); if (recording) { span.setAttribute(_opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE, response.status); span.setStatus({ code: response.status < 500 ? _opentelemetry_api.SpanStatusCode.OK : _opentelemetry_api.SpanStatusCode.ERROR }); for (const header of responseHeaderAttrs) { const value = response.headers.get(header); if (value != null) span.setAttribute((0, _opentelemetry_semantic_conventions.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: _opentelemetry_api.SpanStatusCode.ERROR, message: err?.message }); } throw err; } finally { span.end(); } }); } //#endregion exports.traceMiddleware = traceMiddleware;