Compare commits
3 Commits
3176a6f2cb
...
6a84a8bb8b
Author | SHA1 | Date | |
---|---|---|---|
6a84a8bb8b | |||
5cc7794daa | |||
4571ede515 |
45
package-lock.json
generated
45
package-lock.json
generated
@ -1,13 +1,16 @@
|
||||
{
|
||||
"name": "@tachybase/opentelemetry-auto-instrumentations",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@tachybase/opentelemetry-auto-instrumentations",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@opentelemetry/instrumentation": "^0.52.0",
|
||||
"@opentelemetry/instrumentation-amqplib": "^0.40.0",
|
||||
@ -26,7 +29,6 @@
|
||||
"@opentelemetry/instrumentation-graphql": "^0.42.0",
|
||||
"@opentelemetry/instrumentation-grpc": "^0.52.0",
|
||||
"@opentelemetry/instrumentation-hapi": "^0.40.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.52.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.42.0",
|
||||
"@opentelemetry/instrumentation-kafkajs": "^0.2.0",
|
||||
"@opentelemetry/instrumentation-knex": "^0.39.0",
|
||||
@ -55,7 +57,8 @@
|
||||
"@opentelemetry/resource-detector-container": "^0.3.11",
|
||||
"@opentelemetry/resource-detector-gcp": "^0.29.10",
|
||||
"@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": {
|
||||
"@opentelemetry/api": "^1.4.1",
|
||||
@ -2931,23 +2934,6 @@
|
||||
"@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": {
|
||||
"version": "0.42.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||
|
16
package.json
16
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"author": "TachyBase Team",
|
||||
"homepage": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations",
|
||||
@ -31,6 +31,13 @@
|
||||
"test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"keywords": [
|
||||
"tachybase",
|
||||
"opentelemetry",
|
||||
"nodejs",
|
||||
"tracing",
|
||||
"instrumentation"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations/issues"
|
||||
},
|
||||
@ -69,7 +76,7 @@
|
||||
"@opentelemetry/instrumentation-graphql": "^0.42.0",
|
||||
"@opentelemetry/instrumentation-grpc": "^0.52.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-kafkajs": "^0.2.0",
|
||||
"@opentelemetry/instrumentation-knex": "^0.39.0",
|
||||
@ -103,7 +110,10 @@
|
||||
"files": [
|
||||
"build/src/**/*.js",
|
||||
"build/src/**/*.js.map",
|
||||
"build/src/**/*.d.ts"
|
||||
"build/src/**/*.d.ts",
|
||||
"doc",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"changelog": {
|
||||
"labels": {
|
||||
|
@ -0,0 +1 @@
|
||||
build
|
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"mocha": true,
|
||||
"node": true
|
||||
},
|
||||
...require('../../../eslint.base.js')
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
/bin
|
||||
/coverage
|
||||
/doc
|
||||
/test
|
2
plugins/node/opentelemetry-instrumentation-http/.npmrc
Normal file
2
plugins/node/opentelemetry-instrumentation-http/.npmrc
Normal file
@ -0,0 +1,2 @@
|
||||
registry=https://registry.npmjs.org/
|
||||
@tachybase:registry=https://registry.npmjs.org/
|
201
plugins/node/opentelemetry-instrumentation-http/LICENSE
Normal file
201
plugins/node/opentelemetry-instrumentation-http/LICENSE
Normal 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.
|
@ -0,0 +1,3 @@
|
||||
# opentelemetry-instrumentations-http
|
||||
|
||||
TachyBase 专属的 OpenTelemetry HTTP 自动插桩库
|
14801
plugins/node/opentelemetry-instrumentation-http/package-lock.json
generated
Normal file
14801
plugins/node/opentelemetry-instrumentation-http/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
88
plugins/node/opentelemetry-instrumentation-http/package.json
Normal file
88
plugins/node/opentelemetry-instrumentation-http/package.json
Normal 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"
|
||||
]
|
||||
}
|
@ -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',
|
||||
}
|
878
plugins/node/opentelemetry-instrumentation-http/src/http.ts
Normal file
878
plugins/node/opentelemetry-instrumentation-http/src/http.ts
Normal 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 ?? []
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
19
plugins/node/opentelemetry-instrumentation-http/src/index.ts
Normal file
19
plugins/node/opentelemetry-instrumentation-http/src/index.ts
Normal 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';
|
134
plugins/node/opentelemetry-instrumentation-http/src/types.ts
Normal file
134
plugins/node/opentelemetry-instrumentation-http/src/types.ts
Normal 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;
|
||||
}
|
613
plugins/node/opentelemetry-instrumentation-http/src/utils.ts
Normal file
613
plugins/node/opentelemetry-instrumentation-http/src/utils.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
43
plugins/node/opentelemetry-instrumentation-http/test/fixtures/google-http.json
vendored
Normal file
43
plugins/node/opentelemetry-instrumentation-http/test/fixtures/google-http.json
vendored
Normal file
File diff suppressed because one or more lines are too long
43
plugins/node/opentelemetry-instrumentation-http/test/fixtures/google-https.json
vendored
Normal file
43
plugins/node/opentelemetry-instrumentation-http/test/fixtures/google-https.json
vendored
Normal file
File diff suppressed because one or more lines are too long
10
plugins/node/opentelemetry-instrumentation-http/test/fixtures/regenerate.sh
vendored
Executable file
10
plugins/node/opentelemetry-instrumentation-http/test/fixtures/regenerate.sh
vendored
Executable 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/"
|
22
plugins/node/opentelemetry-instrumentation-http/test/fixtures/server-cert.pem
vendored
Normal file
22
plugins/node/opentelemetry-instrumentation-http/test/fixtures/server-cert.pem
vendored
Normal 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-----
|
28
plugins/node/opentelemetry-instrumentation-http/test/fixtures/server-key.pem
vendored
Normal file
28
plugins/node/opentelemetry-instrumentation-http/test/fixtures/server-key.pem
vendored
Normal 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-----
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
@ -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}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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]);
|
||||
}
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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));
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
"rootDir": ".",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
}
|
@ -33,7 +33,7 @@ import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-gener
|
||||
import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
|
||||
import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc';
|
||||
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 { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs';
|
||||
import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex';
|
||||
@ -111,7 +111,7 @@ const InstrumentationMap = {
|
||||
'@opentelemetry/instrumentation-graphql': GraphQLInstrumentation,
|
||||
'@opentelemetry/instrumentation-grpc': GrpcInstrumentation,
|
||||
'@opentelemetry/instrumentation-hapi': HapiInstrumentation,
|
||||
'@opentelemetry/instrumentation-http': HttpInstrumentation,
|
||||
'@tachybase/opentelemetry-instrumentation-http': HttpInstrumentation,
|
||||
'@opentelemetry/instrumentation-ioredis': IORedisInstrumentation,
|
||||
'@opentelemetry/instrumentation-kafkajs': KafkaJsInstrumentation,
|
||||
'@opentelemetry/instrumentation-knex': KnexInstrumentation,
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
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 sinon from 'sinon';
|
||||
import { getNodeAutoInstrumentations } from '../src';
|
||||
@ -41,13 +41,13 @@ describe('utils', () => {
|
||||
function applyCustomAttributesOnSpan() {}
|
||||
|
||||
const instrumentations = getNodeAutoInstrumentations({
|
||||
'@opentelemetry/instrumentation-http': {
|
||||
'@tachybase/opentelemetry-instrumentation-http': {
|
||||
applyCustomAttributesOnSpan,
|
||||
},
|
||||
});
|
||||
const instrumentation = instrumentations.find(
|
||||
instr =>
|
||||
instr.instrumentationName === '@opentelemetry/instrumentation-http'
|
||||
instr.instrumentationName === '@tachybase/opentelemetry-instrumentation-http'
|
||||
) as any;
|
||||
const configHttp = instrumentation._config as HttpInstrumentationConfig;
|
||||
|
||||
@ -79,7 +79,7 @@ describe('utils', () => {
|
||||
assert.deepStrictEqual(
|
||||
new Set(instrumentations.map(i => i.instrumentationName)),
|
||||
new Set([
|
||||
'@opentelemetry/instrumentation-http',
|
||||
'@tachybase/opentelemetry-instrumentation-http',
|
||||
'@opentelemetry/instrumentation-aws-sdk',
|
||||
'@opentelemetry/instrumentation-nestjs-core',
|
||||
])
|
||||
@ -123,7 +123,7 @@ describe('utils', () => {
|
||||
assert.deepStrictEqual(
|
||||
new Set(instrumentations.map(i => i.instrumentationName)),
|
||||
new Set([
|
||||
'@opentelemetry/instrumentation-http',
|
||||
'@tachybase/opentelemetry-instrumentation-http',
|
||||
'@opentelemetry/instrumentation-express',
|
||||
])
|
||||
);
|
||||
@ -135,7 +135,7 @@ describe('utils', () => {
|
||||
|
||||
it('should show error for none existing instrumentation', () => {
|
||||
const spy = sinon.stub(diag, 'error');
|
||||
const name = '@opentelemetry/instrumentation-http2';
|
||||
const name = '@tachybase/opentelemetry-instrumentation-http2';
|
||||
const instrumentations = getNodeAutoInstrumentations({
|
||||
// @ts-expect-error verify that wrong name works
|
||||
[name]: {
|
||||
|
Loading…
Reference in New Issue
Block a user