Compare commits

...

3 Commits

Author SHA1 Message Date
6a84a8bb8b feat(metapackage): use tachybase http instrumentation 2024-08-08 12:12:44 +08:00
5cc7794daa fix(http): build error 2024-08-08 12:04:12 +08:00
4571ede515 feat(http): copy official http instrumentation
last commit: 38f6689480
2024-08-08 10:58:05 +08:00
41 changed files with 21798 additions and 32 deletions

45
package-lock.json generated
View File

@ -1,13 +1,16 @@
{ {
"name": "@tachybase/opentelemetry-auto-instrumentations", "name": "@tachybase/opentelemetry-auto-instrumentations",
"version": "0.0.1", "version": "0.0.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@tachybase/opentelemetry-auto-instrumentations", "name": "@tachybase/opentelemetry-auto-instrumentations",
"version": "0.0.1", "version": "0.0.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [
"/*"
],
"dependencies": { "dependencies": {
"@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/instrumentation": "^0.52.0",
"@opentelemetry/instrumentation-amqplib": "^0.40.0", "@opentelemetry/instrumentation-amqplib": "^0.40.0",
@ -26,7 +29,6 @@
"@opentelemetry/instrumentation-graphql": "^0.42.0", "@opentelemetry/instrumentation-graphql": "^0.42.0",
"@opentelemetry/instrumentation-grpc": "^0.52.0", "@opentelemetry/instrumentation-grpc": "^0.52.0",
"@opentelemetry/instrumentation-hapi": "^0.40.0", "@opentelemetry/instrumentation-hapi": "^0.40.0",
"@opentelemetry/instrumentation-http": "^0.52.0",
"@opentelemetry/instrumentation-ioredis": "^0.42.0", "@opentelemetry/instrumentation-ioredis": "^0.42.0",
"@opentelemetry/instrumentation-kafkajs": "^0.2.0", "@opentelemetry/instrumentation-kafkajs": "^0.2.0",
"@opentelemetry/instrumentation-knex": "^0.39.0", "@opentelemetry/instrumentation-knex": "^0.39.0",
@ -55,7 +57,8 @@
"@opentelemetry/resource-detector-container": "^0.3.11", "@opentelemetry/resource-detector-container": "^0.3.11",
"@opentelemetry/resource-detector-gcp": "^0.29.10", "@opentelemetry/resource-detector-gcp": "^0.29.10",
"@opentelemetry/resources": "^1.24.0", "@opentelemetry/resources": "^1.24.0",
"@opentelemetry/sdk-node": "^0.52.0" "@opentelemetry/sdk-node": "^0.52.0",
"@tachybase/opentelemetry-instrumentation-http": "^0.0.1"
}, },
"devDependencies": { "devDependencies": {
"@opentelemetry/api": "^1.4.1", "@opentelemetry/api": "^1.4.1",
@ -2931,23 +2934,6 @@
"@opentelemetry/api": "^1.3.0" "@opentelemetry/api": "^1.3.0"
} }
}, },
"node_modules/@opentelemetry/instrumentation-http": {
"version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.1.tgz",
"integrity": "sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/instrumentation": "0.52.1",
"@opentelemetry/semantic-conventions": "1.25.1",
"semver": "^7.5.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/instrumentation-ioredis": { "node_modules/@opentelemetry/instrumentation-ioredis": {
"version": "0.42.0", "version": "0.42.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz",
@ -3946,6 +3932,23 @@
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
"dev": true "dev": true
}, },
"node_modules/@tachybase/opentelemetry-instrumentation-http": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@tachybase/opentelemetry-instrumentation-http/-/opentelemetry-instrumentation-http-0.0.1.tgz",
"integrity": "sha512-7gnwXS9OesmY36s09YlwJgLEv93EoRSrChqw5HhraRtDzEcatYsFUdhaO/QpGMwnp70SG1Nd4hj5yN2CGvvPcA==",
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/instrumentation": "0.52.1",
"@opentelemetry/semantic-conventions": "1.25.1",
"semver": "^7.5.2"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@tootallnate/once": { "node_modules/@tootallnate/once": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "@tachybase/opentelemetry-auto-instrumentations", "name": "@tachybase/opentelemetry-auto-instrumentations",
"version": "0.0.1", "version": "0.0.2",
"description": "Metapackage of telemetry for TachyBase which bundles OpenTelemetry node core and instrumentations", "description": "Metapackage of telemetry for TachyBase which bundles OpenTelemetry node core and instrumentations",
"author": "TachyBase Team", "author": "TachyBase Team",
"homepage": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations", "homepage": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations",
@ -31,6 +31,13 @@
"test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'", "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'",
"watch": "tsc -w" "watch": "tsc -w"
}, },
"keywords": [
"tachybase",
"opentelemetry",
"nodejs",
"tracing",
"instrumentation"
],
"bugs": { "bugs": {
"url": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations/issues" "url": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations/issues"
}, },
@ -69,7 +76,7 @@
"@opentelemetry/instrumentation-graphql": "^0.42.0", "@opentelemetry/instrumentation-graphql": "^0.42.0",
"@opentelemetry/instrumentation-grpc": "^0.52.0", "@opentelemetry/instrumentation-grpc": "^0.52.0",
"@opentelemetry/instrumentation-hapi": "^0.40.0", "@opentelemetry/instrumentation-hapi": "^0.40.0",
"@opentelemetry/instrumentation-http": "^0.52.0", "@tachybase/opentelemetry-instrumentation-http": "^0.0.1",
"@opentelemetry/instrumentation-ioredis": "^0.42.0", "@opentelemetry/instrumentation-ioredis": "^0.42.0",
"@opentelemetry/instrumentation-kafkajs": "^0.2.0", "@opentelemetry/instrumentation-kafkajs": "^0.2.0",
"@opentelemetry/instrumentation-knex": "^0.39.0", "@opentelemetry/instrumentation-knex": "^0.39.0",
@ -103,7 +110,10 @@
"files": [ "files": [
"build/src/**/*.js", "build/src/**/*.js",
"build/src/**/*.js.map", "build/src/**/*.js.map",
"build/src/**/*.d.ts" "build/src/**/*.d.ts",
"doc",
"LICENSE",
"README.md"
], ],
"changelog": { "changelog": {
"labels": { "labels": {

View File

@ -0,0 +1 @@
build

View File

@ -0,0 +1,7 @@
module.exports = {
"env": {
"mocha": true,
"node": true
},
...require('../../../eslint.base.js')
}

View File

@ -0,0 +1,4 @@
/bin
/coverage
/doc
/test

View File

@ -0,0 +1,2 @@
registry=https://registry.npmjs.org/
@tachybase:registry=https://registry.npmjs.org/

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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
http://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.

View File

@ -0,0 +1,3 @@
# opentelemetry-instrumentations-http
TachyBase 专属的 OpenTelemetry HTTP 自动插桩库

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
{
"name": "@tachybase/opentelemetry-instrumentation-http",
"version": "0.0.1",
"description": "OpenTelemetry instrumentation for `node:http` and `node:https` http client and server modules for TachyBase",
"author": "TachyBase Team",
"homepage": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations/plugins/node/opentelemetry-instrumentation-http",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"main": "build/src/index.js",
"types": "build/src/index.d.ts",
"exports": {
".": "./build/src/index.js",
"./register": "./build/src/register.js"
},
"repository": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations",
"scripts": {
"clean": "tsc --build --clean",
"compile": "tsc --build",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"precompile": "cross-var lerna run version --include-dependencies",
"prewatch": "npm run precompile",
"prepublishOnly": "npm run compile",
"tdd": "npm run test -- --watch-extensions ts --watch",
"test": "npm run test:cjs && npm run test:esm",
"watch": "tsc --build --watch"
},
"keywords": [
"tachybase",
"opentelemetry",
"http",
"nodejs",
"tracing",
"profiling",
"instrumentation"
],
"bugs": {
"url": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations/issues"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
},
"devDependencies": {
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "1.25.1",
"@opentelemetry/sdk-metrics": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1",
"@opentelemetry/sdk-trace-node": "1.25.1",
"@types/mocha": "10.0.7",
"@types/node": "18.6.5",
"@types/request-promise-native": "1.0.21",
"@types/semver": "7.5.8",
"@types/sinon": "17.0.3",
"@types/superagent": "8.1.8",
"axios": "1.6.0",
"codecov": "3.8.3",
"cross-var": "1.1.0",
"lerna": "6.6.2",
"mocha": "10.2.0",
"nock": "13.3.8",
"nyc": "15.1.0",
"request": "2.88.2",
"request-promise-native": "1.0.9",
"sinon": "15.1.2",
"superagent": "9.0.2",
"typescript": "4.4.4"
},
"dependencies": {
"@opentelemetry/core": "1.25.1",
"@opentelemetry/instrumentation": "0.52.1",
"@opentelemetry/semantic-conventions": "1.25.1",
"semver": "^7.5.2"
},
"files": [
"build/src/**/*.js",
"build/src/**/*.js.map",
"build/src/**/*.d.ts",
"doc",
"LICENSE",
"README.md"
]
}

View File

@ -0,0 +1,24 @@
/*
* 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.
*/
/**
* https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md
*/
export enum AttributeNames {
HTTP_ERROR_NAME = 'http.error_name',
HTTP_ERROR_MESSAGE = 'http.error_message',
HTTP_STATUS_TEXT = 'http.status_text',
}

View File

@ -0,0 +1,878 @@
/*
* 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.
*/
import {
context,
HrTime,
INVALID_SPAN_CONTEXT,
propagation,
ROOT_CONTEXT,
Span,
SpanKind,
SpanOptions,
SpanStatus,
SpanStatusCode,
trace,
Histogram,
MetricAttributes,
ValueType,
} from '@opentelemetry/api';
import {
hrTime,
hrTimeDuration,
hrTimeToMilliseconds,
suppressTracing,
} from '@opentelemetry/core';
import type * as http from 'http';
import type * as https from 'https';
import { Socket } from 'net';
import * as semver from 'semver';
import * as url from 'url';
import {
Err,
Func,
Http,
HttpInstrumentationConfig,
HttpRequestArgs,
Https,
} from './types';
import * as utils from './utils';
import packageJson from '../package.json';
import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';
import { RPCMetadata, RPCType, setRPCMetadata } from '@opentelemetry/core';
import { errorMonitor } from 'events';
import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
/**
* Http instrumentation instrumentation for Opentelemetry
*/
export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentationConfig> {
/** keep track on spans not ended */
private readonly _spanNotEnded: WeakSet<Span> = new WeakSet<Span>();
private _headerCapture;
private _httpServerDurationHistogram!: Histogram;
private _httpClientDurationHistogram!: Histogram;
constructor(config: HttpInstrumentationConfig = {}) {
super('@tachybase/opentelemetry-instrumentation-http', packageJson?.version, config);
this._headerCapture = this._createHeaderCapture();
}
protected override _updateMetricInstruments() {
this._httpServerDurationHistogram = this.meter.createHistogram(
'http.server.duration',
{
description: 'Measures the duration of inbound HTTP requests.',
unit: 'ms',
valueType: ValueType.DOUBLE,
}
);
this._httpClientDurationHistogram = this.meter.createHistogram(
'http.client.duration',
{
description: 'Measures the duration of outbound HTTP requests.',
unit: 'ms',
valueType: ValueType.DOUBLE,
}
);
}
override setConfig(config: HttpInstrumentationConfig = {}): void {
super.setConfig(config);
this._headerCapture = this._createHeaderCapture();
}
init(): [
InstrumentationNodeModuleDefinition,
InstrumentationNodeModuleDefinition,
] {
return [this._getHttpsInstrumentation(), this._getHttpInstrumentation()];
}
private _getHttpInstrumentation() {
return new InstrumentationNodeModuleDefinition(
'http',
['*'],
(moduleExports: Http): Http => {
if (!this.getConfig().disableOutgoingRequestInstrumentation) {
const patchedRequest = this._wrap(
moduleExports,
'request',
this._getPatchOutgoingRequestFunction('http')
) as unknown as Func<http.ClientRequest>;
this._wrap(
moduleExports,
'get',
this._getPatchOutgoingGetFunction(patchedRequest)
);
}
if (!this.getConfig().disableIncomingRequestInstrumentation) {
this._wrap(
moduleExports.Server.prototype,
'emit',
this._getPatchIncomingRequestFunction('http')
);
}
return moduleExports;
},
(moduleExports: Http) => {
if (moduleExports === undefined) return;
if (!this.getConfig().disableOutgoingRequestInstrumentation) {
this._unwrap(moduleExports, 'request');
this._unwrap(moduleExports, 'get');
}
if (!this.getConfig().disableIncomingRequestInstrumentation) {
this._unwrap(moduleExports.Server.prototype, 'emit');
}
}
);
}
private _getHttpsInstrumentation() {
return new InstrumentationNodeModuleDefinition(
'https',
['*'],
(moduleExports: Https): Https => {
if (!this.getConfig().disableOutgoingRequestInstrumentation) {
const patchedRequest = this._wrap(
moduleExports,
'request',
this._getPatchHttpsOutgoingRequestFunction('https')
) as unknown as Func<http.ClientRequest>;
this._wrap(
moduleExports,
'get',
this._getPatchHttpsOutgoingGetFunction(patchedRequest)
);
}
if (!this.getConfig().disableIncomingRequestInstrumentation) {
this._wrap(
moduleExports.Server.prototype,
'emit',
this._getPatchIncomingRequestFunction('https')
);
}
return moduleExports;
},
(moduleExports: Https) => {
if (moduleExports === undefined) return;
if (!this.getConfig().disableOutgoingRequestInstrumentation) {
this._unwrap(moduleExports, 'request');
this._unwrap(moduleExports, 'get');
}
if (!this.getConfig().disableIncomingRequestInstrumentation) {
this._unwrap(moduleExports.Server.prototype, 'emit');
}
}
);
}
/**
* Creates spans for incoming requests, restoring spans' context if applied.
*/
protected _getPatchIncomingRequestFunction(component: 'http' | 'https') {
return (
original: (event: string, ...args: unknown[]) => boolean
): ((this: unknown, event: string, ...args: unknown[]) => boolean) => {
return this._incomingRequestFunction(component, original);
};
}
/**
* Creates spans for outgoing requests, sending spans' context for distributed
* tracing.
*/
protected _getPatchOutgoingRequestFunction(component: 'http' | 'https') {
return (original: Func<http.ClientRequest>): Func<http.ClientRequest> => {
return this._outgoingRequestFunction(component, original);
};
}
protected _getPatchOutgoingGetFunction(
clientRequest: (
options: http.RequestOptions | string | url.URL,
...args: HttpRequestArgs
) => http.ClientRequest
) {
return (_original: Func<http.ClientRequest>): Func<http.ClientRequest> => {
// Re-implement http.get. This needs to be done (instead of using
// getPatchOutgoingRequestFunction to patch it) because we need to
// set the trace context header before the returned http.ClientRequest is
// ended. The Node.js docs state that the only differences between
// request and get are that (1) get defaults to the HTTP GET method and
// (2) the returned request object is ended immediately. The former is
// already true (at least in supported Node versions up to v10), so we
// simply follow the latter. Ref:
// https://nodejs.org/dist/latest/docs/api/http.html#http_http_get_options_callback
// https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/instrumentations/instrumentation-http.ts#L198
return function outgoingGetRequest<
T extends http.RequestOptions | string | url.URL,
>(options: T, ...args: HttpRequestArgs): http.ClientRequest {
const req = clientRequest(options, ...args);
req.end();
return req;
};
};
}
/** Patches HTTPS outgoing requests */
private _getPatchHttpsOutgoingRequestFunction(component: 'http' | 'https') {
return (original: Func<http.ClientRequest>): Func<http.ClientRequest> => {
const instrumentation = this;
return function httpsOutgoingRequest(
// eslint-disable-next-line node/no-unsupported-features/node-builtins
options: https.RequestOptions | string | URL,
...args: HttpRequestArgs
): http.ClientRequest {
// Makes sure options will have default HTTPS parameters
if (
component === 'https' &&
typeof options === 'object' &&
options?.constructor?.name !== 'URL'
) {
options = Object.assign({}, options);
instrumentation._setDefaultOptions(options);
}
return instrumentation._getPatchOutgoingRequestFunction(component)(
original
)(options, ...args);
};
};
}
private _setDefaultOptions(options: https.RequestOptions) {
options.protocol = options.protocol || 'https:';
options.port = options.port || 443;
}
/** Patches HTTPS outgoing get requests */
private _getPatchHttpsOutgoingGetFunction(
clientRequest: (
// eslint-disable-next-line node/no-unsupported-features/node-builtins
options: http.RequestOptions | string | URL,
...args: HttpRequestArgs
) => http.ClientRequest
) {
return (original: Func<http.ClientRequest>): Func<http.ClientRequest> => {
const instrumentation = this;
return function httpsOutgoingRequest(
// eslint-disable-next-line node/no-unsupported-features/node-builtins
options: https.RequestOptions | string | URL,
...args: HttpRequestArgs
): http.ClientRequest {
return instrumentation._getPatchOutgoingGetFunction(clientRequest)(
original
)(options, ...args);
};
};
}
/**
* Attach event listeners to a client request to end span and add span attributes.
*
* @param request The original request object.
* @param span representing the current operation
* @param startTime representing the start time of the request to calculate duration in Metric
* @param metricAttributes metric attributes
*/
private _traceClientRequest(
request: http.ClientRequest,
span: Span,
startTime: HrTime,
metricAttributes: MetricAttributes
): http.ClientRequest {
if (this.getConfig().requestHook) {
this._callRequestHook(span, request);
}
/**
* Determines if the request has errored or the response has ended/errored.
*/
let responseFinished = false;
/*
* User 'response' event listeners can be added before our listener,
* force our listener to be the first, so response emitter is bound
* before any user listeners are added to it.
*/
request.prependListener(
'response',
(response: http.IncomingMessage & { aborted?: boolean }) => {
this._diag.debug('outgoingRequest on response()');
if (request.listenerCount('response') <= 1) {
response.resume();
}
const responseAttributes =
utils.getOutgoingRequestAttributesOnResponse(response);
span.setAttributes(responseAttributes);
metricAttributes = Object.assign(
metricAttributes,
utils.getOutgoingRequestMetricAttributesOnResponse(responseAttributes)
);
if (this.getConfig().responseHook) {
this._callResponseHook(span, response);
}
this._headerCapture.client.captureRequestHeaders(span, header =>
request.getHeader(header)
);
this._headerCapture.client.captureResponseHeaders(
span,
header => response.headers[header]
);
context.bind(context.active(), response);
const endHandler = () => {
this._diag.debug('outgoingRequest on end()');
if (responseFinished) {
return;
}
responseFinished = true;
let status: SpanStatus;
if (response.aborted && !response.complete) {
status = { code: SpanStatusCode.ERROR };
} else {
status = {
code: utils.parseResponseStatus(
SpanKind.CLIENT,
response.statusCode
),
};
}
span.setStatus(status);
if (this.getConfig().applyCustomAttributesOnSpan) {
safeExecuteInTheMiddle(
() =>
this.getConfig().applyCustomAttributesOnSpan!(
span,
request,
response
),
() => {},
true
);
}
this._closeHttpSpan(
span,
SpanKind.CLIENT,
startTime,
metricAttributes
);
};
response.on('end', endHandler);
// See https://github.com/open-telemetry/opentelemetry-js/pull/3625#issuecomment-1475673533
if (semver.lt(process.version, '16.0.0')) {
response.on('close', endHandler);
}
response.on(errorMonitor, (error: Err) => {
this._diag.debug('outgoingRequest on error()', error);
if (responseFinished) {
return;
}
responseFinished = true;
utils.setSpanWithError(span, error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
this._closeHttpSpan(
span,
SpanKind.CLIENT,
startTime,
metricAttributes
);
});
}
);
request.on('close', () => {
this._diag.debug('outgoingRequest on request close()');
if (request.aborted || responseFinished) {
return;
}
responseFinished = true;
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
});
request.on(errorMonitor, (error: Err) => {
this._diag.debug('outgoingRequest on request error()', error);
if (responseFinished) {
return;
}
responseFinished = true;
utils.setSpanWithError(span, error);
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
});
this._diag.debug('http.ClientRequest return request');
return request;
}
private _incomingRequestFunction(
component: 'http' | 'https',
original: (event: string, ...args: unknown[]) => boolean
) {
const instrumentation = this;
return function incomingRequest(
this: unknown,
event: string,
...args: unknown[]
): boolean {
// Only traces request events
if (event !== 'request') {
return original.apply(this, [event, ...args]);
}
const request = args[0] as http.IncomingMessage;
const response = args[1] as http.ServerResponse & { socket: Socket };
const pathname = request.url
? url.parse(request.url).pathname || '/'
: '/';
const method = request.method || 'GET';
instrumentation._diag.debug(
`${component} instrumentation incomingRequest`
);
if (
utils.isIgnored(
pathname,
instrumentation.getConfig().ignoreIncomingPaths,
(e: unknown) =>
instrumentation._diag.error('caught ignoreIncomingPaths error: ', e)
) ||
safeExecuteInTheMiddle(
() =>
instrumentation.getConfig().ignoreIncomingRequestHook?.(request),
(e: unknown) => {
if (e != null) {
instrumentation._diag.error(
'caught ignoreIncomingRequestHook error: ',
e
);
}
},
true
)
) {
return context.with(suppressTracing(context.active()), () => {
context.bind(context.active(), request);
context.bind(context.active(), response);
return original.apply(this, [event, ...args]);
});
}
const headers = request.headers;
const spanAttributes = utils.getIncomingRequestAttributes(request, {
component: component,
serverName: instrumentation.getConfig().serverName,
hookAttributes: instrumentation._callStartSpanHook(
request,
instrumentation.getConfig().startIncomingSpanHook
),
});
const spanOptions: SpanOptions = {
kind: SpanKind.SERVER,
attributes: spanAttributes,
};
const startTime = hrTime();
const metricAttributes =
utils.getIncomingRequestMetricAttributes(spanAttributes);
const ctx = propagation.extract(ROOT_CONTEXT, headers);
const span = instrumentation._startHttpSpan(method, spanOptions, ctx);
const rpcMetadata: RPCMetadata = {
type: RPCType.HTTP,
span,
};
return context.with(
setRPCMetadata(trace.setSpan(ctx, span), rpcMetadata),
() => {
context.bind(context.active(), request);
context.bind(context.active(), response);
if (instrumentation.getConfig().requestHook) {
instrumentation._callRequestHook(span, request);
}
if (instrumentation.getConfig().responseHook) {
instrumentation._callResponseHook(span, response);
}
instrumentation._headerCapture.server.captureRequestHeaders(
span,
header => request.headers[header]
);
// After 'error', no further events other than 'close' should be emitted.
let hasError = false;
response.on('close', () => {
if (hasError) {
return;
}
instrumentation._onServerResponseFinish(
request,
response,
span,
metricAttributes,
startTime
);
});
response.on(errorMonitor, (err: Err) => {
hasError = true;
instrumentation._onServerResponseError(
span,
metricAttributes,
startTime,
err
);
});
return safeExecuteInTheMiddle(
() => original.apply(this, [event, ...args]),
error => {
if (error) {
utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(
span,
SpanKind.SERVER,
startTime,
metricAttributes
);
throw error;
}
}
);
}
);
};
}
private _outgoingRequestFunction(
component: 'http' | 'https',
original: Func<http.ClientRequest>
): Func<http.ClientRequest> {
const instrumentation = this;
return function outgoingRequest(
this: unknown,
options: url.URL | http.RequestOptions | string,
...args: unknown[]
): http.ClientRequest {
if (!utils.isValidOptionsType(options)) {
return original.apply(this, [options, ...args]);
}
const extraOptions =
typeof args[0] === 'object' &&
(typeof options === 'string' || options instanceof url.URL)
? (args.shift() as http.RequestOptions)
: undefined;
const { origin, pathname, method, optionsParsed } = utils.getRequestInfo(
options,
extraOptions
);
/**
* Node 8's https module directly call the http one so to avoid creating
* 2 span for the same request we need to check that the protocol is correct
* See: https://github.com/nodejs/node/blob/v8.17.0/lib/https.js#L245
*/
if (
component === 'http' &&
semver.lt(process.version, '9.0.0') &&
optionsParsed.protocol === 'https:'
) {
return original.apply(this, [optionsParsed, ...args]);
}
if (
utils.isIgnored(
origin + pathname,
instrumentation.getConfig().ignoreOutgoingUrls,
(e: unknown) =>
instrumentation._diag.error('caught ignoreOutgoingUrls error: ', e)
) ||
safeExecuteInTheMiddle(
() =>
instrumentation
.getConfig()
.ignoreOutgoingRequestHook?.(optionsParsed),
(e: unknown) => {
if (e != null) {
instrumentation._diag.error(
'caught ignoreOutgoingRequestHook error: ',
e
);
}
},
true
)
) {
return original.apply(this, [optionsParsed, ...args]);
}
const { hostname, port } = utils.extractHostnameAndPort(optionsParsed);
const attributes = utils.getOutgoingRequestAttributes(optionsParsed, {
component,
port,
hostname,
hookAttributes: instrumentation._callStartSpanHook(
optionsParsed,
instrumentation.getConfig().startOutgoingSpanHook
),
});
const startTime = hrTime();
const metricAttributes: MetricAttributes =
utils.getOutgoingRequestMetricAttributes(attributes);
const spanOptions: SpanOptions = {
kind: SpanKind.CLIENT,
attributes,
};
const span = instrumentation._startHttpSpan(method, spanOptions);
const parentContext = context.active();
const requestContext = trace.setSpan(parentContext, span);
if (!optionsParsed.headers) {
optionsParsed.headers = {};
} else {
// Make a copy of the headers object to avoid mutating an object the
// caller might have a reference to.
optionsParsed.headers = Object.assign({}, optionsParsed.headers);
}
propagation.inject(requestContext, optionsParsed.headers);
return context.with(requestContext, () => {
/*
* The response callback is registered before ClientRequest is bound,
* thus it is needed to bind it before the function call.
*/
const cb = args[args.length - 1];
if (typeof cb === 'function') {
args[args.length - 1] = context.bind(parentContext, cb);
}
const request: http.ClientRequest = safeExecuteInTheMiddle(
() => original.apply(this, [optionsParsed, ...args]),
error => {
if (error) {
utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(
span,
SpanKind.CLIENT,
startTime,
metricAttributes
);
throw error;
}
}
);
instrumentation._diag.debug(
`${component} instrumentation outgoingRequest`
);
context.bind(parentContext, request);
return instrumentation._traceClientRequest(
request,
span,
startTime,
metricAttributes
);
});
};
}
private _onServerResponseFinish(
request: http.IncomingMessage,
response: http.ServerResponse,
span: Span,
metricAttributes: MetricAttributes,
startTime: HrTime
) {
const attributes = utils.getIncomingRequestAttributesOnResponse(
request,
response
);
metricAttributes = Object.assign(
metricAttributes,
utils.getIncomingRequestMetricAttributesOnResponse(attributes)
);
this._headerCapture.server.captureResponseHeaders(span, header =>
response.getHeader(header)
);
span.setAttributes(attributes).setStatus({
code: utils.parseResponseStatus(SpanKind.SERVER, response.statusCode),
});
const route = attributes[SEMATTRS_HTTP_ROUTE];
if (route) {
span.updateName(`${request.method || 'GET'} ${route}`);
}
if (this.getConfig().applyCustomAttributesOnSpan) {
safeExecuteInTheMiddle(
() =>
this.getConfig().applyCustomAttributesOnSpan!(
span,
request,
response
),
() => {},
true
);
}
this._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
}
private _onServerResponseError(
span: Span,
metricAttributes: MetricAttributes,
startTime: HrTime,
error: Err
) {
utils.setSpanWithError(span, error);
this._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
}
private _startHttpSpan(
name: string,
options: SpanOptions,
ctx = context.active()
) {
/*
* If a parent is required but not present, we use a `NoopSpan` to still
* propagate context without recording it.
*/
const requireParent =
options.kind === SpanKind.CLIENT
? this.getConfig().requireParentforOutgoingSpans
: this.getConfig().requireParentforIncomingSpans;
let span: Span;
const currentSpan = trace.getSpan(ctx);
if (requireParent === true && currentSpan === undefined) {
span = trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
} else if (requireParent === true && currentSpan?.spanContext().isRemote) {
span = currentSpan;
} else {
span = this.tracer.startSpan(name, options, ctx);
}
this._spanNotEnded.add(span);
return span;
}
private _closeHttpSpan(
span: Span,
spanKind: SpanKind,
startTime: HrTime,
metricAttributes: MetricAttributes
) {
if (!this._spanNotEnded.has(span)) {
return;
}
span.end();
this._spanNotEnded.delete(span);
// Record metrics
const duration = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime()));
if (spanKind === SpanKind.SERVER) {
this._httpServerDurationHistogram.record(duration, metricAttributes);
} else if (spanKind === SpanKind.CLIENT) {
this._httpClientDurationHistogram.record(duration, metricAttributes);
}
}
private _callResponseHook(
span: Span,
response: http.IncomingMessage | http.ServerResponse
) {
safeExecuteInTheMiddle(
() => this.getConfig().responseHook!(span, response),
() => {},
true
);
}
private _callRequestHook(
span: Span,
request: http.ClientRequest | http.IncomingMessage
) {
safeExecuteInTheMiddle(
() => this.getConfig().requestHook!(span, request),
() => {},
true
);
}
private _callStartSpanHook(
request: http.IncomingMessage | http.RequestOptions,
hookFunc: Function | undefined
) {
if (typeof hookFunc === 'function') {
return safeExecuteInTheMiddle(
() => hookFunc(request),
() => {},
true
);
}
}
private _createHeaderCapture() {
const config = this.getConfig();
return {
client: {
captureRequestHeaders: utils.headerCapture(
'request',
config.headersToSpanAttributes?.client?.requestHeaders ?? []
),
captureResponseHeaders: utils.headerCapture(
'response',
config.headersToSpanAttributes?.client?.responseHeaders ?? []
),
},
server: {
captureRequestHeaders: utils.headerCapture(
'request',
config.headersToSpanAttributes?.server?.requestHeaders ?? []
),
captureResponseHeaders: utils.headerCapture(
'response',
config.headersToSpanAttributes?.server?.responseHeaders ?? []
),
},
};
}
}

View File

@ -0,0 +1,19 @@
/*
* 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.
*/
export * from './http';
export * from './types';
export * from './utils';

View File

@ -0,0 +1,134 @@
/*
* 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.
*/
import { Span, SpanAttributes } from '@opentelemetry/api';
import type * as http from 'http';
import type * as https from 'https';
import {
ClientRequest,
get,
IncomingMessage,
request,
ServerResponse,
RequestOptions,
} from 'http';
import * as url from 'url';
import { InstrumentationConfig } from '@opentelemetry/instrumentation';
export type IgnoreMatcher = string | RegExp | ((url: string) => boolean);
export type HttpCallback = (res: IncomingMessage) => void;
export type RequestFunction = typeof request;
export type GetFunction = typeof get;
export type HttpCallbackOptional = HttpCallback | undefined;
// from node 10+
export type RequestSignature = [http.RequestOptions, HttpCallbackOptional] &
HttpCallback;
export type HttpRequestArgs = Array<HttpCallbackOptional | RequestSignature>;
export type ParsedRequestOptions =
| (http.RequestOptions & Partial<url.UrlWithParsedQuery>)
| http.RequestOptions;
export type Http = typeof http;
export type Https = typeof https;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Func<T> = (...args: any[]) => T;
export interface HttpCustomAttributeFunction {
(
span: Span,
request: ClientRequest | IncomingMessage,
response: IncomingMessage | ServerResponse
): void;
}
export interface IgnoreIncomingRequestFunction {
(request: IncomingMessage): boolean;
}
export interface IgnoreOutgoingRequestFunction {
(request: RequestOptions): boolean;
}
export interface HttpRequestCustomAttributeFunction {
(span: Span, request: ClientRequest | IncomingMessage): void;
}
export interface HttpResponseCustomAttributeFunction {
(span: Span, response: IncomingMessage | ServerResponse): void;
}
export interface StartIncomingSpanCustomAttributeFunction {
(request: IncomingMessage): SpanAttributes;
}
export interface StartOutgoingSpanCustomAttributeFunction {
(request: RequestOptions): SpanAttributes;
}
/**
* Options available for the HTTP instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-http#http-instrumentation-options))
*/
export interface HttpInstrumentationConfig extends InstrumentationConfig {
/**
* Not trace all incoming requests that match paths
* @deprecated use `ignoreIncomingRequestHook` instead
*/
ignoreIncomingPaths?: IgnoreMatcher[];
/** Not trace all incoming requests that matched with custom function */
ignoreIncomingRequestHook?: IgnoreIncomingRequestFunction;
/**
* Not trace all outgoing requests that match urls
* @deprecated use `ignoreOutgoingRequestHook` instead
*/
ignoreOutgoingUrls?: IgnoreMatcher[];
/** Not trace all outgoing requests that matched with custom function */
ignoreOutgoingRequestHook?: IgnoreOutgoingRequestFunction;
/** If set to true, incoming requests will not be instrumented at all. */
disableIncomingRequestInstrumentation?: boolean;
/** If set to true, outgoing requests will not be instrumented at all. */
disableOutgoingRequestInstrumentation?: boolean;
/** Function for adding custom attributes after response is handled */
applyCustomAttributesOnSpan?: HttpCustomAttributeFunction;
/** Function for adding custom attributes before request is handled */
requestHook?: HttpRequestCustomAttributeFunction;
/** Function for adding custom attributes before response is handled */
responseHook?: HttpResponseCustomAttributeFunction;
/** Function for adding custom attributes before a span is started in incomingRequest */
startIncomingSpanHook?: StartIncomingSpanCustomAttributeFunction;
/** Function for adding custom attributes before a span is started in outgoingRequest */
startOutgoingSpanHook?: StartOutgoingSpanCustomAttributeFunction;
/** The primary server name of the matched virtual host. */
serverName?: string;
/** Require parent to create span for outgoing requests */
requireParentforOutgoingSpans?: boolean;
/** Require parent to create span for incoming requests */
requireParentforIncomingSpans?: boolean;
/** Map the following HTTP headers to span attributes. */
headersToSpanAttributes?: {
client?: { requestHeaders?: string[]; responseHeaders?: string[] };
server?: { requestHeaders?: string[]; responseHeaders?: string[] };
};
}
export interface Err extends Error {
errno?: number;
code?: string;
path?: string;
syscall?: string;
stack?: string;
}

View File

@ -0,0 +1,613 @@
/*
* 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.
*/
import {
MetricAttributes,
SpanAttributes,
SpanStatusCode,
Span,
context,
SpanKind,
} from '@opentelemetry/api';
import {
NETTRANSPORTVALUES_IP_TCP,
NETTRANSPORTVALUES_IP_UDP,
SEMATTRS_HTTP_CLIENT_IP,
SEMATTRS_HTTP_FLAVOR,
SEMATTRS_HTTP_HOST,
SEMATTRS_HTTP_METHOD,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
SEMATTRS_HTTP_ROUTE,
SEMATTRS_HTTP_SCHEME,
SEMATTRS_HTTP_SERVER_NAME,
SEMATTRS_HTTP_STATUS_CODE,
SEMATTRS_HTTP_TARGET,
SEMATTRS_HTTP_URL,
SEMATTRS_HTTP_USER_AGENT,
SEMATTRS_NET_HOST_IP,
SEMATTRS_NET_HOST_NAME,
SEMATTRS_NET_HOST_PORT,
SEMATTRS_NET_PEER_IP,
SEMATTRS_NET_PEER_NAME,
SEMATTRS_NET_PEER_PORT,
SEMATTRS_NET_TRANSPORT,
} from '@opentelemetry/semantic-conventions';
import {
IncomingHttpHeaders,
IncomingMessage,
OutgoingHttpHeaders,
RequestOptions,
ServerResponse,
} from 'http';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import * as url from 'url';
import { AttributeNames } from './enums/AttributeNames';
import { Err, IgnoreMatcher, ParsedRequestOptions } from './types';
/**
* Get an absolute url
*/
export const getAbsoluteUrl = (
requestUrl: ParsedRequestOptions | null,
headers: IncomingHttpHeaders | OutgoingHttpHeaders,
fallbackProtocol = 'http:'
): string => {
const reqUrlObject = requestUrl || {};
const protocol = reqUrlObject.protocol || fallbackProtocol;
const port = (reqUrlObject.port || '').toString();
const path = reqUrlObject.path || '/';
let host =
reqUrlObject.host || reqUrlObject.hostname || headers.host || 'localhost';
// if there is no port in host and there is a port
// it should be displayed if it's not 80 and 443 (default ports)
if (
(host as string).indexOf(':') === -1 &&
port &&
port !== '80' &&
port !== '443'
) {
host += `:${port}`;
}
return `${protocol}//${host}${path}`;
};
/**
* Parse status code from HTTP response. [More details](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#status)
*/
export const parseResponseStatus = (
kind: SpanKind,
statusCode?: number
): SpanStatusCode => {
const upperBound = kind === SpanKind.CLIENT ? 400 : 500;
// 1xx, 2xx, 3xx are OK on client and server
// 4xx is OK on server
if (statusCode && statusCode >= 100 && statusCode < upperBound) {
return SpanStatusCode.UNSET;
}
// All other codes are error
return SpanStatusCode.ERROR;
};
/**
* Check whether the given obj match pattern
* @param constant e.g URL of request
* @param pattern Match pattern
*/
export const satisfiesPattern = (
constant: string,
pattern: IgnoreMatcher
): boolean => {
if (typeof pattern === 'string') {
return pattern === constant;
} else if (pattern instanceof RegExp) {
return pattern.test(constant);
} else if (typeof pattern === 'function') {
return pattern(constant);
} else {
throw new TypeError('Pattern is in unsupported datatype');
}
};
/**
* Check whether the given request is ignored by configuration
* It will not re-throw exceptions from `list` provided by the client
* @param constant e.g URL of request
* @param [list] List of ignore patterns
* @param [onException] callback for doing something when an exception has
* occurred
*/
export const isIgnored = (
constant: string,
list?: IgnoreMatcher[],
onException?: (error: unknown) => void
): boolean => {
if (!list) {
// No ignored urls - trace everything
return false;
}
// Try/catch outside the loop for failing fast
try {
for (const pattern of list) {
if (satisfiesPattern(constant, pattern)) {
return true;
}
}
} catch (e) {
if (onException) {
onException(e);
}
}
return false;
};
/**
* Sets the span with the error passed in params
* @param {Span} span the span that need to be set
* @param {Error} error error that will be set to span
*/
export const setSpanWithError = (span: Span, error: Err): void => {
const message = error.message;
span.setAttribute(AttributeNames.HTTP_ERROR_NAME, error.name);
span.setAttribute(AttributeNames.HTTP_ERROR_MESSAGE, message);
span.setStatus({ code: SpanStatusCode.ERROR, message });
span.recordException(error);
};
/**
* Adds attributes for request content-length and content-encoding HTTP headers
* @param { IncomingMessage } Request object whose headers will be analyzed
* @param { SpanAttributes } SpanAttributes object to be modified
*/
export const setRequestContentLengthAttribute = (
request: IncomingMessage,
attributes: SpanAttributes
): void => {
const length = getContentLength(request.headers);
if (length === null) return;
if (isCompressed(request.headers)) {
attributes[SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH] = length;
} else {
attributes[SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED] = length;
}
};
/**
* Adds attributes for response content-length and content-encoding HTTP headers
* @param { IncomingMessage } Response object whose headers will be analyzed
* @param { SpanAttributes } SpanAttributes object to be modified
*/
export const setResponseContentLengthAttribute = (
response: IncomingMessage,
attributes: SpanAttributes
): void => {
const length = getContentLength(response.headers);
if (length === null) return;
if (isCompressed(response.headers)) {
attributes[SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH] = length;
} else {
attributes[SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED] = length;
}
};
function getContentLength(
headers: OutgoingHttpHeaders | IncomingHttpHeaders
): number | null {
const contentLengthHeader = headers['content-length'];
if (contentLengthHeader === undefined) return null;
const contentLength = parseInt(contentLengthHeader as string, 10);
if (isNaN(contentLength)) return null;
return contentLength;
}
export const isCompressed = (
headers: OutgoingHttpHeaders | IncomingHttpHeaders
): boolean => {
const encoding = headers['content-encoding'];
return !!encoding && encoding !== 'identity';
};
/**
* Makes sure options is an url object
* return an object with default value and parsed options
* @param options original options for the request
* @param [extraOptions] additional options for the request
*/
export const getRequestInfo = (
options: url.URL | RequestOptions | string,
extraOptions?: RequestOptions
): {
origin: string;
pathname: string;
method: string;
optionsParsed: RequestOptions;
} => {
let pathname = '/';
let origin = '';
let optionsParsed: RequestOptions;
if (typeof options === 'string') {
optionsParsed = url.parse(options);
pathname = (optionsParsed as url.UrlWithStringQuery).pathname || '/';
origin = `${optionsParsed.protocol || 'http:'}//${optionsParsed.host}`;
if (extraOptions !== undefined) {
Object.assign(optionsParsed, extraOptions);
}
} else if (options instanceof url.URL) {
optionsParsed = {
protocol: options.protocol,
hostname:
typeof options.hostname === 'string' && options.hostname.startsWith('[')
? options.hostname.slice(1, -1)
: options.hostname,
path: `${options.pathname || ''}${options.search || ''}`,
};
if (options.port !== '') {
optionsParsed.port = Number(options.port);
}
if (options.username || options.password) {
optionsParsed.auth = `${options.username}:${options.password}`;
}
pathname = options.pathname;
origin = options.origin;
if (extraOptions !== undefined) {
Object.assign(optionsParsed, extraOptions);
}
} else {
optionsParsed = Object.assign(
{ protocol: options.host ? 'http:' : undefined },
options
);
pathname = (options as url.URL).pathname;
if (!pathname && optionsParsed.path) {
pathname = url.parse(optionsParsed.path).pathname || '/';
}
const hostname =
optionsParsed.host ||
(optionsParsed.port != null
? `${optionsParsed.hostname}${optionsParsed.port}`
: optionsParsed.hostname);
origin = `${optionsParsed.protocol || 'http:'}//${hostname}`;
}
// some packages return method in lowercase..
// ensure upperCase for consistency
const method = optionsParsed.method
? optionsParsed.method.toUpperCase()
: 'GET';
return { origin, pathname, method, optionsParsed };
};
/**
* Makes sure options is of type string or object
* @param options for the request
*/
export const isValidOptionsType = (options: unknown): boolean => {
if (!options) {
return false;
}
const type = typeof options;
return type === 'string' || (type === 'object' && !Array.isArray(options));
};
export const extractHostnameAndPort = (
requestOptions: Pick<
ParsedRequestOptions,
'hostname' | 'host' | 'port' | 'protocol'
>
): { hostname: string; port: number | string } => {
if (requestOptions.hostname && requestOptions.port) {
return { hostname: requestOptions.hostname, port: requestOptions.port };
}
const matches = requestOptions.host?.match(/^([^:/ ]+)(:\d{1,5})?/) || null;
const hostname =
requestOptions.hostname || (matches === null ? 'localhost' : matches[1]);
let port = requestOptions.port;
if (!port) {
if (matches && matches[2]) {
// remove the leading ":". The extracted port would be something like ":8080"
port = matches[2].substring(1);
} else {
port = requestOptions.protocol === 'https:' ? '443' : '80';
}
}
return { hostname, port };
};
/**
* Returns outgoing request attributes scoped to the options passed to the request
* @param {ParsedRequestOptions} requestOptions the same options used to make the request
* @param {{ component: string, hostname: string, hookAttributes?: SpanAttributes }} options used to pass data needed to create attributes
*/
export const getOutgoingRequestAttributes = (
requestOptions: ParsedRequestOptions,
options: {
component: string;
hostname: string;
port: string | number;
hookAttributes?: SpanAttributes;
}
): SpanAttributes => {
const hostname = options.hostname;
const port = options.port;
const requestMethod = requestOptions.method;
const method = requestMethod ? requestMethod.toUpperCase() : 'GET';
const headers = requestOptions.headers || {};
const userAgent = headers['user-agent'];
const attributes: SpanAttributes = {
[SEMATTRS_HTTP_URL]: getAbsoluteUrl(
requestOptions,
headers,
`${options.component}:`
),
[SEMATTRS_HTTP_METHOD]: method,
[SEMATTRS_HTTP_TARGET]: requestOptions.path || '/',
[SEMATTRS_NET_PEER_NAME]: hostname,
[SEMATTRS_HTTP_HOST]: headers.host ?? `${hostname}:${port}`,
};
if (userAgent !== undefined) {
attributes[SEMATTRS_HTTP_USER_AGENT] = userAgent;
}
return Object.assign(attributes, options.hookAttributes);
};
/**
* Returns outgoing request Metric attributes scoped to the request data
* @param {SpanAttributes} spanAttributes the span attributes
*/
export const getOutgoingRequestMetricAttributes = (
spanAttributes: SpanAttributes
): MetricAttributes => {
const metricAttributes: MetricAttributes = {};
metricAttributes[SEMATTRS_HTTP_METHOD] = spanAttributes[SEMATTRS_HTTP_METHOD];
metricAttributes[SEMATTRS_NET_PEER_NAME] =
spanAttributes[SEMATTRS_NET_PEER_NAME];
//TODO: http.url attribute, it should substitute any parameters to avoid high cardinality.
return metricAttributes;
};
/**
* Returns attributes related to the kind of HTTP protocol used
* @param {string} [kind] Kind of HTTP protocol used: "1.0", "1.1", "2", "SPDY" or "QUIC".
*/
export const setAttributesFromHttpKind = (
kind: string | undefined,
attributes: SpanAttributes
): void => {
if (kind) {
attributes[SEMATTRS_HTTP_FLAVOR] = kind;
if (kind.toUpperCase() !== 'QUIC') {
attributes[SEMATTRS_NET_TRANSPORT] = NETTRANSPORTVALUES_IP_TCP;
} else {
attributes[SEMATTRS_NET_TRANSPORT] = NETTRANSPORTVALUES_IP_UDP;
}
}
};
/**
* Returns outgoing request attributes scoped to the response data
* @param {IncomingMessage} response the response object
* @param {{ hostname: string }} options used to pass data needed to create attributes
*/
export const getOutgoingRequestAttributesOnResponse = (
response: IncomingMessage
): SpanAttributes => {
const { statusCode, statusMessage, httpVersion, socket } = response;
const attributes: SpanAttributes = {};
if (socket) {
const { remoteAddress, remotePort } = socket;
attributes[SEMATTRS_NET_PEER_IP] = remoteAddress;
attributes[SEMATTRS_NET_PEER_PORT] = remotePort;
}
setResponseContentLengthAttribute(response, attributes);
if (statusCode) {
attributes[SEMATTRS_HTTP_STATUS_CODE] = statusCode;
attributes[AttributeNames.HTTP_STATUS_TEXT] = (
statusMessage || ''
).toUpperCase();
}
setAttributesFromHttpKind(httpVersion, attributes);
return attributes;
};
/**
* Returns outgoing request Metric attributes scoped to the response data
* @param {SpanAttributes} spanAttributes the span attributes
*/
export const getOutgoingRequestMetricAttributesOnResponse = (
spanAttributes: SpanAttributes
): MetricAttributes => {
const metricAttributes: MetricAttributes = {};
metricAttributes[SEMATTRS_NET_PEER_PORT] =
spanAttributes[SEMATTRS_NET_PEER_PORT];
metricAttributes[SEMATTRS_HTTP_STATUS_CODE] =
spanAttributes[SEMATTRS_HTTP_STATUS_CODE];
metricAttributes[SEMATTRS_HTTP_FLAVOR] = spanAttributes[SEMATTRS_HTTP_FLAVOR];
return metricAttributes;
};
/**
* Returns incoming request attributes scoped to the request data
* @param {IncomingMessage} request the request object
* @param {{ component: string, serverName?: string, hookAttributes?: SpanAttributes }} options used to pass data needed to create attributes
*/
export const getIncomingRequestAttributes = (
request: IncomingMessage,
options: {
component: string;
serverName?: string;
hookAttributes?: SpanAttributes;
}
): SpanAttributes => {
const headers = request.headers;
const userAgent = headers['user-agent'];
const ips = headers['x-forwarded-for'];
const method = request.method || 'GET';
const httpVersion = request.httpVersion;
const requestUrl = request.url ? url.parse(request.url) : null;
const host = requestUrl?.host || headers.host;
const hostname =
requestUrl?.hostname ||
host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') ||
'localhost';
const serverName = options.serverName;
const attributes: SpanAttributes = {
[SEMATTRS_HTTP_URL]: getAbsoluteUrl(
requestUrl,
headers,
`${options.component}:`
),
[SEMATTRS_HTTP_HOST]: host,
[SEMATTRS_NET_HOST_NAME]: hostname,
[SEMATTRS_HTTP_METHOD]: method,
[SEMATTRS_HTTP_SCHEME]: options.component,
};
if (typeof ips === 'string') {
attributes[SEMATTRS_HTTP_CLIENT_IP] = ips.split(',')[0];
}
if (typeof serverName === 'string') {
attributes[SEMATTRS_HTTP_SERVER_NAME] = serverName;
}
if (requestUrl) {
attributes[SEMATTRS_HTTP_TARGET] = requestUrl.path || '/';
}
if (userAgent !== undefined) {
attributes[SEMATTRS_HTTP_USER_AGENT] = userAgent;
}
setRequestContentLengthAttribute(request, attributes);
setAttributesFromHttpKind(httpVersion, attributes);
return Object.assign(attributes, options.hookAttributes);
};
/**
* Returns incoming request Metric attributes scoped to the request data
* @param {SpanAttributes} spanAttributes the span attributes
* @param {{ component: string }} options used to pass data needed to create attributes
*/
export const getIncomingRequestMetricAttributes = (
spanAttributes: SpanAttributes
): MetricAttributes => {
const metricAttributes: MetricAttributes = {};
metricAttributes[SEMATTRS_HTTP_SCHEME] = spanAttributes[SEMATTRS_HTTP_SCHEME];
metricAttributes[SEMATTRS_HTTP_METHOD] = spanAttributes[SEMATTRS_HTTP_METHOD];
metricAttributes[SEMATTRS_NET_HOST_NAME] =
spanAttributes[SEMATTRS_NET_HOST_NAME];
metricAttributes[SEMATTRS_HTTP_FLAVOR] = spanAttributes[SEMATTRS_HTTP_FLAVOR];
//TODO: http.target attribute, it should substitute any parameters to avoid high cardinality.
return metricAttributes;
};
/**
* Returns incoming request attributes scoped to the response data
* @param {(ServerResponse & { socket: Socket; })} response the response object
*/
export const getIncomingRequestAttributesOnResponse = (
request: IncomingMessage,
response: ServerResponse
): SpanAttributes => {
// take socket from the request,
// since it may be detached from the response object in keep-alive mode
const { socket } = request;
const { statusCode, statusMessage } = response;
const rpcMetadata = getRPCMetadata(context.active());
const attributes: SpanAttributes = {};
if (socket) {
const { localAddress, localPort, remoteAddress, remotePort } = socket;
attributes[SEMATTRS_NET_HOST_IP] = localAddress;
attributes[SEMATTRS_NET_HOST_PORT] = localPort;
attributes[SEMATTRS_NET_PEER_IP] = remoteAddress;
attributes[SEMATTRS_NET_PEER_PORT] = remotePort;
}
attributes[SEMATTRS_HTTP_STATUS_CODE] = statusCode;
attributes[AttributeNames.HTTP_STATUS_TEXT] = (
statusMessage || ''
).toUpperCase();
if (rpcMetadata?.type === RPCType.HTTP && rpcMetadata.route !== undefined) {
attributes[SEMATTRS_HTTP_ROUTE] = rpcMetadata.route;
}
return attributes;
};
/**
* Returns incoming request Metric attributes scoped to the request data
* @param {SpanAttributes} spanAttributes the span attributes
*/
export const getIncomingRequestMetricAttributesOnResponse = (
spanAttributes: SpanAttributes
): MetricAttributes => {
const metricAttributes: MetricAttributes = {};
metricAttributes[SEMATTRS_HTTP_STATUS_CODE] =
spanAttributes[SEMATTRS_HTTP_STATUS_CODE];
metricAttributes[SEMATTRS_NET_HOST_PORT] =
spanAttributes[SEMATTRS_NET_HOST_PORT];
if (spanAttributes[SEMATTRS_HTTP_ROUTE] !== undefined) {
metricAttributes[SEMATTRS_HTTP_ROUTE] = spanAttributes[SEMATTRS_HTTP_ROUTE];
}
return metricAttributes;
};
export function headerCapture(type: 'request' | 'response', headers: string[]) {
const normalizedHeaders = new Map<string, string>();
for (let i = 0, len = headers.length; i < len; i++) {
const capturedHeader = headers[i].toLowerCase();
normalizedHeaders.set(capturedHeader, capturedHeader.replace(/-/g, '_'));
}
return (
span: Span,
getHeader: (key: string) => undefined | string | string[] | number
) => {
for (const capturedHeader of normalizedHeaders.keys()) {
const value = getHeader(capturedHeader);
if (value === undefined) {
continue;
}
const normalizedHeader = normalizedHeaders.get(capturedHeader);
const key = `http.${type}.header.${normalizedHeader}`;
if (typeof value === 'string') {
span.setAttribute(key, [value]);
} else if (Array.isArray(value)) {
span.setAttribute(key, value);
} else {
span.setAttribute(key, [value]);
}
}
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
#!/usr/bin/env sh
#
# Regenerate TLS certificates that are used by "examples/https/".
# Certs are generated with a one year expiry, so periodic regen is required.
#
# Usage: npm run maint:regenerate-test-certs
EXAMPLE_DIR="../../../../../examples/https"
openssl req -x509 -nodes -newkey rsa -keyout server-key.pem -out server-cert.pem -days 3650 -subj "/C=CL/ST=RM/L=OpenTelemetryTest/O=Root/OU=Test/CN=ca"
cp ./server-*.pem "$EXAMPLE_DIR/"

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDozCCAougAwIBAgIUNmVva8tI/hqzZAB7FZzPmNr/Q0owDQYJKoZIhvcNAQEL
BQAwYTELMAkGA1UEBhMCQ0wxCzAJBgNVBAgMAlJNMRowGAYDVQQHDBFPcGVuVGVs
ZW1ldHJ5VGVzdDENMAsGA1UECgwEUm9vdDENMAsGA1UECwwEVGVzdDELMAkGA1UE
AwwCY2EwHhcNMjQwNjA4MTkxODUyWhcNMzQwNjA2MTkxODUyWjBhMQswCQYDVQQG
EwJDTDELMAkGA1UECAwCUk0xGjAYBgNVBAcMEU9wZW5UZWxlbWV0cnlUZXN0MQ0w
CwYDVQQKDARSb290MQ0wCwYDVQQLDARUZXN0MQswCQYDVQQDDAJjYTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAM80Psz6Kc4/x1Qx4OmYuacrCDL4JNY+
SWzEm1kb25/E1+TrxYC8XluJtXwi9JRC7ovR8UIbZNVgWwPHlSXrTQsVe2sISr4K
oRg87vmcRYzsOval+ZViFelUo9t5sYeiX0xBuMP1ENfEL+gtu2QrkmgDhy/XdRpm
qpcrtJxXaIXedy82y7Av5da7jJyJ+9oaTq0Am0qyZjpzTs3jKEh1roWoc3pd6T4d
asNHLeKrsq3bTasEH/Lo9k4t+2n1j5C+L5gWhPcCv8yzhGsrSiqxn+P9m/USXY/w
HH9AyV7Dk4Jh8BjUOlYaqOLIEow7hwdxVBk1BO6Jmpxv8KI3qQjtLWsCAwEAAaNT
MFEwHQYDVR0OBBYEFOueTx80MsHFiBvP0RO99CpVdj0ZMB8GA1UdIwQYMBaAFOue
Tx80MsHFiBvP0RO99CpVdj0ZMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
BQADggEBAJYqqU3wCDX+YZDZ4zd8k725MSfKk1omCxDzDzjuNAjqD/vuSE0YfdFB
bTWMiBGsCqa6FgWwyT2oMPnJKShScZReFEZcM2s+/odfwUTwF4lD/UvZf6PdDhJ5
kxqhoej2ZT8NY+m+rddmg5MaurIcIfOhOxRjarmhs2lrg5qmGKznauettsmA1ZpR
d2ZZA10WstCKN7jHOUqUmSB/2oC/NZno09yXqgbNwIEwBgsUDNaYkGzIzsKD+mOy
NTxLo/PJ5venVgboomDkWJqOOm+HtqfD64rczWyTS/ZdQhuFlk+aCYAwOhYZZH9Z
IEkKcW8xN5+q8Bi9nsst5F1cuw84LD4=
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDPND7M+inOP8dU
MeDpmLmnKwgy+CTWPklsxJtZG9ufxNfk68WAvF5bibV8IvSUQu6L0fFCG2TVYFsD
x5Ul600LFXtrCEq+CqEYPO75nEWM7Dr2pfmVYhXpVKPbebGHol9MQbjD9RDXxC/o
LbtkK5JoA4cv13UaZqqXK7ScV2iF3ncvNsuwL+XWu4ycifvaGk6tAJtKsmY6c07N
4yhIda6FqHN6Xek+HWrDRy3iq7Kt202rBB/y6PZOLftp9Y+Qvi+YFoT3Ar/Ms4Rr
K0oqsZ/j/Zv1El2P8Bx/QMlew5OCYfAY1DpWGqjiyBKMO4cHcVQZNQTuiZqcb/Ci
N6kI7S1rAgMBAAECggEAA4u31WckhwCFZzXxTN4Ns0QjwEeJzdbEB+X3aJd28y15
qlO0jgbrMHLTVbv9ZnocvezKTaDNYW9kL1TfgwYbS+jCs2qxaL4i0pjcWA4qDIbQ
XX8qnblPea42nZ9f/9ux+MqZIFVVw/N6BGWf8s9iEoCY9CMiQGDoY2zeitxeEAhe
KGZHozaTUVAClrwMqKYlablWjX8zcoi6bKffUwz/MwecI2iVW72y9/xIGTlwhdZW
8kDlkBOea3QV5yafcJQ1VsHrvUX3rf4C5uknCenwKbC3Au4+kBdQjv+bUk6aMR38
OdnMCaUcnuT/UzbbtOSgJ+sVcGW9Jqo0DfS5CNPhuQKBgQD2EtOhjkUErc8KUhPW
Pt8FNBAuoKsk4mRHpFsY2OjGYA50oiCtiphqfTzkQLbNhVtq83AHNUNWUKBjnMUd
nAOIIvhpiEI7O+RuOafBtcFBIrUo2Ey5dwfvLJrpb7pEnYj8DNFVPQ/aBxP+mD4b
RrHRY8DZdVHU9TDMDK7ObpKScwKBgQDXkASnc7O/D0iFxev004sqdTrGo3cBCJUH
h/c68BrUvPw5u6fFulq5Wt6OOaFoWQmUSVyrhZD3vfrZ1Jocid3wn9uuu9vOazcz
M3EOF1rz5bgjwb/vk6CyGh1MfmeVCRnuQ3mBIXQ0xwS4QY71Vvw3099fhbtbMHez
5gyQv1ojKQKBgHkXBqGHmxsUVL/lZ/GT+rdayWPImK+L4IEItfNiOhn0huiKJ75+
Z0Zv3pkL52j3aOcWaFiDs2E0R9j8+lr5vRsu6kM0VDNF48ECCjVetXzSEYfJ+OeD
VZoyvwCEhIrjYtd1j0V8yOInfYVZcaaGgVZi2jGoc95x0dN3okXfT61VAoGAVrCA
bqdFPizzSXkTL15ztRt4C7JphWziIik37+fnom5uMPmauxX6rlgw8JmUo9ZK1+u4
7PHGHxuz6i5p29hF/SMMwfSirRzJEjo52STjWvqjVx3wBnUkRvvhlIWxOmpylzNj
hd7iUOdIqqES0O+udvUp+8xYUI/pU9vMadnKciECgYB3cJ+7EPNgJgdpQ9Pslex8
6iHdQH9DIyIb/KqQVe7em5NZXwANCAOQ6KqzhszM/eH7NoSeN7C9Z9uHHIHDfz4m
QO896l7rM1k1R/USaUK4By/O/t/V/aIGjOnPsXxEGrEPDXXBtzUAEs5yAa+Ne1m7
BiP9MuDgBTVkV1iJx37eqg==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,90 @@
/*
* 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.
*/
import * as assert from 'assert';
import { HttpInstrumentation } from '../../src/http';
import { AddressInfo } from 'net';
import * as nock from 'nock';
import * as sinon from 'sinon';
import { httpRequest } from '../utils/httpRequest';
import { isWrapped } from '@opentelemetry/instrumentation';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
import {
trace,
TracerProvider,
INVALID_SPAN_CONTEXT,
} from '@opentelemetry/api';
describe('HttpInstrumentation', () => {
let server: http.Server;
let serverPort = 0;
describe('disable()', () => {
let provider: TracerProvider;
let startSpanStub: sinon.SinonStub;
before(() => {
provider = {
getTracer: () => {
startSpanStub = sinon
.stub()
.returns(trace.wrapSpanContext(INVALID_SPAN_CONTEXT));
return { startSpan: startSpanStub } as any;
},
};
nock.cleanAll();
nock.enableNetConnect();
instrumentation.enable();
assert.strictEqual(isWrapped(http.Server.prototype.emit), true);
instrumentation.setTracerProvider(provider);
server = http.createServer((request, response) => {
response.end('Test Server Response');
});
server.listen(serverPort);
server.once('listening', () => {
serverPort = (server.address() as AddressInfo).port;
});
});
afterEach(() => {
sinon.restore();
});
after(() => {
server.close();
});
describe('unpatch()', () => {
it('should not call provider methods for creating span', async () => {
instrumentation.disable();
assert.strictEqual(isWrapped(http.Server.prototype.emit), false);
const testPath = '/incoming/unpatch/';
const options = { host: 'localhost', path: testPath, port: serverPort };
await httpRequest.get(options).then(() => {
sinon.assert.notCalled(startSpanStub);
});
});
});
});
});

View File

@ -0,0 +1,160 @@
/*
* 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.
*/
import {
AggregationTemporality,
DataPointType,
InMemoryMetricExporter,
MeterProvider,
} from '@opentelemetry/sdk-metrics';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
SEMATTRS_HTTP_FLAVOR,
SEMATTRS_HTTP_METHOD,
SEMATTRS_HTTP_SCHEME,
SEMATTRS_HTTP_STATUS_CODE,
SEMATTRS_NET_HOST_NAME,
SEMATTRS_NET_HOST_PORT,
SEMATTRS_NET_PEER_NAME,
SEMATTRS_NET_PEER_PORT,
} from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import { HttpInstrumentation } from '../../src/http';
import { httpRequest } from '../utils/httpRequest';
import { TestMetricReader } from '../utils/TestMetricReader';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
let server: http.Server;
const serverPort = 22346;
const protocol = 'http';
const hostname = 'localhost';
const pathname = '/test';
const tracerProvider = new NodeTracerProvider();
const metricsMemoryExporter = new InMemoryMetricExporter(
AggregationTemporality.DELTA
);
const metricReader = new TestMetricReader(metricsMemoryExporter);
const meterProvider = new MeterProvider({ readers: [metricReader] });
instrumentation.setTracerProvider(tracerProvider);
instrumentation.setMeterProvider(meterProvider);
describe('metrics', () => {
beforeEach(() => {
metricsMemoryExporter.reset();
});
before(() => {
instrumentation.enable();
server = http.createServer((request, response) => {
response.end('Test Server Response');
});
server.listen(serverPort);
});
after(() => {
server.close();
instrumentation.disable();
});
it('should add server/client duration metrics', async () => {
const requestCount = 3;
for (let i = 0; i < requestCount; i++) {
await httpRequest.get(
`${protocol}://${hostname}:${serverPort}${pathname}`
);
}
await metricReader.collectAndExport();
const resourceMetrics = metricsMemoryExporter.getMetrics();
const scopeMetrics = resourceMetrics[0].scopeMetrics;
assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count');
const metrics = scopeMetrics[0].metrics;
assert.strictEqual(metrics.length, 2, 'metrics count');
assert.strictEqual(metrics[0].dataPointType, DataPointType.HISTOGRAM);
assert.strictEqual(
metrics[0].descriptor.description,
'Measures the duration of inbound HTTP requests.'
);
assert.strictEqual(metrics[0].descriptor.name, 'http.server.duration');
assert.strictEqual(metrics[0].descriptor.unit, 'ms');
assert.strictEqual(metrics[0].dataPoints.length, 1);
assert.strictEqual(
(metrics[0].dataPoints[0].value as any).count,
requestCount
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_SCHEME],
'http'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_METHOD],
'GET'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_FLAVOR],
'1.1'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[SEMATTRS_NET_HOST_NAME],
'localhost'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_STATUS_CODE],
200
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[SEMATTRS_NET_HOST_PORT],
22346
);
assert.strictEqual(metrics[1].dataPointType, DataPointType.HISTOGRAM);
assert.strictEqual(
metrics[1].descriptor.description,
'Measures the duration of outbound HTTP requests.'
);
assert.strictEqual(metrics[1].descriptor.name, 'http.client.duration');
assert.strictEqual(metrics[1].descriptor.unit, 'ms');
assert.strictEqual(metrics[1].dataPoints.length, 1);
assert.strictEqual(
(metrics[1].dataPoints[0].value as any).count,
requestCount
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_METHOD],
'GET'
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[SEMATTRS_NET_PEER_NAME],
'localhost'
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[SEMATTRS_NET_PEER_PORT],
22346
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_STATUS_CODE],
200
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_FLAVOR],
'1.1'
);
});
});

