Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ const run = async () => {
},
});

// Route registered via a plugin produces a `plugin.hapi` span.
await server.register({
name: 'testPlugin',
version: '1.0.0',
register: async function (pluginServer) {
pluginServer.route({
method: 'GET',
path: '/plugin-route',
handler: () => 'Hello from plugin!',
});
},
});

// Server extension produces a `server.ext.hapi` span.
server.ext('onPreResponse', (request, h) => h.continue);

await Sentry.setupHapiErrorHandler(server);
await server.start();

Expand Down
37 changes: 37 additions & 0 deletions dev-packages/node-integration-tests/suites/tracing/hapi/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,43 @@ describe('hapi auto-instrumentation', () => {
await runner.completed();
});

test('should instrument plugin routes and server extensions.', async () => {
const runner = createRunner()
.expect({
transaction: {
transaction: 'GET /plugin-route',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'GET /plugin-route',
op: 'plugin.hapi',
origin: 'auto.http.otel.hapi',
data: expect.objectContaining({
'http.route': '/plugin-route',
'hapi.type': 'plugin',
'hapi.plugin.name': 'testPlugin',
'sentry.op': 'plugin.hapi',
'sentry.origin': 'auto.http.otel.hapi',
}),
}),
expect.objectContaining({
description: 'ext - onPreResponse',
op: 'server.ext.hapi',
origin: 'auto.http.otel.hapi',
data: expect.objectContaining({
'hapi.type': 'server.ext',
'server.ext.type': 'onPreResponse',
'sentry.op': 'server.ext.hapi',
'sentry.origin': 'auto.http.otel.hapi',
}),
}),
]),
},
})
.start();
runner.makeRequest('get', '/plugin-route');
await runner.completed();
});

