From 4c49d419a5e20b03ec76a944f0f8a783441d2741 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sun, 9 Nov 2025 02:10:28 +0200 Subject: [PATCH 1/6] feat: first draft implementation of tracing channels --- build.config.mjs | 1 + package.json | 1 + src/_middleware.ts | 37 +++- src/tracing.ts | 42 +++++ test/tracing.test.ts | 434 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 509 insertions(+), 6 deletions(-) create mode 100644 src/tracing.ts create mode 100644 test/tracing.test.ts diff --git a/build.config.mjs b/build.config.mjs index d2670f8d..91c90070 100644 --- a/build.config.mjs +++ b/build.config.mjs @@ -12,6 +12,7 @@ export default defineBuildConfig({ "src/cli.ts", "src/static.ts", "src/log.ts", + "src/tracing.ts", ...[ "deno", "bun", diff --git a/package.json b/package.json index f1a8fe0b..9ebefded 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "./cli": "./dist/cli.mjs", "./static": "./dist/static.mjs", "./log": "./dist/log.mjs", + "./tracing": "./dist/tracing.mjs", ".": { "types": "./dist/types.d.mts", "deno": "./dist/adapters/deno.mjs", diff --git a/src/_middleware.ts b/src/_middleware.ts index f2f70d78..e3f7e568 100644 --- a/src/_middleware.ts +++ b/src/_middleware.ts @@ -1,3 +1,4 @@ +import { traceCall } from "./tracing.ts"; import type { Server, ServerRequest, @@ -8,21 +9,45 @@ import type { export function wrapFetch(server: Server): ServerHandler { const fetchHandler = server.options.fetch; const middleware = server.options.middleware || []; - return middleware.length === 0 - ? fetchHandler - : (request) => callMiddleware(request, fetchHandler, middleware, 0); + + if (middleware.length === 0) { + return (request) => + traceCall("fetch", async () => await fetchHandler(request), { + request, + server, + }); + } + + return (request) => + callMiddleware(server, request, fetchHandler, middleware, 0); } function callMiddleware( + server: Server, request: ServerRequest, fetchHandler: ServerHandler, middleware: ServerMiddleware[], index: number, ): Response | Promise { if (index === middleware.length) { - return fetchHandler(request); + return traceCall("fetch", async () => await fetchHandler(request), { + request, + server, + }); } - return middleware[index](request, () => - callMiddleware(request, fetchHandler, middleware, index + 1), + + const currentMiddleware = middleware[index]; + const next = () => + callMiddleware(server, request, fetchHandler, middleware, index + 1); + + return traceCall( + "middleware", + async () => await currentMiddleware(request, next), + { + request, + server, + index, + name: currentMiddleware?.name, + }, ); } diff --git a/src/tracing.ts b/src/tracing.ts new file mode 100644 index 00000000..33248abd --- /dev/null +++ b/src/tracing.ts @@ -0,0 +1,42 @@ +import { tracingChannel, type TracingChannel } from "node:diagnostics_channel"; +import type { Server, ServerRequest } from "./types.ts"; + +export type TraceDataMap = { + fetch: { request: ServerRequest; server: Server }; + middleware: { + request: ServerRequest; + server: Server; + index: number; + name?: string; + }; +}; + +export type TraceChannelName = keyof TraceDataMap; + +const channels: Record< + TraceChannelName, + TracingChannel +> = { + fetch: tracingChannel("srvx.fetch"), + middleware: tracingChannel("srvx.middleware"), +}; + +export function traceCall< + TChannel extends TraceChannelName, + TReturn, + TData extends TraceDataMap[TChannel], +>( + channel: TChannel, + exec: () => Promise, + data: TData, +): Promise { + return channels[channel].tracePromise(exec, data); +} + +export function traceSync< + TChannel extends TraceChannelName, + TReturn, + TData extends TraceDataMap[TChannel], +>(channel: TChannel, exec: () => TReturn, data: TData): TReturn { + return channels[channel].traceSync(exec, data); +} diff --git a/test/tracing.test.ts b/test/tracing.test.ts new file mode 100644 index 00000000..8b394fe7 --- /dev/null +++ b/test/tracing.test.ts @@ -0,0 +1,434 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { tracingChannel } from "node:diagnostics_channel"; +import { serve } from "../src/adapters/node.ts"; +import type { ServerMiddleware } from "../src/types.ts"; + +// Helper to create no-op handlers for unused tracing events +const noop = () => {}; + +describe("tracing channels", () => { + const cleanupFns: Array<() => void> = []; + + afterEach(() => { + // Clean up all subscriptions after each test + for (const cleanup of cleanupFns) { + cleanup(); + } + cleanupFns.length = 0; + }); + + it("should emit fetch tracing events", async () => { + const events: Array<{ type: string; method?: string }> = []; + + const fetchChannel = tracingChannel("srvx.fetch"); + + const startHandler = (data: any) => { + events.push({ type: "fetch.start", method: data.request.method }); + }; + + const endHandler = (data: any) => { + events.push({ type: "fetch.end", method: data.request.method }); + }; + + fetchChannel.subscribe({ + start: startHandler, + end: endHandler, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + + cleanupFns.push(() => { + fetchChannel.unsubscribe({ + start: startHandler, + end: endHandler, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + }); + + const server = serve({ + fetch: () => new Response("OK"), + manual: true, + }); + + const request = new Request("http://localhost:3000/test"); + await server.fetch(request); + + expect(events).toContainEqual({ type: "fetch.start", method: "GET" }); + expect(events).toContainEqual({ type: "fetch.end", method: "GET" }); + }); + + it("should emit middleware tracing events", async () => { + const events: Array<{ type: string; name?: string; index?: number }> = []; + + const middlewareChannel = tracingChannel("srvx.middleware"); + + const startHandler = (data: any) => { + events.push({ + type: "middleware.start", + name: data.name, + index: data.index, + }); + }; + + const endHandler = (data: any) => { + events.push({ + type: "middleware.end", + name: data.name, + index: data.index, + }); + }; + + middlewareChannel.subscribe({ + start: startHandler, + end: endHandler, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + + cleanupFns.push(() => { + middlewareChannel.unsubscribe({ + start: startHandler, + end: endHandler, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + }); + + const middleware1: ServerMiddleware = async (request, next) => { + return next(); + }; + Object.defineProperty(middleware1, "name", { value: "middleware1" }); + + const middleware2: ServerMiddleware = async (request, next) => { + return next(); + }; + Object.defineProperty(middleware2, "name", { value: "middleware2" }); + + const server = serve({ + fetch: () => new Response("OK"), + middleware: [middleware1, middleware2], + manual: true, + }); + + const request = new Request("http://localhost:3000/"); + await server.fetch(request); + + // Check that all middleware events were emitted + expect(events).toContainEqual({ + type: "middleware.start", + name: "middleware1", + index: 0, + }); + expect(events).toContainEqual({ + type: "middleware.end", + name: "middleware1", + index: 0, + }); + expect(events).toContainEqual({ + type: "middleware.start", + name: "middleware2", + index: 1, + }); + expect(events).toContainEqual({ + type: "middleware.end", + name: "middleware2", + index: 1, + }); + }); + + it("should emit asyncStart and asyncEnd events", async () => { + const events: Array = []; + + const middlewareChannel = tracingChannel("srvx.middleware"); + + const asyncStartHandler = () => { + events.push("middleware.asyncStart"); + }; + + const asyncEndHandler = () => { + events.push("middleware.asyncEnd"); + }; + + middlewareChannel.subscribe({ + start: noop, + end: noop, + asyncStart: asyncStartHandler, + asyncEnd: asyncEndHandler, + error: noop, + }); + + cleanupFns.push(() => { + middlewareChannel.unsubscribe({ + start: noop, + end: noop, + asyncStart: asyncStartHandler, + asyncEnd: asyncEndHandler, + error: noop, + }); + }); + + const middleware: ServerMiddleware = async (request, next) => { + return next(); + }; + + const server = serve({ + fetch: () => new Response("OK"), + middleware: [middleware], + manual: true, + }); + + const request = new Request("http://localhost:3000/"); + await server.fetch(request); + + expect(events).toContain("middleware.asyncStart"); + expect(events).toContain("middleware.asyncEnd"); + }); + + it("should emit error events on middleware errors", async () => { + const events: Array<{ type: string; error?: string }> = []; + + const middlewareChannel = tracingChannel("srvx.middleware"); + + const errorHandler = (data: any) => { + events.push({ type: "middleware.error", error: data.error?.message }); + }; + + middlewareChannel.subscribe({ + start: noop, + end: noop, + asyncStart: noop, + asyncEnd: noop, + error: errorHandler, + }); + + cleanupFns.push(() => { + middlewareChannel.unsubscribe({ + start: noop, + end: noop, + asyncStart: noop, + asyncEnd: noop, + error: errorHandler, + }); + }); + + const middleware: ServerMiddleware = async () => { + throw new Error("Test error"); + }; + + const server = serve({ + fetch: () => new Response("OK"), + middleware: [middleware], + manual: true, + }); + + const request = new Request("http://localhost:3000/"); + + // Expect the fetch to throw + await expect(server.fetch(request)).rejects.toThrow("Test error"); + + expect(events).toContainEqual({ + type: "middleware.error", + error: "Test error", + }); + }); + + it("should include request and server data in events", async () => { + let capturedData: any = null; + + const middlewareChannel = tracingChannel("srvx.middleware"); + + const startHandler = (data: any) => { + capturedData = data; + }; + + middlewareChannel.subscribe({ + start: startHandler, + end: noop, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + + cleanupFns.push(() => { + middlewareChannel.unsubscribe({ + start: startHandler, + end: noop, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + }); + + const middleware: ServerMiddleware = async (request, next) => { + return next(); + }; + + const server = serve({ + fetch: () => new Response("OK"), + middleware: [middleware], + manual: true, + port: 3000, + }); + + const request = new Request("http://localhost:3000/test"); + await server.fetch(request); + + expect(capturedData).toBeDefined(); + expect(capturedData.request).toBeDefined(); + expect(capturedData.server).toBeDefined(); + expect(capturedData.index).toBe(0); + expect(capturedData.server.options.port).toBe(3000); + }); + + it("should emit events for multiple middleware in sequence", async () => { + const events: Array<{ type: string; name: string }> = []; + + const middlewareChannel = tracingChannel("srvx.middleware"); + + const startHandler = (data: any) => { + events.push({ type: "start", name: data.name }); + }; + + const endHandler = (data: any) => { + events.push({ type: "end", name: data.name }); + }; + + middlewareChannel.subscribe({ + start: startHandler, + end: endHandler, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + + cleanupFns.push(() => { + middlewareChannel.unsubscribe({ + start: startHandler, + end: endHandler, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + }); + + const mw1: ServerMiddleware = async (req, next) => next(); + Object.defineProperty(mw1, "name", { value: "mw1" }); + + const mw2: ServerMiddleware = async (req, next) => next(); + Object.defineProperty(mw2, "name", { value: "mw2" }); + + const mw3: ServerMiddleware = async (req, next) => next(); + Object.defineProperty(mw3, "name", { value: "mw3" }); + + const server = serve({ + fetch: () => new Response("OK"), + middleware: [mw1, mw2, mw3], + manual: true, + }); + + const request = new Request("http://localhost:3000/"); + await server.fetch(request); + + // Verify all start and end events were emitted + const startEvents = events.filter((e) => e.type === "start"); + const endEvents = events.filter((e) => e.type === "end"); + + expect(startEvents).toHaveLength(3); + expect(endEvents).toHaveLength(3); + + expect(startEvents.map((e) => e.name)).toEqual(["mw1", "mw2", "mw3"]); + expect(endEvents.map((e) => e.name)).toEqual(["mw3", "mw2", "mw1"]); + }); + + it("should emit fetch events when no middleware present", async () => { + const events: Array = []; + + const fetchChannel = tracingChannel("srvx.fetch"); + + const startHandler = () => { + events.push("fetch.start"); + }; + + const endHandler = () => { + events.push("fetch.end"); + }; + + fetchChannel.subscribe({ + start: startHandler, + end: endHandler, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + + cleanupFns.push(() => { + fetchChannel.unsubscribe({ + start: startHandler, + end: endHandler, + asyncStart: noop, + asyncEnd: noop, + error: noop, + }); + }); + + const server = serve({ + fetch: () => new Response("OK"), + manual: true, + }); + + const request = new Request("http://localhost:3000/"); + await server.fetch(request); + + expect(events).toContain("fetch.start"); + expect(events).toContain("fetch.end"); + }); + + it("should provide result in asyncEnd events", async () => { + const results: Array = []; + + const middlewareChannel = tracingChannel("srvx.middleware"); + + const asyncEndHandler = (data: any) => { + results.push(data.result); + }; + + middlewareChannel.subscribe({ + start: noop, + end: noop, + asyncStart: noop, + asyncEnd: asyncEndHandler, + error: noop, + }); + + cleanupFns.push(() => { + middlewareChannel.unsubscribe({ + start: noop, + end: noop, + asyncStart: noop, + asyncEnd: asyncEndHandler, + error: noop, + }); + }); + + const middleware: ServerMiddleware = async (req, next) => next(); + + const server = serve({ + fetch: () => new Response("OK"), + middleware: [middleware], + manual: true, + }); + + const request = new Request("http://localhost:3000/"); + await server.fetch(request); + + // Result should be the Response object + expect(results).toHaveLength(1); + expect(results[0]).toBeDefined(); + expect(results[0]).toBeInstanceOf(Response); + }); +}); From a723db1e3c509c26ef9575a0bc757117d643298a Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 1 Dec 2025 15:07:50 +0100 Subject: [PATCH 2/6] refactor: re-implement as a plugin --- src/_middleware.ts | 29 ++++----------------- src/tracing.ts | 61 +++++++++++++++++++++++++++++++++++++++++++- test/tracing.test.ts | 9 +++++++ 3 files changed, 74 insertions(+), 25 deletions(-) diff --git a/src/_middleware.ts b/src/_middleware.ts index e3f7e568..49579294 100644 --- a/src/_middleware.ts +++ b/src/_middleware.ts @@ -1,4 +1,3 @@ -import { traceCall } from "./tracing.ts"; import type { Server, ServerRequest, @@ -11,43 +10,25 @@ export function wrapFetch(server: Server): ServerHandler { const middleware = server.options.middleware || []; if (middleware.length === 0) { - return (request) => - traceCall("fetch", async () => await fetchHandler(request), { - request, - server, - }); + return fetchHandler; } - return (request) => - callMiddleware(server, request, fetchHandler, middleware, 0); + return (request) => callMiddleware(request, fetchHandler, middleware, 0); } function callMiddleware( - server: Server, request: ServerRequest, fetchHandler: ServerHandler, middleware: ServerMiddleware[], index: number, ): Response | Promise { if (index === middleware.length) { - return traceCall("fetch", async () => await fetchHandler(request), { - request, - server, - }); + return fetchHandler(request); } const currentMiddleware = middleware[index]; const next = () => - callMiddleware(server, request, fetchHandler, middleware, index + 1); + callMiddleware(request, fetchHandler, middleware, index + 1); - return traceCall( - "middleware", - async () => await currentMiddleware(request, next), - { - request, - server, - index, - name: currentMiddleware?.name, - }, - ); + return currentMiddleware(request, next); } diff --git a/src/tracing.ts b/src/tracing.ts index 33248abd..cd827512 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -1,5 +1,10 @@ import { tracingChannel, type TracingChannel } from "node:diagnostics_channel"; -import type { Server, ServerRequest } from "./types.ts"; +import type { + Server, + ServerRequest, + ServerPlugin, + ServerMiddleware, +} from "./types.ts"; export type TraceDataMap = { fetch: { request: ServerRequest; server: Server }; @@ -40,3 +45,57 @@ export function traceSync< >(channel: TChannel, exec: () => TReturn, data: TData): TReturn { return channels[channel].traceSync(exec, data); } + +/** + * Tracing plugin that adds diagnostics channel tracing to middleware and fetch handlers. + * + * This plugin wraps all middleware and the fetch handler with tracing instrumentation, + * allowing you to subscribe to `srvx.fetch` and `srvx.middleware` tracing channels. + * + * @example + * ```ts + * import { serve, tracingPlugin } from "srvx/node"; + * + * const server = serve({ + * fetch: (req) => new Response("OK"), + * middleware: [myMiddleware], + * plugins: [tracingPlugin], + * }); + * ``` + */ +export const tracingPlugin: ServerPlugin = (server) => { + // Wrap middleware with tracing + const originalMiddleware = server.options.middleware; + const wrappedMiddleware: ServerMiddleware[] = originalMiddleware.map( + (middleware, index) => { + return (request, next) => { + return traceCall( + "middleware", + async () => await middleware(request, next), + { + request, + server, + index, + name: middleware?.name, + }, + ); + }; + }, + ); + + // Replace middleware array with wrapped versions + server.options.middleware.splice( + 0, + server.options.middleware.length, + ...wrappedMiddleware, + ); + + // Wrap the fetch handler with tracing + const originalFetch = server.options.fetch; + server.options.fetch = (request) => { + return traceCall("fetch", async () => await originalFetch(request), { + request, + server, + }); + }; +}; diff --git a/test/tracing.test.ts b/test/tracing.test.ts index 8b394fe7..4a8c97df 100644 --- a/test/tracing.test.ts +++ b/test/tracing.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, afterEach } from "vitest"; import { tracingChannel } from "node:diagnostics_channel"; import { serve } from "../src/adapters/node.ts"; +import { tracingPlugin } from "../src/tracing.ts"; import type { ServerMiddleware } from "../src/types.ts"; // Helper to create no-op handlers for unused tracing events @@ -50,6 +51,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), + plugins: [tracingPlugin], manual: true, }); @@ -112,6 +114,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware1, middleware2], + plugins: [tracingPlugin], manual: true, }); @@ -179,6 +182,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware], + plugins: [tracingPlugin], manual: true, }); @@ -223,6 +227,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware], + plugins: [tracingPlugin], manual: true, }); @@ -271,6 +276,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware], + plugins: [tracingPlugin], manual: true, port: 3000, }); @@ -328,6 +334,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [mw1, mw2, mw3], + plugins: [tracingPlugin], manual: true, }); @@ -378,6 +385,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), + plugins: [tracingPlugin], manual: true, }); @@ -420,6 +428,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware], + plugins: [tracingPlugin], manual: true, }); From c8e96a0341cd5c00f81e4d55115c155792a9318d Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 1 Dec 2025 15:41:25 +0100 Subject: [PATCH 3/6] ref: revert non-relevant changes --- src/_middleware.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/_middleware.ts b/src/_middleware.ts index 49579294..f2f70d78 100644 --- a/src/_middleware.ts +++ b/src/_middleware.ts @@ -8,12 +8,9 @@ import type { export function wrapFetch(server: Server): ServerHandler { const fetchHandler = server.options.fetch; const middleware = server.options.middleware || []; - - if (middleware.length === 0) { - return fetchHandler; - } - - return (request) => callMiddleware(request, fetchHandler, middleware, 0); + return middleware.length === 0 + ? fetchHandler + : (request) => callMiddleware(request, fetchHandler, middleware, 0); } function callMiddleware( @@ -25,10 +22,7 @@ function callMiddleware( if (index === middleware.length) { return fetchHandler(request); } - - const currentMiddleware = middleware[index]; - const next = () => - callMiddleware(request, fetchHandler, middleware, index + 1); - - return currentMiddleware(request, next); + return middleware[index](request, () => + callMiddleware(request, fetchHandler, middleware, index + 1), + ); } From 440d884881a9f29983d5bc7f61307122a602d469 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 11 Dec 2025 02:44:53 +0100 Subject: [PATCH 4/6] simplify and add example --- examples/tracing/package.json | 12 ++++ examples/tracing/server.ts | 45 ++++++++++++ pnpm-lock.yaml | 6 ++ src/tracing.ts | 124 ++++++++++++++-------------------- 4 files changed, 115 insertions(+), 72 deletions(-) create mode 100644 examples/tracing/package.json create mode 100644 examples/tracing/server.ts diff --git a/examples/tracing/package.json b/examples/tracing/package.json new file mode 100644 index 00000000..75a735b2 --- /dev/null +++ b/examples/tracing/package.json @@ -0,0 +1,12 @@ +{ + "name": "srvx-examples-tracing", + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "srvx --prod", + "dev": "srvx" + }, + "devDependencies": { + "srvx": "latest" + } +} diff --git a/examples/tracing/server.ts b/examples/tracing/server.ts new file mode 100644 index 00000000..ec26720f --- /dev/null +++ b/examples/tracing/server.ts @@ -0,0 +1,45 @@ +import { tracingPlugin } from "srvx/tracing"; + +export default { + plugins: [tracingPlugin()], + fetch(req: Request) { + return Response.json({ hello: "world!" }); + }, +}; + +// --- debug tracing channels --- + +debugChannel("srvx.middleware"); +debugChannel("srvx.fetch"); + +function debugChannel(name: string) { + const { tracingChannel } = process.getBuiltinModule( + "node:diagnostics_channel", + ); + + const log = (...args: unknown[]) => console.log(`[tracing:${name}]`, ...args); + const noop = () => {}; + const serializeData = (data: any) => + Object.entries(data) + .map(([key, value]) => { + if (key === "request") { + return `request(url=${(value as Request).url})`; + } + if (key === "server") { + return "server"; + } + if (key === "result") { + return `result(status=${(value as Response).status})`; + } + return `${key}=${value}`; + }) + .join(", "); + + tracingChannel(name).subscribe({ + start: noop, + end: noop, + asyncStart: (data) => log("asyncStart", serializeData(data)), + asyncEnd: (data) => log("asyncEnd", serializeData(data)), + error: (data) => log("error", serializeData(data)), + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4c80a9f..bb4ef1a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,6 +183,12 @@ importers: specifier: link:../.. version: link:../.. + examples/tracing: + devDependencies: + srvx: + specifier: link:../.. + version: link:../.. + examples/websocket: devDependencies: crossws: diff --git a/src/tracing.ts b/src/tracing.ts index cd827512..1d835dd9 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -1,4 +1,3 @@ -import { tracingChannel, type TracingChannel } from "node:diagnostics_channel"; import type { Server, ServerRequest, @@ -6,46 +5,12 @@ import type { ServerMiddleware, } from "./types.ts"; -export type TraceDataMap = { - fetch: { request: ServerRequest; server: Server }; - middleware: { - request: ServerRequest; - server: Server; - index: number; - name?: string; - }; -}; - -export type TraceChannelName = keyof TraceDataMap; - -const channels: Record< - TraceChannelName, - TracingChannel -> = { - fetch: tracingChannel("srvx.fetch"), - middleware: tracingChannel("srvx.middleware"), +export type RequestData = { + server: Server; + request: ServerRequest; + middlewareName?: string; }; -export function traceCall< - TChannel extends TraceChannelName, - TReturn, - TData extends TraceDataMap[TChannel], ->( - channel: TChannel, - exec: () => Promise, - data: TData, -): Promise { - return channels[channel].tracePromise(exec, data); -} - -export function traceSync< - TChannel extends TraceChannelName, - TReturn, - TData extends TraceDataMap[TChannel], ->(channel: TChannel, exec: () => TReturn, data: TData): TReturn { - return channels[channel].traceSync(exec, data); -} - /** * Tracing plugin that adds diagnostics channel tracing to middleware and fetch handlers. * @@ -54,48 +19,63 @@ export function traceSync< * * @example * ```ts - * import { serve, tracingPlugin } from "srvx/node"; + * import { serve } from "srvx"; + * import { tracingPlugin } from "srvx/tracing"; * * const server = serve({ * fetch: (req) => new Response("OK"), * middleware: [myMiddleware], - * plugins: [tracingPlugin], + * plugins: [tracingPlugin()], * }); * ``` */ -export const tracingPlugin: ServerPlugin = (server) => { - // Wrap middleware with tracing - const originalMiddleware = server.options.middleware; - const wrappedMiddleware: ServerMiddleware[] = originalMiddleware.map( - (middleware, index) => { - return (request, next) => { - return traceCall( - "middleware", - async () => await middleware(request, next), - { - request, - server, - index, - name: middleware?.name, - }, +export function tracingPlugin( + opts: { middleware?: boolean; fetch?: boolean } = {}, +): ServerPlugin { + return (server) => { + // No-op if tracingChannel is not available + const { tracingChannel } = + globalThis.process?.getBuiltinModule?.("node:diagnostics_channel") || {}; + if (!tracingChannel) { + return; + } + + // Wrap the fetch handler with tracing + if (opts.fetch !== false) { + const fetchChannel = tracingChannel("srvx.fetch"); + const originalFetch = server.options.fetch; + server.options.fetch = (request) => { + return fetchChannel.tracePromise( + async () => await originalFetch(request), + { request, server }, ); }; - }, - ); + } - // Replace middleware array with wrapped versions - server.options.middleware.splice( - 0, - server.options.middleware.length, - ...wrappedMiddleware, - ); + // Wrap middleware with tracing + if (opts.middleware !== false) { + const middlewareChannel = tracingChannel( + "srvx.middleware", + ); + const originalMiddleware = server.options.middleware; + const wrappedMiddleware: ServerMiddleware[] = originalMiddleware.map( + (middleware, index) => { + const middlewareName = middleware?.name || `middleware#${index}`; + return (request, next) => { + return middlewareChannel.tracePromise( + async () => await middleware(request, next), + { request, server, middlewareName }, + ); + }; + }, + ); - // Wrap the fetch handler with tracing - const originalFetch = server.options.fetch; - server.options.fetch = (request) => { - return traceCall("fetch", async () => await originalFetch(request), { - request, - server, - }); + // Replace middleware array with wrapped versions + server.options.middleware.splice( + 0, + server.options.middleware.length, + ...wrappedMiddleware, + ); + } }; -}; +} From 564af869dfcdfe978a2c3b974b2627134624b326 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 11 Dec 2025 02:55:01 +0100 Subject: [PATCH 5/6] update types and test --- src/tracing.ts | 19 +++++++++++-------- test/tracing.test.ts | 30 +++++++++++++++--------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/tracing.ts b/src/tracing.ts index 1d835dd9..c148c140 100644 --- a/src/tracing.ts +++ b/src/tracing.ts @@ -5,10 +5,13 @@ import type { ServerMiddleware, } from "./types.ts"; -export type RequestData = { +export type RequestEvent = { server: Server; request: ServerRequest; - middlewareName?: string; + middleware?: { + index: number; + handler: ServerMiddleware; + }; }; /** @@ -42,7 +45,7 @@ export function tracingPlugin( // Wrap the fetch handler with tracing if (opts.fetch !== false) { - const fetchChannel = tracingChannel("srvx.fetch"); + const fetchChannel = tracingChannel("srvx.fetch"); const originalFetch = server.options.fetch; server.options.fetch = (request) => { return fetchChannel.tracePromise( @@ -54,17 +57,17 @@ export function tracingPlugin( // Wrap middleware with tracing if (opts.middleware !== false) { - const middlewareChannel = tracingChannel( + const middlewareChannel = tracingChannel( "srvx.middleware", ); const originalMiddleware = server.options.middleware; const wrappedMiddleware: ServerMiddleware[] = originalMiddleware.map( - (middleware, index) => { - const middlewareName = middleware?.name || `middleware#${index}`; + (handler, index) => { + const middleware = Object.freeze({ index, handler }); return (request, next) => { return middlewareChannel.tracePromise( - async () => await middleware(request, next), - { request, server, middlewareName }, + async () => await handler(request, next), + { request, server, middleware }, ); }; }, diff --git a/test/tracing.test.ts b/test/tracing.test.ts index 4a8c97df..83a4245b 100644 --- a/test/tracing.test.ts +++ b/test/tracing.test.ts @@ -51,7 +51,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), - plugins: [tracingPlugin], + plugins: [tracingPlugin()], manual: true, }); @@ -70,16 +70,16 @@ describe("tracing channels", () => { const startHandler = (data: any) => { events.push({ type: "middleware.start", - name: data.name, - index: data.index, + name: data.middleware.handler.name, + index: data.middleware.index, }); }; const endHandler = (data: any) => { events.push({ type: "middleware.end", - name: data.name, - index: data.index, + name: data.middleware.handler.name, + index: data.middleware.index, }); }; @@ -114,7 +114,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware1, middleware2], - plugins: [tracingPlugin], + plugins: [tracingPlugin()], manual: true, }); @@ -182,7 +182,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware], - plugins: [tracingPlugin], + plugins: [tracingPlugin()], manual: true, }); @@ -227,7 +227,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware], - plugins: [tracingPlugin], + plugins: [tracingPlugin()], manual: true, }); @@ -276,7 +276,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware], - plugins: [tracingPlugin], + plugins: [tracingPlugin()], manual: true, port: 3000, }); @@ -287,7 +287,7 @@ describe("tracing channels", () => { expect(capturedData).toBeDefined(); expect(capturedData.request).toBeDefined(); expect(capturedData.server).toBeDefined(); - expect(capturedData.index).toBe(0); + expect(capturedData.middleware.index).toBe(0); expect(capturedData.server.options.port).toBe(3000); }); @@ -297,11 +297,11 @@ describe("tracing channels", () => { const middlewareChannel = tracingChannel("srvx.middleware"); const startHandler = (data: any) => { - events.push({ type: "start", name: data.name }); + events.push({ type: "start", name: data.middleware.handler.name }); }; const endHandler = (data: any) => { - events.push({ type: "end", name: data.name }); + events.push({ type: "end", name: data.middleware.handler.name }); }; middlewareChannel.subscribe({ @@ -334,7 +334,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [mw1, mw2, mw3], - plugins: [tracingPlugin], + plugins: [tracingPlugin()], manual: true, }); @@ -385,7 +385,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), - plugins: [tracingPlugin], + plugins: [tracingPlugin()], manual: true, }); @@ -428,7 +428,7 @@ describe("tracing channels", () => { const server = serve({ fetch: () => new Response("OK"), middleware: [middleware], - plugins: [tracingPlugin], + plugins: [tracingPlugin()], manual: true, }); From 9742f74fa6168db85c6d165cc5e56cb2f53a5d2d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 01:56:34 +0000 Subject: [PATCH 6/6] chore: apply automated updates --- README.md | 1 + docs/1.guide/1.index.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 91492d19..3d8bc609 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ $ bunx --bun srvx | `jsx` | [examples/jsx](https://github.com/h3js/srvx/tree/main/examples/jsx/) | `npx giget gh:h3js/srvx/examples/jsx srvx-jsx` | | `node-handler` | [examples/node-handler](https://github.com/h3js/srvx/tree/main/examples/node-handler/) | `npx giget gh:h3js/srvx/examples/node-handler srvx-node-handler` | | `service-worker` | [examples/service-worker](https://github.com/h3js/srvx/tree/main/examples/service-worker/) | `npx giget gh:h3js/srvx/examples/service-worker srvx-service-worker` | +| `tracing` | [examples/tracing](https://github.com/h3js/srvx/tree/main/examples/tracing/) | `npx giget gh:h3js/srvx/examples/tracing srvx-tracing` | | `websocket` | [examples/websocket](https://github.com/h3js/srvx/tree/main/examples/websocket/) | `npx giget gh:h3js/srvx/examples/websocket srvx-websocket` | diff --git a/docs/1.guide/1.index.md b/docs/1.guide/1.index.md index 3eda0aa9..a22ce65b 100644 --- a/docs/1.guide/1.index.md +++ b/docs/1.guide/1.index.md @@ -98,6 +98,7 @@ bun run server.mjs | `jsx` | [examples/jsx](https://github.com/h3js/srvx/tree/main/examples/jsx/) | `npx giget gh:h3js/srvx/examples/jsx srvx-jsx` | | `node-handler` | [examples/node-handler](https://github.com/h3js/srvx/tree/main/examples/node-handler/) | `npx giget gh:h3js/srvx/examples/node-handler srvx-node-handler` | | `service-worker` | [examples/service-worker](https://github.com/h3js/srvx/tree/main/examples/service-worker/) | `npx giget gh:h3js/srvx/examples/service-worker srvx-service-worker` | +| `tracing` | [examples/tracing](https://github.com/h3js/srvx/tree/main/examples/tracing/) | `npx giget gh:h3js/srvx/examples/tracing srvx-tracing` | | `websocket` | [examples/websocket](https://github.com/h3js/srvx/tree/main/examples/websocket/) | `npx giget gh:h3js/srvx/examples/websocket srvx-websocket` |