serverstruct
Version:
Type safe and modular servers with H3
121 lines (118 loc) • 4.93 kB
JavaScript
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.2";
//#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;