Skip to content

Commit d6b5f2a

Browse files
authored
feat: experimental tracing support (#1251)
1 parent eb83aad commit d6b5f2a

4 files changed

Lines changed: 1373 additions & 1 deletion

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"./cloudflare": "./dist/_entries/cloudflare.mjs",
2323
"./service-worker": "./dist/_entries/service-worker.mjs",
2424
"./node": "./dist/_entries/node.mjs",
25-
"./generic": "./dist/_entries/generic.mjs"
25+
"./generic": "./dist/_entries/generic.mjs",
26+
"./tracing": "./dist/tracing.mjs"
2627
},
2728
"types": "./dist/_entries/generic.d.mts",
2829
"files": [

src/tracing.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import type { H3Event } from "./event.ts";
2+
import type { H3, H3Core } from "./h3.ts";
3+
import {
4+
type H3Plugin,
5+
type H3Route,
6+
type MiddlewareOptions,
7+
} from "./types/h3.ts";
8+
import type { EventHandler, Middleware } from "./types/handler.ts";
9+
10+
/**
11+
* Payload sent to the tracing channels.
12+
*/
13+
export interface TracingRequestEvent {
14+
type: "middleware" | "route";
15+
event: H3Event;
16+
}
17+
18+
type MaybeTracedMiddleware = Middleware & { __traced__?: boolean };
19+
type MaybeTracedEventHandler = EventHandler & { __traced__?: boolean };
20+
21+
/**
22+
* Options for the tracing plugin.
23+
*/
24+
export interface TracingPluginOptions {
25+
/**
26+
* Whether to trace middleware executions.
27+
*/
28+
traceMiddleware?: boolean;
29+
/**
30+
* Whether to trace route executions.
31+
*/
32+
traceRoutes?: boolean;
33+
}
34+
35+
/**
36+
* Enables tracing for H3 apps.
37+
*/
38+
export function tracingPlugin(traceOpts?: TracingPluginOptions): H3Plugin {
39+
return (h3: H3 | H3Core) => {
40+
const { tracingChannel } =
41+
globalThis.process?.getBuiltinModule?.("diagnostics_channel") ?? {};
42+
43+
// If tracingChannel is not available, then we can't trace request handlers
44+
if (!tracingChannel) {
45+
return;
46+
}
47+
48+
const requestHandlerChannel = tracingChannel("h3.fetch");
49+
50+
function wrapMiddleware(middleware: MaybeTracedMiddleware): Middleware {
51+
if (middleware.__traced__ || traceOpts?.traceMiddleware === false) {
52+
return middleware;
53+
}
54+
55+
const wrappedMiddleware: MaybeTracedMiddleware = (...args) => {
56+
return requestHandlerChannel.tracePromise(
57+
async () => middleware(...args),
58+
{
59+
event: args[0],
60+
type: "middleware",
61+
} satisfies TracingRequestEvent,
62+
);
63+
};
64+
wrappedMiddleware.__traced__ = true;
65+
66+
return wrappedMiddleware;
67+
}
68+
69+
function wrapEventHandler(handler: MaybeTracedEventHandler): EventHandler {
70+
if (handler.__traced__ || traceOpts?.traceRoutes === false) {
71+
return handler;
72+
}
73+
74+
const wrappedHandler: MaybeTracedEventHandler = (...args) => {
75+
return requestHandlerChannel.tracePromise(
76+
async () => handler(...args),
77+
{
78+
event: args[0],
79+
type: "route",
80+
} satisfies TracingRequestEvent,
81+
);
82+
};
83+
wrappedHandler.__traced__ = true;
84+
85+
return wrappedHandler;
86+
}
87+
88+
h3["~middleware"] = h3["~middleware"].map((m) => wrapMiddleware(m));
89+
h3["~routes"] = h3["~routes"].map((route) => {
90+
return {
91+
...route,
92+
handler: wrapEventHandler(route.handler),
93+
middleware: route.middleware
94+
? route.middleware.map((m) => wrapMiddleware(m))
95+
: undefined,
96+
} satisfies H3Route;
97+
});
98+
99+
if ("on" in h3 && typeof h3.on === "function") {
100+
const originalOn = h3.on;
101+
102+
h3.on = (...args) => {
103+
const instance = originalOn.apply(h3, args);
104+
// Since it uses route push, we can wrap the last route handler added
105+
// Wrapping the handler at the arg level is problematic because we need the `event` to be passed to the tracePromise.
106+
// Which is only available with `toEventHandler` and it is already called in the `on` method.
107+
// eslint-disable-next-line unicorn/prefer-at
108+
const lastRoute = instance["~routes"][instance["~routes"].length - 1];
109+
if (lastRoute) {
110+
lastRoute.handler = wrapEventHandler(lastRoute.handler);
111+
lastRoute.middleware = lastRoute.middleware?.map((m) =>
112+
wrapMiddleware(m),
113+
);
114+
}
115+
116+
return instance;
117+
};
118+
}
119+
120+
if ("use" in h3 && typeof h3.use === "function") {
121+
const originalUse = h3.use;
122+
h3.use = (arg1: unknown, arg2?: unknown, arg3?: unknown) => {
123+
// Middlewares should be wrapped at the arg level to avoid creating trace events for skipped wrappers added by h3
124+
let route: string | undefined;
125+
let fn: Middleware;
126+
let opts: MiddlewareOptions | undefined;
127+
128+
if (typeof arg1 === "string") {
129+
route = arg1 as string;
130+
fn = arg2 as Middleware;
131+
opts = arg3 as MiddlewareOptions;
132+
133+
// @ts-expect-error - call not accepting the route signature
134+
return originalUse.call(h3, route, wrapMiddleware(fn), opts);
135+
}
136+
137+
fn = arg1 as Middleware;
138+
opts = arg2 as MiddlewareOptions;
139+
140+
return originalUse.call(h3, wrapMiddleware(fn), opts);
141+
};
142+
}
143+
144+
if ("mount" in h3 && typeof h3.mount === "function") {
145+
const originalMount = h3.mount;
146+
h3.mount = (base, input) => {
147+
// If the input is an H3 instance
148+
// then we can register the tracing plugin on it to propagate the tracing to the nested app
149+
if ("register" in input) {
150+
input.register(tracingPlugin(traceOpts));
151+
}
152+
153+
return originalMount.call(h3, base, input);
154+
};
155+
}
156+
157+
return h3;
158+
};
159+
}

test/_setup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
NodeHandler,
66
HTTPError,
77
} from "../src/index.ts";
8+
import { tracingPlugin } from "../src/tracing.ts";
89
import { Server as NodeServer } from "node:http";
910
import { getRandomPort } from "get-port-please";
1011
import {
@@ -156,6 +157,7 @@ function setupBaseTest(
156157
onError: ctx.hooks.onError,
157158
onRequest: ctx.hooks.onRequest,
158159
onResponse: ctx.hooks.onResponse,
160+
plugins: [opts.tracing ? tracingPlugin() : undefined].filter((p) => !!p),
159161
});
160162
});
161163

@@ -184,6 +186,7 @@ export interface TestOptions {
184186
allowUnhandledErrors?: boolean;
185187
startServer?: boolean;
186188
debug?: boolean;
189+
tracing?: boolean;
187190
}
188191

189192
export interface TestContext {

0 commit comments

Comments
 (0)