Skip to content

Commit 2317e2f

Browse files
authored
feat(instrumentation-hapi): support migration to stable HTTP semconv, v1.23.1 (#2863)
`instrumentation-hapi`, in addition to providing the HTTP route for the *HTTP* instrumentation, also generates hapi spans for each plugin/route. Attributes on these hapi spans include some covered by HTTP semantic conventions. This change adds support for controlled migration from the old to the stable HTTP semconv via the `OTEL_SEMCONV_STABILITY_OPT_IN` envvar. See https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/ This also updates from the deprecated `SpanAttributes` api type to `Attributes`. Refs: open-telemetry/opentelemetry-js#5663 (HTTP semconv) Refs: open-telemetry/opentelemetry-js#4175 (Attributes)
1 parent 7481f71 commit 2317e2f

File tree

7 files changed

+262
-31
lines changed

7 files changed

+262
-31
lines changed

plugins/node/opentelemetry-instrumentation-hapi/README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ hapiInstrumentation.setTracerProvider(provider);
6060

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

63-
<!--
63+
<!--
6464
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
6565
-->
6666

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

7171
## Semantic Conventions
7272

73-
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)
73+
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).
7474

75-
Attributes collected:
75+
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).
7676

77-
| Attribute | Short Description |
78-
|---------------------|----------------------------------------------------|
79-
| `http.method` | HTTP method |
80-
| `http.route` | Route assigned to handler. Ex: `/users/:id` |
77+
To select which semconv version(s) is emitted from this instrumentation, use the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable.
78+
79+
- `http`: emit the new (stable) v1.23.0+ semantics
80+
- `http/dup`: emit **both** the old v1.7.0 and the new (stable) v1.23.0+ semantics
81+
- By default, if `OTEL_SEMCONV_STABILITY_OPT_IN` includes neither of the above tokens, the old v1.7.0 semconv is used.
82+
83+
### Attributes collected
84+
85+
The following semconv attributes are collected on hapi route spans:
86+
87+
| v1.7.0 semconv | v1.23.0 semconv | Notes |
88+
| -------------- | --------------------- | ----- |
89+
| `http.method` | `http.request.method` | HTTP request method |
90+
| `http.route` | `http.route` (same) | Route assigned to handler. Ex: `/users/:id` |
8191

8292
## Useful links
8393

plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
InstrumentationConfig,
2222
InstrumentationNodeModuleDefinition,
2323
isWrapped,
24+
SemconvStability,
25+
semconvStabilityFromStr,
2426
} from '@opentelemetry/instrumentation';
2527