View File

@ -0,0 +1,144 @@
/*
* 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.
*/
import { context, SpanKind, Span, propagation } from '@opentelemetry/api';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import * as assert from 'assert';
import * as path from 'path';
import * as url from 'url';
import { HttpInstrumentation } from '../../src/http';
import { assertSpan } from '../utils/assertSpan';
import { DummyPropagation } from '../utils/DummyPropagation';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
import request from 'request-promise-native';
import * as superagent from 'superagent';
// Temporarily removed. See https://github.com/open-telemetry/opentelemetry-js/issues/3344
// import * as got from 'got';
import * as nock from 'nock';
import axios, { AxiosResponse } from 'axios';
const memoryExporter = new InMemorySpanExporter();
const protocol = 'http';
const customAttributeFunction = (span: Span): void => {
span.setAttribute('span kind', SpanKind.CLIENT);
};
describe('Packages', () => {
beforeEach(() => {
context.setGlobalContextManager(new AsyncHooksContextManager().enable());
});
afterEach(() => {
context.disable();
});
describe('get', () => {
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
instrumentation.setTracerProvider(provider);
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
propagation.setGlobalPropagator(new DummyPropagation());
instrumentation.setConfig({
applyCustomAttributesOnSpan: customAttributeFunction,
});
instrumentation.enable();
});
after(() => {
// back to normal
propagation.disable();
nock.cleanAll();
nock.enableNetConnect();
});
let resHeaders: http.IncomingHttpHeaders;
[
{ name: 'axios', httpPackage: axios }, //keep first
{ name: 'superagent', httpPackage: superagent },
// { name: 'got', httpPackage: { get: (url: string) => got(url) } },
{
name: 'request',
httpPackage: { get: (url: string) => request(url) },
},
].forEach(({ name, httpPackage }) => {
it(`should create a span for GET requests and add propagation headers by using ${name} package`, async () => {
if (process.versions.node.startsWith('12') && name === 'got') {
// got complains with nock and node version 12+
// > RequestError: The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type function
// so let's make a real call
nock.cleanAll();
nock.enableNetConnect();
} else {
nock.load(path.join(__dirname, '../', '/fixtures/google-http.json'));
}
const urlparsed = url.parse(
`${protocol}://www.google.com/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8`
);
const result = await httpPackage.get(urlparsed.href!);
if (!resHeaders) {
const res = result as AxiosResponse<unknown>;
resHeaders = res.headers as any;
}
const spans = memoryExporter.getFinishedSpans();
const span = spans[0];
const validations = {
hostname: urlparsed.hostname!,
httpStatusCode: 200,
httpMethod: 'GET',
pathname: urlparsed.pathname!,
path: urlparsed.path,
resHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 1);
assert.strictEqual(span.name, 'GET');
switch (name) {
case 'axios':
assert.ok(
result.request._headers[DummyPropagation.TRACE_CONTEXT_KEY]
);
assert.ok(
result.request._headers[DummyPropagation.SPAN_CONTEXT_KEY]
);
break;
case 'got':
case 'superagent':
break;
default:
break;
}
assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT);
assertSpan(span, SpanKind.CLIENT, validations);
});
});
});
});

