|
| 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 | +} |
0 commit comments