opentelemetry-js/experimental/packages/opentelemetry-instrumentati.../test/functionals/utils.test.ts

464 lines
15 KiB
TypeScript

/*
* 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': '<client>, <proxy1>, <proxy2>',
};
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');
});
});
});