test('should handle returned plain errors in routes.', async () => {
const runner = createRunner()
.expect({
Expand Down
34 changes: 1 addition & 33 deletions packages/node/src/integrations/tracing/hapi/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { HapiInstrumentation } from './vendored/instrumentation';
import type { IntegrationFn, Span } from '@sentry/core';
import type { IntegrationFn } from '@sentry/core';
import {
captureException,
debug,
defineIntegration,
getClient,
getDefaultIsolationScope,
getIsolationScope,
SDK_VERSION,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
} from '@sentry/core';
import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
import { DEBUG_BUILD } from '../../../debug-build';
Expand Down Expand Up @@ -108,33 +104,5 @@ export const hapiErrorPlugin = {
*/
export async function setupHapiErrorHandler(server: Server): Promise<void> {
await server.register(hapiErrorPlugin);

// Sadly, middleware spans do not go through `requestHook`, so we handle those here
// We register this hook in this method, because if we register it in the integration `setup`,
// it would always run even for users that are not even using hapi
const client = getClient();
if (client) {
client.on('spanStart', span => {
addHapiSpanAttributes(span);
});
}

ensureIsWrapped(server.register, 'hapi');
}

function addHapiSpanAttributes(span: Span): void {
const attributes = spanToJSON(span).data;

// this is one of: router, plugin, server.ext
const type = attributes['hapi.type'];

// If this is already set, or we have no Hapi span, no need to process again...
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
return;
}

span.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hapi',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.hapi`,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,27 @@
* - Upstream version: @opentelemetry/instrumentation-hapi@0.64.0
* - Types vendored from @hapi/hapi as simplified interfaces
* - Minor TypeScript strictness adjustments for this repository's compiler settings
* - Span creation migrated to the @sentry/core API; op/origin folded into span creation
*/
/* eslint-disable */

import * as api from '@opentelemetry/api';
import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute';
import {
InstrumentationBase,
InstrumentationConfig,
InstrumentationNodeModuleDefinition,
isWrapped,
SemconvStability,
semconvStabilityFromStr,
} from '@opentelemetry/instrumentation';
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation';

import type * as Hapi from './hapi-types';
import { SDK_VERSION } from '@sentry/core';
import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import { AttributeNames } from './enums/AttributeNames';
import {
HapiComponentName,
HapiServerRouteInput,
handlerPatched,
PatchableServerRoute,
HapiServerRouteInputMethod,
HapiPluginInput,
RegisterFunction,
PatchableExtMethod,
ServerExtDirectInput,
type HapiServerRouteInput,
type PatchableServerRoute,
type HapiServerRouteInputMethod,
type HapiPluginInput,
type RegisterFunction,
type PatchableExtMethod,
type ServerExtDirectInput,
} from './internal-types';
import {
getRouteMetadata,
Expand All @@ -60,11 +55,8 @@ const PACKAGE_NAME = '@sentry/instrumentation-hapi';

/** Hapi instrumentation for OpenTelemetry */
export class HapiInstrumentation extends InstrumentationBase {
private _semconvStability: SemconvStability;

constructor(config: InstrumentationConfig = {}) {
super(PACKAGE_NAME, SDK_VERSION, config);
this._semconvStability = semconvStabilityFromStr('http', process.env.OTEL_SEMCONV_STABILITY_OPT_IN);
}

protected init() {
Expand All @@ -74,11 +66,11 @@ export class HapiInstrumentation extends InstrumentationBase {
(module: any) => {
const moduleExports: typeof Hapi = module[Symbol.toStringTag] === 'Module' ? module.default : module;
if (!isWrapped(moduleExports.server)) {
this._wrap(moduleExports, 'server', this._getServerPatch.bind(this) as any);
this._wrap(moduleExports, 'server', this._getServerPatch.bind(this));
}

if (!isWrapped(moduleExports.Server)) {
this._wrap(moduleExports, 'Server', this._getServerPatch.bind(this) as any);
this._wrap(moduleExports, 'Server', this._getServerPatch.bind(this));
}
return moduleExports;
},
Expand Down Expand Up @@ -115,7 +107,7 @@ export class HapiInstrumentation extends InstrumentationBase {

// Casting as any is necessary here due to multiple overloads on the Hapi.Server.register
// function, which requires supporting a variety of different types of Plugin inputs
self._wrap(newServer, 'register', instrumentation._getServerRegisterPatch.bind(instrumentation) as any);
self._wrap(newServer, 'register', instrumentation._getServerRegisterPatch.bind(instrumentation));
return newServer;
};
}
Expand Down Expand Up @@ -202,6 +194,7 @@ export class HapiInstrumentation extends InstrumentationBase {
route[i] = newRoute;
}
} else {
// oxlint-disable-next-line no-param-reassign
route = instrumentation._wrapRouteHandler.call(instrumentation, route, pluginName);
}
return original.apply(this, [route]);
Expand Down Expand Up @@ -254,38 +247,29 @@ export class HapiInstrumentation extends InstrumentationBase {
const instrumentation: HapiInstrumentation = this;
if (method instanceof Array) {
for (let i = 0; i < method.length; i++) {
method[i] = instrumentation._wrapExtMethods(method[i]!, extPoint) as PatchableExtMethod;
method[i] = instrumentation._wrapExtMethods(method[i]!, extPoint);
}
return method;
} else if (isPatchableExtMethod(method)) {
if (method[handlerPatched] === true) return method;
method[handlerPatched] = true;

const newHandler: PatchableExtMethod = async function (this: any, ...params: Parameters<Hapi.Lifecycle.Method>) {
const newHandler: PatchableExtMethod = function (this: any, ...params: Parameters<Hapi.Lifecycle.Method>) {
if (api.trace.getSpan(api.context.active()) === undefined) {
return await method.apply(this, params);
return method.apply(this, params);
}
const metadata = getExtMetadata(extPoint, pluginName, method.name);
const span = instrumentation.tracer.startSpan(metadata.name, {
attributes: metadata.attributes,
});
try {
return await api.context.with<Parameters<Hapi.Lifecycle.Method>, Hapi.Lifecycle.Method>(
api.trace.setSpan(api.context.active(), span),
method,
undefined,
...params,
);
} catch (err: any) {
span.recordException(err);
span.setStatus({
code: api.SpanStatusCode.ERROR,
message: err.message,
});
throw err;
} finally {
span.end();
}
return startSpan(
{
name: metadata.name,
op: `${metadata.attributes[AttributeNames.HAPI_TYPE]}.hapi`,
attributes: {
...metadata.attributes,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hapi',
},
},
() => method.apply(undefined, params),
);
};
return newHandler as T;
}
Expand All @@ -300,34 +284,27 @@ export class HapiInstrumentation extends InstrumentationBase {
* for adding this server route. Else, signifies that the route was added directly
*/
private _wrapRouteHandler(route: PatchableServerRoute, pluginName?: string): PatchableServerRoute {
const instrumentation: HapiInstrumentation = this;
if (route[handlerPatched] === true) return route;
route[handlerPatched] = true;

const wrapHandler: (oldHandler: Hapi.Lifecycle.Method) => Hapi.Lifecycle.Method = oldHandler => {
return async function (this: any, ...params: Parameters<Hapi.Lifecycle.Method>) {
return function (this: any, ...params: Parameters<Hapi.Lifecycle.Method>) {
if (api.trace.getSpan(api.context.active()) === undefined) {
return await oldHandler.call(this, ...params);
return oldHandler.call(this, ...params);
}
setHttpServerSpanRouteAttribute(route.path);
const metadata = getRouteMetadata(route, instrumentation._semconvStability, pluginName);
const span = instrumentation.tracer.startSpan(metadata.name, {
attributes: metadata.attributes,
});
try {
return await api.context.with(api.trace.setSpan(api.context.active(), span), () =>
oldHandler.call(this, ...params),
);
} catch (err: any) {
span.recordException(err);
span.setStatus({
code: api.SpanStatusCode.ERROR,
message: err.message,
});
throw err;
} finally {
span.end();
}
const metadata = getRouteMetadata(route, pluginName);
return startSpan(
{
name: metadata.name,
op: `${metadata.attributes[AttributeNames.HAPI_TYPE]}.hapi`,
attributes: {
...metadata.attributes,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.hapi',
},
},
() => oldHandler.call(this, ...params),
);
};
};

Expand Down
25 changes: 6 additions & 19 deletions packages/node/src/integrations/tracing/hapi/vendored/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,19 @@
* - Upstream version: @opentelemetry/instrumentation-hapi@0.64.0
* - Types vendored from @hapi/hapi as simplified interfaces
*/
/* eslint-disable */

import { Attributes } from '@opentelemetry/api';
import { ATTR_HTTP_ROUTE, ATTR_HTTP_REQUEST_METHOD } from '@opentelemetry/semantic-conventions';
import type { Attributes } from '@opentelemetry/api';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Should we already move this to the sentry conventions?

import { ATTR_HTTP_METHOD } from './semconv';
import type * as Hapi from './hapi-types';
import {
HapiLayerType,
HapiLifecycleMethodNames,
HapiPluginObject,
PatchableExtMethod,
ServerExtDirectInput,
type HapiPluginObject,
type PatchableExtMethod,
type ServerExtDirectInput,
} from './internal-types';
import { AttributeNames } from './enums/AttributeNames';
import { SemconvStability } from '@opentelemetry/instrumentation';

export function getPluginName<T>(plugin: Hapi.Plugin<T>): string {
if ((plugin as Hapi.PluginNameVersion).name) {
Expand Down Expand Up @@ -70,26 +68,15 @@ export const isPatchableExtMethod = (

export const getRouteMetadata = (
route: Hapi.ServerRoute,
semconvStability: SemconvStability,
pluginName?: string,
): {
attributes: Attributes;
name: string;
} => {
const attributes: Attributes = {
[ATTR_HTTP_ROUTE]: route.path,
[ATTR_HTTP_METHOD]: route.method,
};
if (semconvStability & SemconvStability.OLD) {
attributes[ATTR_HTTP_METHOD] = route.method;
}
if (semconvStability & SemconvStability.STABLE) {
// Note: This currently does *not* normalize the method name to uppercase
// and conditionally include `http.request.method.original` as described
// at https://opentelemetry.io/docs/specs/semconv/http/http-spans/
// These attributes are for a *hapi* span, and not the parent HTTP span,
// so the HTTP span guidance doesn't strictly apply.
attributes[ATTR_HTTP_REQUEST_METHOD] = route.method;
}

let name;
if (pluginName) {
Expand Down
Loading
Loading