Skip to content

Commit 6f47778

Browse files
committed
feature(node): Add instrumentation to the handler in Hono
1 parent 7dbaa94 commit 6f47778

File tree

2 files changed

+167
-7
lines changed

2 files changed

+167
-7
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const AttributeNames = {
2+
HONO_TYPE: 'hono.type',
3+
HONO_NAME: 'hono.name',
4+
} as const;
5+
6+
export type AttributeNames = (typeof AttributeNames)[keyof typeof AttributeNames];
7+
8+
export const HonoTypes = {
9+
MIDDLEWARE: 'middleware',
10+
REQUEST_HANDLER: 'request_handler',
11+
} as const;
12+
13+
export type HonoTypes = (typeof HonoTypes)[keyof typeof HonoTypes];

packages/node/src/integrations/tracing/hono/instrumentation.ts

Lines changed: 154 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
import type { Span } from '@opentelemetry/api';
2+
import { context, SpanStatusCode, trace } from '@opentelemetry/api';
13
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
2-
import type { HandlerInterface, Hono, HonoInstance, MiddlewareHandlerInterface, OnHandlerInterface } from './types';
4+
import { AttributeNames, HonoTypes } from './constants';
5+
import type {
6+
Context,
7+
Handler,
8+
HandlerInterface,
9+
Hono,
10+
HonoInstance,
11+
MiddlewareHandler,
12+
MiddlewareHandlerInterface,
13+
Next,
14+
OnHandlerInterface,
15+
} from './types';
316

417
const PACKAGE_NAME = '@sentry/instrumentation-hono';
518
const PACKAGE_VERSION = '0.0.1';
@@ -50,10 +63,38 @@ export class HonoInstrumentation extends InstrumentationBase {
5063
* Patches the route handler to instrument it.
5164
*/
5265
private _patchHandler(): (original: HandlerInterface) => HandlerInterface {
66+
// eslint-disable-next-line @typescript-eslint/no-this-alias
67+
const instrumentation = this;
68+
5369
return function (original: HandlerInterface) {
5470
return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
55-
// TODO: Add OpenTelemetry tracing logic here
56-
return original.apply(this, args);
71+
if (typeof args[0] === 'string') {
72+
const path = args[0];
73+
if (args.length === 1) {
74+
return original.apply(this, [path]);
75+
}
76+
77+
const handlers = args.slice(1);
78+
return original.apply(this, [
79+
path,
80+
...handlers.map((handler, index) =>
81+
instrumentation._wrapHandler(
82+
index + 1 === handlers.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE,
83+
handler as Handler | MiddlewareHandler,
84+
),
85+
),
86+
]);
87+
}
88+
89+
return original.apply(
90+
this,
91+
args.map((handler, index) =>
92+
instrumentation._wrapHandler(
93+
index + 1 === args.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE,
94+
handler as Handler | MiddlewareHandler,
95+
),
96+
),
97+
);
5798
};
5899
};
59100
}
@@ -62,10 +103,21 @@ export class HonoInstrumentation extends InstrumentationBase {
62103
* Patches the 'on' handler to instrument it.
63104
*/
64105
private _patchOnHandler(): (original: OnHandlerInterface) => OnHandlerInterface {
106+
// eslint-disable-next-line @typescript-eslint/no-this-alias
107+
const instrumentation = this;
108+
65109
return function (original: OnHandlerInterface) {
66110
return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
67-
// TODO: Add OpenTelemetry tracing logic here
68-
return original.apply(this, args);
111+
const handlers = args.slice(2);
112+
return original.apply(this, [
113+
...args.slice(0, 2),
114+
...handlers.map((handler, index) =>
115+
instrumentation._wrapHandler(
116+
index + 1 === handlers.length ? HonoTypes.REQUEST_HANDLER : HonoTypes.MIDDLEWARE,
117+
handler as Handler | MiddlewareHandler,
118+
),
119+
),
120+
]);
69121
};
70122
};
71123
}
@@ -74,11 +126,106 @@ export class HonoInstrumentation extends InstrumentationBase {
74126
* Patches the middleware handler to instrument it.
75127
*/
76128
private _patchMiddlewareHandler(): (original: MiddlewareHandlerInterface) => MiddlewareHandlerInterface {
129+
// eslint-disable-next-line @typescript-eslint/no-this-alias
130+
const instrumentation = this;
131+
77132
return function (original: MiddlewareHandlerInterface) {
78133
return function wrappedHandler(this: HonoInstance, ...args: unknown[]) {
79-
// TODO: Add OpenTelemetry tracing logic here
80-
return original.apply(this, args);
134+
if (typeof args[0] === 'string') {
135+
const path = args[0];
136+
if (args.length === 1) {
137+
return original.apply(this, [path]);
138+
}
139+
140+
const handlers = args.slice(1);
141+
return original.apply(this, [
142+
path,
143+
...handlers.map(handler =>
144+
instrumentation._wrapHandler(HonoTypes.MIDDLEWARE, handler as MiddlewareHandler),
145+
),
146+
]);
147+
}
148+
149+
return original.apply(
150+
this,
151+
args.map(handler => instrumentation._wrapHandler(HonoTypes.MIDDLEWARE, handler as MiddlewareHandler)),
152+
);
81153
};
82154
};
83155
}
156+
157+
/**
158+
* Wraps a handler or middleware handler to apply instrumentation.
159+
*/
160+
private _wrapHandler(type: HonoTypes, handler: Handler | MiddlewareHandler): Handler | MiddlewareHandler {
161+
// eslint-disable-next-line @typescript-eslint/no-this-alias
162+
const instrumentation = this;
163+
164+
return function (this: unknown, c: Context, next: Next) {
165+
if (!instrumentation.isEnabled()) {
166+
return handler.apply(this, [c, next]);
167+
}
168+
169+
const path = c.req.path;
170+
const spanName = `${type.replace('_', ' ')} - ${path}`;
171+
const span = instrumentation.tracer.startSpan(spanName, {
172+
attributes: {
173+
[AttributeNames.HONO_TYPE]: type,
174+
[AttributeNames.HONO_NAME]: type === 'request_handler' ? path : handler.name || 'anonymous',
175+
},
176+
});
177+
178+
return context.with(trace.setSpan(context.active(), span), () => {
179+
return instrumentation._safeExecute(
180+
() => handler.apply(this, [c, next]),
181+
() => span.end(),
182+
error => {
183+
instrumentation._handleError(span, error);
184+
span.end();
185+
},
186+
);
187+
});
188+
};
189+
}
190+
191+
/**
192+
* Safely executes a function and handles errors.
193+
*/
194+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
195+
private _safeExecute(execute: () => any, onSuccess: () => void, onFailure: (error: unknown) => void): () => any {
196+
try {
197+
const result = execute();
198+
199+
if (
200+
result &&
201+
typeof result === 'object' &&
202+
typeof Object.getOwnPropertyDescriptor(result, 'then')?.value === 'function'
203+
) {
204+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
205+
result.then(
206+
() => onSuccess(),
207+
(error: unknown) => onFailure(error),
208+
);
209+
}
210+
211+
onSuccess();
212+
return result;
213+
} catch (error: unknown) {
214+
onFailure(error);
215+
throw error;
216+
}
217+
}
218+
219+
/**
220+
* Handles errors by setting the span status and recording the exception.
221+
*/
222+
private _handleError(span: Span, error: unknown): void {
223+
if (error instanceof Error) {
224+
span.setStatus({
225+
code: SpanStatusCode.ERROR,
226+
message: error.message,
227+
});
228+
span.recordException(error);
229+
}
230+
}
84231
}

0 commit comments

Comments
 (0)