Skip to content

feat(instrumentation-hapi): support migration to stable HTTP semconv, v1.23.1 #2863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
24 changes: 17 additions & 7 deletions plugins/node/opentelemetry-instrumentation-hapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ hapiInstrumentation.setTracerProvider(provider);

See [examples/hapi](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/examples/hapi) for a short example using Hapi

<!--
<!--
The dev dependency of `@hapi/podium@4.1.1` is required to force the compatible type declarations. See: https://github.com/hapijs/hapi/issues/4240
-->

Expand All @@ -70,14 +70,24 @@ This package provides automatic tracing for hapi server routes and [request life

## Semantic Conventions

This package uses `@opentelemetry/semantic-conventions` version `1.22+`, which implements Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md)
Prior to version `0.48.0`, this instrumentation created spans targeting an experimental semantic convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md).

Attributes collected:
HTTP semantic conventions (semconv) were stabilized in v1.23.0, and a [migration process](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/non-normative/http-migration.md#http-semantic-convention-stability-migration) was defined. `instrumentation-hapi` versions 0.48.0 and later include support for migrating to stable HTTP semantic conventions, as described below. The intent is to provide an approximate 6 month time window for users of this instrumentation to migrate to the new HTTP semconv, after which a new minor version will use the *new* semconv by default and drop support for the old semconv. See the [HTTP semconv migration plan for OpenTelemetry JS instrumentations](https://github.com/open-telemetry/opentelemetry-js/issues/5646).

| Attribute | Short Description |
|---------------------|----------------------------------------------------|
| `http.method` | HTTP method |
| `http.route` | Route assigned to handler. Ex: `/users/:id` |
To select which semconv version(s) is emitted from this instrumentation, use the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable.

- `http`: emit the new (stable) v1.23.0+ semantics
- `http/dup`: emit **both** the old v1.7.0 and the new (stable) v1.23.0+ semantics
- By default, if `OTEL_SEMCONV_STABILITY_OPT_IN` includes neither of the above tokens, the old v1.7.0 semconv is used.

### Attributes collected

The following semconv attributes are collected on hapi route spans:

| v1.7.0 semconv | v1.23.0 semconv | Notes |
| -------------- | --------------------- | ----- |
| `http.method` | `http.request.method` | HTTP request method |
| `http.route` | `http.route` (same) | Route assigned to handler. Ex: `/users/:id` |

## Useful links

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
InstrumentationConfig,
InstrumentationNodeModuleDefinition,
isWrapped,
SemconvStability,
semconvStabilityFromStr,
} from '@opentelemetry/instrumentation';

