/* * Copyright The OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Attributes, SpanStatusCode, SpanKind, context, Span, diag, } from '@opentelemetry/api'; import { ATTR_ERROR_TYPE, ATTR_HTTP_ROUTE, ATTR_URL_PATH, ATTR_URL_QUERY, } from '@opentelemetry/semantic-conventions'; import * as assert from 'assert'; import { IncomingMessage, ServerResponse } from 'http'; import { Socket } from 'net'; import * as sinon from 'sinon'; import * as url from 'url'; import { IgnoreMatcher, ParsedRequestOptions } from '../../src/internal-types'; import * as utils from '../../src/utils'; import { RPCType, setRPCMetadata } from '@opentelemetry/core'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { extractHostnameAndPort } from '../../src/utils'; describe('Utility', () => { describe('parseResponseStatus()', () => { it('should return ERROR code by default', () => { const status = utils.parseResponseStatus(SpanKind.CLIENT, undefined); assert.deepStrictEqual(status, SpanStatusCode.ERROR); }); it('should return UNSET for Success HTTP status code', () => { for (let index = 100; index < 400; index++) { const status = utils.parseResponseStatus(SpanKind.CLIENT, index); assert.deepStrictEqual(status, SpanStatusCode.UNSET); } for (let index = 100; index < 500; index++) { const status = utils.parseResponseStatus(SpanKind.SERVER, index); assert.deepStrictEqual(status, SpanStatusCode.UNSET); } }); it('should return ERROR for bad status codes', () => { for (let index = 400; index <= 600; index++) { const status = utils.parseResponseStatus(SpanKind.CLIENT, index); assert.notStrictEqual(status, SpanStatusCode.UNSET); } for (let index = 500; index <= 600; index++) { const status = utils.parseResponseStatus(SpanKind.SERVER, index); assert.notStrictEqual(status, SpanStatusCode.UNSET); } }); }); describe('getRequestInfo()', () => { it('should get options object', () => { const webUrl = 'http://u:p@google.fr/aPath?qu=ry'; const urlParsed = url.parse(webUrl); const urlParsedWithoutPathname = { ...urlParsed, pathname: undefined, }; const urlParsedWithUndefinedHostAndPort = { ...urlParsed, host: undefined, port: undefined, }; const urlParsedWithUndefinedHostAndNullPort = { ...urlParsed, host: undefined, port: null, }; const whatWgUrl = new url.URL(webUrl); for (const param of [ webUrl, urlParsed, urlParsedWithoutPathname, urlParsedWithUndefinedHostAndPort, urlParsedWithUndefinedHostAndNullPort, whatWgUrl, ]) { const result = utils.getRequestInfo(diag, param); assert.strictEqual(result.optionsParsed.hostname, 'google.fr'); assert.strictEqual(result.optionsParsed.protocol, 'http:'); assert.strictEqual(result.optionsParsed.path, '/aPath?qu=ry'); assert.strictEqual(result.pathname, '/aPath'); assert.strictEqual(result.origin, 'http://google.fr'); } }); }); describe('satisfiesPattern()', () => { it('string pattern', () => { const answer1 = utils.satisfiesPattern('/test/1', '/test/1'); assert.strictEqual(answer1, true); const answer2 = utils.satisfiesPattern('/test/1', '/test/11'); assert.strictEqual(answer2, false); }); it('regex pattern', () => { const answer1 = utils.satisfiesPattern('/TeSt/1', /\/test/i); assert.strictEqual(answer1, true); const answer2 = utils.satisfiesPattern('/2/tEst/1', /\/test/); assert.strictEqual(answer2, false); }); it('should throw if type is unknown', () => { try { utils.satisfiesPattern('/TeSt/1', true as unknown as IgnoreMatcher); assert.fail(); } catch (error) { assert.strictEqual(error instanceof TypeError, true); } }); it('function pattern', () => { const answer1 = utils.satisfiesPattern( '/test/home', (url: string) => url === '/test/home' ); assert.strictEqual(answer1, true); const answer2 = utils.satisfiesPattern( '/test/home', (url: string) => url !== '/test/home' ); assert.strictEqual(answer2, false); }); }); describe('getAbsoluteUrl()', () => { it('should return absolute url with localhost', () => { const path = '/test/1'; const result = utils.getAbsoluteUrl(url.parse(path), {}); assert.strictEqual(result, `http://localhost${path}`); }); it('should return absolute url', () => { const absUrl = 'http://www.google/test/1?query=1'; const result = utils.getAbsoluteUrl(url.parse(absUrl), {}); assert.strictEqual(result, absUrl); }); it('should return default url', () => { const result = utils.getAbsoluteUrl(null, {}); assert.strictEqual(result, 'http://localhost/'); }); it("{ path: '/helloworld', port: 8080 } should return http://localhost:8080/helloworld", () => { const result = utils.getAbsoluteUrl( { path: '/helloworld', port: 8080 }, {} ); assert.strictEqual(result, 'http://localhost:8080/helloworld'); }); }); describe('setSpanWithError()', () => { it('should have error attributes', () => { const errorMessage = 'test error'; const error = new Error(errorMessage); const span = { setAttribute: () => undefined, setStatus: () => undefined, recordException: () => undefined, } as unknown as Span; const mock = sinon.mock(span); mock.expects('setAttribute').calledWithExactly(ATTR_ERROR_TYPE, 'Error'); mock.expects('setStatus').calledWithExactly({ code: SpanStatusCode.ERROR, message: errorMessage, }); mock.expects('recordException').calledWithExactly(error); utils.setSpanWithError(span, error); mock.verify(); }); }); describe('isValidOptionsType()', () => { ['', false, true, 1, 0, []].forEach(options => { it(`should return false with the following value: ${JSON.stringify( options )}`, () => { assert.strictEqual(utils.isValidOptionsType(options), false); }); }); for (const options of ['url', url.parse('http://url.com'), {}]) { it(`should return true with the following value: ${JSON.stringify( options )}`, () => { assert.strictEqual(utils.isValidOptionsType(options), true); }); } }); describe('getIncomingRequestAttributesOnResponse()', () => { it('should correctly parse the middleware stack if present', done => { context.setGlobalContextManager(new AsyncHooksContextManager().enable()); const request = { socket: {}, } as IncomingMessage; context.with( setRPCMetadata(context.active(), { type: RPCType.HTTP, route: '/user/:id', span: null as unknown as Span, }), () => { const attributes = utils.getIncomingRequestAttributesOnResponse( request, {} as ServerResponse ); assert.deepStrictEqual(attributes[ATTR_HTTP_ROUTE], '/user/:id'); context.disable(); return done(); } ); }); it('should successfully process without middleware stack', () => { const request = { socket: {}, } as IncomingMessage; const attributes = utils.getIncomingRequestAttributesOnResponse(request, { socket: {}, } as ServerResponse & { socket: Socket }); assert.deepEqual(attributes[ATTR_HTTP_ROUTE], undefined); }); }); describe('getIncomingStableRequestMetricAttributesOnResponse()', () => { it('should correctly add http_route if span has it', () => { const spanAttributes: Attributes = { [ATTR_HTTP_ROUTE]: '/user/:id', }; const metricAttributes = utils.getIncomingStableRequestMetricAttributesOnResponse( spanAttributes ); assert.deepStrictEqual(metricAttributes[ATTR_HTTP_ROUTE], '/user/:id'); }); it('should skip http_route if span does not have it', () => { const spanAttributes: Attributes = {}; const metricAttributes = utils.getIncomingStableRequestMetricAttributesOnResponse( spanAttributes ); assert.deepEqual(metricAttributes[ATTR_HTTP_ROUTE], undefined); }); }); describe('getIncomingRequestAttributes()', () => { it('should not set http.route in http span attributes', () => { const request = { url: 'http://hostname/user/:id', method: 'GET', socket: {}, } as IncomingMessage; request.headers = { 'user-agent': 'chrome', 'x-forwarded-for': ', , ', }; const attributes = utils.getIncomingRequestAttributes( request, { component: 'http', }, diag ); assert.strictEqual(attributes[ATTR_HTTP_ROUTE], undefined); }); it('should set http.target as path in http span attributes', () => { const request = { url: 'http://hostname/user/?q=val', method: 'GET', socket: {}, } as IncomingMessage; request.headers = { 'user-agent': 'chrome', }; const attributes = utils.getIncomingRequestAttributes( request, { component: 'http', }, diag ); const path = String(attributes[ATTR_URL_PATH] ?? ''); const query = String(attributes[ATTR_URL_QUERY] ?? ''); assert.strictEqual(path + '?' + query, '/user/?q=val'); }); }); describe('headers to span attributes capture', () => { let span: Span; let mock: sinon.SinonMock; beforeEach(() => { span = { setAttribute: () => undefined, } as unknown as Span; mock = sinon.mock(span); }); it('should set attributes for request and response keys', () => { mock .expects('setAttribute') .calledWithExactly('http.request.header.origin', ['localhost']); mock .expects('setAttribute') .calledWithExactly('http.response.header.cookie', ['token=123']); utils.headerCapture('request', ['Origin'])(span, () => 'localhost'); utils.headerCapture('response', ['Cookie'])(span, () => 'token=123'); mock.verify(); }); it('should set attributes for multiple values', () => { mock .expects('setAttribute') .calledWithExactly('http.request.header.origin', [ 'localhost', 'www.example.com', ]); utils.headerCapture('request', ['Origin'])(span, () => [ 'localhost', 'www.example.com', ]); mock.verify(); }); it('sets attributes for multiple headers', () => { mock .expects('setAttribute') .calledWithExactly('http.request.header.origin', ['localhost']); mock .expects('setAttribute') .calledWithExactly('http.request.header.foo', [42]); utils.headerCapture('request', ['Origin', 'Foo'])(span, header => { if (header === 'origin') { return 'localhost'; } if (header === 'foo') { return 42; } return undefined; }); mock.verify(); }); it('should normalize header names', () => { mock .expects('setAttribute') .calledWithExactly('http.request.header.x_forwarded_for', ['foo']); utils.headerCapture('request', ['X-Forwarded-For'])(span, () => 'foo'); mock.verify(); }); it('ignores non-existent headers', () => { mock .expects('setAttribute') .once() .calledWithExactly('http.request.header.origin', ['localhost']); utils.headerCapture('request', ['Origin', 'Accept'])(span, header => { if (header === 'origin') { return 'localhost'; } return undefined; }); mock.verify(); }); }); describe('extractHostnameAndPort', () => { it('should return the hostname and port defined in the parsedOptions', () => { type tmpParsedOption = Pick< ParsedRequestOptions, 'hostname' | 'host' | 'port' | 'protocol' >; const parsedOption: tmpParsedOption = { hostname: 'www.google.com', port: '80', host: 'www.google.com', protocol: 'http:', }; const { hostname, port } = extractHostnameAndPort(parsedOption); assert.strictEqual(hostname, parsedOption.hostname); assert.strictEqual(port, parsedOption.port); }); it('should return the hostname and port based on host field defined in the parsedOptions when hostname and port are missing', () => { type tmpParsedOption = Pick< ParsedRequestOptions, 'hostname' | 'host' | 'port' | 'protocol' >; const parsedOption: tmpParsedOption = { hostname: null, port: null, host: 'www.google.com:8181', protocol: 'http:', }; const { hostname, port } = extractHostnameAndPort(parsedOption); assert.strictEqual(hostname, 'www.google.com'); assert.strictEqual(port, '8181'); }); it('should infer the port number based on protocol https when can not extract it from host field', () => { type tmpParsedOption = Pick< ParsedRequestOptions, 'hostname' | 'host' | 'port' | 'protocol' >; const parsedOption: tmpParsedOption = { hostname: null, port: null, host: 'www.google.com', protocol: 'https:', }; const { hostname, port } = extractHostnameAndPort(parsedOption); assert.strictEqual(hostname, 'www.google.com'); assert.strictEqual(port, '443'); }); it('should infer the port number based on protocol http when can not extract it from host field', () => { type tmpParsedOption = Pick< ParsedRequestOptions, 'hostname' | 'host' | 'port' | 'protocol' >; const parsedOption: tmpParsedOption = { hostname: null, port: null, host: 'www.google.com', protocol: 'http:', }; const { hostname, port } = extractHostnameAndPort(parsedOption); assert.strictEqual(hostname, 'www.google.com'); assert.strictEqual(port, '80'); }); }); });