View File

@ -0,0 +1,98 @@
/*
* 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.
*/
import * as assert from 'assert';
import * as fs from 'fs';
import type { AddressInfo } from 'net';
import * as nock from 'nock';
import * as sinon from 'sinon';
import { HttpInstrumentation } from '../../src';
import { isWrapped } from '@opentelemetry/instrumentation';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as https from 'https';
import { httpsRequest } from '../utils/httpsRequest';
import {
INVALID_SPAN_CONTEXT,
trace,
TracerProvider,
} from '@opentelemetry/api';
describe('HttpsInstrumentation', () => {
let server: https.Server;
let serverPort = 0;
describe('disable()', () => {
let provider: TracerProvider;
let startSpanStub: sinon.SinonStub;
before(() => {
provider = {
getTracer: () => {
startSpanStub = sinon
.stub()
.returns(trace.wrapSpanContext(INVALID_SPAN_CONTEXT));
return { startSpan: startSpanStub } as any;
},
};
nock.cleanAll();
nock.enableNetConnect();
instrumentation.enable();
assert.strictEqual(isWrapped(https.Server.prototype.emit), true);
instrumentation.setTracerProvider(provider);
server = https.createServer(
{
key: fs.readFileSync('test/fixtures/server-key.pem'),
cert: fs.readFileSync('test/fixtures/server-cert.pem'),
},
(request, response) => {
response.end('Test Server Response');
}
);
server.listen(serverPort);
server.once('listening', () => {
serverPort = (server.address() as AddressInfo).port;
});
});
afterEach(() => {
sinon.restore();
});
after(() => {
server.close();
});
describe('unpatch()', () => {
it('should not call tracer methods for creating span', async () => {
instrumentation.disable();
const testPath = '/incoming/unpatch/';
const options = { host: 'localhost', path: testPath, port: serverPort };
await httpsRequest.get(options).then(() => {
sinon.assert.notCalled(startSpanStub);
assert.strictEqual(isWrapped(https.Server.prototype.emit), false);
});
});
});
});
});

