Files
WRNexusJS/packages/observability/src/server.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

210 lines
7.1 KiB
TypeScript

import type { Context, Middleware } from "@wrnexus/core";
import { MetricsRegistry, type MetricPoint } from "./metrics.ts";
export interface MetricsMiddlewareOptions {
registry?: MetricsRegistry;
routeLabel?: (ctx: Context) => string;
includePath?: boolean;
}
export function metricsMiddleware(options: MetricsMiddlewareOptions = {}): Middleware {
const registry = options.registry ?? defaultMetrics;
let active = 0;
return async (ctx, next) => {
const started = performance.now();
active++;
registry.gauge("http.server.active_requests", active);
try {
const response = await next();
const route =
options.routeLabel?.(ctx) ?? (options.includePath ? ctx.url.pathname : "unknown");
const labels = { method: ctx.req.method, status: response.status, route };
registry.increment("http.server.requests", 1, labels);
registry.observe("http.server.duration_ms", performance.now() - started, labels);
return response;
} catch (error) {
registry.increment("http.server.errors", 1, { method: ctx.req.method });
throw error;
} finally {
active--;
registry.gauge("http.server.active_requests", active);
}
};
}
export interface WebVitalRecord {
name: "LCP" | "INP" | "CLS" | "FCP" | "TTFB";
value: number;
rating?: "good" | "needs-improvement" | "poor";
route?: string;
navigationType?: string;
}
export interface WebVitalsHandlerOptions {
registry?: MetricsRegistry;
onRecord?: (record: WebVitalRecord, request: Request) => void | Promise<void>;
maxBodyBytes?: number;
}
const VITAL_NAMES = new Set(["LCP", "INP", "CLS", "FCP", "TTFB"]);
export function createWebVitalsHandler(options: WebVitalsHandlerOptions = {}) {
const registry = options.registry ?? defaultMetrics;
const maxBodyBytes = options.maxBodyBytes ?? 8 * 1024;
return async (request: Request): Promise<Response> => {
if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
if (request.headers.get("sec-fetch-site") === "cross-site") {
return new Response("Forbidden", { status: 403 });
}
const origin = request.headers.get("origin");
if (origin) {
try {
if (new URL(origin).origin !== new URL(request.url).origin) {
return new Response("Forbidden", { status: 403 });
}
} catch {
return new Response("Forbidden", { status: 403 });
}
}
const length = Number(request.headers.get("content-length") ?? "0");
if (length > maxBodyBytes) return new Response("Payload Too Large", { status: 413 });
let body: unknown;
try {
const text = await request.text();
if (new TextEncoder().encode(text).byteLength > maxBodyBytes) {
return new Response("Payload Too Large", { status: 413 });
}
body = JSON.parse(text);
} catch {
return new Response("Invalid JSON", { status: 400 });
}
if (!body || typeof body !== "object") return new Response("Invalid metric", { status: 400 });
const raw = body as Record<string, unknown>;
if (
!VITAL_NAMES.has(String(raw.name)) ||
typeof raw.value !== "number" ||
!Number.isFinite(raw.value)
) {
return new Response("Invalid metric", { status: 400 });
}
const record: WebVitalRecord = {
name: raw.name as WebVitalRecord["name"],
value: raw.value,
...(typeof raw.rating === "string" ? { rating: raw.rating as WebVitalRecord["rating"] } : {}),
...(typeof raw.route === "string" ? { route: raw.route.slice(0, 256) } : {}),
...(typeof raw.navigationType === "string"
? { navigationType: raw.navigationType.slice(0, 64) }
: {}),
};
registry.observe(`web.vitals.${record.name.toLowerCase()}`, record.value, {
rating: record.rating ?? "unknown",
route: record.route ?? "unknown",
});
await options.onRecord?.(record, request);
return new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
};
}
export interface MetricExporter {
export(points: readonly MetricPoint[]): Promise<void> | void;
}
export function createHttpMetricExporter(
endpoint: string,
options: { headers?: HeadersInit; fetch?: typeof fetch } = {},
): MetricExporter {
const send = options.fetch ?? fetch;
return {
async export(points) {
const response = await send(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
...Object.fromEntries(new Headers(options.headers)),
},
body: JSON.stringify({ resourceMetrics: points }),
});
if (!response.ok) throw new Error(`Metric export failed with ${response.status}.`);
},
};
}
export function createOtlpMetricExporter(
endpoint: string,
options: { headers?: HeadersInit; fetch?: typeof fetch; serviceName?: string } = {},
): MetricExporter {
const send = options.fetch ?? fetch;
return {
async export(points) {
if (!points.length) return;
const metrics = points.map((point) => {
const attributes = Object.entries(point.labels).map(([key, value]) => ({
key,
value:
typeof value === "boolean"
? { boolValue: value }
: typeof value === "number"
? { doubleValue: value }
: { stringValue: value },
}));
const timeUnixNano = String(BigInt(Math.trunc(point.timestamp)) * 1_000_000n);
if (point.type === "histogram") {
return {
name: point.name,
histogram: {
aggregationTemporality: 2,
dataPoints: [
{
attributes,
timeUnixNano,
count: String(point.count ?? 0),
sum: point.sum ?? 0,
min: point.min,
max: point.max,
bucketCounts: [String(point.count ?? 0)],
explicitBounds: [],
},
],
},
};
}
const kind = point.type === "counter" ? "sum" : "gauge";
return {
name: point.name,
[kind]: {
...(kind === "sum" ? { aggregationTemporality: 2, isMonotonic: true } : {}),
dataPoints: [{ attributes, timeUnixNano, asDouble: point.value }],
},
};
});
const response = await send(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
...Object.fromEntries(new Headers(options.headers)),
},
body: JSON.stringify({
resourceMetrics: [
{
resource: {
attributes: [
{ key: "service.name", value: { stringValue: options.serviceName ?? "wrnexus" } },
],
},
scopeMetrics: [
{
scope: { name: "@wrnexus/observability", version: "0.8.0" },
metrics,
},
],
},
],
}),
});
if (!response.ok) throw new Error(`OTLP metric export failed with ${response.status}.`);
},
};
}
export const defaultMetrics = new MetricsRegistry();