feat(plugin-https): patch https requests (#379)
* feat(plugin-https): patch https requests closes #375 add tests Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca> * docs(plugin-https): add jaeger image Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca> * fix: add mayurkale22 recommendations Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca> * fix: add markwolff recommendations Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca> * fix: file name utils * fix: add danielkhan and bg451 recommendations Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>
This commit is contained in:
parent
32572c6c5c
commit
8567061992
|
@ -0,0 +1,77 @@
|
|||
# Overview
|
||||
|
||||
OpenTelemetry HTTPS Instrumentation allows the user to automatically collect trace data and export them to the backend of choice (we can use Zipkin or Jaeger for this example), to give observability to distributed systems.
|
||||
|
||||
This is a simple example that demonstrates tracing HTTPS request from client to server. The example
|
||||
shows key aspects of tracing such as
|
||||
- Root Span (on Client)
|
||||
- Child Span (on Client)
|
||||
- Child Span from a Remote Parent (on Server)
|
||||
- SpanContext Propagation (from Client to Server)
|
||||
- Span Events
|
||||
- Span Attributes
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
$ # from this directory
|
||||
$ npm install
|
||||
```
|
||||
|
||||
Setup [Zipkin Tracing](https://zipkin.io/pages/quickstart.html)
|
||||
or
|
||||
Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one)
|
||||
|
||||
## Run the Application
|
||||
|
||||
### Zipkin
|
||||
|
||||
- Run the server
|
||||
|
||||
```sh
|
||||
$ # from this directory
|
||||
$ npm run zipkin:server
|
||||
```
|
||||
|
||||
- Run the client
|
||||
|
||||
```sh
|
||||
$ # from this directory
|
||||
$ npm run zipkin:client
|
||||
```
|
||||
|
||||
#### Zipkin UI
|
||||
`zipkin:server` script should output the `traceid` in the terminal (e.g `traceid: 4815c3d576d930189725f1f1d1bdfcc6`).
|
||||
Go to Zipkin with your browser [http://localhost:9411/zipkin/traces/(your-trace-id)]() (e.g http://localhost:9411/zipkin/traces/4815c3d576d930189725f1f1d1bdfcc6)
|
||||
|
||||
<p align="center"><img src="./images/zipkin-ui.png?raw=true"/></p>
|
||||
|
||||
### Jaeger
|
||||
|
||||
- Run the server
|
||||
|
||||
```sh
|
||||
$ # from this directory
|
||||
$ npm run jaeger:server
|
||||
```
|
||||
|
||||
- Run the client
|
||||
|
||||
```sh
|
||||
$ # from this directory
|
||||
$ npm run jaeger:client
|
||||
```
|
||||
#### Jaeger UI
|
||||
|
||||
`jaeger:server` script should output the `traceid` in the terminal (e.g `traceid: 4815c3d576d930189725f1f1d1bdfcc6`).
|
||||
Go to Jaeger with your browser [http://localhost:16686/trace/(your-trace-id)]() (e.g http://localhost:16686/trace/4815c3d576d930189725f1f1d1bdfcc6)
|
||||
|
||||
<p align="center"><img src="images/jaeger-ui.png?raw=true"/></p>
|
||||
|
||||
## Useful links
|
||||
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
|
||||
- For more information on OpenTelemetry for Node.js, visit: <https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node-sdk>
|
||||
|
||||
## LICENSE
|
||||
|
||||
Apache License 2.0
|
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
const opentelemetry = require('@opentelemetry/core');
|
||||
const config = require('./setup');
|
||||
/**
|
||||
* The trace instance needs to be initialized first, if you want to enable
|
||||
* automatic tracing for built-in plugins (HTTPs in this case).
|
||||
*/
|
||||
config.setupTracerAndExporters('https-client-service');
|
||||
|
||||
const https = require('https');
|
||||
const tracer = opentelemetry.getTracer();
|
||||
|
||||
/** A function which makes requests and handles response. */
|
||||
function makeRequest() {
|
||||
// span corresponds to outgoing requests. Here, we have manually created
|
||||
// the span, which is created to track work that happens outside of the
|
||||
// request lifecycle entirely.
|
||||
const span = tracer.startSpan('makeRequest');
|
||||
tracer.withSpan(span, () => {
|
||||
https.get({
|
||||
host: 'localhost',
|
||||
port: 443,
|
||||
path: '/helloworld'
|
||||
}, (response) => {
|
||||
let body = [];
|
||||
response.on('data', chunk => body.push(chunk));
|
||||
response.on('end', () => {
|
||||
console.log(body.toString());
|
||||
span.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// The process must live for at least the interval past any traces that
|
||||
// must be exported, or some risk being lost if they are recorded after the
|
||||
// last export.
|
||||
console.log('Sleeping 5 seconds before shutdown to ensure all records are flushed.')
|
||||
setTimeout(() => { console.log('Completed.'); }, 5000);
|
||||
}
|
||||
|
||||
makeRequest();
|
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "https-example",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Example of HTTPs integration with OpenTelemetry",
|
||||
"main": "build/src/index.js",
|
||||
"types": "build/src/index.d.ts",
|
||||
"scripts": {
|
||||
"zipkin:server": "cross-env EXPORTER=zipkin node ./server.js",
|
||||
"zipkin:client": "cross-env EXPORTER=zipkin node ./client.js",
|
||||
"jaeger:server": "cross-env EXPORTER=jaeger node ./server.js",
|
||||
"jaeger:client": "cross-env EXPORTER=jaeger node ./client.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/open-telemetry/opentelemetry-js.git"
|
||||
},
|
||||
"keywords": [
|
||||
"opentelemetry",
|
||||
"https",
|
||||
"tracing"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"author": "OpenTelemetry Authors",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/open-telemetry/opentelemetry-js/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "^0.1.0",
|
||||
"@opentelemetry/exporter-jaeger": "^0.1.0",
|
||||
"@opentelemetry/exporter-zipkin": "^0.1.0",
|
||||
"@opentelemetry/node-sdk": "^0.1.0",
|
||||
"@opentelemetry/plugin-https": "^0.1.0",
|
||||
"@opentelemetry/tracer-basic": "^0.1.0"
|
||||
},
|
||||
"homepage": "https://github.com/open-telemetry/opentelemetry-js#readme",
|
||||
"devDependencies": {
|
||||
"cross-env": "^6.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBqzCCARQCCQDLcUeJsLDL5jANBgkqhkiG9w0BAQUFADAaMQswCQYDVQQGEwJD
|
||||
QTELMAkGA1UECAwCUUMwHhcNMTkwOTI5MjIwMDI2WhcNMTkxMDI5MjIwMDI2WjAa
|
||||
MQswCQYDVQQGEwJDQTELMAkGA1UECAwCUUMwgZ8wDQYJKoZIhvcNAQEBBQADgY0A
|
||||
MIGJAoGBALhfi1dwIyC1Jha4N/j/VtlPPi+j+SZQGZqLNVVgzzGY7+cc3VkCySZD
|
||||
yXh3Z+/ftp9DDKdHRutJQE0R4peSDussC/IQDJKzuKN/O9S6tnNlgUr5YZLRENxL
|
||||
FSJIY5cIkty50IrEhlN5QeDJP8p4yrYq9J6M0yzyfdqIWI3CBqbzAgMBAAEwDQYJ
|
||||
KoZIhvcNAQEFBQADgYEArnOeXmXXJTK39Ma25elHxlYUZiYOBu/truy5zmx4umyS
|
||||
GyehAv+jRIanoCRWtOBnrjS5CY/6cC64aIVLMoqXEFIL7q/GD0wEM/DS8rN7KTcp
|
||||
w+nIX98srYaAFeQZScPioS6WpXz5AjbTVhvAwkIm2/s6dOlX31+1zu6Zu6ASSuQ=
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQC4X4tXcCMgtSYWuDf4/1bZTz4vo/kmUBmaizVVYM8xmO/nHN1Z
|
||||
AskmQ8l4d2fv37afQwynR0brSUBNEeKXkg7rLAvyEAySs7ijfzvUurZzZYFK+WGS
|
||||
0RDcSxUiSGOXCJLcudCKxIZTeUHgyT/KeMq2KvSejNMs8n3aiFiNwgam8wIDAQAB
|
||||
AoGBAKBztcYQduGeBFm9VCjDvgc8KTg4kTlAeCfAglec+nOFTzJoMlGmVPuR/qFx
|
||||
+OgOXtXW+goRw6w7gVQQ/os9tvCCp7awSC5UCfPejHh6bW2B0BF2lZJ6B9y+u5Fa
|
||||
/p8oKoJGcC4eagVnDojuoYJHSqWBf7d7V/U54NpxwgBTsHAhAkEA8PJROgWzjMl2
|
||||
Gs5j8oBldEqzrC/d4K1uMEvCTb4RJ+t6jWq+Ug/vqvCfIcLfxHbOmTbOHTfhpv/d
|
||||
NUf9eDyBGwJBAMPkZaHP5vPDd900MqypLVasollzxgPnMUg35EEQJLAbb/5xG3X9
|
||||
ZbaVDTRtLQYNFvDZLlTpRpCPxZCgrn9hJwkCQQDPEVChLrkpqxFm5CydAZ8vG+vh
|
||||
dJmYNzPVKaZorYmM5yBBXJUHbU6pd3UqzJEGBJx0q9bi4V156bYvzhiVNlo1AkBu
|
||||
1hbvFCwPtoRmg3c8nEhL50fApzHd2XzX6M/cRF8Nyah3ZdXsz6AyS2l6RV+ZMeTO
|
||||
B4QghRDpEH/vUgsJhZXJAkB5GQZPJh6/kozc5+Ffc60ThN/58SX0KEFeKnWRlzfr
|
||||
vfBXwcmaz1oNXN+kcWdLnKbr/tx+3UQ6weRRmeYX/hOi
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,61 @@
|
|||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const opentelemetry = require('@opentelemetry/core');
|
||||
const config = require('./setup');
|
||||
/**
|
||||
* The trace instance needs to be initialized first, if you want to enable
|
||||
* automatic tracing for built-in plugins (HTTPs in this case).
|
||||
*/
|
||||
config.setupTracerAndExporters('https-server-service');
|
||||
|
||||
const https = require('https');
|
||||
const tracer = opentelemetry.getTracer();
|
||||
|
||||
/** Starts a HTTPs server that receives requests on sample server port. */
|
||||
function startServer (port) {
|
||||
const options = {
|
||||
key: fs.readFileSync('./server-key.pem'),
|
||||
cert: fs.readFileSync('./server-cert.pem')
|
||||
};
|
||||
// Creates a server
|
||||
const server = https.createServer(options, handleRequest);
|
||||
// Starts the server
|
||||
server.listen(port, err => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
console.log(`Node HTTPs listening on ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
/** A function which handles requests and send response. */
|
||||
function handleRequest (request, response) {
|
||||
const currentSpan = tracer.getCurrentSpan();
|
||||
// display traceid in the terminal
|
||||
console.log(`traceid: ${currentSpan.context().traceId}`);
|
||||
const span = tracer.startSpan('handleRequest', {
|
||||
parent: currentSpan,
|
||||
kind: 1, // server
|
||||
attributes: { key:'value' }
|
||||
});
|
||||
// Annotate our span to capture metadata about the operation
|
||||
span.addEvent('invoking handleRequest');
|
||||
try {
|
||||
let body = [];
|
||||
request.on('error', err => console.log(err));
|
||||
request.on('data', chunk => body.push(chunk));
|
||||
request.on('end', () => {
|
||||
// deliberately sleeping to mock some action.
|
||||
setTimeout(() => {
|
||||
span.end();
|
||||
response.end('Hello World!');
|
||||
}, 2000);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
startServer(443);
|
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
const opentelemetry = require('@opentelemetry/core');
|
||||
const { NodeTracer } = require('@opentelemetry/node-sdk');
|
||||
const { SimpleSpanProcessor } = require('@opentelemetry/tracer-basic');
|
||||
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
|
||||
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
|
||||
const EXPORTER = process.env.EXPORTER || '';
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
function setupTracerAndExporters(service) {
|
||||
let exporter;
|
||||
const tracer = new NodeTracer();
|
||||
|
||||
if (EXPORTER.toLowerCase().startsWith('z')) {
|
||||
exporter = new ZipkinExporter({
|
||||
serviceName: service
|
||||
});
|
||||
} else {
|
||||
exporter = new JaegerExporter({
|
||||
serviceName: service,
|
||||
// The default flush interval is 5 seconds.
|
||||
flushInterval: 2000
|
||||
});
|
||||
}
|
||||
|
||||
tracer.addSpanProcessor(new SimpleSpanProcessor(exporter));
|
||||
|
||||
// Initialize the OpenTelemetry APIs to use the BasicTracer bindings
|
||||
opentelemetry.initGlobalTracer(tracer);
|
||||
}
|
||||
|
||||
exports.setupTracerAndExporters = setupTracerAndExporters;
|
|
@ -17,7 +17,7 @@ npm install --save @opentelemetry/plugin-http
|
|||
|
||||
## Usage
|
||||
|
||||
OpenTelemetry HTTP Instrumentation allows the user to automatically collect trace data and export them to the backend of choice, to give observability to distributed systems.
|
||||
OpenTelemetry HTTP Instrumentation allows the user to automatically collect trace data and export them to their backend of choice, to give observability to distributed systems.
|
||||
|
||||
To load a specific plugin (HTTP in this case), specify it in the Node Tracer's configuration.
|
||||
```js
|
||||
|
|
|
@ -48,11 +48,13 @@ import * as utils from './utils';
|
|||
* Http instrumentation plugin for Opentelemetry
|
||||
*/
|
||||
export class HttpPlugin extends BasePlugin<Http> {
|
||||
static readonly component = 'http';
|
||||
readonly component: string;
|
||||
protected _config!: HttpPluginConfig;
|
||||
|
||||
constructor(readonly moduleName: string, readonly version: string) {
|
||||
super();
|
||||
// For now component is equal to moduleName but it can change in the future.
|
||||
this.component = this.moduleName;
|
||||
this._config = {};
|
||||
}
|
||||
|
||||
|
@ -76,7 +78,7 @@ export class HttpPlugin extends BasePlugin<Http> {
|
|||
shimmer.wrap(
|
||||
this._moduleExports,
|
||||
'get',
|
||||
this._getPatchOutgoingGetFunction()
|
||||
this._getPatchOutgoingGetFunction(request)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -134,7 +136,12 @@ export class HttpPlugin extends BasePlugin<Http> {
|
|||
};
|
||||
}
|
||||
|
||||
protected _getPatchOutgoingGetFunction() {
|
||||
protected _getPatchOutgoingGetFunction(
|
||||
clientRequest: (
|
||||
options: RequestOptions | string | URL,
|
||||
...args: HttpRequestArgs
|
||||
) => ClientRequest
|
||||
) {
|
||||
return (original: Func<ClientRequest>): Func<ClientRequest> => {
|
||||
// Re-implement http.get. This needs to be done (instead of using
|
||||
// getPatchOutgoingRequestFunction to patch it) because we need to
|
||||
|
@ -148,10 +155,10 @@ export class HttpPlugin extends BasePlugin<Http> {
|
|||
// https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/plugins/plugin-http.ts#L198
|
||||
return function outgoingGetRequest<
|
||||
T extends RequestOptions | string | URL
|
||||
>(options: T, ...args: HttpRequestArgs) {
|
||||
const req = request(options, ...args);
|
||||
>(options: T, ...args: HttpRequestArgs): ClientRequest {
|
||||
const req = clientRequest(options, ...args);
|
||||
req.end();
|
||||
return req as ClientRequest;
|
||||
return req;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -180,7 +187,7 @@ export class HttpPlugin extends BasePlugin<Http> {
|
|||
[AttributeNames.HTTP_URL]: utils.getAbsoluteUrl(
|
||||
options,
|
||||
headers,
|
||||
`${HttpPlugin.component}:`
|
||||
`${this.component}:`
|
||||
),
|
||||
[AttributeNames.HTTP_HOSTNAME]: host,
|
||||
[AttributeNames.HTTP_METHOD]: method,
|
||||
|
@ -311,7 +318,7 @@ export class HttpPlugin extends BasePlugin<Http> {
|
|||
[AttributeNames.HTTP_URL]: utils.getAbsoluteUrl(
|
||||
requestUrl,
|
||||
headers,
|
||||
`${HttpPlugin.component}:`
|
||||
`${plugin.component}:`
|
||||
),
|
||||
[AttributeNames.HTTP_HOSTNAME]: hostname,
|
||||
[AttributeNames.HTTP_METHOD]: method,
|
||||
|
@ -433,7 +440,7 @@ export class HttpPlugin extends BasePlugin<Http> {
|
|||
private _startHttpSpan(name: string, options: SpanOptions) {
|
||||
return this._tracer
|
||||
.startSpan(name, options)
|
||||
.setAttribute(AttributeNames.COMPONENT, HttpPlugin.component);
|
||||
.setAttribute(AttributeNames.COMPONENT, this.component);
|
||||
}
|
||||
private _safeExecute<
|
||||
T extends (...args: unknown[]) => ReturnType<T>,
|
||||
|
@ -461,7 +468,4 @@ export class HttpPlugin extends BasePlugin<Http> {
|
|||
}
|
||||
}
|
||||
|
||||
export const plugin = new HttpPlugin(
|
||||
HttpPlugin.component,
|
||||
process.versions.node
|
||||
);
|
||||
export const plugin = new HttpPlugin('http', process.versions.node);
|
||||
|
|
|
@ -103,10 +103,9 @@ describe('HttpPlugin', () => {
|
|||
},
|
||||
};
|
||||
pluginWithBadOptions = new HttpPlugin(
|
||||
HttpPlugin.component,
|
||||
plugin.component,
|
||||
process.versions.node
|
||||
);
|
||||
|
||||
pluginWithBadOptions.enable(http, tracer, tracer.logger, config);
|
||||
server = http.createServer((request, response) => {
|
||||
response.end('Test Server Response');
|
||||
|
@ -133,6 +132,7 @@ describe('HttpPlugin', () => {
|
|||
pathname,
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 2);
|
||||
|
@ -156,7 +156,6 @@ describe('HttpPlugin', () => {
|
|||
assert.strictEqual(spans.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with good plugin options', () => {
|
||||
beforeEach(() => {
|
||||
memoryExporter.reset();
|
||||
|
@ -195,7 +194,7 @@ describe('HttpPlugin', () => {
|
|||
|
||||
it("should not patch if it's not a http module", () => {
|
||||
const httpNotPatched = new HttpPlugin(
|
||||
HttpPlugin.component,
|
||||
plugin.component,
|
||||
process.versions.node
|
||||
).enable({} as Http, tracer, tracer.logger, {});
|
||||
assert.strictEqual(Object.keys(httpNotPatched).length, 0);
|
||||
|
@ -214,6 +213,7 @@ describe('HttpPlugin', () => {
|
|||
pathname,
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 2);
|
||||
|
@ -269,6 +269,7 @@ describe('HttpPlugin', () => {
|
|||
pathname: testPath,
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assertSpan(reqSpan, SpanKind.CLIENT, validations);
|
||||
|
@ -294,6 +295,7 @@ describe('HttpPlugin', () => {
|
|||
pathname: testPath,
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0);
|
||||
|
@ -336,6 +338,7 @@ describe('HttpPlugin', () => {
|
|||
pathname: testPath,
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0);
|
||||
|
|
|
@ -108,6 +108,7 @@ describe('Packages', () => {
|
|||
pathname: urlparsed.pathname!,
|
||||
path: urlparsed.path,
|
||||
resHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 1);
|
||||
|
|
|
@ -23,7 +23,7 @@ import { assertSpan } from '../utils/assertSpan';
|
|||
import { DummyPropagation } from '../utils/DummyPropagation';
|
||||
import { httpRequest } from '../utils/httpRequest';
|
||||
import * as url from 'url';
|
||||
import { Utils } from '../utils/Utils';
|
||||
import * as utils from '../utils/utils';
|
||||
import { NodeTracer } from '@opentelemetry/node-sdk';
|
||||
import {
|
||||
InMemorySpanExporter,
|
||||
|
@ -48,7 +48,7 @@ describe('HttpPlugin Integration tests', () => {
|
|||
return;
|
||||
}
|
||||
|
||||
Utils.checkInternet(isConnected => {
|
||||
utils.checkInternet(isConnected => {
|
||||
if (!isConnected) {
|
||||
this.skip();
|
||||
// don't disturbe people
|
||||
|
@ -105,6 +105,7 @@ describe('HttpPlugin Integration tests', () => {
|
|||
path: '/?query=test',
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 1);
|
||||
|
@ -123,6 +124,7 @@ describe('HttpPlugin Integration tests', () => {
|
|||
pathname: '/',
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 1);
|
||||
|
@ -149,6 +151,7 @@ describe('HttpPlugin Integration tests', () => {
|
|||
pathname: '/',
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 1);
|
||||
|
|
|
@ -22,7 +22,6 @@ import { AttributeNames } from '../../src/enums/AttributeNames';
|
|||
import * as utils from '../../src/utils';
|
||||
import { DummyPropagation } from './DummyPropagation';
|
||||
import { ReadableSpan } from '@opentelemetry/tracer-basic';
|
||||
import { HttpPlugin } from '../../src/http';
|
||||
|
||||
export const assertSpan = (
|
||||
span: ReadableSpan,
|
||||
|
@ -36,6 +35,7 @@ export const assertSpan = (
|
|||
reqHeaders?: http.OutgoingHttpHeaders;
|
||||
path?: string;
|
||||
forceStatus?: Status;
|
||||
component: string;
|
||||
}
|
||||
) => {
|
||||
assert.strictEqual(span.spanContext.traceId.length, 32);
|
||||
|
@ -47,7 +47,7 @@ export const assertSpan = (
|
|||
);
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.COMPONENT],
|
||||
HttpPlugin.component
|
||||
validations.component
|
||||
);
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
|
||||
|
|
|
@ -16,14 +16,12 @@
|
|||
|
||||
import * as dns from 'dns';
|
||||
|
||||
export class Utils {
|
||||
static checkInternet(cb: (isConnected: boolean) => void) {
|
||||
dns.lookup('google.com', err => {
|
||||
if (err && err.code === 'ENOTFOUND') {
|
||||
cb(false);
|
||||
} else {
|
||||
cb(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
export const checkInternet = (cb: (isConnected: boolean) => void) => {
|
||||
dns.lookup('google.com', err => {
|
||||
if (err && err.code === 'ENOTFOUND') {
|
||||
cb(false);
|
||||
} else {
|
||||
cb(true);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -17,12 +17,43 @@ npm install --save @opentelemetry/plugin-https
|
|||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const opentelemetry = require('@opentelemetry/plugin-https');
|
||||
OpenTelemetry HTTPS Instrumentation allows the user to automatically collect trace data and export them to their backend of choice, to give observability to distributed systems.
|
||||
|
||||
// TODO: DEMONSTRATE API
|
||||
To load a specific plugin (HTTPS in this case), specify it in the Node Tracer's configuration.
|
||||
```js
|
||||
const { NodeTracer } = require('@opentelemetry/node-sdk');
|
||||
|
||||
const tracer = new NodeTracer({
|
||||
plugins: {
|
||||
https: {
|
||||
enabled: true,
|
||||
// You may use a package name or absolute path to the file.
|
||||
path: '@opentelemetry/plugin-https',
|
||||
// https plugin options
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
To load all the [supported plugins](https://github.com/open-telemetry/opentelemetry-js#plugins), use below approach. Each plugin is only loaded when the module that it patches is loaded; in other words, there is no computational overhead for listing plugins for unused modules.
|
||||
```js
|
||||
const { NodeTracer } = require('@opentelemetry/node-sdk');
|
||||
|
||||
const tracer = new NodeTracer();
|
||||
```
|
||||
|
||||
See [examples/https](https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/https) for a short example.
|
||||
|
||||
### Https Plugin Options
|
||||
|
||||
Https plugin has few options available to choose from. You can set the following:
|
||||
|
||||
| Options | Type | Description |
|
||||
| ------- | ---- | ----------- |
|
||||
| [`applyCustomAttributesOnSpan`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L52) | `HttpCustomAttributeFunction` | Function for adding custom attributes |
|
||||
| [`ignoreIncomingPaths`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L28) | `IgnoreMatcher[]` | Http plugin will not trace all incoming requests that match paths |
|
||||
| [`ignoreOutgoingUrls`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L28) | `IgnoreMatcher[]` | Http plugin will not trace all outgoing requests that match urls |
|
||||
|
||||
## Useful links
|
||||
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
|
||||
- For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
"name": "@opentelemetry/plugin-https",
|
||||
"version": "0.1.0",
|
||||
"description": "OpenTelemetry https automatic instrumentation package.",
|
||||
"private": true,
|
||||
"main": "build/src/index.js",
|
||||
"types": "build/src/index.d.ts",
|
||||
"repository": "open-telemetry/opentelemetry-js",
|
||||
|
@ -38,22 +37,41 @@
|
|||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/got": "^9.6.7",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/node": "^12.6.9",
|
||||
"codecov": "^3.5.0",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^12.7.8",
|
||||
"@types/request-promise-native": "^1.0.17",
|
||||
"@types/semver": "^6.0.2",
|
||||
"@types/shimmer": "^1.0.1",
|
||||
"@types/sinon": "^7.0.13",
|
||||
"@types/superagent": "^4.1.3",
|
||||
"@opentelemetry/tracer-basic": "^0.1.0",
|
||||
"@opentelemetry/node-sdk": "^0.1.0",
|
||||
"@opentelemetry/scope-base": "^0.1.0",
|
||||
"axios": "^0.19.0",
|
||||
"got": "^9.6.0",
|
||||
"request": "^2.88.0",
|
||||
"request-promise-native": "^1.0.7",
|
||||
"superagent": "5.1.0",
|
||||
"codecov": "^3.6.1",
|
||||
"gts": "^1.1.0",
|
||||
"mocha": "^6.2.0",
|
||||
"mocha": "^6.2.1",
|
||||
"nock": "^11.3.5",
|
||||
"nyc": "^14.1.1",
|
||||
"rimraf": "^3.0.0",
|
||||
"sinon": "^7.5.0",
|
||||
"tslint-microsoft-contrib": "^6.2.0",
|
||||
"tslint-consistent-codestyle": "^1.15.1",
|
||||
"tslint-consistent-codestyle": "^1.16.0",
|
||||
"ts-mocha": "^6.0.0",
|
||||
"ts-node": "^8.3.0",
|
||||
"ts-node": "^8.4.1",
|
||||
"typescript": "^3.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/types": "^0.1.0",
|
||||
"@opentelemetry/core": "^0.1.0",
|
||||
"@opentelemetry/node-sdk": "^0.1.0",
|
||||
"@opentelemetry/types": "^0.1.0"
|
||||
"@opentelemetry/plugin-http": "^0.1.0",
|
||||
"semver": "^6.3.0",
|
||||
"shimmer": "^1.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 { HttpPlugin, Func, HttpRequestArgs } from '@opentelemetry/plugin-http';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as semver from 'semver';
|
||||
import * as shimmer from 'shimmer';
|
||||
import * as utils from './utils';
|
||||
|
||||
/**
|
||||
* Https instrumentation plugin for Opentelemetry
|
||||
*/
|
||||
export class HttpsPlugin extends HttpPlugin {
|
||||
/** Constructs a new HttpsPlugin instance. */
|
||||
constructor(readonly version: string) {
|
||||
super('https', version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches HTTPS incoming and outcoming request functions.
|
||||
*/
|
||||
protected patch() {
|
||||
this._logger.debug(
|
||||
'applying patch to %s@%s',
|
||||
this.moduleName,
|
||||
this.version
|
||||
);
|
||||
|
||||
if (
|
||||
this._moduleExports &&
|
||||
this._moduleExports.Server &&
|
||||
this._moduleExports.Server.prototype
|
||||
) {
|
||||
shimmer.wrap(
|
||||
this._moduleExports.Server.prototype,
|
||||
'emit',
|
||||
this._getPatchIncomingRequestFunction()
|
||||
);
|
||||
} else {
|
||||
this._logger.error(
|
||||
'Could not apply patch to %s.emit. Interface is not as expected.',
|
||||
this.moduleName
|
||||
);
|
||||
}
|
||||
|
||||
shimmer.wrap(
|
||||
this._moduleExports,
|
||||
'request',
|
||||
this._getPatchHttpsOutgoingRequestFunction()
|
||||
);
|
||||
|
||||
// In Node 8-12, http.get calls a private request method, therefore we patch it
|
||||
// here too.
|
||||
if (semver.satisfies(this.version, '>=8.0.0')) {
|
||||
shimmer.wrap(
|
||||
this._moduleExports,
|
||||
'get',
|
||||
this._getPatchHttpsOutgoingGetFunction(https.request)
|
||||
);
|
||||
}
|
||||
|
||||
return this._moduleExports;
|
||||
}
|
||||
|
||||
/** Patches HTTPS outgoing requests */
|
||||
private _getPatchHttpsOutgoingRequestFunction() {
|
||||
return (original: Func<http.ClientRequest>): Func<http.ClientRequest> => {
|
||||
const plugin = this;
|
||||
return function httpsOutgoingRequest(
|
||||
options,
|
||||
...args: HttpRequestArgs
|
||||
): http.ClientRequest {
|
||||
// Makes sure options will have default HTTPS parameters
|
||||
if (typeof options === 'object') {
|
||||
utils.setDefaultOptions(options);
|
||||
}
|
||||
return plugin._getPatchOutgoingRequestFunction()(original)(
|
||||
options,
|
||||
...args
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** Patches HTTPS outgoing get requests */
|
||||
private _getPatchHttpsOutgoingGetFunction(
|
||||
clientRequest: (
|
||||
options: http.RequestOptions | string | URL,
|
||||
...args: HttpRequestArgs
|
||||
) => http.ClientRequest
|
||||
) {
|
||||
return (original: Func<http.ClientRequest>): Func<http.ClientRequest> => {
|
||||
return function httpsOutgoingRequest(
|
||||
options: https.RequestOptions | string,
|
||||
...args: HttpRequestArgs
|
||||
): http.ClientRequest {
|
||||
const optionsType = typeof options;
|
||||
// Makes sure options will have default HTTPS parameters
|
||||
if (optionsType === 'object') {
|
||||
utils.setDefaultOptions(options as https.RequestOptions);
|
||||
} else if (typeof args[0] === 'object' && optionsType === 'string') {
|
||||
utils.setDefaultOptions(args[0] as https.RequestOptions);
|
||||
}
|
||||
|
||||
return plugin._getPatchOutgoingGetFunction(clientRequest)(original)(
|
||||
options,
|
||||
...args
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** Unpatches all HTTPS patched function. */
|
||||
protected unpatch(): void {
|
||||
if (
|
||||
this._moduleExports &&
|
||||
this._moduleExports.Server &&
|
||||
this._moduleExports.Server.prototype
|
||||
) {
|
||||
shimmer.unwrap(this._moduleExports.Server.prototype, 'emit');
|
||||
}
|
||||
shimmer.unwrap(this._moduleExports, 'request');
|
||||
if (semver.satisfies(this.version, '>=8.0.0')) {
|
||||
shimmer.unwrap(this._moduleExports, 'get');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const plugin = new HttpsPlugin(process.versions.node);
|
|
@ -13,3 +13,5 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './https';
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 https from 'https';
|
||||
|
||||
export const setDefaultOptions = (options: https.RequestOptions) => {
|
||||
options.protocol = options.protocol || 'https:';
|
||||
options.port = options.port || 443;
|
||||
options.agent = options.agent || https.globalAgent;
|
||||
};
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,11 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBqzCCARQCCQDLcUeJsLDL5jANBgkqhkiG9w0BAQUFADAaMQswCQYDVQQGEwJD
|
||||
QTELMAkGA1UECAwCUUMwHhcNMTkwOTI5MjIwMDI2WhcNMTkxMDI5MjIwMDI2WjAa
|
||||
MQswCQYDVQQGEwJDQTELMAkGA1UECAwCUUMwgZ8wDQYJKoZIhvcNAQEBBQADgY0A
|
||||
MIGJAoGBALhfi1dwIyC1Jha4N/j/VtlPPi+j+SZQGZqLNVVgzzGY7+cc3VkCySZD
|
||||
yXh3Z+/ftp9DDKdHRutJQE0R4peSDussC/IQDJKzuKN/O9S6tnNlgUr5YZLRENxL
|
||||
FSJIY5cIkty50IrEhlN5QeDJP8p4yrYq9J6M0yzyfdqIWI3CBqbzAgMBAAEwDQYJ
|
||||
KoZIhvcNAQEFBQADgYEArnOeXmXXJTK39Ma25elHxlYUZiYOBu/truy5zmx4umyS
|
||||
GyehAv+jRIanoCRWtOBnrjS5CY/6cC64aIVLMoqXEFIL7q/GD0wEM/DS8rN7KTcp
|
||||
w+nIX98srYaAFeQZScPioS6WpXz5AjbTVhvAwkIm2/s6dOlX31+1zu6Zu6ASSuQ=
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,15 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQC4X4tXcCMgtSYWuDf4/1bZTz4vo/kmUBmaizVVYM8xmO/nHN1Z
|
||||
AskmQ8l4d2fv37afQwynR0brSUBNEeKXkg7rLAvyEAySs7ijfzvUurZzZYFK+WGS
|
||||
0RDcSxUiSGOXCJLcudCKxIZTeUHgyT/KeMq2KvSejNMs8n3aiFiNwgam8wIDAQAB
|
||||
AoGBAKBztcYQduGeBFm9VCjDvgc8KTg4kTlAeCfAglec+nOFTzJoMlGmVPuR/qFx
|
||||
+OgOXtXW+goRw6w7gVQQ/os9tvCCp7awSC5UCfPejHh6bW2B0BF2lZJ6B9y+u5Fa
|
||||
/p8oKoJGcC4eagVnDojuoYJHSqWBf7d7V/U54NpxwgBTsHAhAkEA8PJROgWzjMl2
|
||||
Gs5j8oBldEqzrC/d4K1uMEvCTb4RJ+t6jWq+Ug/vqvCfIcLfxHbOmTbOHTfhpv/d
|
||||
NUf9eDyBGwJBAMPkZaHP5vPDd900MqypLVasollzxgPnMUg35EEQJLAbb/5xG3X9
|
||||
ZbaVDTRtLQYNFvDZLlTpRpCPxZCgrn9hJwkCQQDPEVChLrkpqxFm5CydAZ8vG+vh
|
||||
dJmYNzPVKaZorYmM5yBBXJUHbU6pd3UqzJEGBJx0q9bi4V156bYvzhiVNlo1AkBu
|
||||
1hbvFCwPtoRmg3c8nEhL50fApzHd2XzX6M/cRF8Nyah3ZdXsz6AyS2l6RV+ZMeTO
|
||||
B4QghRDpEH/vUgsJhZXJAkB5GQZPJh6/kozc5+Ffc60ThN/58SX0KEFeKnWRlzfr
|
||||
vfBXwcmaz1oNXN+kcWdLnKbr/tx+3UQ6weRRmeYX/hOi
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,95 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 { NoopLogger } from '@opentelemetry/core';
|
||||
import { NodeTracer } from '@opentelemetry/node-sdk';
|
||||
import { Http } from '@opentelemetry/plugin-http';
|
||||
import * as assert from 'assert';
|
||||
import * as fs from 'fs';
|
||||
import * as https from 'https';
|
||||
import { AddressInfo } from 'net';
|
||||
import * as nock from 'nock';
|
||||
import * as sinon from 'sinon';
|
||||
import { plugin } from '../../src/https';
|
||||
import { DummyPropagation } from '../utils/DummyPropagation';
|
||||
import { httpsRequest } from '../utils/httpsRequest';
|
||||
|
||||
describe('HttpsPlugin', () => {
|
||||
let server: https.Server;
|
||||
let serverPort = 0;
|
||||
|
||||
describe('disable()', () => {
|
||||
const httpTextFormat = new DummyPropagation();
|
||||
const logger = new NoopLogger();
|
||||
const tracer = new NodeTracer({
|
||||
logger,
|
||||
httpTextFormat,
|
||||
});
|
||||
before(() => {
|
||||
nock.cleanAll();
|
||||
nock.enableNetConnect();
|
||||
|
||||
plugin.enable((https as unknown) as Http, tracer, tracer.logger);
|
||||
// Ensure that https module is patched.
|
||||
assert.strictEqual(https.Server.prototype.emit.__wrapped, true);
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
tracer.startSpan = sinon.spy();
|
||||
tracer.withSpan = sinon.spy();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
server.close();
|
||||
});
|
||||
describe('unpatch()', () => {
|
||||
it('should not call tracer methods for creating span', async () => {
|
||||
plugin.disable();
|
||||
const testPath = '/incoming/unpatch/';
|
||||
|
||||
const options = { host: 'localhost', path: testPath, port: serverPort };
|
||||
|
||||
await httpsRequest.get(options).then(result => {
|
||||
assert.strictEqual(
|
||||
(tracer.startSpan as sinon.SinonSpy).called,
|
||||
false
|
||||
);
|
||||
|
||||
assert.strictEqual(https.Server.prototype.emit.__wrapped, undefined);
|
||||
assert.strictEqual((tracer.withSpan as sinon.SinonSpy).called, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,481 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 {
|
||||
InMemorySpanExporter,
|
||||
SimpleSpanProcessor,
|
||||
} from '@opentelemetry/tracer-basic';
|
||||
import { NoopLogger } from '@opentelemetry/core';
|
||||
import { NodeTracer } from '@opentelemetry/node-sdk';
|
||||
import {
|
||||
Http,
|
||||
HttpPluginConfig,
|
||||
OT_REQUEST_HEADER,
|
||||
} from '@opentelemetry/plugin-http';
|
||||
import { CanonicalCode, Span as ISpan, SpanKind } from '@opentelemetry/types';
|
||||
import * as assert from 'assert';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as nock from 'nock';
|
||||
import { HttpsPlugin, plugin } from '../../src/https';
|
||||
import { assertSpan } from '../utils/assertSpan';
|
||||
import { DummyPropagation } from '../utils/DummyPropagation';
|
||||
import { httpsRequest } from '../utils/httpsRequest';
|
||||
|
||||
let server: https.Server;
|
||||
const serverPort = 32345;
|
||||
const protocol = 'https';
|
||||
const hostname = 'localhost';
|
||||
const pathname = '/test';
|
||||
const memoryExporter = new InMemorySpanExporter();
|
||||
|
||||
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('HttpsPlugin', () => {
|
||||
it('should return a plugin', () => {
|
||||
assert.ok(plugin instanceof HttpsPlugin);
|
||||
});
|
||||
|
||||
it('should match version', () => {
|
||||
assert.strictEqual(process.versions.node, plugin.version);
|
||||
});
|
||||
|
||||
it('moduleName should be https', () => {
|
||||
assert.strictEqual('https', plugin.moduleName);
|
||||
});
|
||||
|
||||
describe('enable()', () => {
|
||||
const httpTextFormat = new DummyPropagation();
|
||||
const logger = new NoopLogger();
|
||||
const tracer = new NodeTracer({
|
||||
logger,
|
||||
httpTextFormat,
|
||||
});
|
||||
tracer.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
|
||||
beforeEach(() => {
|
||||
memoryExporter.reset();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
const config: HttpPluginConfig = {
|
||||
ignoreIncomingPaths: [
|
||||
`/ignored/string`,
|
||||
/\/ignored\/regexp$/i,
|
||||
(url: string) => url.endsWith(`/ignored/function`),
|
||||
],
|
||||
ignoreOutgoingUrls: [
|
||||
`${protocol}://${hostname}:${serverPort}/ignored/string`,
|
||||
/\/ignored\/regexp$/i,
|
||||
(url: string) => url.endsWith(`/ignored/function`),
|
||||
],
|
||||
applyCustomAttributesOnSpan: customAttributeFunction,
|
||||
};
|
||||
plugin.enable((https as unknown) as Http, tracer, tracer.logger, config);
|
||||
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();
|
||||
plugin.disable();
|
||||
});
|
||||
|
||||
it('https module should be patched', () => {
|
||||
assert.strictEqual(https.Server.prototype.emit.__wrapped, true);
|
||||
});
|
||||
|
||||
it("should not patch if it's not a http module", () => {
|
||||
const httpNotPatched = new HttpsPlugin(process.versions.node).enable(
|
||||
{} as Http,
|
||||
tracer,
|
||||
tracer.logger,
|
||||
{}
|
||||
);
|
||||
assert.strictEqual(Object.keys(httpNotPatched).length, 0);
|
||||
});
|
||||
|
||||
it('should generate valid spans (client side and server side)', async () => {
|
||||
const result = await httpsRequest.get(
|
||||
`${protocol}://${hostname}:${serverPort}${pathname}`
|
||||
);
|
||||
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: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 2);
|
||||
assertSpan(incomingSpan, SpanKind.SERVER, validations);
|
||||
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
|
||||
});
|
||||
|
||||
it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
|
||||
const testPath = '/outgoing/do-not-trace';
|
||||
doNock(hostname, testPath, 200, 'Ok');
|
||||
|
||||
const options = {
|
||||
host: hostname,
|
||||
path: testPath,
|
||||
headers: { [OT_REQUEST_HEADER]: 1 },
|
||||
};
|
||||
|
||||
const result = await httpsRequest.get(options);
|
||||
const spans = memoryExporter.getFinishedSpans();
|
||||
assert.strictEqual(result.data, 'Ok');
|
||||
assert.strictEqual(spans.length, 0);
|
||||
});
|
||||
|
||||
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 https 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: plugin.component,
|
||||
};
|
||||
|
||||
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 tracer.withSpan(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: plugin.component,
|
||||
};
|
||||
|
||||
assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0);
|
||||
assert.strictEqual(spans.length, 2);
|
||||
assert.ok(reqSpan.name.indexOf(testPath) >= 0);
|
||||
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 https 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 tracer.withSpan(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: plugin.component,
|
||||
};
|
||||
|
||||
assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0);
|
||||
assert.strictEqual(spans.length, 2);
|
||||
assert.ok(reqSpan.name.indexOf(testPath) >= 0);
|
||||
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 tracer.withSpan(span, async () => {
|
||||
for (let i = 0; i < num; i++) {
|
||||
await httpsRequest.get(`${protocol}://${hostname}${testPath}`);
|
||||
const spans = memoryExporter.getFinishedSpans();
|
||||
assert.ok(spans[i].name.indexOf(testPath) >= 0);
|
||||
assert.strictEqual(
|
||||
span.context().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 (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);
|
||||
});
|
||||
}
|
||||
|
||||
for (const arg of ['string', '', {}, new Date()]) {
|
||||
it(`should be tracable and not throw exception in https plugin when passing the following argument ${JSON.stringify(
|
||||
arg
|
||||
)}`, async () => {
|
||||
try {
|
||||
await httpsRequest.get(arg);
|
||||
} catch (error) {
|
||||
// https 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 plugin when passing the following argument ${JSON.stringify(
|
||||
arg
|
||||
)}`, async () => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
await httpsRequest.get(arg);
|
||||
} catch (error) {
|
||||
// https request has been made
|
||||
// nock throw
|
||||
assert.ok(
|
||||
error.stack.indexOf('/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', () => {
|
||||
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 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('/')
|
||||
.socketDelay(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();
|
||||
reject('timeout');
|
||||
});
|
||||
return req.end();
|
||||
});
|
||||
|
||||
try {
|
||||
await promiseRequest;
|
||||
assert.fail();
|
||||
} catch (error) {
|
||||
const spans = memoryExporter.getFinishedSpans();
|
||||
const [span] = spans;
|
||||
assert.strictEqual(spans.length, 1);
|
||||
assert.strictEqual(span.status.code, CanonicalCode.ABORTED);
|
||||
assert.ok(Object.keys(span.attributes).length > 7);
|
||||
}
|
||||
});
|
||||
|
||||
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.abort();
|
||||
data += chunk;
|
||||
});
|
||||
resp.on('end', () => {
|
||||
resolve(data);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return req.end();
|
||||
});
|
||||
|
||||
try {
|
||||
await promiseRequest;
|
||||
assert.fail();
|
||||
} catch (error) {
|
||||
const spans = memoryExporter.getFinishedSpans();
|
||||
const [span] = spans;
|
||||
assert.strictEqual(spans.length, 1);
|
||||
assert.strictEqual(span.status.code, CanonicalCode.ABORTED);
|
||||
assert.ok(Object.keys(span.attributes).length > 7);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,139 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 { NoopLogger } from '@opentelemetry/core';
|
||||
import { SpanKind, Span } from '@opentelemetry/types';
|
||||
import * as assert from 'assert';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import * as nock from 'nock';
|
||||
import { plugin } from '../../src/https';
|
||||
import { assertSpan } from '../utils/assertSpan';
|
||||
import { DummyPropagation } from '../utils/DummyPropagation';
|
||||
import * as url from 'url';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import * as superagent from 'superagent';
|
||||
import * as got from 'got';
|
||||
import * as request from 'request-promise-native';
|
||||
import * as path from 'path';
|
||||
import { NodeTracer } from '@opentelemetry/node-sdk';
|
||||
import {
|
||||
InMemorySpanExporter,
|
||||
SimpleSpanProcessor,
|
||||
} from '@opentelemetry/tracer-basic';
|
||||
import { Http } from '@opentelemetry/plugin-http';
|
||||
|
||||
const memoryExporter = new InMemorySpanExporter();
|
||||
|
||||
export const customAttributeFunction = (span: Span): void => {
|
||||
span.setAttribute('span kind', SpanKind.CLIENT);
|
||||
};
|
||||
|
||||
describe('Packages', () => {
|
||||
describe('get', () => {
|
||||
const httpTextFormat = new DummyPropagation();
|
||||
const logger = new NoopLogger();
|
||||
|
||||
const tracer = new NodeTracer({
|
||||
logger,
|
||||
httpTextFormat,
|
||||
});
|
||||
tracer.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
|
||||
beforeEach(() => {
|
||||
memoryExporter.reset();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
plugin.enable((https as unknown) as Http, tracer, tracer.logger);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
// back to normal
|
||||
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.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<{}>;
|
||||
resHeaders = res.headers;
|
||||
}
|
||||
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: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 1);
|
||||
assert.ok(span.name.indexOf(`GET ${urlparsed.pathname}`) >= 0);
|
||||
|
||||
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,227 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 {
|
||||
InMemorySpanExporter,
|
||||
SimpleSpanProcessor,
|
||||
} from '@opentelemetry/tracer-basic';
|
||||
import { NoopLogger } from '@opentelemetry/core';
|
||||
import { NodeTracer } from '@opentelemetry/node-sdk';
|
||||
import { HttpPluginConfig, Http } from '@opentelemetry/plugin-http';
|
||||
import { Span, SpanKind } from '@opentelemetry/types';
|
||||
import * as assert from 'assert';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as url from 'url';
|
||||
import { plugin } from '../../src/https';
|
||||
import { assertSpan } from '../utils/assertSpan';
|
||||
import { DummyPropagation } from '../utils/DummyPropagation';
|
||||
import { httpsRequest } from '../utils/httpsRequest';
|
||||
import * as utils from '../utils/utils';
|
||||
|
||||
const serverPort = 42345;
|
||||
const hostname = 'localhost';
|
||||
const memoryExporter = new InMemorySpanExporter();
|
||||
|
||||
export const customAttributeFunction = (span: Span): void => {
|
||||
span.setAttribute('span kind', SpanKind.CLIENT);
|
||||
};
|
||||
|
||||
describe('HttpsPlugin Integration tests', () => {
|
||||
describe('enable()', () => {
|
||||
before(function(done) {
|
||||
// mandatory
|
||||
if (process.env.CI) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
utils.checkInternet(isConnected => {
|
||||
if (!isConnected) {
|
||||
this.skip();
|
||||
// don't disturbe people
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
const httpTextFormat = new DummyPropagation();
|
||||
const logger = new NoopLogger();
|
||||
const tracer = new NodeTracer({
|
||||
logger,
|
||||
httpTextFormat,
|
||||
});
|
||||
tracer.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
|
||||
beforeEach(() => {
|
||||
memoryExporter.reset();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
const ignoreConfig = [
|
||||
`https://${hostname}:${serverPort}/ignored/string`,
|
||||
/\/ignored\/regexp$/i,
|
||||
(url: string) => url.endsWith(`/ignored/function`),
|
||||
];
|
||||
const config: HttpPluginConfig = {
|
||||
ignoreIncomingPaths: ignoreConfig,
|
||||
ignoreOutgoingUrls: ignoreConfig,
|
||||
applyCustomAttributesOnSpan: customAttributeFunction,
|
||||
};
|
||||
try {
|
||||
plugin.disable();
|
||||
} catch (e) {}
|
||||
plugin.enable((https as unknown) as Http, tracer, tracer.logger, config);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
plugin.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(`https://google.fr/?query=test`);
|
||||
|
||||
spans = memoryExporter.getFinishedSpans();
|
||||
const span = spans[0];
|
||||
const validations = {
|
||||
hostname: 'google.fr',
|
||||
httpStatusCode: result.statusCode!,
|
||||
httpMethod: 'GET',
|
||||
pathname: '/',
|
||||
path: '/?query=test',
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 1);
|
||||
assert.ok(span.name.indexOf('GET /') >= 0);
|
||||
assertSpan(span, SpanKind.CLIENT, validations);
|
||||
});
|
||||
|
||||
it('custom attributes should show up on client spans', async () => {
|
||||
const result = await httpsRequest.get(`https://google.fr/`);
|
||||
const spans = memoryExporter.getFinishedSpans();
|
||||
const span = spans[0];
|
||||
const validations = {
|
||||
hostname: 'google.fr',
|
||||
httpStatusCode: result.statusCode!,
|
||||
httpMethod: 'GET',
|
||||
pathname: '/',
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 1);
|
||||
assert.ok(span.name.indexOf('GET /') >= 0);
|
||||
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('https://google.fr/')
|
||||
);
|
||||
|
||||
const result = await httpsRequest.get(options);
|
||||
spans = memoryExporter.getFinishedSpans();
|
||||
const span = spans[0];
|
||||
const validations = {
|
||||
hostname: 'google.fr',
|
||||
httpStatusCode: 301,
|
||||
httpMethod: 'GET',
|
||||
pathname: '/',
|
||||
resHeaders: result.resHeaders,
|
||||
reqHeaders: result.reqHeaders,
|
||||
component: plugin.component,
|
||||
};
|
||||
|
||||
assert.strictEqual(spans.length, 1);
|
||||
assert.ok(span.name.indexOf('GET /') >= 0);
|
||||
|
||||
try {
|
||||
assertSpan(span, SpanKind.CLIENT, validations);
|
||||
} catch (error) {
|
||||
// temporary redirect is also correct
|
||||
validations.httpStatusCode = 307;
|
||||
assertSpan(span, SpanKind.CLIENT, validations);
|
||||
}
|
||||
});
|
||||
for (const headers of [
|
||||
{ Expect: '100-continue', 'user-agent': 'https-plugin-test' },
|
||||
{ 'user-agent': 'https-plugin-test' },
|
||||
]) {
|
||||
it(`should create a span for GET requests and add propagation when using the following signature: https.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(
|
||||
'https://google.fr/',
|
||||
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: 'google.fr',
|
||||
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();
|
||||
assert.strictEqual(spans.length, 1);
|
||||
assert.ok(spans[0].name.indexOf('GET /') >= 0);
|
||||
assert.ok(data);
|
||||
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
|
||||
assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 { SpanContext, HttpTextFormat } from '@opentelemetry/types';
|
||||
import * as http from 'http';
|
||||
|
||||
export class DummyPropagation implements HttpTextFormat {
|
||||
static TRACE_CONTEXT_KEY = 'x-dummy-trace-id';
|
||||
static SPAN_CONTEXT_KEY = 'x-dummy-span-id';
|
||||
extract(format: string, carrier: http.OutgoingHttpHeaders): SpanContext {
|
||||
return {
|
||||
traceId: carrier[DummyPropagation.TRACE_CONTEXT_KEY] as string,
|
||||
spanId: DummyPropagation.SPAN_CONTEXT_KEY,
|
||||
};
|
||||
}
|
||||
inject(
|
||||
spanContext: SpanContext,
|
||||
format: string,
|
||||
headers: { [custom: string]: string }
|
||||
): void {
|
||||
headers[DummyPropagation.TRACE_CONTEXT_KEY] = spanContext.traceId;
|
||||
headers[DummyPropagation.SPAN_CONTEXT_KEY] = spanContext.spanId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 } from '@opentelemetry/types';
|
||||
import { hrTimeToNanoseconds } from '@opentelemetry/core';
|
||||
import * as assert from 'assert';
|
||||
import * as http from 'http';
|
||||
import { DummyPropagation } from './DummyPropagation';
|
||||
import { ReadableSpan } from '@opentelemetry/tracer-basic';
|
||||
import {
|
||||
AttributeNames,
|
||||
parseResponseStatus,
|
||||
} from '@opentelemetry/plugin-http';
|
||||
|
||||
export const assertSpan = (
|
||||
span: ReadableSpan,
|
||||
kind: SpanKind,
|
||||
validations: {
|
||||
httpStatusCode: number;
|
||||
httpMethod: string;
|
||||
resHeaders: http.IncomingHttpHeaders;
|
||||
hostname: string;
|
||||
pathname: string;
|
||||
reqHeaders?: http.OutgoingHttpHeaders;
|
||||
path?: string;
|
||||
component: string;
|
||||
}
|
||||
) => {
|
||||
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} ${validations.pathname}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.COMPONENT],
|
||||
validations.component
|
||||
);
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
|
||||
span.status.message
|
||||
);
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.HTTP_HOSTNAME],
|
||||
validations.hostname
|
||||
);
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.HTTP_METHOD],
|
||||
validations.httpMethod
|
||||
);
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.HTTP_PATH],
|
||||
validations.path || validations.pathname
|
||||
);
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.HTTP_STATUS_CODE],
|
||||
validations.httpStatusCode
|
||||
);
|
||||
assert.ok(span.endTime);
|
||||
assert.strictEqual(span.links.length, 0);
|
||||
assert.strictEqual(span.events.length, 0);
|
||||
assert.deepStrictEqual(
|
||||
span.status,
|
||||
parseResponseStatus(validations.httpStatusCode)
|
||||
);
|
||||
|
||||
assert.ok(hrTimeToNanoseconds(span.duration), 'must have positive duration');
|
||||
|
||||
if (validations.reqHeaders) {
|
||||
const userAgent = validations.reqHeaders['user-agent'];
|
||||
if (userAgent) {
|
||||
assert.strictEqual(
|
||||
span.attributes[AttributeNames.HTTP_USER_AGENT],
|
||||
userAgent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (span.kind === SpanKind.SERVER) {
|
||||
assert.strictEqual(span.parentSpanId, DummyPropagation.SPAN_CONTEXT_KEY);
|
||||
} else if (validations.reqHeaders) {
|
||||
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
|
||||
assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
/*!
|
||||
* Copyright 2019, 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 { RequestOptions } from 'https';
|
||||
import * as url from 'url';
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
export const httpsRequest = {
|
||||
get: (
|
||||
options: string | RequestOptions
|
||||
): Promise<{
|
||||
data: string;
|
||||
statusCode: number | undefined;
|
||||
resHeaders: http.IncomingHttpHeaders;
|
||||
reqHeaders: http.OutgoingHttpHeaders;
|
||||
method: string | undefined;
|
||||
}> => {
|
||||
const _options =
|
||||
typeof options === 'string'
|
||||
? Object.assign(url.parse(options), {
|
||||
headers: {
|
||||
'user-agent': 'https-plugin-test',
|
||||
},
|
||||
})
|
||||
: options;
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(_options, (resp: http.IncomingMessage) => {
|
||||
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,
|
||||
/* tslint:disable:no-any */
|
||||
reqHeaders: (res.req as any).getHeaders
|
||||
? (res.req as any).getHeaders()
|
||||
: (res.req as any)._headers,
|
||||
/* tslint:enable:no-any */
|
||||
resHeaders: res.headers,
|
||||
method: res.req.method,
|
||||
});
|
||||
});
|
||||
resp.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
req.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
return req;
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*!
|
||||
* Copyright 2019, 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);
|
||||
}
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue