release: WRNexusJS 0.3.0
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import type { Context, Middleware } from "./context.ts";
|
||||
|
||||
export interface SpanRecord {
|
||||
name: string;
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
durationMs?: number;
|
||||
status?: "ok" | "error";
|
||||
attributes: Record<string, string | number | boolean>;
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export interface Tracer {
|
||||
startSpan(name: string, attributes?: SpanRecord["attributes"]): Span;
|
||||
records(): readonly SpanRecord[];
|
||||
}
|
||||
|
||||
export interface Span {
|
||||
setAttribute(name: string, value: string | number | boolean): void;
|
||||
end(status?: "ok" | "error", error?: unknown): SpanRecord;
|
||||
}
|
||||
|
||||
export function createTracer(clock: () => number = () => performance.now()): Tracer {
|
||||
const spans: SpanRecord[] = [];
|
||||
return {
|
||||
startSpan(name, attributes = {}) {
|
||||
const record: SpanRecord = { name, startTime: clock(), attributes: { ...attributes } };
|
||||
spans.push(record);
|
||||
let ended = false;
|
||||
return {
|
||||
setAttribute(key, value) {
|
||||
record.attributes[key] = value;
|
||||
},
|
||||
end(status = "ok", error) {
|
||||
if (!ended) {
|
||||
ended = true;
|
||||
record.endTime = clock();
|
||||
record.durationMs = record.endTime - record.startTime;
|
||||
record.status = status;
|
||||
record.error = error;
|
||||
}
|
||||
return record;
|
||||
},
|
||||
};
|
||||
},
|
||||
records: () => spans,
|
||||
};
|
||||
}
|
||||
|
||||
export async function withSpan<T>(
|
||||
tracer: Tracer,
|
||||
name: string,
|
||||
run: (span: Span) => T | Promise<T>,
|
||||
attributes?: SpanRecord["attributes"],
|
||||
): Promise<T> {
|
||||
const span = tracer.startSpan(name, attributes);
|
||||
try {
|
||||
const result = await run(span);
|
||||
span.end("ok");
|
||||
return result;
|
||||
} catch (error) {
|
||||
span.end("error", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TracingMiddlewareOptions {
|
||||
/** Include W3C Server-Timing response headers. Defaults to true. */
|
||||
serverTiming?: boolean;
|
||||
/** Fraction of requests to trace, from 0 to 1. Defaults to 1. */
|
||||
sampleRate?: number;
|
||||
/** Called after a traced response completes. */
|
||||
onComplete?: (ctx: Context, records: readonly SpanRecord[]) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function tracingMiddleware(
|
||||
tracerFactory: (ctx: Context) => Tracer = () => createTracer(),
|
||||
options: TracingMiddlewareOptions = {},
|
||||
): Middleware {
|
||||
const sampleRate = Math.max(0, Math.min(1, options.sampleRate ?? 1));
|
||||
return async (ctx, next) => {
|
||||
if (sampleRate === 0 || (sampleRate < 1 && Math.random() > sampleRate)) return next();
|
||||
|
||||
const tracer = tracerFactory(ctx);
|
||||
ctx.tracer = tracer;
|
||||
const response = await withSpan(tracer, "http.request", () => next(), {
|
||||
method: ctx.req.method,
|
||||
path: ctx.url.pathname,
|
||||
});
|
||||
const records = tracer.records();
|
||||
await options.onComplete?.(ctx, records);
|
||||
|
||||
if (options.serverTiming === false) return response;
|
||||
|
||||
const headers = new Headers(response.headers);
|
||||
const timings = records
|
||||
.filter((record) => record.durationMs !== undefined)
|
||||
.map(
|
||||
(record, index) =>
|
||||
`wrn${index};dur=${record.durationMs!.toFixed(2)};desc="${record.name.replace(/"/g, "")}"`,
|
||||
);
|
||||
if (timings.length) headers.set("server-timing", timings.join(", "));
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user