View File

@ -0,0 +1,764 @@
/*
* 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.
*/
import {
SpanStatusCode,
context,
propagation,
Span as ISpan,
SpanKind,
trace,
} from '@opentelemetry/api';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import { ContextManager } from '@opentelemetry/api';
import {
BasicTracerProvider,
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import {
NETTRANSPORTVALUES_IP_TCP,
SEMATTRS_HTTP_CLIENT_IP,
SEMATTRS_HTTP_FLAVOR,
SEMATTRS_HTTP_STATUS_CODE,
SEMATTRS_NET_HOST_PORT,
SEMATTRS_NET_PEER_PORT,
SEMATTRS_NET_TRANSPORT,
} from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import * as fs from 'fs';
import nock from 'nock';
import * as path from 'path';
import { HttpInstrumentation } from '../../src/http';
import { assertSpan } from '../utils/assertSpan';
import { DummyPropagation } from '../utils/DummyPropagation';
import { isWrapped } from '@opentelemetry/instrumentation';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
import * as https from 'https';
import { httpsRequest } from '../utils/httpsRequest';
const applyCustomAttributesOnSpanErrorMessage =
'bad applyCustomAttributesOnSpan function';
let server: https.Server;
const serverPort = 32345;
const protocol = 'https';
const hostname = 'localhost';
const serverName = 'my.server.name';
const pathname = '/test';
const memoryExporter = new InMemorySpanExporter();
const provider = new BasicTracerProvider();
instrumentation.setTracerProvider(provider);
const tracer = provider.getTracer('test-https');
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
function doNock(
hostname: string,
path: string,
httpCode: number,
respBody: string,
times?: number
) {
const i = times || 1;
nock(`${protocol}://${hostname}`)
.get(path)
.times(i)
.reply(httpCode, respBody);
}
export const customAttributeFunction = (span: ISpan): void => {
span.setAttribute('span kind', SpanKind.CLIENT);
};
describe('HttpsInstrumentation', () => {
let contextManager: ContextManager;
beforeEach(() => {
contextManager = new AsyncHooksContextManager().enable();
propagation.setGlobalPropagator(new DummyPropagation());
context.setGlobalContextManager(contextManager);
});
afterEach(() => {
contextManager.disable();
context.disable();
propagation.disable();
});
describe('enable()', () => {
describe('with bad instrumentation options', () => {
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
instrumentation.setConfig({
ignoreIncomingPaths: [
(url: string) => {
throw new Error('bad ignoreIncomingPaths function');
},
],
ignoreIncomingRequestHook: _request => {
throw new Error('bad ignoreIncomingRequestHook function');
},
ignoreOutgoingUrls: [
(url: string) => {
throw new Error('bad ignoreOutgoingUrls function');
},
],
ignoreOutgoingRequestHook: _request => {
throw new Error('bad ignoreOutgoingRequestHook function');
},
applyCustomAttributesOnSpan: () => {
throw new Error(applyCustomAttributesOnSpanErrorMessage);
},
});
instrumentation.enable();
server = https.createServer(
{
key: fs.readFileSync('test/fixtures/server-key.pem'),
cert: fs.readFileSync('test/fixtures/server-cert.pem'),
},
(request, response) => {
response.end('Test Server Response');
}
);
server.listen(serverPort);
});
after(() => {
server.close();
instrumentation.disable();
});
it('should generate valid spans (client side and server side)', async () => {
const result = await httpsRequest.get(
`${protocol}://${hostname}:${serverPort}${pathname}`,
{
headers: {
'user-agent': 'tester',
},
}
);
const spans = memoryExporter.getFinishedSpans();
const [incomingSpan, outgoingSpan] = spans;
const validations = {
hostname,
httpStatusCode: result.statusCode!,
httpMethod: result.method!,
pathname,
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
assert.strictEqual(
incomingSpan.attributes[SEMATTRS_NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[SEMATTRS_NET_PEER_PORT],
serverPort
);
});
});
describe('with good instrumentation options', () => {
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
instrumentation.setConfig({
ignoreIncomingPaths: [
'/ignored/string',
/\/ignored\/regexp$/i,
(url: string) => url.endsWith('/ignored/function'),
],
ignoreIncomingRequestHook: request => {
return (
request.headers['user-agent']?.match('ignored-string') != null
);
},
ignoreOutgoingUrls: [
`${protocol}://${hostname}:${serverPort}/ignored/string`,
/\/ignored\/regexp$/i,
(url: string) => url.endsWith('/ignored/function'),
],
ignoreOutgoingRequestHook: request => {
if (request.headers?.['user-agent'] != null) {
return (
`${request.headers['user-agent']}`.match('ignored-string') !=
null
);
}
return false;
},
applyCustomAttributesOnSpan: customAttributeFunction,
serverName,
});
instrumentation.enable();
server = https.createServer(
{
key: fs.readFileSync('test/fixtures/server-key.pem'),
cert: fs.readFileSync('test/fixtures/server-cert.pem'),
},
(request, response) => {
if (request.url?.includes('/ignored')) {
tracer.startSpan('some-span').end();
}
response.end('Test Server Response');
}
);
server.listen(serverPort);
});
after(() => {
server.close();
instrumentation.disable();
});
it(`${protocol} module should be patched`, () => {
assert.strictEqual(isWrapped(https.Server.prototype.emit), true);
});
it('should generate valid spans (client side and server side)', async () => {
const result = await httpsRequest.get(
`${protocol}://${hostname}:${serverPort}${pathname}`,
{
headers: {
'x-forwarded-for': '<client>, <proxy1>, <proxy2>',
'user-agent': 'chrome',
},
}
);
const spans = memoryExporter.getFinishedSpans();
const [incomingSpan, outgoingSpan] = spans;
const validations = {
hostname,
httpStatusCode: result.statusCode!,
httpMethod: result.method!,
pathname,
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
serverName,
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(
incomingSpan.attributes[SEMATTRS_HTTP_CLIENT_IP],
'<client>'
);
assert.strictEqual(
incomingSpan.attributes[SEMATTRS_NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[SEMATTRS_NET_PEER_PORT],
serverPort
);
[
{ span: incomingSpan, kind: SpanKind.SERVER },
{ span: outgoingSpan, kind: SpanKind.CLIENT },
].forEach(({ span, kind }) => {
assert.strictEqual(span.attributes[SEMATTRS_HTTP_FLAVOR], '1.1');
assert.strictEqual(
span.attributes[SEMATTRS_NET_TRANSPORT],
NETTRANSPORTVALUES_IP_TCP
);
assertSpan(span, kind, validations);
});
});
const httpErrorCodes = [400, 401, 403, 404, 429, 501, 503, 504, 500, 505];
for (let i = 0; i < httpErrorCodes.length; i++) {
it(`should test span for GET requests with http error ${httpErrorCodes[i]}`, async () => {
const testPath = '/outgoing/rootSpan/1';
doNock(
hostname,
testPath,
httpErrorCodes[i],
httpErrorCodes[i].toString()
);
const isReset = memoryExporter.getFinishedSpans().length === 0;
assert.ok(isReset);
const result = await httpsRequest.get(
`${protocol}://${hostname}${testPath}`
);
const spans = memoryExporter.getFinishedSpans();
const reqSpan = spans[0];
assert.strictEqual(result.data, httpErrorCodes[i].toString());
assert.strictEqual(spans.length, 1);
const validations = {
hostname,
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: testPath,
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assertSpan(reqSpan, SpanKind.CLIENT, validations);
});
}
it('should create a child span for GET requests', async () => {
const testPath = '/outgoing/rootSpan/childs/1';
doNock(hostname, testPath, 200, 'Ok');
const name = 'TestRootSpan';
const span = tracer.startSpan(name);
return context.with(trace.setSpan(context.active(), span), async () => {
const result = await httpsRequest.get(
`${protocol}://${hostname}${testPath}`
);
span.end();
const spans = memoryExporter.getFinishedSpans();
const [reqSpan, localSpan] = spans;
const validations = {
hostname,
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: testPath,
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0);
assert.strictEqual(spans.length, 2);
assert.strictEqual(reqSpan.name, 'GET');
assert.strictEqual(
localSpan.spanContext().traceId,
reqSpan.spanContext().traceId
);
assertSpan(reqSpan, SpanKind.CLIENT, validations);
assert.notStrictEqual(
localSpan.spanContext().spanId,
reqSpan.spanContext().spanId
);
});
});
for (let i = 0; i < httpErrorCodes.length; i++) {
it(`should test child spans for GET requests with http error ${httpErrorCodes[i]}`, async () => {
const testPath = '/outgoing/rootSpan/childs/1';
doNock(
hostname,
testPath,
httpErrorCodes[i],
httpErrorCodes[i].toString()
);
const name = 'TestRootSpan';
const span = tracer.startSpan(name);
return context.with(
trace.setSpan(context.active(), span),
async () => {
const result = await httpsRequest.get(
`${protocol}://${hostname}${testPath}`
);
span.end();
const spans = memoryExporter.getFinishedSpans();
const [reqSpan, localSpan] = spans;
const validations = {
hostname,
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: testPath,
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0);
assert.strictEqual(spans.length, 2);
assert.strictEqual(reqSpan.name, 'GET');
assert.strictEqual(
localSpan.spanContext().traceId,
reqSpan.spanContext().traceId
);
assertSpan(reqSpan, SpanKind.CLIENT, validations);
assert.notStrictEqual(
localSpan.spanContext().spanId,
reqSpan.spanContext().spanId
);
}
);
});
}
it('should create multiple child spans for GET requests', async () => {
const testPath = '/outgoing/rootSpan/childs';
const num = 5;
doNock(hostname, testPath, 200, 'Ok', num);
const name = 'TestRootSpan';
const span = tracer.startSpan(name);
await context.with(trace.setSpan(context.active(), span), async () => {
for (let i = 0; i < num; i++) {
await httpsRequest.get(`${protocol}://${hostname}${testPath}`);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans[i].name, 'GET');
assert.strictEqual(
span.spanContext().traceId,
spans[i].spanContext().traceId
);
}
span.end();
const spans = memoryExporter.getFinishedSpans();
// 5 child spans ended + 1 span (root)
assert.strictEqual(spans.length, 6);
});
});
for (const ignored of ['string', 'function', 'regexp']) {
it(`should not trace ignored requests with paths (client and server side) with type ${ignored}`, async () => {
const testPath = `/ignored/${ignored}`;
await httpsRequest.get(
`${protocol}://${hostname}:${serverPort}${testPath}`
);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
});
}
it('should not trace ignored requests with headers (client and server side)', async () => {
const testValue = 'ignored-string';
await Promise.all([
httpsRequest.get(`${protocol}://${hostname}:${serverPort}`, {
headers: {
'user-agent': testValue,
},
}),
]);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
});
for (const arg of ['string', {}, new Date()]) {
it(`should be traceable and not throw exception in ${protocol} instrumentation when passing the following argument ${JSON.stringify(
arg
)}`, async () => {
try {
await httpsRequest.get(arg);
} catch (error: any) {
// request has been made
// nock throw
assert.ok(error.message.startsWith('Nock: No match for request'));
}
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
});
}
for (const arg of [true, 1, false, 0, '']) {
it(`should not throw exception in https instrumentation when passing the following argument ${JSON.stringify(
arg
)}`, async () => {
try {
await httpsRequest.get(arg as any);
} catch (error: any) {
// request has been made
// nock throw
assert.ok(
error.stack.indexOf(
path.normalize('/node_modules/nock/lib/intercept.js')
) > 0
);
}
const spans = memoryExporter.getFinishedSpans();
// for this arg with don't provide trace. We pass arg to original method (https.get)
assert.strictEqual(spans.length, 0);
});
}
it('should have 1 ended span when request throw on bad "options" object', () => {
try {
https.request({ protocol: 'telnet' });
} catch (error) {
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
}
});
it('should have 1 ended span when response.end throw an exception', async () => {
const testPath = '/outgoing/rootSpan/childs/1';
doNock(hostname, testPath, 400, 'Not Ok');
const promiseRequest = new Promise((resolve, reject) => {
const req = https.request(
`${protocol}://${hostname}${testPath}`,
(resp: http.IncomingMessage) => {
let data = '';
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
reject(new Error(data));
});
}
);
return req.end();
});
try {
await promiseRequest;
assert.fail();
} catch (error) {
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
}
});
it('should have 1 ended span when request throw on bad "options" object', () => {
nock.cleanAll();
nock.enableNetConnect();
try {
https.request({ protocol: 'telnet' });
assert.fail();
} catch (error) {
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
}
});
it('should have 2 ended spans when provided "options" are an object without a constructor', async () => {
// Related issue: https://github.com/open-telemetry/opentelemetry-js/issues/2008
const testPath = '/outgoing/test';
const options = Object.create(null);
options.hostname = hostname;
options.port = serverPort;
options.path = pathname;
options.method = 'GET';
doNock(hostname, testPath, 200, 'Ok');
const promiseRequest = new Promise((resolve, _reject) => {
const req = https.request(options, (resp: http.IncomingMessage) => {
resp.on('data', () => {});
resp.on('end', () => {
resolve({});
});
});
return req.end();
});
await promiseRequest;
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 2);
});
it('should have 1 ended span when response.end throw an exception', async () => {
const testPath = '/outgoing/rootSpan/childs/1';
doNock(hostname, testPath, 400, 'Not Ok');
const promiseRequest = new Promise((resolve, reject) => {
const req = https.request(
`${protocol}://${hostname}${testPath}`,
(resp: http.IncomingMessage) => {
let data = '';
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
reject(new Error(data));
});
}
);
return req.end();
});
try {
await promiseRequest;
assert.fail();
} catch (error) {
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
}
});
it('should have 1 ended span when request is aborted', async () => {
nock(`${protocol}://my.server.com`)
.get('/')
.delayConnection(50)
.reply(200, '<html></html>');
const promiseRequest = new Promise((resolve, reject) => {
const req = https.request(
`${protocol}://my.server.com`,
(resp: http.IncomingMessage) => {
let data = '';
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
resolve(data);
});
}
);
req.setTimeout(10, () => {
req.abort();
});
// Instrumentation should not swallow error event.
assert.strictEqual(req.listeners('error').length, 0);
req.on('error', err => {
reject(err);
});
return req.end();
});
await assert.rejects(promiseRequest, /Error: socket hang up/);
const spans = memoryExporter.getFinishedSpans();
const [span] = spans;
assert.strictEqual(spans.length, 1);
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
assert.ok(Object.keys(span.attributes).length >= 6);
});
it('should have 1 ended span when request is aborted after receiving response', async () => {
nock(`${protocol}://my.server.com`)
.get('/')
.delay({
body: 50,
})
.replyWithFile(200, `${process.cwd()}/package.json`);
const promiseRequest = new Promise((resolve, reject) => {
const req = https.request(
`${protocol}://my.server.com`,
(resp: http.IncomingMessage) => {
let data = '';
resp.on('data', chunk => {
req.destroy(Error('request destroyed'));
data += chunk;
});
resp.on('end', () => {
resolve(data);
});
}
);
// Instrumentation should not swallow error event.
assert.strictEqual(req.listeners('error').length, 0);
req.on('error', err => {
reject(err);
});
return req.end();
});
await assert.rejects(promiseRequest, /Error: request destroyed/);
const spans = memoryExporter.getFinishedSpans();
const [span] = spans;
assert.strictEqual(spans.length, 1);
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
assert.ok(Object.keys(span.attributes).length > 7);
});
it("should have 1 ended span when response is listened by using req.on('response')", done => {
const host = `${protocol}://${hostname}`;
nock(host).get('/').reply(404);
const req = https.request(`${host}/`);
req.on('response', response => {
response.on('data', () => {});
response.on('end', () => {
const spans = memoryExporter.getFinishedSpans();
const [span] = spans;
assert.strictEqual(spans.length, 1);
assert.ok(Object.keys(span.attributes).length > 6);
assert.strictEqual(span.attributes[SEMATTRS_HTTP_STATUS_CODE], 404);
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
done();
});
});
req.end();
});
});
describe('partially disable instrumentation', () => {
beforeEach(() => {
memoryExporter.reset();
});
afterEach(() => {
server.close();
instrumentation.disable();
});
it('allows to disable outgoing request instrumentation', () => {
instrumentation.setConfig({
disableOutgoingRequestInstrumentation: true,
});
instrumentation.enable();
server = https.createServer(
{
key: fs.readFileSync('test/fixtures/server-key.pem'),
cert: fs.readFileSync('test/fixtures/server-cert.pem'),
},
(request, response) => {
response.end('Test Server Response');
}
);
server.listen(serverPort);
assert.strictEqual(isWrapped(http.Server.prototype.emit), true);
assert.strictEqual(isWrapped(http.get), false);
assert.strictEqual(isWrapped(http.request), false);
});
it('allows to disable incoming request instrumentation', () => {
instrumentation.setConfig({
disableIncomingRequestInstrumentation: true,
});
instrumentation.enable();
server = https.createServer(
{
key: fs.readFileSync('test/fixtures/server-key.pem'),
cert: fs.readFileSync('test/fixtures/server-cert.pem'),
},
(request, response) => {
response.end('Test Server Response');
}
);
server.listen(serverPort);
assert.strictEqual(isWrapped(http.Server.prototype.emit), false);
assert.strictEqual(isWrapped(http.get), true);
assert.strictEqual(isWrapped(http.request), true);
});
});
});
});