2628
import type * as Hapi from '@hapi/hapi';
@@ -50,8 +52,14 @@ import {
5052

5153
/** Hapi instrumentation for OpenTelemetry */
5254
export class HapiInstrumentation extends InstrumentationBase {
55+
private _semconvStability: SemconvStability;
56+
5357
constructor(config: InstrumentationConfig = {}) {
5458
super(PACKAGE_NAME, PACKAGE_VERSION, config);
59+
this._semconvStability = semconvStabilityFromStr(
60+
'http',
61+
process.env.OTEL_SEMCONV_STABILITY_OPT_IN
62+
);
5563
}
5664

5765
protected init() {
@@ -387,7 +395,11 @@ export class HapiInstrumentation extends InstrumentationBase {
387395
if (rpcMetadata?.type === RPCType.HTTP) {
388396
rpcMetadata.route = route.path;
389397
}
390-
const metadata = getRouteMetadata(route, pluginName);
398+
const metadata = getRouteMetadata(
399+
route,
400+
instrumentation._semconvStability,
401+
pluginName
402+
);
391403
const span = instrumentation.tracer.startSpan(metadata.name, {
392404
attributes: metadata.attributes,
393405
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/*
18+
* This file contains a copy of unstable semantic convention definitions
19+
* used by this package.
20+
* @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
21+
*/
22+
23+
/**
24+
* Deprecated, use `http.request.method` instead.
25+
*
26+
* @example GET
27+
* @example POST
28+
* @example HEAD
29+
*
30+
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
31+
*
32+
* @deprecated Replaced by `http.request.method`.
33+
*/
34+
export const ATTR_HTTP_METHOD = 'http.method' as const;

plugins/node/opentelemetry-instrumentation-hapi/src/utils.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { SpanAttributes } from '@opentelemetry/api';
17+
import { Attributes } from '@opentelemetry/api';
1818
import {
19-
SEMATTRS_HTTP_METHOD,
20-
SEMATTRS_HTTP_ROUTE,
19+
ATTR_HTTP_ROUTE,
20+
ATTR_HTTP_REQUEST_METHOD,
2121
} from '@opentelemetry/semantic-conventions';
22+
import { ATTR_HTTP_METHOD } from './semconv';
2223
import type * as Hapi from '@hapi/hapi';
2324
import {
2425
HapiLayerType,
@@ -28,6 +29,7 @@ import {
2829
ServerExtDirectInput,
2930
} from './internal-types';
3031
import { AttributeNames } from './enums/AttributeNames';
32+
import { SemconvStability } from '@opentelemetry/instrumentation';
3133

3234
export function getPluginName<T>(plugin: Hapi.Plugin<T>): string {
3335
if ((plugin as Hapi.PluginNameVersion).name) {
@@ -72,37 +74,45 @@ export const isPatchableExtMethod = (
7274

7375
export const getRouteMetadata = (
7476
route: Hapi.ServerRoute,
77+
semconvStability: SemconvStability,
7578
pluginName?: string
7679
): {
77-
attributes: SpanAttributes;
80+
attributes: Attributes;
7881
name: string;
7982
} => {
83+
const attributes: Attributes = {
84+
[ATTR_HTTP_ROUTE]: route.path,
85+
};
86+
if (semconvStability & SemconvStability.OLD) {
87+
attributes[ATTR_HTTP_METHOD] = route.method;
88+
}
89+
if (semconvStability & SemconvStability.STABLE) {
90+
// Note: This currently does *not* normalize the method name to uppercase
91+
// and conditionally include `http.request.method.original` as described
92+
// at https://opentelemetry.io/docs/specs/semconv/http/http-spans/
93+
// These attributes are for a *hapi* span, and not the parent HTTP span,
94+
// so the HTTP span guidance doesn't strictly apply.
95+
attributes[ATTR_HTTP_REQUEST_METHOD] = route.method;
96+
}
97+
98+
let name;
8099
if (pluginName) {
81-
return {
82-
attributes: {
83-
[SEMATTRS_HTTP_ROUTE]: route.path,
84-
[SEMATTRS_HTTP_METHOD]: route.method,
85-
[AttributeNames.HAPI_TYPE]: HapiLayerType.PLUGIN,
86-
[AttributeNames.PLUGIN_NAME]: pluginName,
87-
},
88-
name: `${pluginName}: route - ${route.path}`,
89-
};
100+
attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.PLUGIN;
101+
attributes[AttributeNames.PLUGIN_NAME] = pluginName;
102+
name = `${pluginName}: route - ${route.path}`;
103+
} else {
104+
attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.ROUTER;
105+
name = `route - ${route.path}`;
90106
}
91-
return {
92-
attributes: {
93-
[SEMATTRS_HTTP_ROUTE]: route.path,
94-
[SEMATTRS_HTTP_METHOD]: route.method,
95-
[AttributeNames.HAPI_TYPE]: HapiLayerType.ROUTER,
96-
},
97-
name: `route - ${route.path}`,
98-
};
107+
108+
return { attributes, name };
99109
};
100110

101111
export const getExtMetadata = (
102112
extPoint: Hapi.ServerRequestExtType,
103113
pluginName?: string
104114
): {
105-
attributes: SpanAttributes;
115+
attributes: Attributes;
106116
name: string;
107117
} => {
108118
if (pluginName) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const { createTestNodeSdk } = require('@opentelemetry/contrib-test-utils');
18+
19+
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
20+
const { HapiInstrumentation } = require('../../build/src/index.js');
21+
22+
const sdk = createTestNodeSdk({
23+
serviceName: 'use-hapi',
24+
instrumentations: [
25+
new HttpInstrumentation(),
26+
new HapiInstrumentation()
27+
]
28+
})
29+
sdk.start();
30+
31+
const Hapi = require('@hapi/hapi');
32+
const http = require('http');
33+
34+
async function main() {
35+
// Start a Hapi server.
36+
const server = new Hapi.Server({
37+
port: 0,
38+
host: 'localhost'
39+
});
40+
41+
server.route({
42+
method: 'GET',
43+
path: '/route/{param}',
44+
handler: function() {
45+
return { hello: 'world' };
46+
}
47+
});
48+
49+
await server.start();
50+
51+
// Make a single request to it.
52+
await new Promise(resolve => {
53+
http.get(`http://${server.info.host}:${server.info.port}/route/test`, (res) => {
54+
res.resume();
55+
res.on('end', () => {
56+
resolve();
57+
});
58+
});
59+
});
60+
61+
await server.stop();
62+
await sdk.shutdown();
63+
}
64+
65+
main();

plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ await new Promise(resolve => {
5757
res.on('end', () => {
5858
resolve();
5959
});
60-
})
60+
});
6161
});
6262

6363
await server.stop();

plugins/node/opentelemetry-instrumentation-hapi/test/hapi.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import {
2626
runTestFixture,
2727
TestCollector,
28+
TestSpan,
2829
} from '@opentelemetry/contrib-test-utils';
2930
import { getPlugin } from './plugin';
3031
const plugin = getPlugin();
@@ -33,6 +34,16 @@ import * as assert from 'assert';
3334
import * as hapi from '@hapi/hapi';
3435
import { HapiLayerType } from '../src/internal-types';
3536
import { AttributeNames } from '../src/enums/AttributeNames';
37+
import { ATTR_HTTP_REQUEST_METHOD } from '@opentelemetry/semantic-conventions';
38+
import { ATTR_HTTP_METHOD } from '../src/semconv';
39+
40+
function getTestSpanAttr(span: TestSpan, name: string) {
41+
const kv = span.attributes.filter(a => a.key === name)[0];
42+
if (kv) {
43+
return kv.value;
44+
}
45+
return undefined;
46+
}
3647

3748
describe('Hapi Instrumentation - Core Tests', () => {
3849
const memoryExporter = new InMemorySpanExporter();
@@ -582,4 +593,93 @@ describe('Hapi Instrumentation - Core Tests', () => {
582593
});
583594
});
584595
});
596+
597+
describe('HTTP semconv migration', () => {
598+
it('should emit only old HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN unset', async () => {
599+
await runTestFixture({
600+
cwd: __dirname,
601+
argv: ['fixtures/use-hapi.js'],
602+
env: {
603+
OTEL_SEMCONV_STABILITY_OPT_IN: '',
604+
},
605+
checkResult: (err, stdout, stderr) => {
606+
assert.ifError(err);
607+
},
608+
checkCollector: (collector: TestCollector) => {
609+
const spans = collector.sortedSpans;
610+
assert.equal(spans.length, 3);
611+
const hapiSpan = spans[2];
612+
assert.equal(
613+
hapiSpan.instrumentationScope.name,
614+
'@opentelemetry/instrumentation-hapi'
615+
);
616+
assert.equal(
617+
getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD)?.stringValue,
618+
'GET'
619+
);
620+
assert.equal(
621+
getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD),
622+
undefined
623+
);
624+
},
625+
});
626+
});
627+
628+
it('should emit only stable HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN=http', async () => {
629+
await runTestFixture({
630+
cwd: __dirname,
631+
argv: ['fixtures/use-hapi.js'],
632+
env: {
633+
OTEL_SEMCONV_STABILITY_OPT_IN: 'http',
634+
},
635+
checkResult: (err, stdout, stderr) => {
636+
assert.ifError(err);
637+
},
638+
checkCollector: (collector: TestCollector) => {
639+
const spans = collector.sortedSpans;
640+
assert.equal(spans.length, 3);
641+
const hapiSpan = spans[2];
642+
assert.equal(
643+
hapiSpan.instrumentationScope.name,
644+
'@opentelemetry/instrumentation-hapi'
645+
);
646+
assert.equal(getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD), undefined);
647+
assert.equal(
648+
getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD)?.stringValue,
649+
'GET'
650+
);
651+
},
652+
});
653+
});
654+
655+
it('should emit both old and stable HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN=http/dup', async () => {
656+
await runTestFixture({
657+
cwd: __dirname,
658+
argv: ['fixtures/use-hapi.js'],
659+
env: {
660+
OTEL_SEMCONV_STABILITY_OPT_IN: 'http/dup',
661+
},
662+
checkResult: (err, stdout, stderr) => {
663+
assert.ifError(err);
664+
},
665+
checkCollector: (collector: TestCollector) => {
666+
const spans = collector.sortedSpans;
667+
assert.equal(spans.length, 3);
668+
const hapiSpan = spans[2];
669+
assert.equal(
670+
hapiSpan.instrumentationScope.name,
671+
'@opentelemetry/instrumentation-hapi'
672+
);
673+
assert.equal(
674+
getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD)?.stringValue,
675+
'GET'
676+
);
677+
assert.equal(
678+
getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD)?.stringValue,
679+
'GET'
680+
);
681+
},
682+
});
683+
});
684+
});
585685
});

0 commit comments

Comments
 (0)