import type * as Hapi from '@hapi/hapi';
Expand Down Expand Up @@ -50,8 +52,14 @@ import {

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

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

protected init() {
Expand Down Expand Up @@ -387,7 +395,11 @@ export class HapiInstrumentation extends InstrumentationBase {
if (rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = route.path;
}
const metadata = getRouteMetadata(route, pluginName);
const metadata = getRouteMetadata(
route,
instrumentation._semconvStability,
pluginName
);
const span = instrumentation.tracer.startSpan(metadata.name, {
attributes: metadata.attributes,
});
Expand Down
34 changes: 34 additions & 0 deletions plugins/node/opentelemetry-instrumentation-hapi/src/semconv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/*
* This file contains a copy of unstable semantic convention definitions
* used by this package.
* @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
*/

/**
* Deprecated, use `http.request.method` instead.
*
* @example GET
* @example POST
* @example HEAD
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `http.request.method`.
*/
export const ATTR_HTTP_METHOD = 'http.method' as const;
54 changes: 32 additions & 22 deletions plugins/node/opentelemetry-instrumentation-hapi/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
* limitations under the License.
*/

import { SpanAttributes } from '@opentelemetry/api';
import { Attributes } from '@opentelemetry/api';
import {
SEMATTRS_HTTP_METHOD,
SEMATTRS_HTTP_ROUTE,
ATTR_HTTP_ROUTE,
ATTR_HTTP_REQUEST_METHOD,
} from '@opentelemetry/semantic-conventions';
import { ATTR_HTTP_METHOD } from './semconv';
import type * as Hapi from '@hapi/hapi';
import {
HapiLayerType,
Expand All @@ -28,6 +29,7 @@ import {
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 @@ -72,37 +74,45 @@ export const isPatchableExtMethod = (

export const getRouteMetadata = (
route: Hapi.ServerRoute,
semconvStability: SemconvStability,
pluginName?: string
): {
attributes: SpanAttributes;
attributes: Attributes;
name: string;
} => {
const attributes: Attributes = {
[ATTR_HTTP_ROUTE]: route.path,
};
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) {
return {
attributes: {
[SEMATTRS_HTTP_ROUTE]: route.path,
[SEMATTRS_HTTP_METHOD]: route.method,
[AttributeNames.HAPI_TYPE]: HapiLayerType.PLUGIN,
[AttributeNames.PLUGIN_NAME]: pluginName,
},
name: `${pluginName}: route - ${route.path}`,
};
attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.PLUGIN;
attributes[AttributeNames.PLUGIN_NAME] = pluginName;
name = `${pluginName}: route - ${route.path}`;
} else {
attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.ROUTER;
name = `route - ${route.path}`;
}
return {
attributes: {
[SEMATTRS_HTTP_ROUTE]: route.path,
[SEMATTRS_HTTP_METHOD]: route.method,
[AttributeNames.HAPI_TYPE]: HapiLayerType.ROUTER,
},
name: `route - ${route.path}`,
};

return { attributes, name };
};

export const getExtMetadata = (
extPoint: Hapi.ServerRequestExtType,
pluginName?: string
): {
attributes: SpanAttributes;
attributes: Attributes;
name: string;
} => {
if (pluginName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const { createTestNodeSdk } = require('@opentelemetry/contrib-test-utils');

const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { HapiInstrumentation } = require('../../build/src/index.js');

const sdk = createTestNodeSdk({
serviceName: 'use-hapi',
instrumentations: [
new HttpInstrumentation(),
new HapiInstrumentation()
]
})
sdk.start();

const Hapi = require('@hapi/hapi');
const http = require('http');

async function main() {
// Start a Hapi server.
const server = new Hapi.Server({
port: 0,
host: 'localhost'
});

server.route({
method: 'GET',
path: '/route/{param}',
handler: function() {
return { hello: 'world' };
}
});

await server.start();

// Make a single request to it.
await new Promise(resolve => {
http.get(`http://${server.info.host}:${server.info.port}/route/test`, (res) => {
res.resume();
res.on('end', () => {
resolve();
});
});
});

await server.stop();
await sdk.shutdown();
}

main();
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ await new Promise(resolve => {
res.on('end', () => {
resolve();
});
})
});
});

await server.stop();
Expand Down
100 changes: 100 additions & 0 deletions plugins/node/opentelemetry-instrumentation-hapi/test/hapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import {
runTestFixture,
TestCollector,
TestSpan,
} from '@opentelemetry/contrib-test-utils';
import { getPlugin } from './plugin';
const plugin = getPlugin();
Expand All @@ -33,6 +34,16 @@ import * as assert from 'assert';
import * as hapi from '@hapi/hapi';
import { HapiLayerType } from '../src/internal-types';
import { AttributeNames } from '../src/enums/AttributeNames';
import { ATTR_HTTP_REQUEST_METHOD } from '@opentelemetry/semantic-conventions';
import { ATTR_HTTP_METHOD } from '../src/semconv';

function getTestSpanAttr(span: TestSpan, name: string) {
const kv = span.attributes.filter(a => a.key === name)[0];
if (kv) {
return kv.value;
}
return undefined;
}

describe('Hapi Instrumentation - Core Tests', () => {
const memoryExporter = new InMemorySpanExporter();
Expand Down Expand Up @@ -582,4 +593,93 @@ describe('Hapi Instrumentation - Core Tests', () => {
});
});
});

describe('HTTP semconv migration', () => {
it('should emit only old HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN unset', async () => {
await runTestFixture({
cwd: __dirname,
argv: ['fixtures/use-hapi.js'],
env: {
OTEL_SEMCONV_STABILITY_OPT_IN: '',
},
checkResult: (err, stdout, stderr) => {
assert.ifError(err);
},
checkCollector: (collector: TestCollector) => {
const spans = collector.sortedSpans;
assert.equal(spans.length, 3);
const hapiSpan = spans[2];
assert.equal(
hapiSpan.instrumentationScope.name,
'@opentelemetry/instrumentation-hapi'
);
assert.equal(
getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD)?.stringValue,
'GET'
);
assert.equal(
getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD),
undefined
);
},
});
});

it('should emit only stable HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN=http', async () => {
await runTestFixture({
cwd: __dirname,
argv: ['fixtures/use-hapi.js'],
env: {
OTEL_SEMCONV_STABILITY_OPT_IN: 'http',
},
checkResult: (err, stdout, stderr) => {
assert.ifError(err);
},
checkCollector: (collector: TestCollector) => {
const spans = collector.sortedSpans;
assert.equal(spans.length, 3);
const hapiSpan = spans[2];
assert.equal(
hapiSpan.instrumentationScope.name,
'@opentelemetry/instrumentation-hapi'
);
assert.equal(getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD), undefined);
assert.equal(
getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD)?.stringValue,
'GET'
);
},
});
});

it('should emit both old and stable HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN=http/dup', async () => {
await runTestFixture({
cwd: __dirname,
argv: ['fixtures/use-hapi.js'],
env: {
OTEL_SEMCONV_STABILITY_OPT_IN: 'http/dup',
},
checkResult: (err, stdout, stderr) => {
assert.ifError(err);
},
checkCollector: (collector: TestCollector) => {
const spans = collector.sortedSpans;
assert.equal(spans.length, 3);
const hapiSpan = spans[2];
assert.equal(
hapiSpan.instrumentationScope.name,
'@opentelemetry/instrumentation-hapi'
);
assert.equal(
getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD)?.stringValue,
'GET'
);
assert.equal(
getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD)?.stringValue,
'GET'
);
},
});
});
});
});