View File

@ -0,0 +1,150 @@
/*
* 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.
*/
import { context, SpanKind, propagation, Span } from '@opentelemetry/api';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import * as assert from 'assert';
import * as path from 'path';
import * as url from 'url';
import { HttpInstrumentation } from '../../src/http';
import { assertSpan } from '../utils/assertSpan';
import { DummyPropagation } from '../utils/DummyPropagation';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
import request from 'request-promise-native';
import * as superagent from 'superagent';
// Temporarily removed. See https://github.com/open-telemetry/opentelemetry-js/issues/3344
// import * as got from 'got';
import * as nock from 'nock';
import axios, { AxiosResponse } from 'axios';
const memoryExporter = new InMemorySpanExporter();
const customAttributeFunction = (span: Span): void => {
span.setAttribute('span kind', SpanKind.CLIENT);
};
describe('Packages', () => {
beforeEach(() => {
memoryExporter.reset();
context.setGlobalContextManager(new AsyncHooksContextManager().enable());
});
afterEach(() => {
context.disable();
});
describe('get', () => {
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
instrumentation.setTracerProvider(provider);
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
propagation.setGlobalPropagator(new DummyPropagation());
instrumentation.setConfig({
applyCustomAttributesOnSpan: customAttributeFunction,
});
instrumentation.enable();
});
after(() => {
// back to normal
nock.cleanAll();
nock.enableNetConnect();
propagation.disable();
});
let resHeaders: http.IncomingHttpHeaders;
[
{ name: 'axios', httpPackage: axios }, //keep first
{ name: 'superagent', httpPackage: superagent },
// { name: 'got', httpPackage: { get: (url: string) => got(url) } },
{
name: 'request',
httpPackage: { get: (url: string) => request(url) },
},
].forEach(({ name, httpPackage }) => {
it(`should create a span for GET requests and add propagation headers by using ${name} package`, async () => {
if (process.versions.node.startsWith('12') && name === 'got') {
// got complains with nock and node version 12+
// > RequestError: The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type function
// so let's make a real call
nock.cleanAll();
nock.enableNetConnect();
} else {
nock.load(path.join(__dirname, '../', '/fixtures/google-https.json'));
}
const urlparsed = url.parse(
name === 'got' && process.versions.node.startsWith('12')
? // there is an issue with got 9.6 version and node 12 when redirecting so url above will not work
// https://github.com/nock/nock/pull/1551
// https://github.com/sindresorhus/got/commit/bf1aa5492ae2bc78cbbec6b7d764906fb156e6c2#diff-707a4781d57c42085155dcb27edb9ccbR258
// TODO: check if this is still the case when new version
'https://www.google.com'
: 'https://www.google.com/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8'
);
const result = await httpPackage.get(urlparsed.href!);
if (!resHeaders) {
const res = result as AxiosResponse<unknown>;
resHeaders = res.headers as any;
}
const spans = memoryExporter.getFinishedSpans();
const span = spans[0];
const validations = {
hostname: urlparsed.hostname!,
httpStatusCode: 200,
httpMethod: 'GET',
pathname: urlparsed.pathname!,
path: urlparsed.path,
resHeaders,
component: 'https',
};
assert.strictEqual(spans.length, 1);
assert.strictEqual(span.name, 'GET');
switch (name) {
case 'axios':
assert.ok(
result.request._headers[DummyPropagation.TRACE_CONTEXT_KEY]
);
assert.ok(
result.request._headers[DummyPropagation.SPAN_CONTEXT_KEY]
);
break;
case 'got':
case 'superagent':
break;
default:
break;
}
assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT);
assertSpan(span, SpanKind.CLIENT, validations);
});
});
});
});

