release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { MetricsRegistry } from "../src/metrics.ts";
|
||||
import {
|
||||
createOperationTracer,
|
||||
createOtlpLogExporter,
|
||||
createPerformanceProfiler,
|
||||
createPrometheusPushExporter,
|
||||
createSentryCompatibleReporter,
|
||||
createZipkinExporter,
|
||||
renderPrometheus,
|
||||
} from "../src/integrations.ts";
|
||||
|
||||
describe("observability integrations", () => {
|
||||
test("records every framework operation kind and failures", async () => {
|
||||
const spans: any[] = [];
|
||||
const tracer = createOperationTracer({
|
||||
exporter: {
|
||||
export: (records) => {
|
||||
spans.push(...records);
|
||||
},
|
||||
},
|
||||
now: (() => {
|
||||
let time = 0;
|
||||
return () => (time += 5);
|
||||
})(),
|
||||
});
|
||||
for (const kind of [
|
||||
"database",
|
||||
"cache",
|
||||
"queue",
|
||||
"realtime",
|
||||
"server-action",
|
||||
"application",
|
||||
] as const)
|
||||
expect(await tracer.span(kind, `${kind}.work`, async () => kind)).toBe(kind);
|
||||
await expect(
|
||||
tracer.span("application", "failure", async () => {
|
||||
throw new Error("boom");
|
||||
}),
|
||||
).rejects.toThrow("boom");
|
||||
expect(spans.map((span) => span.attributes["wrnexus.span.kind"])).toContain("database");
|
||||
expect(spans.at(-1).status).toBe("error");
|
||||
});
|
||||
|
||||
test("exports Prometheus, Zipkin/Jaeger, OTLP logs and Sentry-compatible errors", async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
||||
const send = (async (url: URL | RequestInfo, init?: RequestInit) => {
|
||||
requests.push({ url: String(url), init });
|
||||
return new Response(null, { status: 200 });
|
||||
}) as typeof fetch;
|
||||
const registry = new MetricsRegistry(() => 10);
|
||||
registry.increment("http.requests", 1, { route: "/" });
|
||||
expect(renderPrometheus(registry.snapshot())).toContain('http_requests{route="/"} 1');
|
||||
await createPrometheusPushExporter("https://prom.test", { fetch: send }).export(
|
||||
registry.snapshot(),
|
||||
);
|
||||
const span = {
|
||||
name: "db",
|
||||
traceId: "1".repeat(32),
|
||||
spanId: "2".repeat(16),
|
||||
sampled: true,
|
||||
startTime: 1,
|
||||
endTime: 2,
|
||||
durationMs: 1,
|
||||
status: "ok" as const,
|
||||
attributes: {},
|
||||
};
|
||||
await createZipkinExporter("https://zipkin.test", { fetch: send }).export([span]);
|
||||
await createOtlpLogExporter("https://otlp.test", { fetch: send }).export([
|
||||
{
|
||||
timestamp: new Date(0).toISOString(),
|
||||
level: "info",
|
||||
message: "ready",
|
||||
service: "app",
|
||||
attributes: {},
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
await createSentryCompatibleReporter("https://sentry.test", { fetch: send }).capture(
|
||||
new Error("bad"),
|
||||
),
|
||||
).toHaveLength(32);
|
||||
expect(requests).toHaveLength(4);
|
||||
});
|
||||
|
||||
test("profiles application operations", async () => {
|
||||
const profiles: any[] = [];
|
||||
let time = 0;
|
||||
const profile = createPerformanceProfiler({
|
||||
now: () => (time += 4),
|
||||
onProfile: (value) => profiles.push(value),
|
||||
});
|
||||
expect(await profile("render", () => "ok")).toBe("ok");
|
||||
expect(profiles[0]).toMatchObject({ name: "render", durationMs: 4 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createContext, HealthRegistry } from "@wrnexus/core";
|
||||
import {
|
||||
createLivenessHandler,
|
||||
createOtlpMetricExporter,
|
||||
createOtlpTraceExporter,
|
||||
createReadinessHandler,
|
||||
createStructuredLogger,
|
||||
formatTraceparent,
|
||||
MetricsRegistry,
|
||||
parseTraceparent,
|
||||
traceMiddleware,
|
||||
type SpanRecord,
|
||||
} from "../src/index.ts";
|
||||
|
||||
describe("production observability operations", () => {
|
||||
test("parses and formats strict W3C trace context", () => {
|
||||
const value = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
|
||||
const parsed = parseTraceparent(value);
|
||||
expect(parsed).toEqual({
|
||||
version: "00",
|
||||
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
|
||||
spanId: "00f067aa0ba902b7",
|
||||
sampled: true,
|
||||
});
|
||||
expect(formatTraceparent(parsed!)).toBe(value);
|
||||
expect(parseTraceparent("00-00000000000000000000000000000000-00f067aa0ba902b7-01")).toBeNull();
|
||||
expect(parseTraceparent("ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")).toBeNull();
|
||||
});
|
||||
|
||||
test("propagates trace context, correlates locals, and records a framework span", async () => {
|
||||
const parent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
|
||||
const ctx = createContext(
|
||||
new Request("https://example.test/orders", { headers: { traceparent: parent } }),
|
||||
new URL("https://example.test/orders"),
|
||||
);
|
||||
const spans: SpanRecord[] = [];
|
||||
const times = [100, 125];
|
||||
let randomValue = 1;
|
||||
const middleware = traceMiddleware({
|
||||
serviceName: "orders",
|
||||
now: () => times.shift() ?? 125,
|
||||
random: (target) => target.fill(randomValue++),
|
||||
onSpan: (span) => {
|
||||
spans.push(span);
|
||||
},
|
||||
});
|
||||
const response = await middleware(ctx, () => new Response("ok", { status: 201 }));
|
||||
|
||||
expect(ctx.locals.traceId).toBe("4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
expect(ctx.locals.requestId).toBe(ctx.locals.traceId);
|
||||
expect(response.headers.get("traceparent")).toMatch(
|
||||
/^00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01$/,
|
||||
);
|
||||
expect(response.headers.get("x-request-id")).toBe(String(ctx.locals.traceId));
|
||||
expect(spans[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
parentSpanId: "00f067aa0ba902b7",
|
||||
durationMs: 25,
|
||||
status: "ok",
|
||||
}),
|
||||
);
|
||||
expect(ctx.tracer?.records()[0]).toEqual(expect.objectContaining({ status: "ok" }));
|
||||
});
|
||||
|
||||
test("does not break responses when a telemetry exporter fails", async () => {
|
||||
const failures: unknown[] = [];
|
||||
const ctx = createContext(
|
||||
new Request("https://example.test/"),
|
||||
new URL("https://example.test/"),
|
||||
);
|
||||
const middleware = traceMiddleware({
|
||||
random: (target) => target.fill(7),
|
||||
exporter: { export: () => Promise.reject(new Error("collector unavailable")) },
|
||||
onExportError: (error) => {
|
||||
failures.push(error);
|
||||
},
|
||||
});
|
||||
const response = await middleware(ctx, () => new Response("ok"));
|
||||
expect(response.status).toBe(200);
|
||||
expect(failures).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("exports OTLP JSON traces and metrics", async () => {
|
||||
const requests: unknown[] = [];
|
||||
const send = (async (_url: URL | RequestInfo, init?: RequestInit) => {
|
||||
requests.push(JSON.parse(String(init?.body)));
|
||||
return new Response(null, { status: 200 });
|
||||
}) as typeof fetch;
|
||||
const span: SpanRecord = {
|
||||
name: "GET /",
|
||||
traceId: "1".repeat(32),
|
||||
spanId: "2".repeat(16),
|
||||
sampled: true,
|
||||
startTime: 100,
|
||||
endTime: 125,
|
||||
durationMs: 25,
|
||||
status: "ok",
|
||||
attributes: { "http.response.status_code": 200 },
|
||||
};
|
||||
await createOtlpTraceExporter("https://collector.test/v1/traces", { fetch: send }).export([
|
||||
span,
|
||||
]);
|
||||
const metrics = new MetricsRegistry(() => 200);
|
||||
metrics.increment("requests", 1, { route: "/" });
|
||||
await createOtlpMetricExporter("https://collector.test/v1/metrics", { fetch: send }).export(
|
||||
metrics.snapshot(),
|
||||
);
|
||||
expect(requests[0]).toHaveProperty(
|
||||
"resourceSpans.0.scopeSpans.0.spans.0.traceId",
|
||||
span.traceId,
|
||||
);
|
||||
expect(requests[1]).toHaveProperty(
|
||||
"resourceMetrics.0.scopeMetrics.0.metrics.0.name",
|
||||
"requests",
|
||||
);
|
||||
});
|
||||
|
||||
test("serves liveness and dependency readiness without leaking details by default", async () => {
|
||||
const registry = new HealthRegistry();
|
||||
registry.register("database", () => ({ status: "down", message: "connection refused" }));
|
||||
const live = await createLivenessHandler()(new Request("https://example.test/live"));
|
||||
const ready = await createReadinessHandler(registry)(new Request("https://example.test/ready"));
|
||||
const detailed = await createReadinessHandler(registry, { exposeDetails: true })(
|
||||
new Request("https://example.test/ready"),
|
||||
);
|
||||
expect(live.status).toBe(200);
|
||||
expect(ready.status).toBe(503);
|
||||
expect(await ready.json()).toEqual({ status: "down" });
|
||||
expect(await detailed.json()).toHaveProperty("checks.database.message", "connection refused");
|
||||
});
|
||||
|
||||
test("creates correlated structured child logs with secret redaction", () => {
|
||||
const records: unknown[] = [];
|
||||
const logger = createStructuredLogger({
|
||||
service: "api",
|
||||
now: () => new Date("2026-08-02T00:00:00.000Z"),
|
||||
sink: (record) => records.push(record),
|
||||
}).child({ traceId: "trace", requestId: "request" });
|
||||
logger.info("signed in", { userId: "user-1", token: "secret-value" });
|
||||
expect(records[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
service: "api",
|
||||
traceId: "trace",
|
||||
requestId: "request",
|
||||
attributes: expect.objectContaining({ token: "[REDACTED]", userId: "user-1" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user