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",
|
"name": "@tachybase/opentelemetry-auto-instrumentations",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@tachybase/opentelemetry-auto-instrumentations",
|
"name": "@tachybase/opentelemetry-auto-instrumentations",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"workspaces": [
|
||||||
|
"/*"
|
||||||
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opentelemetry/instrumentation": "^0.52.0",
|
"@opentelemetry/instrumentation": "^0.52.0",
|
||||||
"@opentelemetry/instrumentation-amqplib": "^0.40.0",
|
"@opentelemetry/instrumentation-amqplib": "^0.40.0",
|
||||||
@ -26,7 +29,6 @@
|
|||||||
"@opentelemetry/instrumentation-graphql": "^0.42.0",
|
"@opentelemetry/instrumentation-graphql": "^0.42.0",
|
||||||
"@opentelemetry/instrumentation-grpc": "^0.52.0",
|
"@opentelemetry/instrumentation-grpc": "^0.52.0",
|
||||||
"@opentelemetry/instrumentation-hapi": "^0.40.0",
|
"@opentelemetry/instrumentation-hapi": "^0.40.0",
|
||||||
"@opentelemetry/instrumentation-http": "^0.52.0",
|
|
||||||
"@opentelemetry/instrumentation-ioredis": "^0.42.0",
|
"@opentelemetry/instrumentation-ioredis": "^0.42.0",
|
||||||
"@opentelemetry/instrumentation-kafkajs": "^0.2.0",
|
"@opentelemetry/instrumentation-kafkajs": "^0.2.0",
|
||||||
"@opentelemetry/instrumentation-knex": "^0.39.0",
|
"@opentelemetry/instrumentation-knex": "^0.39.0",
|
||||||
@ -55,7 +57,8 @@
|
|||||||
"@opentelemetry/resource-detector-container": "^0.3.11",
|
"@opentelemetry/resource-detector-container": "^0.3.11",
|
||||||
"@opentelemetry/resource-detector-gcp": "^0.29.10",
|
"@opentelemetry/resource-detector-gcp": "^0.29.10",
|
||||||
"@opentelemetry/resources": "^1.24.0",
|
"@opentelemetry/resources": "^1.24.0",
|
||||||
"@opentelemetry/sdk-node": "^0.52.0"
|
"@opentelemetry/sdk-node": "^0.52.0",
|
||||||
|
"@tachybase/opentelemetry-instrumentation-http": "^0.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@opentelemetry/api": "^1.4.1",
|
"@opentelemetry/api": "^1.4.1",
|
||||||
@ -2931,23 +2934,6 @@
|
|||||||
"@opentelemetry/api": "^1.3.0"
|
"@opentelemetry/api": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@opentelemetry/instrumentation-http": {
|
|
||||||
"version": "0.52.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.1.tgz",
|
|
||||||
"integrity": "sha512-dG/aevWhaP+7OLv4BQQSEKMJv8GyeOp3Wxl31NHqE8xo9/fYMfEljiZphUHIfyg4gnZ9swMyWjfOQs5GUQe54Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"@opentelemetry/core": "1.25.1",
|
|
||||||
"@opentelemetry/instrumentation": "0.52.1",
|
|
||||||
"@opentelemetry/semantic-conventions": "1.25.1",
|
|
||||||
"semver": "^7.5.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@opentelemetry/api": "^1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@opentelemetry/instrumentation-ioredis": {
|
"node_modules/@opentelemetry/instrumentation-ioredis": {
|
||||||
"version": "0.42.0",
|
"version": "0.42.0",
|
||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz",
|
||||||
@ -3946,6 +3932,23 @@
|
|||||||
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
|
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@tachybase/opentelemetry-instrumentation-http": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tachybase/opentelemetry-instrumentation-http/-/opentelemetry-instrumentation-http-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-7gnwXS9OesmY36s09YlwJgLEv93EoRSrChqw5HhraRtDzEcatYsFUdhaO/QpGMwnp70SG1Nd4hj5yN2CGvvPcA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@opentelemetry/core": "1.25.1",
|
||||||
|
"@opentelemetry/instrumentation": "0.52.1",
|
||||||
|
"@opentelemetry/semantic-conventions": "1.25.1",
|
||||||
|
"semver": "^7.5.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@opentelemetry/api": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||||
|
16
package.json
16
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@tachybase/opentelemetry-auto-instrumentations",
|
"name": "@tachybase/opentelemetry-auto-instrumentations",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"description": "Metapackage of telemetry for TachyBase which bundles OpenTelemetry node core and instrumentations",
|
"description": "Metapackage of telemetry for TachyBase which bundles OpenTelemetry node core and instrumentations",
|
||||||
"author": "TachyBase Team",
|
"author": "TachyBase Team",
|
||||||
"homepage": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations",
|
"homepage": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations",
|
||||||
@ -31,6 +31,13 @@
|
|||||||
"test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'",
|
"test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'",
|
||||||
"watch": "tsc -w"
|
"watch": "tsc -w"
|
||||||
},
|
},
|
||||||
|
"keywords": [
|
||||||
|
"tachybase",
|
||||||
|
"opentelemetry",
|
||||||
|
"nodejs",
|
||||||
|
"tracing",
|
||||||
|
"instrumentation"
|
||||||
|
],
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations/issues"
|
"url": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations/issues"
|
||||||
},
|
},
|
||||||
@ -69,7 +76,7 @@
|
|||||||
"@opentelemetry/instrumentation-graphql": "^0.42.0",
|
"@opentelemetry/instrumentation-graphql": "^0.42.0",
|
||||||
"@opentelemetry/instrumentation-grpc": "^0.52.0",
|
"@opentelemetry/instrumentation-grpc": "^0.52.0",
|
||||||
"@opentelemetry/instrumentation-hapi": "^0.40.0",
|
"@opentelemetry/instrumentation-hapi": "^0.40.0",
|
||||||
"@opentelemetry/instrumentation-http": "^0.52.0",
|
"@tachybase/opentelemetry-instrumentation-http": "^0.0.1",
|
||||||
"@opentelemetry/instrumentation-ioredis": "^0.42.0",
|
"@opentelemetry/instrumentation-ioredis": "^0.42.0",
|
||||||
"@opentelemetry/instrumentation-kafkajs": "^0.2.0",
|
"@opentelemetry/instrumentation-kafkajs": "^0.2.0",
|
||||||
"@opentelemetry/instrumentation-knex": "^0.39.0",
|
"@opentelemetry/instrumentation-knex": "^0.39.0",
|
||||||
@ -103,7 +110,10 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"build/src/**/*.js",
|
"build/src/**/*.js",
|
||||||
"build/src/**/*.js.map",
|
"build/src/**/*.js.map",
|
||||||
"build/src/**/*.d.ts"
|
"build/src/**/*.d.ts",
|
||||||
|
"doc",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
],
|
],
|
||||||
"changelog": {
|
"changelog": {
|
||||||
"labels": {
|
"labels": {
|
||||||
|
@ -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 { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql';
|
||||||
import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc';
|
import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc';
|
||||||
import { HapiInstrumentation } from '@opentelemetry/instrumentation-hapi';
|
import { HapiInstrumentation } from '@opentelemetry/instrumentation-hapi';
|
||||||
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
import { HttpInstrumentation } from '@tachybase/opentelemetry-instrumentation-http';
|
||||||
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
|
import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis';
|
||||||
import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs';
|
import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs';
|
||||||
import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex';
|
import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex';
|
||||||
@ -111,7 +111,7 @@ const InstrumentationMap = {
|
|||||||
'@opentelemetry/instrumentation-graphql': GraphQLInstrumentation,
|
'@opentelemetry/instrumentation-graphql': GraphQLInstrumentation,
|
||||||
'@opentelemetry/instrumentation-grpc': GrpcInstrumentation,
|
'@opentelemetry/instrumentation-grpc': GrpcInstrumentation,
|
||||||
'@opentelemetry/instrumentation-hapi': HapiInstrumentation,
|
'@opentelemetry/instrumentation-hapi': HapiInstrumentation,
|
||||||
'@opentelemetry/instrumentation-http': HttpInstrumentation,
|
'@tachybase/opentelemetry-instrumentation-http': HttpInstrumentation,
|
||||||
'@opentelemetry/instrumentation-ioredis': IORedisInstrumentation,
|
'@opentelemetry/instrumentation-ioredis': IORedisInstrumentation,
|
||||||
'@opentelemetry/instrumentation-kafkajs': KafkaJsInstrumentation,
|
'@opentelemetry/instrumentation-kafkajs': KafkaJsInstrumentation,
|
||||||
'@opentelemetry/instrumentation-knex': KnexInstrumentation,
|
'@opentelemetry/instrumentation-knex': KnexInstrumentation,
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { diag } from '@opentelemetry/api';
|
import { diag } from '@opentelemetry/api';
|
||||||
import { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http';
|
import { HttpInstrumentationConfig } from '@tachybase/opentelemetry-instrumentation-http';
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { getNodeAutoInstrumentations } from '../src';
|
import { getNodeAutoInstrumentations } from '../src';
|
||||||
@ -41,13 +41,13 @@ describe('utils', () => {
|
|||||||
function applyCustomAttributesOnSpan() {}
|
function applyCustomAttributesOnSpan() {}
|
||||||
|
|
||||||
const instrumentations = getNodeAutoInstrumentations({
|
const instrumentations = getNodeAutoInstrumentations({
|
||||||
'@opentelemetry/instrumentation-http': {
|
'@tachybase/opentelemetry-instrumentation-http': {
|
||||||
applyCustomAttributesOnSpan,
|
applyCustomAttributesOnSpan,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const instrumentation = instrumentations.find(
|
const instrumentation = instrumentations.find(
|
||||||
instr =>
|
instr =>
|
||||||
instr.instrumentationName === '@opentelemetry/instrumentation-http'
|
instr.instrumentationName === '@tachybase/opentelemetry-instrumentation-http'
|
||||||
) as any;
|
) as any;
|
||||||
const configHttp = instrumentation._config as HttpInstrumentationConfig;
|
const configHttp = instrumentation._config as HttpInstrumentationConfig;
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ describe('utils', () => {
|
|||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
new Set(instrumentations.map(i => i.instrumentationName)),
|
new Set(instrumentations.map(i => i.instrumentationName)),
|
||||||
new Set([
|
new Set([
|
||||||
'@opentelemetry/instrumentation-http',
|
'@tachybase/opentelemetry-instrumentation-http',
|
||||||
'@opentelemetry/instrumentation-aws-sdk',
|
'@opentelemetry/instrumentation-aws-sdk',
|
||||||
'@opentelemetry/instrumentation-nestjs-core',
|
'@opentelemetry/instrumentation-nestjs-core',
|
||||||
])
|
])
|
||||||
@ -123,7 +123,7 @@ describe('utils', () => {
|
|||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
new Set(instrumentations.map(i => i.instrumentationName)),
|
new Set(instrumentations.map(i => i.instrumentationName)),
|
||||||
new Set([
|
new Set([
|
||||||
'@opentelemetry/instrumentation-http',
|
'@tachybase/opentelemetry-instrumentation-http',
|
||||||
'@opentelemetry/instrumentation-express',
|
'@opentelemetry/instrumentation-express',
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
@ -135,7 +135,7 @@ describe('utils', () => {
|
|||||||
|
|
||||||
it('should show error for none existing instrumentation', () => {
|
it('should show error for none existing instrumentation', () => {
|
||||||
const spy = sinon.stub(diag, 'error');
|
const spy = sinon.stub(diag, 'error');
|
||||||
const name = '@opentelemetry/instrumentation-http2';
|
const name = '@tachybase/opentelemetry-instrumentation-http2';
|
||||||
const instrumentations = getNodeAutoInstrumentations({
|
const instrumentations = getNodeAutoInstrumentations({
|
||||||
// @ts-expect-error verify that wrong name works
|
// @ts-expect-error verify that wrong name works
|
||||||
[name]: {
|
[name]: {
|
||||||
|
Loading…
Reference in New Issue
Block a user