View File

@ -0,0 +1,669 @@
/*
* 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.
*/
import {
SpanAttributes,
SpanStatusCode,
ROOT_CONTEXT,
SpanKind,
TraceFlags,
context,
Attributes,
} from '@opentelemetry/api';
import { BasicTracerProvider, Span } from '@opentelemetry/sdk-trace-base';
import {
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
SEMATTRS_HTTP_ROUTE,
SEMATTRS_HTTP_TARGET,
} from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import { IncomingMessage, ServerResponse } from 'http';
import { Socket } from 'net';
import * as sinon from 'sinon';
import * as url from 'url';
import { IgnoreMatcher, ParsedRequestOptions } from '../../src/types';
import * as utils from '../../src/utils';
import { AttributeNames } from '../../src/enums/AttributeNames';
import { RPCType, setRPCMetadata } from '@opentelemetry/core';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import { extractHostnameAndPort } from '../../src/utils';
describe('Utility', () => {
describe('parseResponseStatus()', () => {
it('should return ERROR code by default', () => {
const status = utils.parseResponseStatus(SpanKind.CLIENT, undefined);
assert.deepStrictEqual(status, SpanStatusCode.ERROR);
});
it('should return UNSET for Success HTTP status code', () => {
for (let index = 100; index < 400; index++) {
const status = utils.parseResponseStatus(SpanKind.CLIENT, index);
assert.deepStrictEqual(status, SpanStatusCode.UNSET);
}
for (let index = 100; index < 500; index++) {
const status = utils.parseResponseStatus(SpanKind.SERVER, index);
assert.deepStrictEqual(status, SpanStatusCode.UNSET);
}
});
it('should return ERROR for bad status codes', () => {
for (let index = 400; index <= 600; index++) {
const status = utils.parseResponseStatus(SpanKind.CLIENT, index);
assert.notStrictEqual(status, SpanStatusCode.UNSET);
}
for (let index = 500; index <= 600; index++) {
const status = utils.parseResponseStatus(SpanKind.SERVER, index);
assert.notStrictEqual(status, SpanStatusCode.UNSET);
}
});
});
describe('getRequestInfo()', () => {
it('should get options object', () => {
const webUrl = 'http://u:p@google.fr/aPath?qu=ry';
const urlParsed = url.parse(webUrl);
const urlParsedWithoutPathname = {
...urlParsed,
pathname: undefined,
};
const urlParsedWithUndefinedHostAndPort = {
...urlParsed,
host: undefined,
port: undefined,
};
const urlParsedWithUndefinedHostAndNullPort = {
...urlParsed,
host: undefined,
port: null,
};
const whatWgUrl = new url.URL(webUrl);
for (const param of [
webUrl,
urlParsed,
urlParsedWithoutPathname,
urlParsedWithUndefinedHostAndPort,
urlParsedWithUndefinedHostAndNullPort,
whatWgUrl,
]) {
const result = utils.getRequestInfo(param);
assert.strictEqual(result.optionsParsed.hostname, 'google.fr');
assert.strictEqual(result.optionsParsed.protocol, 'http:');
assert.strictEqual(result.optionsParsed.path, '/aPath?qu=ry');
assert.strictEqual(result.pathname, '/aPath');
assert.strictEqual(result.origin, 'http://google.fr');
}
});
});
describe('satisfiesPattern()', () => {
it('string pattern', () => {
const answer1 = utils.satisfiesPattern('/test/1', '/test/1');
assert.strictEqual(answer1, true);
const answer2 = utils.satisfiesPattern('/test/1', '/test/11');
assert.strictEqual(answer2, false);
});
it('regex pattern', () => {
const answer1 = utils.satisfiesPattern('/TeSt/1', /\/test/i);
assert.strictEqual(answer1, true);
const answer2 = utils.satisfiesPattern('/2/tEst/1', /\/test/);
assert.strictEqual(answer2, false);
});
it('should throw if type is unknown', () => {
try {
utils.satisfiesPattern('/TeSt/1', true as unknown as IgnoreMatcher);
assert.fail();
} catch (error) {
assert.strictEqual(error instanceof TypeError, true);
}
});
it('function pattern', () => {
const answer1 = utils.satisfiesPattern(
'/test/home',
(url: string) => url === '/test/home'
);
assert.strictEqual(answer1, true);
const answer2 = utils.satisfiesPattern(
'/test/home',
(url: string) => url !== '/test/home'
);
assert.strictEqual(answer2, false);
});
});
describe('isIgnored()', () => {
beforeEach(() => {
sinon.spy(utils, 'satisfiesPattern');
});
afterEach(() => {
sinon.restore();
});
it('should call isSatisfyPattern, n match', () => {
const answer1 = utils.isIgnored('/test/1', ['/test/11']);
assert.strictEqual(answer1, false);
assert.strictEqual(
(utils.satisfiesPattern as sinon.SinonSpy).callCount,
1
);
});
it('should call isSatisfyPattern, match for function', () => {
const answer1 = utils.isIgnored('/test/1', [
url => url.endsWith('/test/1'),
]);
assert.strictEqual(answer1, true);
});
it('should not re-throw when function throws an exception', () => {
const onException = (e: unknown) => {
// Do nothing
};
for (const callback of [undefined, onException]) {
assert.doesNotThrow(() =>
utils.isIgnored(
'/test/1',
[
() => {
throw new Error('test');
},
],
callback
)
);
}
});
it('should call onException when function throws an exception', () => {
const onException = sinon.spy();
assert.doesNotThrow(() =>
utils.isIgnored(
'/test/1',
[
() => {
throw new Error('test');
},
],
onException
)
);
assert.strictEqual((onException as sinon.SinonSpy).callCount, 1);
});
it('should not call isSatisfyPattern', () => {
utils.isIgnored('/test/1', []);
assert.strictEqual(
(utils.satisfiesPattern as sinon.SinonSpy).callCount,
0
);
});
it('should return false on empty list', () => {
const answer1 = utils.isIgnored('/test/1', []);
assert.strictEqual(answer1, false);
});
it('should not throw and return false when list is undefined', () => {
const answer2 = utils.isIgnored('/test/1', undefined);
assert.strictEqual(answer2, false);
});
});
describe('getAbsoluteUrl()', () => {
it('should return absolute url with localhost', () => {
const path = '/test/1';
const result = utils.getAbsoluteUrl(url.parse(path), {});
assert.strictEqual(result, `http://localhost${path}`);
});
it('should return absolute url', () => {
const absUrl = 'http://www.google/test/1?query=1';
const result = utils.getAbsoluteUrl(url.parse(absUrl), {});
assert.strictEqual(result, absUrl);
});
it('should return default url', () => {
const result = utils.getAbsoluteUrl(null, {});
assert.strictEqual(result, 'http://localhost/');
});
it("{ path: '/helloworld', port: 8080 } should return http://localhost:8080/helloworld", () => {
const result = utils.getAbsoluteUrl(
{ path: '/helloworld', port: 8080 },
{}
);
assert.strictEqual(result, 'http://localhost:8080/helloworld');
});
});
describe('setSpanWithError()', () => {
it('should have error attributes', () => {
const errorMessage = 'test error';
const span = new Span(
new BasicTracerProvider().getTracer('default'),
ROOT_CONTEXT,
'test',
{ spanId: '', traceId: '', traceFlags: TraceFlags.SAMPLED },
SpanKind.INTERNAL
);
/* tslint:disable-next-line:no-any */
utils.setSpanWithError(span, new Error(errorMessage));
const attributes = span.attributes;
assert.strictEqual(
attributes[AttributeNames.HTTP_ERROR_MESSAGE],
errorMessage
);
assert.strictEqual(span.events.length, 1);
assert.strictEqual(span.events[0].name, 'exception');
assert.ok(attributes[AttributeNames.HTTP_ERROR_NAME]);
});
});
describe('isValidOptionsType()', () => {
['', false, true, 1, 0, []].forEach(options => {
it(`should return false with the following value: ${JSON.stringify(
options
)}`, () => {
assert.strictEqual(utils.isValidOptionsType(options), false);
});
});
for (const options of ['url', url.parse('http://url.com'), {}]) {
it(`should return true with the following value: ${JSON.stringify(
options
)}`, () => {
assert.strictEqual(utils.isValidOptionsType(options), true);
});
}
});
describe('getIncomingRequestAttributesOnResponse()', () => {
it('should correctly parse the middleware stack if present', done => {
context.setGlobalContextManager(new AsyncHooksContextManager().enable());
const request = {
socket: {},
} as IncomingMessage;
context.with(
setRPCMetadata(context.active(), {
type: RPCType.HTTP,
route: '/user/:id',
span: null as unknown as Span,
}),
() => {
const attributes = utils.getIncomingRequestAttributesOnResponse(
request,
{} as ServerResponse
);
assert.deepStrictEqual(attributes[SEMATTRS_HTTP_ROUTE], '/user/:id');
context.disable();
return done();
}
);
});
it('should successfully process without middleware stack', () => {
const request = {
socket: {},
} as IncomingMessage;
const attributes = utils.getIncomingRequestAttributesOnResponse(request, {
socket: {},
} as ServerResponse & { socket: Socket });
assert.deepEqual(attributes[SEMATTRS_HTTP_ROUTE], undefined);
});
});
describe('getIncomingRequestMetricAttributesOnResponse()', () => {
it('should correctly add http_route if span has it', () => {
const spanAttributes: Attributes = {
[SEMATTRS_HTTP_ROUTE]: '/user/:id',
};
const metricAttributes =
utils.getIncomingRequestMetricAttributesOnResponse(spanAttributes);
assert.deepStrictEqual(
metricAttributes[SEMATTRS_HTTP_ROUTE],
'/user/:id'
);
});
it('should skip http_route if span does not have it', () => {
const spanAttributes: Attributes = {};
const metricAttributes =
utils.getIncomingRequestMetricAttributesOnResponse(spanAttributes);
assert.deepEqual(metricAttributes[SEMATTRS_HTTP_ROUTE], undefined);
});
});
// Verify the key in the given attributes is set to the given value,
// and that no other HTTP Content Length attributes are set.
function verifyValueInAttributes(
attributes: SpanAttributes,
key: string | undefined,
value: number
) {
const SemanticAttributess = [
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH,
];
for (const attr of SemanticAttributess) {
if (attr === key) {
assert.strictEqual(attributes[attr], value);
} else {
assert.strictEqual(attributes[attr], undefined);
}
}
}
describe('setRequestContentLengthAttributes()', () => {
it('should set request content-length uncompressed attribute with no content-encoding header', () => {
const attributes: SpanAttributes = {};
const request = {} as IncomingMessage;
request.headers = {
'content-length': '1200',
};
utils.setRequestContentLengthAttribute(request, attributes);
verifyValueInAttributes(
attributes,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});
it('should set request content-length uncompressed attribute with "identity" content-encoding header', () => {
const attributes: SpanAttributes = {};
const request = {} as IncomingMessage;
request.headers = {
'content-length': '1200',
'content-encoding': 'identity',
};
utils.setRequestContentLengthAttribute(request, attributes);
verifyValueInAttributes(
attributes,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});
it('should set request content-length compressed attribute with "gzip" content-encoding header', () => {
const attributes: SpanAttributes = {};
const request = {} as IncomingMessage;
request.headers = {
'content-length': '1200',
'content-encoding': 'gzip',
};
utils.setRequestContentLengthAttribute(request, attributes);
verifyValueInAttributes(
attributes,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH,
1200
);
});
});
describe('setResponseContentLengthAttributes()', () => {
it('should set response content-length uncompressed attribute with no content-encoding header', () => {
const attributes: SpanAttributes = {};
const response = {} as IncomingMessage;
response.headers = {
'content-length': '1200',
};
utils.setResponseContentLengthAttribute(response, attributes);
verifyValueInAttributes(
attributes,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});
it('should set response content-length uncompressed attribute with "identity" content-encoding header', () => {
const attributes: SpanAttributes = {};
const response = {} as IncomingMessage;
response.headers = {
'content-length': '1200',
'content-encoding': 'identity',
};
utils.setResponseContentLengthAttribute(response, attributes);
verifyValueInAttributes(
attributes,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});
it('should set response content-length compressed attribute with "gzip" content-encoding header', () => {
const attributes: SpanAttributes = {};
const response = {} as IncomingMessage;
response.headers = {
'content-length': '1200',
'content-encoding': 'gzip',
};
utils.setResponseContentLengthAttribute(response, attributes);
verifyValueInAttributes(
attributes,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH,
1200
);
});
it('should set no attributes with no content-length header', () => {
const attributes: SpanAttributes = {};
const message = {} as IncomingMessage;
message.headers = {
'content-encoding': 'gzip',
};
utils.setResponseContentLengthAttribute(message, attributes);
verifyValueInAttributes(attributes, undefined, 1200);
});
});
describe('getIncomingRequestAttributes()', () => {
it('should not set http.route in http span attributes', () => {
const request = {
url: 'http://hostname/user/:id',
method: 'GET',
} as IncomingMessage;
request.headers = {
'user-agent': 'chrome',
'x-forwarded-for': '<client>, <proxy1>, <proxy2>',
};
const attributes = utils.getIncomingRequestAttributes(request, {
component: 'http',
});
assert.strictEqual(attributes[SEMATTRS_HTTP_ROUTE], undefined);
});
it('should set http.target as path in http span attributes', () => {
const request = {
url: 'http://hostname/user/?q=val',
method: 'GET',
} as IncomingMessage;
request.headers = {
'user-agent': 'chrome',
};
const attributes = utils.getIncomingRequestAttributes(request, {
component: 'http',
});
assert.strictEqual(attributes[SEMATTRS_HTTP_TARGET], '/user/?q=val');
});
});
describe('headers to span attributes capture', () => {
let span: Span;
beforeEach(() => {
span = new Span(
new BasicTracerProvider().getTracer('default'),
ROOT_CONTEXT,
'test',
{ spanId: '', traceId: '', traceFlags: TraceFlags.SAMPLED },
SpanKind.INTERNAL
);
});
it('should set attributes for request and response keys', () => {
utils.headerCapture('request', ['Origin'])(span, () => 'localhost');
utils.headerCapture('response', ['Cookie'])(span, () => 'token=123');
assert.deepStrictEqual(span.attributes['http.request.header.origin'], [
'localhost',
]);
assert.deepStrictEqual(span.attributes['http.response.header.cookie'], [
'token=123',
]);
});
it('should set attributes for multiple values', () => {
utils.headerCapture('request', ['Origin'])(span, () => [
'localhost',
'www.example.com',
]);
assert.deepStrictEqual(span.attributes['http.request.header.origin'], [
'localhost',
'www.example.com',
]);
});
it('sets attributes for multiple headers', () => {
utils.headerCapture('request', ['Origin', 'Foo'])(span, header => {
if (header === 'origin') {
return 'localhost';
}
if (header === 'foo') {
return 42;
}
return undefined;
});
assert.deepStrictEqual(span.attributes['http.request.header.origin'], [
'localhost',
]);
assert.deepStrictEqual(span.attributes['http.request.header.foo'], [42]);
});
it('should normalize header names', () => {
utils.headerCapture('request', ['X-Forwarded-For'])(span, () => 'foo');
assert.deepStrictEqual(
span.attributes['http.request.header.x_forwarded_for'],
['foo']
);
});
it('ignores non-existent headers', () => {
utils.headerCapture('request', ['Origin', 'Accept'])(span, header => {
if (header === 'origin') {
return 'localhost';
}
return undefined;
});
assert.deepStrictEqual(span.attributes['http.request.header.origin'], [
'localhost',
]);
assert.deepStrictEqual(
span.attributes['http.request.header.accept'],
undefined
);
});
});
describe('extractHostnameAndPort', () => {
it('should return the hostname and port defined in the parsedOptions', () => {
type tmpParsedOption = Pick<
ParsedRequestOptions,
'hostname' | 'host' | 'port' | 'protocol'
>;
const parsedOption: tmpParsedOption = {
hostname: 'www.google.com',
port: '80',
host: 'www.google.com',
protocol: 'http:',
};
const { hostname, port } = extractHostnameAndPort(parsedOption);
assert.strictEqual(hostname, parsedOption.hostname);
assert.strictEqual(port, parsedOption.port);
});
it('should return the hostname and port based on host field defined in the parsedOptions when hostname and port are missing', () => {
type tmpParsedOption = Pick<
ParsedRequestOptions,
'hostname' | 'host' | 'port' | 'protocol'
>;
const parsedOption: tmpParsedOption = {
hostname: null,
port: null,
host: 'www.google.com:8181',
protocol: 'http:',
};
const { hostname, port } = extractHostnameAndPort(parsedOption);
assert.strictEqual(hostname, 'www.google.com');
assert.strictEqual(port, '8181');
});
it('should infer the port number based on protocol https when can not extract it from host field', () => {
type tmpParsedOption = Pick<
ParsedRequestOptions,
'hostname' | 'host' | 'port' | 'protocol'
>;
const parsedOption: tmpParsedOption = {
hostname: null,
port: null,
host: 'www.google.com',
protocol: 'https:',
};
const { hostname, port } = extractHostnameAndPort(parsedOption);
assert.strictEqual(hostname, 'www.google.com');
assert.strictEqual(port, '443');
});
it('should infer the port number based on protocol http when can not extract it from host field', () => {
type tmpParsedOption = Pick<
ParsedRequestOptions,
'hostname' | 'host' | 'port' | 'protocol'
>;
const parsedOption: tmpParsedOption = {
hostname: null,
port: null,
host: 'www.google.com',
protocol: 'http:',
};
const { hostname, port } = extractHostnameAndPort(parsedOption);
assert.strictEqual(hostname, 'www.google.com');
assert.strictEqual(port, '80');
});
});
});

View File

@ -0,0 +1,219 @@
/*
* 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.
*/
// Test that instrumentation-http works when used from ESM code.
import * as assert from 'assert';
import * as fs from 'fs';
import * as http from 'http';
import * as https from 'https';
import { SpanKind } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import { assertSpan } from '../../build/test/utils/assertSpan.js';
import { HttpInstrumentation } from '../../build/src/index.js';
const provider = new NodeTracerProvider();
const memoryExporter = new InMemorySpanExporter();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
const instrumentation = new HttpInstrumentation();
instrumentation.setTracerProvider(provider);
describe('HttpInstrumentation ESM Integration tests', () => {
let port;
let server;
before(done => {
server = http.createServer((req, res) => {
req.resume();
req.on('end', () => {
res.writeHead(200);
res.end('pong');
});
});
server.listen(0, '127.0.0.1', () => {
port = server.address().port;
assert.ok(Number.isInteger(port));
done();
});
});
after(done => {
server.close(done);
});
beforeEach(() => {
memoryExporter.reset();
});
it('should instrument http requests using http.request', async () => {
const spanValidations = {
httpStatusCode: 200,
httpMethod: 'GET',
hostname: '127.0.0.1',
pathname: '/http.request',
component: 'http',
};
await new Promise(resolve => {
const clientReq = http.request(
`http://127.0.0.1:${port}/http.request`,
clientRes => {
spanValidations.resHeaders = clientRes.headers;
clientRes.resume();
clientRes.on('end', resolve);
}
);
clientReq.end();
});
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 2);
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, spanValidations);
});
it('should instrument http requests using http.get', async () => {
const spanValidations = {
httpStatusCode: 200,
httpMethod: 'GET',
hostname: '127.0.0.1',
pathname: '/http.get',
component: 'http',
};
await new Promise(resolve => {
http.get(`http://127.0.0.1:${port}/http.get`, clientRes => {
spanValidations.resHeaders = clientRes.headers;
clientRes.resume();
clientRes.on('end', resolve);
});
});
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 2);
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, spanValidations);
});
});
describe('HttpsInstrumentation ESM Integration tests', () => {
let port;
let server;
before(done => {
server = https.createServer(
{
key: fs.readFileSync(
new URL('../fixtures/server-key.pem', import.meta.url)
),
cert: fs.readFileSync(
new URL('../fixtures/server-cert.pem', import.meta.url)
),
},
(req, res) => {
req.resume();
req.on('end', () => {
res.writeHead(200);
res.end('pong');
});
}
);
server.listen(0, '127.0.0.1', () => {
port = server.address().port;
assert.ok(Number.isInteger(port));
done();
});
});
after(done => {
server.close(done);
});
beforeEach(() => {
memoryExporter.reset();
});
it('should instrument https requests using https.request', async () => {
const spanValidations = {
httpStatusCode: 200,
httpMethod: 'GET',
hostname: '127.0.0.1',
pathname: '/https.request',
component: 'https',
};
await new Promise(resolve => {
const clientReq = https.request(
`https://127.0.0.1:${port}/https.request`,
{
rejectUnauthorized: false,
},
clientRes => {
spanValidations.resHeaders = clientRes.headers;
clientRes.resume();
clientRes.on('end', resolve);
}
);
clientReq.end();
});
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 2);
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, spanValidations);
});
it('should instrument http requests using https.get', async () => {
const spanValidations = {
httpStatusCode: 200,
httpMethod: 'GET',
hostname: '127.0.0.1',
pathname: '/https.get',
component: 'https',
};
await new Promise(resolve => {
https.get(
`https://127.0.0.1:${port}/https.get`,
{
rejectUnauthorized: false,
},
clientRes => {
spanValidations.resHeaders = clientRes.headers;
clientRes.resume();
clientRes.on('end', resolve);
}
);
});
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 2);
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, spanValidations);
});
});

