diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index 94a5baa11..8bf529240 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -6,7 +6,7 @@ "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js", "scripts": { - "test": "nyc ts-mocha -p tsconfig.json test/**/*/*.test.ts", + "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", "check": "gts check", diff --git a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts index 6f49e45f5..979d5622e 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts @@ -29,7 +29,7 @@ import { HttpPlugin, plugin } from '../../src/http'; import { assertSpan } from '../utils/assertSpan'; import { DummyPropagation } from '../utils/DummyPropagation'; import { httpRequest } from '../utils/httpRequest'; -import * as utils from '../../src/utils'; +import { OT_REQUEST_HEADER } from '../../src/utils'; import { HttpPluginConfig, Http } from '../../src/types'; import { AttributeNames } from '../../src/enums/AttributeNames'; @@ -77,8 +77,8 @@ describe('HttpPlugin', () => { assert.strictEqual(process.versions.node, plugin.version); }); - it('moduleName should be http', () => { - assert.strictEqual('http', plugin.moduleName); + it(`moduleName should be ${protocol}`, () => { + assert.strictEqual(protocol, plugin.moduleName); }); describe('enable()', () => { @@ -123,7 +123,7 @@ describe('HttpPlugin', () => { it('should generate valid spans (client side and server side)', async () => { const result = await httpRequest.get( - `http://${hostname}:${serverPort}${pathname}` + `${protocol}://${hostname}:${serverPort}${pathname}` ); const spans = memoryExporter.getFinishedSpans(); const [incomingSpan, outgoingSpan] = spans; @@ -142,14 +142,14 @@ describe('HttpPlugin', () => { assertSpan(outgoingSpan, SpanKind.CLIENT, validations); }); - it(`should not trace requests with '${utils.OT_REQUEST_HEADER}' header`, async () => { + 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: { [utils.OT_REQUEST_HEADER]: 1 }, + headers: { [OT_REQUEST_HEADER]: 1 }, }; const result = await httpRequest.get(options); @@ -171,7 +171,7 @@ describe('HttpPlugin', () => { (url: string) => url.endsWith(`/ignored/function`), ], ignoreOutgoingUrls: [ - `http://${hostname}:${serverPort}/ignored/string`, + `${protocol}://${hostname}:${serverPort}/ignored/string`, /\/ignored\/regexp$/i, (url: string) => url.endsWith(`/ignored/function`), ], @@ -190,11 +190,11 @@ describe('HttpPlugin', () => { plugin.disable(); }); - it('http module should be patched', () => { + it(`${protocol} module should be patched`, () => { assert.strictEqual(http.Server.prototype.emit.__wrapped, true); }); - it("should not patch if it's not a http module", () => { + it(`should not patch if it's not a ${protocol} module`, () => { const httpNotPatched = new HttpPlugin( plugin.component, process.versions.node @@ -204,7 +204,7 @@ describe('HttpPlugin', () => { it('should generate valid spans (client side and server side)', async () => { const result = await httpRequest.get( - `http://${hostname}:${serverPort}${pathname}` + `${protocol}://${hostname}:${serverPort}${pathname}` ); const spans = memoryExporter.getFinishedSpans(); const [incomingSpan, outgoingSpan] = spans; @@ -223,14 +223,14 @@ describe('HttpPlugin', () => { assertSpan(outgoingSpan, SpanKind.CLIENT, validations); }); - it(`should not trace requests with '${utils.OT_REQUEST_HEADER}' header`, async () => { + 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: { [utils.OT_REQUEST_HEADER]: 1 }, + headers: { [OT_REQUEST_HEADER]: 1 }, }; const result = await httpRequest.get(options); @@ -395,13 +395,13 @@ describe('HttpPlugin', () => { } for (const arg of ['string', {}, new Date()]) { - it(`should be tracable and not throw exception in http plugin when passing the following argument ${JSON.stringify( + it(`should be tracable and not throw exception in ${protocol} plugin when passing the following argument ${JSON.stringify( arg )}`, async () => { try { await httpRequest.get(arg); } catch (error) { - // http request has been made + // request has been made // nock throw assert.ok(error.message.startsWith('Nock: No match for request')); } @@ -411,14 +411,14 @@ describe('HttpPlugin', () => { } for (const arg of [true, 1, false, 0, '']) { - it(`should not throw exception in http plugin when passing the following argument ${JSON.stringify( + it(`should not throw exception in ${protocol} plugin when passing the following argument ${JSON.stringify( arg )}`, async () => { try { // @ts-ignore await httpRequest.get(arg); } catch (error) { - // http request has been made + // request has been made // nock throw assert.ok( error.stack.indexOf( @@ -447,7 +447,7 @@ describe('HttpPlugin', () => { const promiseRequest = new Promise((resolve, reject) => { const req = http.request( - `http://${hostname}${testPath}`, + `${protocol}://${hostname}${testPath}`, (resp: http.IncomingMessage) => { let data = ''; resp.on('data', chunk => { @@ -488,7 +488,7 @@ describe('HttpPlugin', () => { const promiseRequest = new Promise((resolve, reject) => { const req = http.request( - `http://${hostname}${testPath}`, + `${protocol}://${hostname}${testPath}`, (resp: http.IncomingMessage) => { let data = ''; resp.on('data', chunk => { @@ -512,14 +512,14 @@ describe('HttpPlugin', () => { }); it('should have 1 ended span when request is aborted', async () => { - nock('http://my.server.com') + nock(`${protocol}://my.server.com`) .get('/') .socketDelay(50) .reply(200, ''); const promiseRequest = new Promise((resolve, reject) => { const req = http.request( - 'http://my.server.com', + `${protocol}://my.server.com`, (resp: http.IncomingMessage) => { let data = ''; resp.on('data', chunk => { diff --git a/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts index 278c6aec2..4976e88a3 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts @@ -38,6 +38,7 @@ import { HttpPluginConfig } from '../../src/types'; import { customAttributeFunction } from './http-enable.test'; const memoryExporter = new InMemorySpanExporter(); +const protocol = 'http'; describe('Packages', () => { describe('get', () => { @@ -93,8 +94,8 @@ describe('Packages', () => { // 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 - 'http://info.cern.ch/' - : `http://www.google.com/search?q=axios&oq=axios&aqs=chrome.0.69i59l2j0l3j69i60.811j0j7&sourceid=chrome&ie=UTF-8` + `${protocol}://info.cern.ch/` + : `${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) { diff --git a/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts index 0e2ffcdc7..9f3eb84a7 100644 --- a/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/integrations/http-enable.test.ts @@ -30,7 +30,7 @@ import { SimpleSpanProcessor, } from '@opentelemetry/tracing'; import { HttpPluginConfig } from '../../src/types'; - +const protocol = 'http'; const serverPort = 32345; const hostname = 'localhost'; const memoryExporter = new InMemorySpanExporter(); @@ -70,7 +70,7 @@ describe('HttpPlugin Integration tests', () => { before(() => { const ignoreConfig = [ - `http://${hostname}:${serverPort}/ignored/string`, + `${protocol}://${hostname}:${serverPort}/ignored/string`, /\/ignored\/regexp$/i, (url: string) => url.endsWith(`/ignored/function`), ]; @@ -93,7 +93,9 @@ describe('HttpPlugin Integration tests', () => { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - const result = await httpRequest.get(`http://google.fr/?query=test`); + const result = await httpRequest.get( + `${protocol}://google.fr/?query=test` + ); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; @@ -118,7 +120,7 @@ describe('HttpPlugin Integration tests', () => { assert.strictEqual(spans.length, 0); const result = await httpRequest.get( - new url.URL('http://google.fr/?query=test') + new url.URL(`${protocol}://google.fr/?query=test`) ); spans = memoryExporter.getFinishedSpans(); @@ -144,7 +146,7 @@ describe('HttpPlugin Integration tests', () => { assert.strictEqual(spans.length, 0); const result = await httpRequest.get( - new url.URL('http://google.fr/?query=test'), + new url.URL(`${protocol}://google.fr/?query=test`), { headers: { 'x-foo': 'foo' } } ); @@ -168,7 +170,7 @@ describe('HttpPlugin Integration tests', () => { }); it('custom attributes should show up on client spans', async () => { - const result = await httpRequest.get(`http://google.fr/`); + const result = await httpRequest.get(`${protocol}://google.fr/`); const spans = memoryExporter.getFinishedSpans(); const span = spans[0]; const validations = { @@ -192,7 +194,7 @@ describe('HttpPlugin Integration tests', () => { assert.strictEqual(spans.length, 0); const options = Object.assign( { headers: { Expect: '100-continue' } }, - url.parse('http://google.fr/') + url.parse(`${protocol}://google.fr/`) ); const result = await httpRequest.get(options); @@ -223,7 +225,7 @@ describe('HttpPlugin Integration tests', () => { { 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: http.get(url, options, callback) and following headers: ${JSON.stringify( + 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: { @@ -239,7 +241,7 @@ describe('HttpPlugin Integration tests', () => { assert.strictEqual(spans.length, 0); const options = { headers }; const req = http.get( - 'http://google.fr/', + `${protocol}://google.fr/`, options, (resp: http.IncomingMessage) => { const res = (resp as unknown) as http.IncomingMessage & { @@ -266,7 +268,7 @@ describe('HttpPlugin Integration tests', () => { } ); - req.once('close', () => { + req.on('close', () => { const spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 1); assert.ok(spans[0].name.indexOf('GET /') >= 0); diff --git a/packages/opentelemetry-plugin-https/README.md b/packages/opentelemetry-plugin-https/README.md index 677acc8cb..c8aff1026 100644 --- a/packages/opentelemetry-plugin-https/README.md +++ b/packages/opentelemetry-plugin-https/README.md @@ -5,7 +5,7 @@ [![devDependencies][devDependencies-image]][devDependencies-url] [![Apache License][license-image]][license-image] -This module provides automatic instrumentation for [`https`](http://nodejs.org/dist/latest/docs/api/https.html). +This module provides automatic instrumentation for [`https`](http://nodejs.org/api/https.html). For automatic instrumentation see the [@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node) package. diff --git a/packages/opentelemetry-plugin-https/package.json b/packages/opentelemetry-plugin-https/package.json index 8d3a19264..6480dd2f3 100644 --- a/packages/opentelemetry-plugin-https/package.json +++ b/packages/opentelemetry-plugin-https/package.json @@ -6,10 +6,11 @@ "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js", "scripts": { - "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'", + "test": "nyc ts-mocha -p tsconfig.json test/**/*.test.ts", "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", "check": "gts check", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", "precompile": "tsc --version", "compile": "tsc -p .", "fix": "gts fix", diff --git a/packages/opentelemetry-plugin-https/src/https.ts b/packages/opentelemetry-plugin-https/src/https.ts index 2073ea722..cbf5d1b91 100644 --- a/packages/opentelemetry-plugin-https/src/https.ts +++ b/packages/opentelemetry-plugin-https/src/https.ts @@ -17,6 +17,7 @@ import { HttpPlugin, Func, HttpRequestArgs } from '@opentelemetry/plugin-http'; import * as http from 'http'; import * as https from 'https'; +import { URL } from 'url'; import * as semver from 'semver'; import * as shimmer from 'shimmer'; import * as utils from './utils'; @@ -81,12 +82,13 @@ export class HttpsPlugin extends HttpPlugin { return (original: Func): Func => { const plugin = this; return function httpsOutgoingRequest( - options, + options: https.RequestOptions | string | URL, ...args: HttpRequestArgs ): http.ClientRequest { // Makes sure options will have default HTTPS parameters - if (typeof options === 'object') { - utils.setDefaultOptions(options); + if (typeof options === 'object' && !(options instanceof URL)) { + options = Object.assign({}, options); + utils.setDefaultOptions(options as https.RequestOptions); } return plugin._getPatchOutgoingRequestFunction()(original)( options, @@ -105,17 +107,9 @@ export class HttpsPlugin extends HttpPlugin { ) { return (original: Func): Func => { return function httpsOutgoingRequest( - options: https.RequestOptions | string, + options: https.RequestOptions | string | URL, ...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 diff --git a/packages/opentelemetry-plugin-https/test/functionals/https-disable.test.ts b/packages/opentelemetry-plugin-https/test/functionals/https-disable.test.ts index 049af1be4..cc5f6edca 100644 --- a/packages/opentelemetry-plugin-https/test/functionals/https-disable.test.ts +++ b/packages/opentelemetry-plugin-https/test/functionals/https-disable.test.ts @@ -14,16 +14,17 @@ * limitations under the License. */ -import { NoopLogger } from '@opentelemetry/core'; -import { NodeTracer } from '@opentelemetry/node'; -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 { NodeTracer } from '@opentelemetry/node'; +import { NoopLogger } from '@opentelemetry/core'; +import { Http } from '@opentelemetry/plugin-http'; +import { AddressInfo } from 'net'; import { DummyPropagation } from '../utils/DummyPropagation'; import { httpsRequest } from '../utils/httpsRequest'; diff --git a/packages/opentelemetry-plugin-https/test/functionals/https-enable.test.ts b/packages/opentelemetry-plugin-https/test/functionals/https-enable.test.ts index cc449ddb4..1bfcc9410 100644 --- a/packages/opentelemetry-plugin-https/test/functionals/https-enable.test.ts +++ b/packages/opentelemetry-plugin-https/test/functionals/https-enable.test.ts @@ -24,24 +24,36 @@ import { Http, HttpPluginConfig, OT_REQUEST_HEADER, + AttributeNames, } 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 path from 'path'; 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'; +const applyCustomAttributesOnSpanErrorMessage = + 'bad applyCustomAttributesOnSpan function'; + let server: https.Server; const serverPort = 32345; const protocol = 'https'; const hostname = 'localhost'; const pathname = '/test'; const memoryExporter = new InMemorySpanExporter(); +const httpTextFormat = new DummyPropagation(); +const logger = new NoopLogger(); +const tracer = new NodeTracer({ + logger, + httpTextFormat, +}); +tracer.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); function doNock( hostname: string, @@ -70,191 +82,231 @@ describe('HttpsPlugin', () => { assert.strictEqual(process.versions.node, plugin.version); }); - it('moduleName should be https', () => { - assert.strictEqual('https', plugin.moduleName); + it(`moduleName should be ${protocol}`, () => { + assert.strictEqual(protocol, 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); + describe('with bad plugin options', () => { + let pluginWithBadOptions: HttpsPlugin; + beforeEach(() => { + memoryExporter.reset(); }); - } - 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}` + before(() => { + const config: HttpPluginConfig = { + ignoreIncomingPaths: [ + (url: string) => { + throw new Error('bad ignoreIncomingPaths function'); + }, + ], + ignoreOutgoingUrls: [ + (url: string) => { + throw new Error('bad ignoreOutgoingUrls function'); + }, + ], + applyCustomAttributesOnSpan: () => { + throw new Error(applyCustomAttributesOnSpanErrorMessage); + }, + }; + pluginWithBadOptions = new HttpsPlugin(process.versions.node); + pluginWithBadOptions.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(); + pluginWithBadOptions.disable(); + }); + + it('should generate valid spans (client side and server side)', async () => { + const result = await httpsRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` ); - span.end(); const spans = memoryExporter.getFinishedSpans(); - const [reqSpan, localSpan] = spans; + const [incomingSpan, outgoingSpan] = spans; const validations = { hostname, httpStatusCode: result.statusCode!, - httpMethod: 'GET', - pathname: testPath, + httpMethod: result.method!, + pathname, 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 - ); + 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); }); }); + describe('with good plugin options', () => { + beforeEach(() => { + memoryExporter.reset(); + }); - 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() + 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(`${protocol} module should be patched`, () => { + assert.strictEqual(https.Server.prototype.emit.__wrapped, true); + }); + + it(`should not patch if it's not a ${protocol} module`, () => { + const httpsNotPatched = new HttpsPlugin(process.versions.node).enable( + {} as Http, + tracer, + tracer.logger, + {} + ); + assert.strictEqual(Object.keys(httpsNotPatched).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 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: 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 () => { @@ -288,194 +340,302 @@ describe('HttpsPlugin', () => { ); }); }); - } - 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 + 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() ); - } - span.end(); - const spans = memoryExporter.getFinishedSpans(); - // 5 child spans ended + 1 span (root) - assert.strictEqual(spans.length, 6); - }); - }); + 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, + }; - 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, ''); - - 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'); + 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 + ); + }); }); - 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 > 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.abort(); - data += chunk; - }); - resp.on('end', () => { - resolve(data); - }); + 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 + ); } - ); - - return req.end(); + span.end(); + const spans = memoryExporter.getFinishedSpans(); + // 5 child spans ended + 1 span (root) + assert.strictEqual(spans.length, 6); + }); }); - 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); + 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 ${protocol} plugin when passing the following argument ${JSON.stringify( + arg + )}`, async () => { + try { + await httpsRequest.get(arg); + } catch (error) { + // 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) { + // 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 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, ''); + + 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 > 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.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); + } + }); + + 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[AttributeNames.HTTP_STATUS_CODE], + 404 + ); + assert.strictEqual(span.status.code, CanonicalCode.NOT_FOUND); + done(); + }); + }); + req.end(); + }); }); }); }); diff --git a/packages/opentelemetry-plugin-https/test/functionals/https-package.test.ts b/packages/opentelemetry-plugin-https/test/functionals/https-package.test.ts index 0604149ff..811a3e32c 100644 --- a/packages/opentelemetry-plugin-https/test/functionals/https-package.test.ts +++ b/packages/opentelemetry-plugin-https/test/functionals/https-package.test.ts @@ -15,7 +15,7 @@ */ import { NoopLogger } from '@opentelemetry/core'; -import { SpanKind, Span } from '@opentelemetry/types'; +import { SpanKind } from '@opentelemetry/types'; import * as assert from 'assert'; import * as https from 'https'; import * as http from 'http'; @@ -34,13 +34,12 @@ import { InMemorySpanExporter, SimpleSpanProcessor, } from '@opentelemetry/tracing'; -import { Http } from '@opentelemetry/plugin-http'; + +import { Http, HttpPluginConfig } from '@opentelemetry/plugin-http'; +import { customAttributeFunction } from './https-enable.test'; const memoryExporter = new InMemorySpanExporter(); - -export const customAttributeFunction = (span: Span): void => { - span.setAttribute('span kind', SpanKind.CLIENT); -}; +const protocol = 'https'; describe('Packages', () => { describe('get', () => { @@ -57,7 +56,10 @@ describe('Packages', () => { }); before(() => { - plugin.enable((https as unknown) as Http, tracer, tracer.logger); + const config: HttpPluginConfig = { + applyCustomAttributesOnSpan: customAttributeFunction, + }; + plugin.enable((https as unknown) as Http, tracer, tracer.logger, config); }); after(() => { @@ -93,8 +95,8 @@ describe('Packages', () => { // 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` + `${protocol}://www.google.com` + : `${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) { diff --git a/packages/opentelemetry-plugin-https/test/integrations/https-enable.test.ts b/packages/opentelemetry-plugin-https/test/integrations/https-enable.test.ts index a3ad0e487..4c2ca192d 100644 --- a/packages/opentelemetry-plugin-https/test/integrations/https-enable.test.ts +++ b/packages/opentelemetry-plugin-https/test/integrations/https-enable.test.ts @@ -14,24 +14,25 @@ * limitations under the License. */ -import { - InMemorySpanExporter, - SimpleSpanProcessor, -} from '@opentelemetry/tracing'; import { NoopLogger } from '@opentelemetry/core'; -import { NodeTracer } from '@opentelemetry/node'; import { HttpPluginConfig, Http } from '@opentelemetry/plugin-http'; -import { Span, SpanKind } from '@opentelemetry/types'; +import { SpanKind, Span } 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 url from 'url'; import * as utils from '../utils/utils'; +import { NodeTracer } from '@opentelemetry/node'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +const protocol = 'https'; const serverPort = 42345; const hostname = 'localhost'; const memoryExporter = new InMemorySpanExporter(); @@ -71,7 +72,7 @@ describe('HttpsPlugin Integration tests', () => { before(() => { const ignoreConfig = [ - `https://${hostname}:${serverPort}/ignored/string`, + `${protocol}://${hostname}:${serverPort}/ignored/string`, /\/ignored\/regexp$/i, (url: string) => url.endsWith(`/ignored/function`), ]; @@ -94,7 +95,9 @@ describe('HttpsPlugin Integration tests', () => { let spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 0); - const result = await httpsRequest.get(`https://google.fr/?query=test`); + const result = await httpsRequest.get( + `${protocol}://google.fr/?query=test` + ); spans = memoryExporter.getFinishedSpans(); const span = spans[0]; @@ -114,8 +117,62 @@ describe('HttpsPlugin Integration tests', () => { 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}://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('should create a rootSpan for GET requests and add propagation headers 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}://google.fr/?query=test`), + { headers: { 'x-foo': 'foo' } } + ); + + 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); + assert.strictEqual(result.reqHeaders['x-foo'], 'foo'); + assertSpan(span, SpanKind.CLIENT, validations); + }); + it('custom attributes should show up on client spans', async () => { - const result = await httpsRequest.get(`https://google.fr/`); + const result = await httpsRequest.get(`${protocol}://google.fr/`); const spans = memoryExporter.getFinishedSpans(); const span = spans[0]; const validations = { @@ -139,7 +196,7 @@ describe('HttpsPlugin Integration tests', () => { assert.strictEqual(spans.length, 0); const options = Object.assign( { headers: { Expect: '100-continue' } }, - url.parse('https://google.fr/') + url.parse(`${protocol}://google.fr/`) ); const result = await httpsRequest.get(options); @@ -170,7 +227,7 @@ describe('HttpsPlugin Integration tests', () => { { 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( + 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: { @@ -186,7 +243,7 @@ describe('HttpsPlugin Integration tests', () => { assert.strictEqual(spans.length, 0); const options = { headers }; const req = https.get( - 'https://google.fr/', + `${protocol}://google.fr/`, options, (resp: http.IncomingMessage) => { const res = (resp as unknown) as http.IncomingMessage & { @@ -212,6 +269,7 @@ describe('HttpsPlugin Integration tests', () => { }); } ); + req.on('close', () => { const spans = memoryExporter.getFinishedSpans(); assert.strictEqual(spans.length, 1); diff --git a/packages/opentelemetry-plugin-https/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-https/test/utils/assertSpan.ts index 639a16d98..4e856a687 100644 --- a/packages/opentelemetry-plugin-https/test/utils/assertSpan.ts +++ b/packages/opentelemetry-plugin-https/test/utils/assertSpan.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { SpanKind } from '@opentelemetry/types'; +import { SpanKind, Status } 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/tracing'; import { AttributeNames, parseResponseStatus, } from '@opentelemetry/plugin-http'; +import { DummyPropagation } from './DummyPropagation'; +import { ReadableSpan } from '@opentelemetry/tracing'; export const assertSpan = ( span: ReadableSpan, @@ -36,6 +36,7 @@ export const assertSpan = ( pathname: string; reqHeaders?: http.OutgoingHttpHeaders; path?: string | null; + forceStatus?: Status; component: string; } ) => { @@ -70,14 +71,22 @@ export const assertSpan = ( 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) + validations.forceStatus || parseResponseStatus(validations.httpStatusCode) ); + assert.ok( + (span.attributes[AttributeNames.HTTP_URL] as string).indexOf( + span.attributes[AttributeNames.HTTP_HOSTNAME] as string + ) > -1, + 'should be consistent' + ); + assert.ok(span.endTime, 'must be finished'); assert.ok(hrTimeToNanoseconds(span.duration), 'must have positive duration'); if (validations.reqHeaders) { diff --git a/packages/opentelemetry-plugin-https/test/utils/httpsRequest.ts b/packages/opentelemetry-plugin-https/test/utils/httpsRequest.ts index c5436be76..e96a12fce 100644 --- a/packages/opentelemetry-plugin-https/test/utils/httpsRequest.ts +++ b/packages/opentelemetry-plugin-https/test/utils/httpsRequest.ts @@ -16,59 +16,56 @@ import * as http from 'http'; import * as https from 'https'; -import { RequestOptions } from 'https'; -import * as url from 'url'; +import { 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); +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) => { + 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, }); }); - req.on('error', err => { + resp.on('error', err => { reject(err); }); - return req; + } + req = + options != null + ? https.get(input, options, onGetResponseCb) + : https.get(input, onGetResponseCb); + req.on('error', err => { + reject(err); }); - }, + return req; + }); +} + +export const httpsRequest = { + get, };