release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+35
View File
@@ -0,0 +1,35 @@
import type { HealthRegistry } from "@wrnexus/core";
export interface HealthHandlerOptions {
exposeDetails?: boolean;
cacheControl?: string;
}
export function createLivenessHandler(options: HealthHandlerOptions = {}) {
return async (request: Request): Promise<Response> => {
if (request.method !== "GET" && request.method !== "HEAD") {
return new Response("Method Not Allowed", { status: 405, headers: { allow: "GET, HEAD" } });
}
return Response.json(
{ status: "up" },
{ status: 200, headers: { "cache-control": options.cacheControl ?? "no-store" } },
);
};
}
export function createReadinessHandler(
registry: HealthRegistry,
options: HealthHandlerOptions = {},
) {
return async (request: Request): Promise<Response> => {
if (request.method !== "GET" && request.method !== "HEAD") {
return new Response("Method Not Allowed", { status: 405, headers: { allow: "GET, HEAD" } });
}
const result = await registry.check();
const body = options.exposeDetails ? result : { status: result.status };
return Response.json(body, {
status: result.status === "down" ? 503 : 200,
headers: { "cache-control": options.cacheControl ?? "no-store" },
});
};
}
+28
View File
@@ -2,6 +2,7 @@ export { MetricsRegistry } from "./metrics.ts";
export type { MetricLabels, MetricPoint } from "./metrics.ts";
export {
createHttpMetricExporter,
createOtlpMetricExporter,
createWebVitalsHandler,
defaultMetrics,
metricsMiddleware,
@@ -14,3 +15,30 @@ export type {
} from "./server.ts";
export { webVitalsClient } from "./client.ts";
export type { WebVitalsClientOptions } from "./client.ts";
export {
createOtlpTraceExporter,
formatTraceparent,
parseTraceparent,
traceMiddleware,
} from "./trace.ts";
export type { SpanExporter, SpanRecord, TraceContext, TraceMiddlewareOptions } from "./trace.ts";
export { createLivenessHandler, createReadinessHandler } from "./health.ts";
export type { HealthHandlerOptions } from "./health.ts";
export { createStructuredLogger } from "./logging.ts";
export type { LogLevel, LogRecord, StructuredLogger, StructuredLoggerOptions } from "./logging.ts";
export {
createJaegerExporter,
createOperationTracer,
createOtlpLogExporter,
createPerformanceProfiler,
createPrometheusPushExporter,
createSentryCompatibleReporter,
createZipkinExporter,
renderPrometheus,
} from "./integrations.ts";
export type {
ErrorReporter,
FrameworkSpanKind,
LogExporter,
OperationTracer,
} from "./integrations.ts";
+239
View File
@@ -0,0 +1,239 @@
import type { MetricPoint } from "./metrics.ts";
import type { LogRecord } from "./logging.ts";
import type { MetricExporter } from "./server.ts";
import type { SpanExporter, SpanRecord, TraceContext } from "./trace.ts";
export type FrameworkSpanKind =
"database" | "cache" | "queue" | "realtime" | "server-action" | "application";
export interface OperationTracer {
span<T>(
kind: FrameworkSpanKind,
name: string,
operation: () => T | Promise<T>,
attributes?: Record<string, string | number | boolean>,
): Promise<T>;
}
const randomHex = (bytes: number) =>
Array.from(crypto.getRandomValues(new Uint8Array(bytes)), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
export function createOperationTracer(
options: {
exporter?: SpanExporter;
context?: () => Partial<TraceContext>;
now?: () => number;
onExportError?: (error: unknown) => void;
} = {},
): OperationTracer {
const now = options.now ?? Date.now;
return {
async span(kind, name, operation, attributes = {}) {
const context = options.context?.() ?? {};
const started = now();
let status: SpanRecord["status"] = "ok";
let failure: unknown;
try {
return await operation();
} catch (error) {
status = "error";
failure = error;
throw error;
} finally {
const ended = now();
const record: SpanRecord = {
name,
traceId: context.traceId ?? randomHex(16),
spanId: randomHex(8),
...(context.spanId ? { parentSpanId: context.spanId } : {}),
sampled: context.sampled ?? true,
startTime: started,
endTime: ended,
durationMs: ended - started,
status,
attributes: { "wrnexus.span.kind": kind, ...attributes },
...(failure
? {
error: {
name: failure instanceof Error ? failure.name : "Error",
message: failure instanceof Error ? failure.message : String(failure),
},
}
: {}),
};
try {
await options.exporter?.export([record]);
} catch (error) {
options.onExportError?.(error);
}
}
},
};
}
const safeMetricName = (name: string) => name.replace(/[^a-zA-Z0-9_:]/g, "_");
const labels = (point: MetricPoint) => {
const entries = Object.entries(point.labels);
return entries.length
? `{${entries.map(([key, value]) => `${safeMetricName(key)}=${JSON.stringify(String(value))}`).join(",")}}`
: "";
};
export function renderPrometheus(points: readonly MetricPoint[]): string {
return `${points.map((point) => `${safeMetricName(point.name)}${labels(point)} ${point.type === "histogram" ? (point.sum ?? 0) : point.value} ${point.timestamp}`).join("\n")}\n`;
}
export function createPrometheusPushExporter(
endpoint: string,
options: { fetch?: typeof fetch; headers?: HeadersInit } = {},
): MetricExporter {
return {
async export(points) {
const response = await (options.fetch ?? fetch)(endpoint, {
method: "POST",
headers: {
"content-type": "text/plain; version=0.0.4",
...Object.fromEntries(new Headers(options.headers)),
},
body: renderPrometheus(points),
});
if (!response.ok) throw new Error(`Prometheus export failed with ${response.status}.`);
},
};
}
export function createZipkinExporter(
endpoint: string,
options: { fetch?: typeof fetch; serviceName?: string } = {},
): SpanExporter {
return {
async export(spans) {
const body = spans.map((span) => ({
traceId: span.traceId,
id: span.spanId,
parentId: span.parentSpanId,
name: span.name,
timestamp: Math.trunc(span.startTime * 1000),
duration: Math.trunc(span.durationMs * 1000),
localEndpoint: { serviceName: options.serviceName ?? "wrnexus" },
tags: Object.fromEntries(
Object.entries(span.attributes).map(([key, value]) => [key, String(value)]),
),
...(span.error ? { error: span.error.message } : {}),
}));
const response = await (options.fetch ?? fetch)(endpoint, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`Zipkin export failed with ${response.status}.`);
},
};
}
/** Jaeger accepts Zipkin v2 JSON at its compatibility endpoint. */
export const createJaegerExporter = createZipkinExporter;
export interface LogExporter {
export(records: readonly LogRecord[]): void | Promise<void>;
}
export function createOtlpLogExporter(
endpoint: string,
options: { fetch?: typeof fetch; headers?: HeadersInit } = {},
): LogExporter {
return {
async export(records) {
const response = await (options.fetch ?? fetch)(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
...Object.fromEntries(new Headers(options.headers)),
},
body: JSON.stringify({
resourceLogs: [
{
scopeLogs: [
{
scope: { name: "@wrnexus/observability" },
logRecords: records.map((record) => ({
timeUnixNano: String(BigInt(new Date(record.timestamp).getTime()) * 1_000_000n),
severityText: record.level.toUpperCase(),
body: { stringValue: record.message },
attributes: Object.entries(record.attributes).map(([key, value]) => ({
key,
value: { stringValue: String(value) },
})),
})),
},
],
},
],
}),
});
if (!response.ok) throw new Error(`OTLP log export failed with ${response.status}.`);
},
};
}
export interface ErrorReporter {
capture(error: unknown, context?: Record<string, unknown>): Promise<string>;
}
export function createSentryCompatibleReporter(
endpoint: string,
options: { fetch?: typeof fetch; publicKey?: string } = {},
): ErrorReporter {
return {
async capture(error, context = {}) {
const eventId = randomHex(16);
const failure = error instanceof Error ? error : new Error(String(error));
const response = await (options.fetch ?? fetch)(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
...(options.publicKey
? { "x-sentry-auth": `Sentry sentry_key=${options.publicKey}, sentry_version=7` }
: {}),
},
body: JSON.stringify({
event_id: eventId,
timestamp: new Date().toISOString(),
platform: "javascript",
level: "error",
exception: {
values: [{ type: failure.name, value: failure.message, stacktrace: failure.stack }],
},
contexts: context,
}),
});
if (!response.ok) throw new Error(`Error report failed with ${response.status}.`);
return eventId;
},
};
}
export function createPerformanceProfiler(
options: {
now?: () => number;
onProfile?: (profile: {
name: string;
durationMs: number;
attributes: Record<string, unknown>;
}) => void;
} = {},
) {
const now = options.now ?? performance.now.bind(performance);
return async <T>(
name: string,
operation: () => T | Promise<T>,
attributes: Record<string, unknown> = {},
): Promise<T> => {
const started = now();
try {
return await operation();
} finally {
options.onProfile?.({ name, durationMs: now() - started, attributes });
}
};
}
+79
View File
@@ -0,0 +1,79 @@
export type LogLevel = "debug" | "info" | "warn" | "error";
export interface LogRecord {
timestamp: string;
level: LogLevel;
message: string;
service: string;
traceId?: string;
spanId?: string;
requestId?: string;
attributes: Record<string, unknown>;
}
export interface StructuredLogger {
log(level: LogLevel, message: string, attributes?: Record<string, unknown>): void;
debug(message: string, attributes?: Record<string, unknown>): void;
info(message: string, attributes?: Record<string, unknown>): void;
warn(message: string, attributes?: Record<string, unknown>): void;
error(message: string, attributes?: Record<string, unknown>): void;
child(attributes: Record<string, unknown>): StructuredLogger;
}
export interface StructuredLoggerOptions {
service?: string;
level?: LogLevel;
now?: () => Date;
sink?: (record: LogRecord, line: string) => void;
redact?: readonly string[];
attributes?: Record<string, unknown>;
}
const LEVELS: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };
function scrub(value: Record<string, unknown>, keys: Set<string>): Record<string, unknown> {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [
key,
keys.has(key.toLowerCase()) ? "[REDACTED]" : item,
]),
);
}
export function createStructuredLogger(options: StructuredLoggerOptions = {}): StructuredLogger {
const threshold = LEVELS[options.level ?? "info"];
const now = options.now ?? (() => new Date());
const sink = options.sink ?? ((_record, line) => console.log(line));
const redacted = new Set(
(options.redact ?? ["authorization", "cookie", "password", "secret", "token"]).map((key) =>
key.toLowerCase(),
),
);
const base = { ...(options.attributes ?? {}) };
const logger = (attributes: Record<string, unknown>): StructuredLogger => {
const log = (level: LogLevel, message: string, values: Record<string, unknown> = {}) => {
if (LEVELS[level] < threshold) return;
const combined = scrub({ ...attributes, ...values }, redacted);
const record: LogRecord = {
timestamp: now().toISOString(),
level,
message,
service: options.service ?? "wrnexus",
...(typeof combined.traceId === "string" ? { traceId: combined.traceId } : {}),
...(typeof combined.spanId === "string" ? { spanId: combined.spanId } : {}),
...(typeof combined.requestId === "string" ? { requestId: combined.requestId } : {}),
attributes: combined,
};
sink(record, JSON.stringify(record));
};
return {
log,
debug: (message, values) => log("debug", message, values),
info: (message, values) => log("info", message, values),
warn: (message, values) => log("warn", message, values),
error: (message, values) => log("error", message, values),
child: (values) => logger({ ...attributes, ...values }),
};
};
return logger(base);
}
+77
View File
@@ -129,4 +129,81 @@ export function createHttpMetricExporter(
};
}
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();
+226
View File
@@ -0,0 +1,226 @@
import { createTracer, type Context, type Middleware } from "@wrnexus/core";
export interface TraceContext {
version: "00";
traceId: string;
spanId: string;
sampled: boolean;
}
export interface SpanRecord {
name: string;
traceId: string;
spanId: string;
parentSpanId?: string;
sampled: boolean;
startTime: number;
endTime: number;
durationMs: number;
status: "ok" | "error";
attributes: Record<string, string | number | boolean>;
error?: { name: string; message: string };
}
export interface SpanExporter {
export(spans: readonly SpanRecord[]): Promise<void> | void;
}
const TRACEPARENT = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
export function parseTraceparent(value: string | null | undefined): TraceContext | null {
if (!value) return null;
const match = TRACEPARENT.exec(value.trim().toLowerCase());
if (!match || /^0+$/.test(match[1]!) || /^0+$/.test(match[2]!)) return null;
return {
version: "00",
traceId: match[1]!,
spanId: match[2]!,
sampled: (Number.parseInt(match[3]!, 16) & 1) === 1,
};
}
export function formatTraceparent(context: TraceContext): string {
return `00-${context.traceId}-${context.spanId}-${context.sampled ? "01" : "00"}`;
}
function randomHex(bytes: number, random: (target: Uint8Array) => Uint8Array): string {
let value = "";
while (!value || /^0+$/.test(value)) {
value = Array.from(random(new Uint8Array(bytes)), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
}
return value;
}
export interface TraceMiddlewareOptions {
serviceName?: string;
sampleRate?: number;
exporter?: SpanExporter;
onSpan?: (span: SpanRecord) => void | Promise<void>;
now?: () => number;
random?: (target: Uint8Array) => Uint8Array;
routeName?: (ctx: Context) => string;
serverTiming?: boolean;
onExportError?: (error: unknown, span: SpanRecord) => void | Promise<void>;
}
export function traceMiddleware(options: TraceMiddlewareOptions = {}): Middleware {
const sampleRate = options.sampleRate ?? 1;
if (!Number.isFinite(sampleRate) || sampleRate < 0 || sampleRate > 1) {
throw new Error("Observability trace sampleRate must be between 0 and 1.");
}
const now = options.now ?? Date.now;
const random = options.random ?? ((target: Uint8Array) => crypto.getRandomValues(target));
return async (ctx, next) => {
const parent = parseTraceparent(ctx.req.headers.get("traceparent"));
const sampled = parent?.sampled ?? Math.random() < sampleRate;
const traceId = parent?.traceId ?? randomHex(16, random);
const spanId = randomHex(8, random);
const trace: TraceContext = { version: "00", traceId, spanId, sampled };
const started = now();
ctx.locals.traceId = traceId;
ctx.locals.spanId = spanId;
ctx.locals.traceparent = formatTraceparent(trace);
ctx.locals.requestId ??= traceId;
ctx.tracer ??= createTracer(now);
const frameworkSpan = ctx.tracer.startSpan("http.request", {
traceId,
spanId,
method: ctx.req.method,
path: ctx.url.pathname,
});
let response: Response | undefined;
let failure: unknown;
let failed = false;
try {
response = await next();
} catch (error) {
failure = error;
failed = true;
} finally {
const ended = now();
const span: SpanRecord = {
name: options.routeName?.(ctx) ?? `${ctx.req.method} ${ctx.url.pathname}`,
traceId,
spanId,
...(parent ? { parentSpanId: parent.spanId } : {}),
sampled,
startTime: started,
endTime: ended,
durationMs: ended - started,
status: failed || (response?.status ?? 500) >= 500 ? "error" : "ok",
attributes: {
"service.name": options.serviceName ?? "wrnexus",
"http.request.method": ctx.req.method,
"url.path": ctx.url.pathname,
"http.response.status_code": response?.status ?? 500,
},
...(failed
? {
error: {
name: failure instanceof Error ? failure.name : "Error",
message: failure instanceof Error ? failure.message : String(failure),
},
}
: {}),
};
frameworkSpan.end(span.status, failure);
if (sampled) {
try {
await options.onSpan?.(span);
await options.exporter?.export([span]);
} catch (error) {
await options.onExportError?.(error, span);
}
}
}
if (failed) throw failure;
const headers = new Headers(response!.headers);
headers.set("traceparent", formatTraceparent(trace));
headers.set("x-request-id", String(ctx.locals.requestId));
if (options.serverTiming !== false) {
headers.append("server-timing", `trace;dur=${Math.max(0, now() - started).toFixed(2)}`);
}
return new Response(response!.body, {
status: response!.status,
statusText: response!.statusText,
headers,
});
};
}
function otlpValue(value: string | number | boolean) {
if (typeof value === "boolean") return { boolValue: value };
if (typeof value === "number")
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
return { stringValue: value };
}
export function createOtlpTraceExporter(
endpoint: string,
options: { headers?: HeadersInit; fetch?: typeof fetch; serviceName?: string } = {},
): SpanExporter {
const send = options.fetch ?? fetch;
return {
async export(spans) {
if (!spans.length) return;
const body = {
resourceSpans: [
{
resource: {
attributes: [
{ key: "service.name", value: { stringValue: options.serviceName ?? "wrnexus" } },
],
},
scopeSpans: [
{
scope: { name: "@wrnexus/observability", version: "0.8.0" },
spans: spans.map((span) => ({
traceId: span.traceId,
spanId: span.spanId,
...(span.parentSpanId ? { parentSpanId: span.parentSpanId } : {}),
name: span.name,
kind: 2,
startTimeUnixNano: String(BigInt(Math.trunc(span.startTime)) * 1_000_000n),
endTimeUnixNano: String(BigInt(Math.trunc(span.endTime)) * 1_000_000n),
attributes: Object.entries(span.attributes).map(([key, value]) => ({
key,
value: otlpValue(value),
})),
status: { code: span.status === "error" ? 2 : 1 },
...(span.error
? {
events: [
{
timeUnixNano: String(BigInt(Math.trunc(span.endTime)) * 1_000_000n),
name: "exception",
attributes: [
{ key: "exception.type", value: { stringValue: span.error.name } },
{
key: "exception.message",
value: { stringValue: span.error.message },
},
],
},
],
}
: {}),
})),
},
],
},
],
};
const response = await send(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
...Object.fromEntries(new Headers(options.headers)),
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`OTLP trace export failed with ${response.status}.`);
},
};
}