View File

@ -0,0 +1,407 @@
/*
* 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.
*/
import { SpanKind, Span, context, propagation } from '@opentelemetry/api';
import {
HTTPFLAVORVALUES_HTTP_1_1,
NETTRANSPORTVALUES_IP_TCP,
SEMATTRS_HTTP_FLAVOR,
SEMATTRS_HTTP_HOST,
SEMATTRS_NET_TRANSPORT,
} from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import * as url from 'url';
import { HttpInstrumentation } from '../../src/http';
import { assertSpan } from '../utils/assertSpan';
import * as utils from '../utils/utils';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
import { httpRequest } from '../utils/httpRequest';
import { DummyPropagation } from '../utils/DummyPropagation';
import { Socket } from 'net';
import { sendRequestTwice } from '../utils/rawRequest';
const protocol = 'http';
const serverPort = 32345;
const hostname = 'localhost';
const memoryExporter = new InMemorySpanExporter();
const customAttributeFunction = (span: Span): void => {
span.setAttribute('span kind', SpanKind.CLIENT);
};
describe('HttpInstrumentation Integration tests', () => {
let mockServerPort = 0;
let mockServer: http.Server;
const sockets: Array<Socket> = [];
before(done => {
mockServer = http.createServer((req, res) => {
if (req.url === '/timeout') {
setTimeout(() => {
res.end();
}, 1000);
}
res.statusCode = 200;
res.setHeader('content-type', 'application/json');
res.write(
JSON.stringify({
success: true,
})
);
res.end();
});
mockServer.listen(0, () => {
const addr = mockServer.address();
if (addr == null) {
done(new Error('unexpected addr null'));
return;
}
if (typeof addr === 'string') {
done(new Error(`unexpected addr ${addr}`));
return;
}
if (addr.port <= 0) {
done(new Error('Could not get port'));
return;
}
mockServerPort = addr.port;
done();
});
});
after(done => {
sockets.forEach(s => s.destroy());
mockServer.close(done);
});
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
propagation.setGlobalPropagator(new DummyPropagation());
context.setGlobalContextManager(new AsyncHooksContextManager().enable());
});
after(() => {
context.disable();
propagation.disable();
});
describe('enable()', () => {
before(function (done) {
// mandatory
if (process.env.CI) {
done();
return;
}
utils.checkInternet(isConnected => {
if (!isConnected) {
this.skip();
// don't disturb people
}
done();
});
});
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
instrumentation.setTracerProvider(provider);
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
const ignoreConfig = [
`${protocol}://${hostname}:${serverPort}/ignored/string`,
/\/ignored\/regexp$/i,
(url: string) => url.endsWith('/ignored/function'),
];
instrumentation.setConfig({
ignoreIncomingPaths: ignoreConfig,
ignoreOutgoingUrls: ignoreConfig,
applyCustomAttributesOnSpan: customAttributeFunction,
});
instrumentation.enable();
});
after(() => {
instrumentation.disable();
});
it('should create a rootSpan for GET requests and add propagation headers', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const result = await httpRequest.get(
`${protocol}://localhost:${mockServerPort}/?query=test`
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should create a rootSpan for GET requests and add propagation headers if URL is used', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const result = await httpRequest.get(
new url.URL(`${protocol}://localhost:${mockServerPort}/?query=test`)
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should create a valid rootSpan with propagation headers for GET requests if URL and options are used', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const result = await httpRequest.get(
new url.URL(`${protocol}://localhost:${mockServerPort}/?query=test`),
{
headers: { 'x-foo': 'foo' },
}
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(result.reqHeaders['x-foo'], 'foo');
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_FLAVOR],
HTTPFLAVORVALUES_HTTP_1_1
);
assert.strictEqual(
span.attributes[SEMATTRS_NET_TRANSPORT],
NETTRANSPORTVALUES_IP_TCP
);
assertSpan(span, SpanKind.CLIENT, validations);
});
it('custom attributes should show up on client spans', async () => {
const result = await httpRequest.get(
`${protocol}://localhost:${mockServerPort}/`
);
const spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT);
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should not mutate given headers object when adding propagation headers', async () => {
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const headers = { 'x-foo': 'foo' };
const result = await httpRequest.get(
new url.URL(`${protocol}://localhost:${mockServerPort}/?query=test`),
{ headers }
);
assert.deepStrictEqual(headers, { 'x-foo': 'foo' });
assert.ok(result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
assert.ok(result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
});
it('should create a span for GET requests and add propagation headers with Expect headers', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const options = Object.assign(
{ headers: { Expect: '100-continue' } },
url.parse(`${protocol}://localhost:${mockServerPort}/`)
);
const result = await httpRequest.get(options);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: 200,
httpMethod: 'GET',
pathname: '/',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, validations);
});
for (const headers of [
{ Expect: '100-continue', 'user-agent': 'http-plugin-test' },
{ 'user-agent': 'http-plugin-test' },
]) {
it(`should create a span for GET requests and add propagation when using the following signature: get(url, options, callback) and following headers: ${JSON.stringify(
headers
)}`, done => {
let validations: {
hostname: string;
httpStatusCode: number;
httpMethod: string;
pathname: string;
reqHeaders: http.OutgoingHttpHeaders;
resHeaders: http.IncomingHttpHeaders;
};
let data = '';
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const options = { headers };
const req = http.get(
`${protocol}://localhost:${mockServerPort}/`,
options,
(resp: http.IncomingMessage) => {
const res = resp as unknown as http.IncomingMessage & {
req: http.IncomingMessage;
};
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
validations = {
hostname: 'localhost',
httpStatusCode: 301,
httpMethod: 'GET',
pathname: '/',
resHeaders: resp.headers,
/* tslint:disable:no-any */
reqHeaders: (res.req as any).getHeaders
? (res.req as any).getHeaders()
: (res.req as any)._headers,
/* tslint:enable:no-any */
};
});
}
);
req.on('close', () => {
const spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.ok(data);
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
done();
});
});
}
it('should work for multiple active requests in keep-alive mode', async () => {
await sendRequestTwice(hostname, mockServerPort);
const spans = memoryExporter.getFinishedSpans();
const span = spans.find((s: any) => s.kind === SpanKind.SERVER);
assert.ok(span);
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
});
it('should have correct spans even when request timeout', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
try {
await httpRequest.get(
`${protocol}://localhost:${mockServerPort}/timeout`,
{ timeout: 1 }
);
} catch (err: any) {
assert.ok(err.message.startsWith('timeout'));
}
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_HOST],
`localhost:${mockServerPort}`
);
});
});
});

View File

@ -0,0 +1,364 @@
/*
* 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.
*/
import { SpanKind, Span, context, propagation } from '@opentelemetry/api';
import {
HTTPFLAVORVALUES_HTTP_1_1,
NETTRANSPORTVALUES_IP_TCP,
SEMATTRS_HTTP_FLAVOR,
SEMATTRS_NET_TRANSPORT,
} from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
import { Socket } from 'net';
import { assertSpan } from '../utils/assertSpan';
import * as url from 'url';
import * as utils from '../utils/utils';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import { HttpInstrumentation } from '../../src';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as https from 'https';
import { httpsRequest } from '../utils/httpsRequest';
import { DummyPropagation } from '../utils/DummyPropagation';
const protocol = 'https';
const serverPort = 42345;
const hostname = 'localhost';
const memoryExporter = new InMemorySpanExporter();
export const customAttributeFunction = (span: Span): void => {
span.setAttribute('span kind', SpanKind.CLIENT);
};
describe('HttpsInstrumentation Integration tests', () => {
let mockServerPort = 0;
let mockServer: https.Server;
const sockets: Array<Socket> = [];
before(done => {
mockServer = https.createServer(
{
key: fs.readFileSync(
path.join(__dirname, '..', 'fixtures', 'server-key.pem')
),
cert: fs.readFileSync(
path.join(__dirname, '..', 'fixtures', 'server-cert.pem')
),
},
(req, res) => {
res.statusCode = 200;
res.setHeader('content-type', 'application/json');
res.write(
JSON.stringify({
success: true,
})
);
res.end();
}
);
mockServer.listen(0, () => {
const addr = mockServer.address();
if (addr == null) {
done(new Error('unexpected addr null'));
return;
}
if (typeof addr === 'string') {
done(new Error(`unexpected addr ${addr}`));
return;
}
if (addr.port <= 0) {
done(new Error('Could not get port'));
return;
}
mockServerPort = addr.port;
done();
});
});
after(done => {
sockets.forEach(s => s.destroy());
mockServer.close(done);
});
beforeEach(() => {
memoryExporter.reset();
context.setGlobalContextManager(new AsyncHooksContextManager().enable());
});
afterEach(() => {
context.disable();
});
describe('enable()', () => {
before(function (done) {
// mandatory
if (process.env.CI) {
done();
return;
}
utils.checkInternet(isConnected => {
if (!isConnected) {
this.skip();
// don't disturb people
}
done();
});
});
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
instrumentation.setTracerProvider(provider);
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
const ignoreConfig = [
`${protocol}://${hostname}:${serverPort}/ignored/string`,
/\/ignored\/regexp$/i,
(url: string) => url.endsWith('/ignored/function'),
];
propagation.setGlobalPropagator(new DummyPropagation());
instrumentation.setConfig({
ignoreIncomingPaths: ignoreConfig,
ignoreOutgoingUrls: ignoreConfig,
applyCustomAttributesOnSpan: customAttributeFunction,
});
instrumentation.enable();
});
after(() => {
instrumentation.disable();
propagation.disable();
});
it('should create a rootSpan for GET requests and add propagation headers', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const result = await httpsRequest.get(
`${protocol}://localhost:${mockServerPort}/?query=test`
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should create a rootSpan for GET requests and add propagation headers if URL is used', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const result = await httpsRequest.get(
new url.URL(`${protocol}://localhost:${mockServerPort}/?query=test`)
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should create a valid rootSpan with propagation headers for GET requests if URL and options are used', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const result = await httpsRequest.get(
new url.URL(`${protocol}://localhost:${mockServerPort}/?query=test`),
{
headers: { 'x-foo': 'foo' },
}
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(result.reqHeaders['x-foo'], 'foo');
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_FLAVOR],
HTTPFLAVORVALUES_HTTP_1_1
);
assert.strictEqual(
span.attributes[SEMATTRS_NET_TRANSPORT],
NETTRANSPORTVALUES_IP_TCP
);
assertSpan(span, SpanKind.CLIENT, validations);
});
it('custom attributes should show up on client spans', async () => {
const result = await httpsRequest.get(
`${protocol}://localhost:${mockServerPort}/`
);
const spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT);
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should create a span for GET requests and add propagation headers with Expect headers', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const options = Object.assign(
{ headers: { Expect: '100-continue' } },
url.parse(`${protocol}://localhost:${mockServerPort}/`)
);
const result = await httpsRequest.get(options);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: 200,
httpMethod: 'GET',
pathname: '/',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, validations);
});
for (const headers of [
{ Expect: '100-continue', 'user-agent': 'http-plugin-test' },
{ 'user-agent': 'http-plugin-test' },
]) {
it(`should create a span for GET requests and add propagation when using the following signature: get(url, options, callback) and following headers: ${JSON.stringify(
headers
)}`, done => {
let validations: {
hostname: string;
httpStatusCode: number;
httpMethod: string;
pathname: string;
reqHeaders: http.OutgoingHttpHeaders;
resHeaders: http.IncomingHttpHeaders;
};
let data = '';
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const options = { headers };
const req = https.get(
`${protocol}://localhost:${mockServerPort}/`,
options,
(resp: http.IncomingMessage) => {
const res = resp as unknown as http.IncomingMessage & {
req: http.IncomingMessage;
};
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
validations = {
hostname: 'localhost',
httpStatusCode: 301,
httpMethod: 'GET',
pathname: '/',
resHeaders: resp.headers,
/* tslint:disable:no-any */
reqHeaders: (res.req as any).getHeaders
? (res.req as any).getHeaders()
: (res.req as any)._headers,
/* tslint:enable:no-any */
};
});
}
);
req.on('close', () => {
const spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.ok(data);
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
done();
});
});
}
});
});

View File

@ -0,0 +1,51 @@
/*
* 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.
*/
import {
Context,
TextMapPropagator,
trace,
TraceFlags,
} from '@opentelemetry/api';
import type * as http from 'http';
export class DummyPropagation implements TextMapPropagator {
static TRACE_CONTEXT_KEY = 'x-dummy-trace-id';
static SPAN_CONTEXT_KEY = 'x-dummy-span-id';
extract(context: Context, carrier: http.OutgoingHttpHeaders) {
const extractedSpanContext = {
traceId: carrier[DummyPropagation.TRACE_CONTEXT_KEY] as string,
spanId: carrier[DummyPropagation.SPAN_CONTEXT_KEY] as string,
traceFlags: TraceFlags.SAMPLED,
isRemote: true,
};
if (extractedSpanContext.traceId && extractedSpanContext.spanId) {
return trace.setSpanContext(context, extractedSpanContext);
}
return context;
}
inject(context: Context, headers: { [custom: string]: string }): void {
const spanContext = trace.getSpanContext(context);
if (!spanContext) return;
headers[DummyPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId;
headers[DummyPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId;
}
fields(): string[] {
return [
DummyPropagation.TRACE_CONTEXT_KEY,
DummyPropagation.SPAN_CONTEXT_KEY,
];
}
}

View File

@ -0,0 +1,47 @@
/*
* 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.
*/
import { MetricReader, PushMetricExporter } from '@opentelemetry/sdk-metrics';
export class TestMetricReader extends MetricReader {
constructor(private _exporter: PushMetricExporter) {
super({
aggregationTemporalitySelector:
_exporter.selectAggregationTemporality?.bind(_exporter),
});
}
protected onForceFlush(): Promise<void> {
return Promise.resolve(undefined);
}
protected onShutdown(): Promise<void> {
return Promise.resolve(undefined);
}
public async collectAndExport(): Promise<void> {
const result = await this.collect();
await new Promise<void>((resolve, reject) => {
this._exporter.export(result.resourceMetrics, result => {
if (result.error != null) {
reject(result.error);
} else {
resolve();
}
});
});
}
}

View File

@ -0,0 +1,194 @@
/*
* 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.
*/
import {
isValidSpanId,
SpanKind,
SpanStatus,
Exception,
} from '@opentelemetry/api';
import { hrTimeToNanoseconds } from '@opentelemetry/core';
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import {
SEMATTRS_HTTP_METHOD,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH,
SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH,
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
SEMATTRS_HTTP_SCHEME,
SEMATTRS_HTTP_SERVER_NAME,
SEMATTRS_HTTP_STATUS_CODE,
SEMATTRS_HTTP_TARGET,
SEMATTRS_HTTP_URL,
SEMATTRS_HTTP_USER_AGENT,
SEMATTRS_NET_HOST_IP,
SEMATTRS_NET_HOST_PORT,
SEMATTRS_NET_PEER_IP,
SEMATTRS_NET_PEER_NAME,
SEMATTRS_NET_PEER_PORT,
} from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import * as http from 'http';
import * as utils from '../../src/utils';
import { DummyPropagation } from './DummyPropagation';
import { AttributeNames } from '../../src/enums/AttributeNames';
export const assertSpan = (
span: ReadableSpan,
kind: SpanKind,
validations: {
httpStatusCode?: number;
httpMethod: string;
resHeaders: http.IncomingHttpHeaders;
hostname: string;
pathname: string;
reqHeaders?: http.OutgoingHttpHeaders;
path?: string | null;
forceStatus?: SpanStatus;
serverName?: string;
component: string;
noNetPeer?: boolean; // we don't expect net peer info when request throw before being sent
error?: Exception;
}
) => {
assert.strictEqual(span.spanContext().traceId.length, 32);
assert.strictEqual(span.spanContext().spanId.length, 16);
assert.strictEqual(span.kind, kind);
assert.strictEqual(span.name, validations.httpMethod);
assert.strictEqual(
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
span.status.message
);
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_METHOD],
validations.httpMethod
);
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_TARGET],
validations.path || validations.pathname
);
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_STATUS_CODE],
validations.httpStatusCode
);
assert.strictEqual(span.links.length, 0);
if (validations.error) {
assert.strictEqual(span.events.length, 1);
assert.strictEqual(span.events[0].name, 'exception');
const eventAttributes = span.events[0].attributes;
assert.ok(eventAttributes != null);
assert.deepStrictEqual(Object.keys(eventAttributes), [
'exception.type',
'exception.message',
'exception.stacktrace',
]);
} else {
assert.strictEqual(span.events.length, 0);
}
assert.deepStrictEqual(
span.status,
validations.forceStatus || {
code: utils.parseResponseStatus(span.kind, validations.httpStatusCode),
}
);
assert.ok(span.endTime, 'must be finished');
assert.ok(hrTimeToNanoseconds(span.duration), 'must have positive duration');
if (validations.reqHeaders) {
const userAgent = validations.reqHeaders['user-agent'];
if (userAgent) {
assert.strictEqual(span.attributes[SEMATTRS_HTTP_USER_AGENT], userAgent);
}
}
if (span.kind === SpanKind.CLIENT) {
if (validations.resHeaders['content-length']) {
const contentLength = Number(validations.resHeaders['content-length']);
if (
validations.resHeaders['content-encoding'] &&
validations.resHeaders['content-encoding'] !== 'identity'
) {
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH],
contentLength
);
} else {
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED],
contentLength
);
}
}
assert.strictEqual(
span.attributes[SEMATTRS_NET_PEER_NAME],
validations.hostname,
'must be consistent (PEER_NAME and hostname)'
);
if (!validations.noNetPeer) {
assert.ok(span.attributes[SEMATTRS_NET_PEER_IP], 'must have PEER_IP');
assert.ok(span.attributes[SEMATTRS_NET_PEER_PORT], 'must have PEER_PORT');
}
assert.ok(
(span.attributes[SEMATTRS_HTTP_URL] as string).indexOf(
span.attributes[SEMATTRS_NET_PEER_NAME] as string
) > -1,
'must be consistent'
);
}
if (span.kind === SpanKind.SERVER) {
if (validations.reqHeaders && validations.reqHeaders['content-length']) {
const contentLength = validations.reqHeaders['content-length'];
if (
validations.reqHeaders['content-encoding'] &&
validations.reqHeaders['content-encoding'] !== 'identity'
) {
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH],
contentLength
);
} else {
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED],
contentLength
);
}
}
if (validations.serverName) {
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_SERVER_NAME],
validations.serverName,
' must have serverName attribute'
);
assert.ok(span.attributes[SEMATTRS_NET_HOST_PORT], 'must have HOST_PORT');
assert.ok(span.attributes[SEMATTRS_NET_HOST_IP], 'must have HOST_IP');
}
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_SCHEME],
validations.component,
' must have http.scheme attribute'
);
assert.ok(typeof span.parentSpanId === 'string');
assert.ok(isValidSpanId(span.parentSpanId));
} else if (validations.reqHeaders) {
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
}
};

View File

@ -0,0 +1,73 @@
/*
* 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.
*/
import * as http from 'http';
import { URL } from 'url';
type GetResult = Promise<{
data: string;
statusCode: number | undefined;
resHeaders: http.IncomingHttpHeaders;
reqHeaders: http.OutgoingHttpHeaders;
method: string | undefined;
}>;
function get(input: string | URL, options?: http.RequestOptions): GetResult;
function get(input: http.RequestOptions): GetResult;
function get(input: any, options?: any): GetResult {
return new Promise((resolve, reject) => {
// eslint-disable-next-line prefer-const
let req: http.ClientRequest;
function onGetResponseCb(resp: http.IncomingMessage): void {
const res = resp as unknown as http.IncomingMessage & {
req: http.IncomingMessage;
};
let data = '';
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
resolve({
data,
statusCode: res.statusCode,
reqHeaders: req.getHeaders ? req.getHeaders() : (req as any)._headers,
resHeaders: res.headers,
method: res.req.method,
});
});
resp.on('error', err => {
reject(err);
});
}
req =
options != null
? http.get(input, options, onGetResponseCb)
: http.get(input, onGetResponseCb);
req.on('error', err => {
reject(err);
});
req.on('timeout', () => {
const err = new Error('timeout');
req.emit('error', err);
reject(err);
});
return req;
});
}
export const httpRequest = {
get,
};

View File

@ -0,0 +1,72 @@
/*
* 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.
*/
import * as http from 'http';
import * as https from 'https';
import { URL } from 'url';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
type GetResult = Promise<{
data: string;
statusCode: number | undefined;
resHeaders: http.IncomingHttpHeaders;
reqHeaders: http.OutgoingHttpHeaders;
method: string | undefined;
}>;
function get(input: string | URL, options?: https.RequestOptions): GetResult;
function get(input: https.RequestOptions): GetResult;
function get(input: any, options?: any): GetResult {
return new Promise((resolve, reject) => {
// eslint-disable-next-line prefer-const
let req: http.ClientRequest;
function onGetResponseCb(resp: http.IncomingMessage): void {
const res = resp as unknown as http.IncomingMessage & {
req: http.IncomingMessage;
};
let data = '';
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
resolve({
data,
statusCode: res.statusCode,
reqHeaders: req.getHeaders ? req.getHeaders() : (req as any)._headers,
resHeaders: res.headers,
method: res.req.method,
});
});
resp.on('error', err => {
reject(err);
});
}
req =
options != null
? https.get(input, options, onGetResponseCb)
: https.get(input, onGetResponseCb);
req.on('error', err => {
reject(err);
});
return req;
});
}
export const httpsRequest = {
get,
};

View File

@ -0,0 +1,38 @@
/*
* 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.
*/
import * as net from 'net';
// used to reproduce this issue:
// https://github.com/open-telemetry/opentelemetry-js/pull/1939
export async function sendRequestTwice(
host: string,
port: number
): Promise<Buffer> {
return new Promise((resolve, reject) => {
const request = `GET /raw HTTP/1.1\r\nHost: ${host}:${port}\r\n\r\n`;
const socket = net.createConnection({ host, port }, () => {
socket.write(`${request}${request}`, err => {
if (err) reject(err);
});
});
socket.on('data', data => {
// since it's <1kb size, we expect both responses to come in a single chunk
socket.destroy();
resolve(data);
});
socket.on('error', err => reject(err));
});
}

View File

@ -0,0 +1,26 @@
/*
* 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.
*/
import * as dns from 'dns';
export const checkInternet = (cb: (isConnected: boolean) => void) => {
dns.lookup('google.com', err => {
if (err && err.code === 'ENOTFOUND') {
cb(false);
} else {
cb(true);
}
});
};

View File

@ -0,0 +1,13 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "build",
"rootDir": ".",
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": [
"src/**/*.ts",
"test/**/*.ts"
]
}

View File

@ -33,7 +33,7 @@ import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-gener
import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc'; import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc';
import { HapiInstrumentation } from '@opentelemetry/instrumentation-hapi'; import { HapiInstrumentation } from '@opentelemetry/instrumentation-hapi';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { HttpInstrumentation } from '@tachybase/opentelemetry-instrumentation-http';
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis'; import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs'; import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs';
import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex'; import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex';
@ -111,7 +111,7 @@ const InstrumentationMap = {
'@opentelemetry/instrumentation-graphql': GraphQLInstrumentation, '@opentelemetry/instrumentation-graphql': GraphQLInstrumentation,
'@opentelemetry/instrumentation-grpc': GrpcInstrumentation, '@opentelemetry/instrumentation-grpc': GrpcInstrumentation,
'@opentelemetry/instrumentation-hapi': HapiInstrumentation, '@opentelemetry/instrumentation-hapi': HapiInstrumentation,
'@opentelemetry/instrumentation-http': HttpInstrumentation, '@tachybase/opentelemetry-instrumentation-http': HttpInstrumentation,
'@opentelemetry/instrumentation-ioredis': IORedisInstrumentation, '@opentelemetry/instrumentation-ioredis': IORedisInstrumentation,
'@opentelemetry/instrumentation-kafkajs': KafkaJsInstrumentation, '@opentelemetry/instrumentation-kafkajs': KafkaJsInstrumentation,
'@opentelemetry/instrumentation-knex': KnexInstrumentation, '@opentelemetry/instrumentation-knex': KnexInstrumentation,

View File

@ -15,7 +15,7 @@
*/ */
import { diag } from '@opentelemetry/api'; import { diag } from '@opentelemetry/api';
import { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http'; import { HttpInstrumentationConfig } from '@tachybase/opentelemetry-instrumentation-http';
import * as assert from 'assert'; import * as assert from 'assert';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { getNodeAutoInstrumentations } from '../src'; import { getNodeAutoInstrumentations } from '../src';
@ -41,13 +41,13 @@ describe('utils', () => {
function applyCustomAttributesOnSpan() {} function applyCustomAttributesOnSpan() {}
const instrumentations = getNodeAutoInstrumentations({ const instrumentations = getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': { '@tachybase/opentelemetry-instrumentation-http': {
applyCustomAttributesOnSpan, applyCustomAttributesOnSpan,
}, },
}); });
const instrumentation = instrumentations.find( const instrumentation = instrumentations.find(
instr => instr =>
instr.instrumentationName === '@opentelemetry/instrumentation-http' instr.instrumentationName === '@tachybase/opentelemetry-instrumentation-http'
) as any; ) as any;
const configHttp = instrumentation._config as HttpInstrumentationConfig; const configHttp = instrumentation._config as HttpInstrumentationConfig;
@ -79,7 +79,7 @@ describe('utils', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
new Set(instrumentations.map(i => i.instrumentationName)), new Set(instrumentations.map(i => i.instrumentationName)),
new Set([ new Set([
'@opentelemetry/instrumentation-http', '@tachybase/opentelemetry-instrumentation-http',
'@opentelemetry/instrumentation-aws-sdk', '@opentelemetry/instrumentation-aws-sdk',
'@opentelemetry/instrumentation-nestjs-core', '@opentelemetry/instrumentation-nestjs-core',
]) ])
@ -123,7 +123,7 @@ describe('utils', () => {
assert.deepStrictEqual( assert.deepStrictEqual(
new Set(instrumentations.map(i => i.instrumentationName)), new Set(instrumentations.map(i => i.instrumentationName)),
new Set([ new Set([
'@opentelemetry/instrumentation-http', '@tachybase/opentelemetry-instrumentation-http',
'@opentelemetry/instrumentation-express', '@opentelemetry/instrumentation-express',
]) ])
); );
@ -135,7 +135,7 @@ describe('utils', () => {
it('should show error for none existing instrumentation', () => { it('should show error for none existing instrumentation', () => {
const spy = sinon.stub(diag, 'error'); const spy = sinon.stub(diag, 'error');
const name = '@opentelemetry/instrumentation-http2'; const name = '@tachybase/opentelemetry-instrumentation-http2';
const instrumentations = getNodeAutoInstrumentations({ const instrumentations = getNodeAutoInstrumentations({
// @ts-expect-error verify that wrong name works // @ts-expect-error verify that wrong name works
[name]: { [name]: {