1254 lines
40 KiB
TypeScript
1254 lines
40 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 {
|
|
SpanStatusCode,
|
|
context,
|
|
diag,
|
|
propagation,
|
|
Span as ISpan,
|
|
SpanKind,
|
|
trace,
|
|
SpanAttributes,
|
|
DiagConsoleLogger,
|
|
} from '@opentelemetry/api';
|
|
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
import {
|
|
InMemorySpanExporter,
|
|
SimpleSpanProcessor,
|
|
} from '@opentelemetry/sdk-trace-base';
|
|
import {
|
|
NETTRANSPORTVALUES_IP_TCP,
|
|
SEMATTRS_HTTP_CLIENT_IP,
|
|
SEMATTRS_HTTP_FLAVOR,
|
|
SEMATTRS_HTTP_ROUTE,
|
|
SEMATTRS_HTTP_STATUS_CODE,
|
|
SEMATTRS_NET_HOST_PORT,
|
|
SEMATTRS_NET_PEER_PORT,
|
|
SEMATTRS_NET_TRANSPORT,
|
|
} from '@opentelemetry/semantic-conventions';
|
|
import assert from 'assert';
|
|
import nock from 'nock';
|
|
import * as path from 'path';
|
|
import { HttpInstrumentation } from '../../src/http';
|
|
import { HttpInstrumentationConfig } from '../../src/types';
|
|
import { assertSpan } from '../utils/assertSpan';
|
|
import { DummyPropagation } from '../utils/DummyPropagation';
|
|
import { httpRequest } from '../utils/httpRequest';
|
|
import { ContextManager } from '@opentelemetry/api';
|
|
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
|
|
import type {
|
|
ClientRequest,
|
|
IncomingMessage,
|
|
ServerResponse,
|
|
RequestOptions,
|
|
} from 'http';
|
|
import { isWrapped } from '@opentelemetry/instrumentation';
|
|
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
|
|
|
|
const instrumentation = new HttpInstrumentation();
|
|
instrumentation.enable();
|
|
instrumentation.disable();
|
|
|
|
import * as http from 'http';
|
|
|
|
const applyCustomAttributesOnSpanErrorMessage =
|
|
'bad applyCustomAttributesOnSpan function';
|
|
|
|
let server: http.Server;
|
|
const serverPort = 22346;
|
|
const protocol = 'http';
|
|
const hostname = 'localhost';
|
|
const pathname = '/test';
|
|
const serverName = 'my.server.name';
|
|
const memoryExporter = new InMemorySpanExporter();
|
|
const provider = new NodeTracerProvider();
|
|
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
|
|
instrumentation.setTracerProvider(provider);
|
|
|
|
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);
|
|
};
|
|
|
|
export const requestHookFunction = (
|
|
span: ISpan,
|
|
request: ClientRequest | IncomingMessage
|
|
): void => {
|
|
span.setAttribute('custom request hook attribute', 'request');
|
|
};
|
|
|
|
export const responseHookFunction = (
|
|
span: ISpan,
|
|
response: IncomingMessage | ServerResponse
|
|
): void => {
|
|
span.setAttribute('custom response hook attribute', 'response');
|
|
// IncomingMessage (Readable) 'end'.
|
|
response.on('end', () => {
|
|
span.setAttribute('custom incoming message attribute', 'end');
|
|
});
|
|
// ServerResponse (writable) 'finish'.
|
|
response.on('finish', () => {
|
|
span.setAttribute('custom server response attribute', 'finish');
|
|
});
|
|
};
|
|
|
|
export const startIncomingSpanHookFunction = (
|
|
request: IncomingMessage
|
|
): SpanAttributes => {
|
|
return { guid: request.headers?.guid };
|
|
};
|
|
|
|
export const startOutgoingSpanHookFunction = (
|
|
request: RequestOptions
|
|
): SpanAttributes => {
|
|
return { guid: request.headers?.guid };
|
|
};
|
|
|
|
describe('HttpInstrumentation', () => {
|
|
let contextManager: ContextManager;
|
|
|
|
before(() => {
|
|
propagation.setGlobalPropagator(new DummyPropagation());
|
|
});
|
|
|
|
after(() => {
|
|
propagation.disable();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
contextManager = new AsyncHooksContextManager().enable();
|
|
context.setGlobalContextManager(contextManager);
|
|
});
|
|
|
|
afterEach(() => {
|
|
context.disable();
|
|
});
|
|
|
|
describe('enable()', () => {
|
|
describe('with bad instrumentation options', () => {
|
|
beforeEach(() => {
|
|
memoryExporter.reset();
|
|
});
|
|
|
|
before(() => {
|
|
const config: HttpInstrumentationConfig = {
|
|
ignoreIncomingPaths: [
|
|
(url: string) => {
|
|
throw new Error('bad ignoreIncomingPaths function');
|
|
},
|
|
],
|
|
ignoreIncomingRequestHook: _request => {
|
|
throw new Error('bad ignoreIncomingRequestHook function');
|
|
},
|
|
ignoreOutgoingUrls: [
|
|
(url: string) => {
|
|
throw new Error('bad ignoreOutgoingUrls function');
|
|
},
|
|
],
|
|
ignoreOutgoingRequestHook: _request => {
|
|
throw new Error('bad ignoreOutgoingRequestHook function');
|
|
},
|
|
applyCustomAttributesOnSpan: () => {
|
|
throw new Error(applyCustomAttributesOnSpanErrorMessage);
|
|
},
|
|
};
|
|
instrumentation.setConfig(config);
|
|
instrumentation.enable();
|
|
server = http.createServer((request, response) => {
|
|
response.end('Test Server Response');
|
|
});
|
|
|
|
server.listen(serverPort);
|
|
});
|
|
|
|
after(() => {
|
|
server.close();
|
|
instrumentation.disable();
|
|
});
|
|
|
|
it('should generate valid spans (client side and server side)', async () => {
|
|
const result = await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}${pathname}`,
|
|
{
|
|
headers: {
|
|
'user-agent': 'tester',
|
|
},
|
|
}
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
const [incomingSpan, outgoingSpan] = spans;
|
|
const validations = {
|
|
hostname,
|
|
httpStatusCode: result.statusCode!,
|
|
httpMethod: result.method!,
|
|
pathname,
|
|
resHeaders: result.resHeaders,
|
|
reqHeaders: result.reqHeaders,
|
|
component: 'http',
|
|
};
|
|
|
|
assert.strictEqual(spans.length, 2);
|
|
assertSpan(incomingSpan, SpanKind.SERVER, validations);
|
|
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
|
|
assert.strictEqual(
|
|
incomingSpan.attributes[SEMATTRS_NET_HOST_PORT],
|
|
serverPort
|
|
);
|
|
assert.strictEqual(
|
|
outgoingSpan.attributes[SEMATTRS_NET_PEER_PORT],
|
|
serverPort
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('partially disable instrumentation', () => {
|
|
beforeEach(() => {
|
|
memoryExporter.reset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
server.close();
|
|
instrumentation.disable();
|
|
});
|
|
|
|
it('allows to disable outgoing request instrumentation', () => {
|
|
server.close();
|
|
instrumentation.disable();
|
|
|
|
instrumentation.setConfig({
|
|
disableOutgoingRequestInstrumentation: true,
|
|
});
|
|
instrumentation.enable();
|
|
server = http.createServer((_request, response) => {
|
|
response.end('Test Server Response');
|
|
});
|
|
|
|
server.listen(serverPort);
|
|
|
|
assert.strictEqual(isWrapped(http.Server.prototype.emit), true);
|
|
assert.strictEqual(isWrapped(http.get), false);
|
|
assert.strictEqual(isWrapped(http.request), false);
|
|
});
|
|
|
|
it('allows to disable incoming request instrumentation', () => {
|
|
server.close();
|
|
instrumentation.disable();
|
|
|
|
instrumentation.setConfig({
|
|
disableIncomingRequestInstrumentation: true,
|
|
});
|
|
instrumentation.enable();
|
|
server = http.createServer((_request, response) => {
|
|
response.end('Test Server Response');
|
|
});
|
|
|
|
server.listen(serverPort);
|
|
|
|
assert.strictEqual(isWrapped(http.Server.prototype.emit), false);
|
|
assert.strictEqual(isWrapped(http.get), true);
|
|
assert.strictEqual(isWrapped(http.request), true);
|
|
});
|
|
});
|
|
|
|
describe('with good instrumentation options', () => {
|
|
beforeEach(() => {
|
|
memoryExporter.reset();
|
|
});
|
|
|
|
before(() => {
|
|
instrumentation.setConfig({
|
|
ignoreIncomingPaths: [
|
|
'/ignored/string',
|
|
/\/ignored\/regexp$/i,
|
|
(url: string) => url.endsWith('/ignored/function'),
|
|
],
|
|
ignoreIncomingRequestHook: request => {
|
|
return (
|
|
request.headers['user-agent']?.match('ignored-string') != null
|
|
);
|
|
},
|
|
ignoreOutgoingUrls: [
|
|
`${protocol}://${hostname}:${serverPort}/ignored/string`,
|
|
/\/ignored\/regexp$/i,
|
|
(url: string) => url.endsWith('/ignored/function'),
|
|
],
|
|
ignoreOutgoingRequestHook: request => {
|
|
if (request.headers?.['user-agent'] != null) {
|
|
return (
|
|
`${request.headers['user-agent']}`.match('ignored-string') !=
|
|
null
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
applyCustomAttributesOnSpan: customAttributeFunction,
|
|
requestHook: requestHookFunction,
|
|
responseHook: responseHookFunction,
|
|
startIncomingSpanHook: startIncomingSpanHookFunction,
|
|
startOutgoingSpanHook: startOutgoingSpanHookFunction,
|
|
serverName,
|
|
});
|
|
instrumentation.enable();
|
|
server = http.createServer((request, response) => {
|
|
if (request.url?.includes('/premature-close')) {
|
|
response.destroy();
|
|
return;
|
|
}
|
|
if (request.url?.includes('/hang')) {
|
|
// write response headers.
|
|
response.write('');
|
|
// hang the request.
|
|
return;
|
|
}
|
|
if (request.url?.includes('/destroy-request')) {
|
|
// force flush http response header to trigger client response callback
|
|
response.write('');
|
|
setTimeout(() => {
|
|
request.socket.destroy();
|
|
}, 100);
|
|
return;
|
|
}
|
|
if (request.url?.includes('/ignored')) {
|
|
provider.getTracer('test').startSpan('some-span').end();
|
|
}
|
|
if (request.url?.includes('/setroute')) {
|
|
const rpcData = getRPCMetadata(context.active());
|
|
assert.ok(rpcData != null);
|
|
assert.strictEqual(rpcData.type, RPCType.HTTP);
|
|
assert.strictEqual(rpcData.route, undefined);
|
|
rpcData.route = 'TheRoute';
|
|
}
|
|
response.end('Test Server Response');
|
|
});
|
|
|
|
server.listen(serverPort);
|
|
});
|
|
|
|
after(() => {
|
|
server.close();
|
|
instrumentation.disable();
|
|
});
|
|
|
|
it(`${protocol} module should be patched`, () => {
|
|
assert.strictEqual(isWrapped(http.Server.prototype.emit), true);
|
|
});
|
|
|
|
it('should generate valid spans (client side and server side)', async () => {
|
|
const result = await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}${pathname}`,
|
|
{
|
|
headers: {
|
|
'x-forwarded-for': '<client>, <proxy1>, <proxy2>',
|
|
'user-agent': 'chrome',
|
|
},
|
|
}
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
const [incomingSpan, outgoingSpan] = spans;
|
|
const validations = {
|
|
hostname,
|
|
httpStatusCode: result.statusCode!,
|
|
httpMethod: result.method!,
|
|
pathname,
|
|
resHeaders: result.resHeaders,
|
|
reqHeaders: result.reqHeaders,
|
|
component: 'http',
|
|
serverName,
|
|
};
|
|
|
|
assert.strictEqual(spans.length, 2);
|
|
assert.strictEqual(
|
|
incomingSpan.attributes[SEMATTRS_HTTP_CLIENT_IP],
|
|
'<client>'
|
|
);
|
|
assert.strictEqual(
|
|
incomingSpan.attributes[SEMATTRS_NET_HOST_PORT],
|
|
serverPort
|
|
);
|
|
assert.strictEqual(
|
|
outgoingSpan.attributes[SEMATTRS_NET_PEER_PORT],
|
|
serverPort
|
|
);
|
|
[
|
|
{ span: incomingSpan, kind: SpanKind.SERVER },
|
|
{ span: outgoingSpan, kind: SpanKind.CLIENT },
|
|
].forEach(({ span, kind }) => {
|
|
assert.strictEqual(span.attributes[SEMATTRS_HTTP_FLAVOR], '1.1');
|
|
assert.strictEqual(
|
|
span.attributes[SEMATTRS_NET_TRANSPORT],
|
|
NETTRANSPORTVALUES_IP_TCP
|
|
);
|
|
assertSpan(span, kind, validations);
|
|
});
|
|
});
|
|
|
|
it('should respect HTTP_ROUTE', async () => {
|
|
await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}/setroute`
|
|
);
|
|
const span = memoryExporter.getFinishedSpans()[0];
|
|
|
|
assert.strictEqual(span.kind, SpanKind.SERVER);
|
|
assert.strictEqual(span.attributes[SEMATTRS_HTTP_ROUTE], 'TheRoute');
|
|
assert.strictEqual(span.name, 'GET TheRoute');
|
|
});
|
|
|
|
const httpErrorCodes = [
|
|
400, 401, 403, 404, 429, 501, 503, 504, 500, 505, 597,
|
|
];
|
|
|
|
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 httpRequest.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: 'http',
|
|
};
|
|
|
|
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 = provider.getTracer('default').startSpan(name);
|
|
return context.with(trace.setSpan(context.active(), span), async () => {
|
|
const result = await httpRequest.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: 'http',
|
|
};
|
|
|
|
assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0);
|
|
assert.strictEqual(spans.length, 2);
|
|
assert.strictEqual(reqSpan.name, 'GET');
|
|
assert.strictEqual(
|
|
localSpan.spanContext().traceId,
|
|
reqSpan.spanContext().traceId
|
|
);
|
|
assertSpan(reqSpan, SpanKind.CLIENT, validations);
|
|
assert.notStrictEqual(
|
|
localSpan.spanContext().spanId,
|
|
reqSpan.spanContext().spanId
|
|
);
|
|
});
|
|
});
|
|
|
|
for (let i = 0; i < httpErrorCodes.length; i++) {
|
|
it(`should test child spans for GET requests with http error ${httpErrorCodes[i]}`, async () => {
|
|
const testPath = '/outgoing/rootSpan/childs/1';
|
|
doNock(
|
|
hostname,
|
|
testPath,
|
|
httpErrorCodes[i],
|
|
httpErrorCodes[i].toString()
|
|
);
|
|
const name = 'TestRootSpan';
|
|
const span = provider.getTracer('default').startSpan(name);
|
|
return context.with(
|
|
trace.setSpan(context.active(), span),
|
|
async () => {
|
|
const result = await httpRequest.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: 'http',
|
|
};
|
|
|
|
assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0);
|
|
assert.strictEqual(spans.length, 2);
|
|
assert.strictEqual(reqSpan.name, 'GET');
|
|
assert.strictEqual(
|
|
localSpan.spanContext().traceId,
|
|
reqSpan.spanContext().traceId
|
|
);
|
|
assertSpan(reqSpan, SpanKind.CLIENT, validations);
|
|
assert.notStrictEqual(
|
|
localSpan.spanContext().spanId,
|
|
reqSpan.spanContext().spanId
|
|
);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
it('should create multiple child spans for GET requests', async () => {
|
|
const testPath = '/outgoing/rootSpan/childs';
|
|
const num = 5;
|
|
doNock(hostname, testPath, 200, 'Ok', num);
|
|
const name = 'TestRootSpan';
|
|
const span = provider.getTracer('default').startSpan(name);
|
|
await context.with(trace.setSpan(context.active(), span), async () => {
|
|
for (let i = 0; i < num; i++) {
|
|
await httpRequest.get(`${protocol}://${hostname}${testPath}`);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans[i].name, 'GET');
|
|
assert.strictEqual(
|
|
span.spanContext().traceId,
|
|
spans[i].spanContext().traceId
|
|
);
|
|
}
|
|
span.end();
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
// 5 child spans ended + 1 span (root)
|
|
assert.strictEqual(spans.length, 6);
|
|
});
|
|
});
|
|
|
|
for (const ignored of ['string', 'function', 'regexp']) {
|
|
it(`should not trace ignored requests with paths (client and server side) with type ${ignored}`, async () => {
|
|
const testPath = `/ignored/${ignored}`;
|
|
|
|
await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}${testPath}`
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 0);
|
|
});
|
|
}
|
|
|
|
it('should not trace ignored requests with headers (client and server side)', async () => {
|
|
const testValue = 'ignored-string';
|
|
|
|
await Promise.all([
|
|
httpRequest.get(`${protocol}://${hostname}:${serverPort}`, {
|
|
headers: {
|
|
'user-agent': testValue,
|
|
},
|
|
}),
|
|
]);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 0);
|
|
});
|
|
|
|
it('should trace not ignored requests with headers (client and server side)', async () => {
|
|
await httpRequest.get(`${protocol}://${hostname}:${serverPort}`, {
|
|
headers: {
|
|
'user-agent': 'test-bot',
|
|
},
|
|
});
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 2);
|
|
});
|
|
|
|
for (const arg of ['string', {}, new Date()]) {
|
|
it(`should be traceable and not throw exception in ${protocol} instrumentation when passing the following argument ${JSON.stringify(
|
|
arg
|
|
)}`, async () => {
|
|
try {
|
|
await httpRequest.get(arg);
|
|
} catch (error: any) {
|
|
// request has been made
|
|
// nock throw
|
|
assert.ok(error.message.startsWith('Nock: No match for request'));
|
|
}
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 1);
|
|
});
|
|
}
|
|
|
|
for (const arg of [true, 1, false, 0, '']) {
|
|
it(`should not throw exception in ${protocol} instrumentation when passing the following argument ${JSON.stringify(
|
|
arg
|
|
)}`, async () => {
|
|
try {
|
|
await httpRequest.get(arg as any);
|
|
} catch (error: any) {
|
|
// request has been made
|
|
// nock throw
|
|
assert.ok(
|
|
error.stack.indexOf(
|
|
path.normalize('/node_modules/nock/lib/intercept.js')
|
|
) > 0
|
|
);
|
|
}
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
// for this arg with don't provide trace. We pass arg to original method (http.get)
|
|
assert.strictEqual(spans.length, 0);
|
|
});
|
|
}
|
|
|
|
it('should have 1 ended span when request throw on bad "options" object', () => {
|
|
assert.throws(
|
|
() => http.request({ headers: { cookie: undefined } }),
|
|
(err: unknown) => {
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 1);
|
|
|
|
assert.ok(err instanceof Error);
|
|
|
|
const validations = {
|
|
httpStatusCode: undefined,
|
|
httpMethod: 'GET',
|
|
resHeaders: {},
|
|
hostname: 'localhost',
|
|
pathname: '/',
|
|
forceStatus: {
|
|
code: SpanStatusCode.ERROR,
|
|
message: err.message,
|
|
},
|
|
component: 'http',
|
|
noNetPeer: true,
|
|
error: err,
|
|
};
|
|
assertSpan(spans[0], SpanKind.CLIENT, validations);
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
|
|
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 = http.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 {
|
|
http.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 = http.request(
|
|
`${protocol}://${hostname}${testPath}`,
|
|
(resp: http.IncomingMessage) => {
|
|
let data = '';
|
|
resp.on('data', chunk => {
|
|
data += chunk;
|
|
});
|
|
resp.on('end', () => {
|
|
reject(new Error(data));
|
|
});
|
|
}
|
|
);
|
|
return req.end();
|
|
});
|
|
|
|
try {
|
|
await promiseRequest;
|
|
assert.fail();
|
|
} catch (error) {
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 1);
|
|
}
|
|
});
|
|
|
|
it('should have 1 ended span when request is aborted', async () => {
|
|
nock(`${protocol}://my.server.com`)
|
|
.get('/')
|
|
.delayConnection(50)
|
|
.reply(200, '<html></html>');
|
|
|
|
const promiseRequest = new Promise((resolve, reject) => {
|
|
const req = http.request(
|
|
`${protocol}://my.server.com`,
|
|
(resp: http.IncomingMessage) => {
|
|
let data = '';
|
|
resp.on('data', chunk => {
|
|
data += chunk;
|
|
});
|
|
resp.on('end', () => {
|
|
resolve(data);
|
|
});
|
|
}
|
|
);
|
|
req.setTimeout(10, () => {
|
|
req.abort();
|
|
});
|
|
// Instrumentation should not swallow error event.
|
|
assert.strictEqual(req.listeners('error').length, 0);
|
|
req.on('error', err => {
|
|
reject(err);
|
|
});
|
|
return req.end();
|
|
});
|
|
|
|
await assert.rejects(promiseRequest, /Error: socket hang up/);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
const [span] = spans;
|
|
assert.strictEqual(spans.length, 1);
|
|
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
|
|
assert.ok(Object.keys(span.attributes).length >= 6);
|
|
});
|
|
|
|
it('should have 1 ended span when request is aborted after receiving response', async () => {
|
|
nock(`${protocol}://my.server.com`)
|
|
.get('/')
|
|
.delay({
|
|
body: 50,
|
|
})
|
|
.replyWithFile(200, `${process.cwd()}/package.json`);
|
|
|
|
const promiseRequest = new Promise((resolve, reject) => {
|
|
const req = http.request(
|
|
`${protocol}://my.server.com`,
|
|
(resp: http.IncomingMessage) => {
|
|
let data = '';
|
|
resp.on('data', chunk => {
|
|
req.destroy(Error('request destroyed'));
|
|
data += chunk;
|
|
});
|
|
resp.on('end', () => {
|
|
resolve(data);
|
|
});
|
|
}
|
|
);
|
|
// Instrumentation should not swallow error event.
|
|
assert.strictEqual(req.listeners('error').length, 0);
|
|
req.on('error', err => {
|
|
reject(err);
|
|
});
|
|
|
|
return req.end();
|
|
});
|
|
|
|
await assert.rejects(promiseRequest, /Error: request destroyed/);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
const [span] = spans;
|
|
assert.strictEqual(spans.length, 1);
|
|
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
|
|
assert.ok(Object.keys(span.attributes).length > 7);
|
|
});
|
|
|
|
it("should have 1 ended client span when request doesn't listening response", done => {
|
|
// nock doesn't emit close event.
|
|
nock.cleanAll();
|
|
nock.enableNetConnect();
|
|
|
|
const req = http.request(`${protocol}://${hostname}:${serverPort}/`);
|
|
req.on('close', () => {
|
|
const spans = memoryExporter
|
|
.getFinishedSpans()
|
|
.filter(it => it.kind === SpanKind.CLIENT);
|
|
assert.strictEqual(spans.length, 1);
|
|
const [span] = spans;
|
|
assert.ok(Object.keys(span.attributes).length > 6);
|
|
done();
|
|
});
|
|
req.end();
|
|
});
|
|
|
|
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 = http.request(`${host}/`);
|
|
req.on('response', response => {
|
|
response.on('data', () => {});
|
|
response.on('end', () => {
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
const [span] = spans;
|
|
assert.strictEqual(spans.length, 1);
|
|
assert.ok(Object.keys(span.attributes).length > 6);
|
|
assert.strictEqual(span.attributes[SEMATTRS_HTTP_STATUS_CODE], 404);
|
|
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
|
|
done();
|
|
});
|
|
});
|
|
req.end();
|
|
});
|
|
|
|
it('custom attributes should show up on client and server spans', async () => {
|
|
await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}${pathname}`,
|
|
{ headers: { guid: 'user_guid' } }
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
const [incomingSpan, outgoingSpan] = spans;
|
|
|
|
// server request
|
|
assert.strictEqual(
|
|
incomingSpan.attributes['custom request hook attribute'],
|
|
'request'
|
|
);
|
|
assert.strictEqual(
|
|
incomingSpan.attributes['custom response hook attribute'],
|
|
'response'
|
|
);
|
|
assert.strictEqual(
|
|
incomingSpan.attributes['custom server response attribute'],
|
|
'finish'
|
|
);
|
|
assert.strictEqual(incomingSpan.attributes['guid'], 'user_guid');
|
|
assert.strictEqual(
|
|
incomingSpan.attributes['span kind'],
|
|
SpanKind.CLIENT
|
|
);
|
|
|
|
// client request
|
|
assert.strictEqual(
|
|
outgoingSpan.attributes['custom request hook attribute'],
|
|
'request'
|
|
);
|
|
assert.strictEqual(
|
|
outgoingSpan.attributes['custom response hook attribute'],
|
|
'response'
|
|
);
|
|
assert.strictEqual(
|
|
outgoingSpan.attributes['custom incoming message attribute'],
|
|
'end'
|
|
);
|
|
assert.strictEqual(outgoingSpan.attributes['guid'], 'user_guid');
|
|
assert.strictEqual(
|
|
outgoingSpan.attributes['span kind'],
|
|
SpanKind.CLIENT
|
|
);
|
|
});
|
|
|
|
it('should not set span as active in context for outgoing request', done => {
|
|
assert.deepStrictEqual(trace.getSpan(context.active()), undefined);
|
|
http.get(`${protocol}://${hostname}:${serverPort}/test`, res => {
|
|
assert.deepStrictEqual(trace.getSpan(context.active()), undefined);
|
|
|
|
res.on('data', () => {
|
|
assert.deepStrictEqual(trace.getSpan(context.active()), undefined);
|
|
});
|
|
|
|
res.on('end', () => {
|
|
assert.deepStrictEqual(trace.getSpan(context.active()), undefined);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should have 2 ended span when client prematurely close', async () => {
|
|
const promise = new Promise<void>(resolve => {
|
|
const req = http.get(
|
|
`${protocol}://${hostname}:${serverPort}/hang`,
|
|
res => {
|
|
res.on('close', () => {});
|
|
res.on('error', () => {});
|
|
}
|
|
);
|
|
// close the socket.
|
|
setTimeout(() => {
|
|
req.destroy();
|
|
}, 10);
|
|
|
|
req.on('error', () => {});
|
|
|
|
req.on('close', () => {
|
|
// yield to server to end the span.
|
|
setTimeout(resolve, 10);
|
|
});
|
|
});
|
|
|
|
await promise;
|
|
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 2);
|
|
const [serverSpan, clientSpan] = spans.sort(
|
|
(lhs, rhs) => lhs.kind - rhs.kind
|
|
);
|
|
assert.strictEqual(serverSpan.kind, SpanKind.SERVER);
|
|
assert.ok(Object.keys(serverSpan.attributes).length >= 6);
|
|
|
|
assert.strictEqual(clientSpan.kind, SpanKind.CLIENT);
|
|
assert.ok(Object.keys(clientSpan.attributes).length >= 6);
|
|
});
|
|
|
|
it('should have 2 ended span when server prematurely close', async () => {
|
|
const promise = new Promise<void>(resolve => {
|
|
const req = http.get(
|
|
`${protocol}://${hostname}:${serverPort}/premature-close`
|
|
);
|
|
req.on('error', err => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
await promise;
|
|
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 2);
|
|
const [serverSpan, clientSpan] = spans.sort(
|
|
(lhs, rhs) => lhs.kind - rhs.kind
|
|
);
|
|
assert.strictEqual(serverSpan.kind, SpanKind.SERVER);
|
|
assert.ok(Object.keys(serverSpan.attributes).length >= 6);
|
|
|
|
assert.strictEqual(clientSpan.kind, SpanKind.CLIENT);
|
|
assert.strictEqual(clientSpan.status.code, SpanStatusCode.ERROR);
|
|
assert.ok(Object.keys(clientSpan.attributes).length >= 6);
|
|
});
|
|
|
|
it('should not end span multiple times if request socket destroyed before response completes', async () => {
|
|
const warnMessages: string[] = [];
|
|
diag.setLogger({
|
|
...new DiagConsoleLogger(),
|
|
warn: message => {
|
|
warnMessages.push(message);
|
|
},
|
|
});
|
|
const promise = new Promise<void>(resolve => {
|
|
const req = http.request(
|
|
`${protocol}://${hostname}:${serverPort}/destroy-request`,
|
|
{
|
|
// Allow `req.write()`.
|
|
method: 'POST',
|
|
},
|
|
res => {
|
|
res.on('end', () => {});
|
|
res.on('close', () => {
|
|
resolve();
|
|
});
|
|
res.on('error', () => {});
|
|
}
|
|
);
|
|
// force flush http request header to trigger client response callback
|
|
req.write('');
|
|
req.on('error', () => {});
|
|
});
|
|
|
|
await promise;
|
|
|
|
diag.disable();
|
|
|
|
assert.deepStrictEqual(warnMessages, []);
|
|
});
|
|
});
|
|
|
|
describe('with require parent span', () => {
|
|
beforeEach(done => {
|
|
memoryExporter.reset();
|
|
instrumentation.setConfig({});
|
|
instrumentation.enable();
|
|
server = http.createServer((request, response) => {
|
|
response.end('Test Server Response');
|
|
});
|
|
server.listen(serverPort, done);
|
|
});
|
|
|
|
afterEach(() => {
|
|
server.close();
|
|
instrumentation.disable();
|
|
});
|
|
|
|
it('should not trace without parent with options enabled (both client & server)', async () => {
|
|
instrumentation.disable();
|
|
instrumentation.setConfig({
|
|
requireParentforIncomingSpans: true,
|
|
requireParentforOutgoingSpans: true,
|
|
});
|
|
instrumentation.enable();
|
|
const testPath = '/test/test';
|
|
await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}${testPath}`
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 0);
|
|
});
|
|
|
|
it('should not trace without parent with options enabled (client only)', async () => {
|
|
instrumentation.disable();
|
|
instrumentation.setConfig({
|
|
requireParentforOutgoingSpans: true,
|
|
});
|
|
instrumentation.enable();
|
|
const testPath = '/test/test';
|
|
const result = await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}${testPath}`
|
|
);
|
|
assert(
|
|
result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !== undefined
|
|
);
|
|
assert(
|
|
result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !== undefined
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 1);
|
|
assert.strictEqual(
|
|
spans.every(span => span.kind === SpanKind.SERVER),
|
|
true
|
|
);
|
|
});
|
|
|
|
it('should not trace without parent with options enabled (server only)', async () => {
|
|
instrumentation.disable();
|
|
instrumentation.setConfig({
|
|
requireParentforIncomingSpans: true,
|
|
});
|
|
instrumentation.enable();
|
|
const testPath = '/test/test';
|
|
const result = await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}${testPath}`
|
|
);
|
|
assert(
|
|
result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !== undefined
|
|
);
|
|
assert(
|
|
result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !== undefined
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 1);
|
|
assert.strictEqual(
|
|
spans.every(span => span.kind === SpanKind.CLIENT),
|
|
true
|
|
);
|
|
});
|
|
|
|
it('should trace with parent with both requireParent options enabled', done => {
|
|
instrumentation.disable();
|
|
instrumentation.setConfig({
|
|
requireParentforIncomingSpans: true,
|
|
requireParentforOutgoingSpans: true,
|
|
});
|
|
instrumentation.enable();
|
|
const testPath = '/test/test';
|
|
const tracer = provider.getTracer('default');
|
|
const span = tracer.startSpan('parentSpan', {
|
|
kind: SpanKind.INTERNAL,
|
|
});
|
|
context.with(trace.setSpan(context.active(), span), () => {
|
|
httpRequest
|
|
.get(`${protocol}://${hostname}:${serverPort}${testPath}`)
|
|
.then(result => {
|
|
span.end();
|
|
assert(
|
|
result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !==
|
|
undefined
|
|
);
|
|
assert(
|
|
result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !==
|
|
undefined
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 2);
|
|
assert.strictEqual(
|
|
spans.filter(span => span.kind === SpanKind.CLIENT).length,
|
|
1
|
|
);
|
|
assert.strictEqual(
|
|
spans.filter(span => span.kind === SpanKind.INTERNAL).length,
|
|
1
|
|
);
|
|
return done();
|
|
})
|
|
.catch(done);
|
|
});
|
|
});
|
|
});
|
|
describe('rpc metadata', () => {
|
|
beforeEach(() => {
|
|
memoryExporter.reset();
|
|
instrumentation.setConfig({ requireParentforOutgoingSpans: true });
|
|
instrumentation.enable();
|
|
});
|
|
|
|
afterEach(() => {
|
|
server.close();
|
|
instrumentation.disable();
|
|
});
|
|
|
|
it('should set rpc metadata for incoming http request', async () => {
|
|
server = http.createServer((request, response) => {
|
|
const rpcMetadata = getRPCMetadata(context.active());
|
|
assert(typeof rpcMetadata !== 'undefined');
|
|
assert(rpcMetadata.type === RPCType.HTTP);
|
|
assert(rpcMetadata.span.setAttribute('key', 'value'));
|
|
response.end('Test Server Response');
|
|
});
|
|
await new Promise<void>(resolve => server.listen(serverPort, resolve));
|
|
await httpRequest.get(`${protocol}://${hostname}:${serverPort}`);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
assert.strictEqual(spans.length, 1);
|
|
assert.strictEqual(spans[0].attributes.key, 'value');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('capturing headers as span attributes', () => {
|
|
beforeEach(() => {
|
|
memoryExporter.reset();
|
|
});
|
|
|
|
before(() => {
|
|
instrumentation.setConfig({
|
|
headersToSpanAttributes: {
|
|
client: {
|
|
requestHeaders: ['X-Client-Header1'],
|
|
responseHeaders: ['X-Server-Header1'],
|
|
},
|
|
server: {
|
|
requestHeaders: ['X-Client-Header2'],
|
|
responseHeaders: ['X-Server-Header2'],
|
|
},
|
|
},
|
|
});
|
|
instrumentation.enable();
|
|
server = http.createServer((request, response) => {
|
|
response.setHeader('X-ServeR-header1', 'server123');
|
|
response.setHeader('X-Server-header2', '123server');
|
|
response.end('Test Server Response');
|
|
});
|
|
|
|
server.listen(serverPort);
|
|
});
|
|
|
|
after(() => {
|
|
server.close();
|
|
instrumentation.disable();
|
|
});
|
|
|
|
it('should convert headers to span attributes', async () => {
|
|
await httpRequest.get(
|
|
`${protocol}://${hostname}:${serverPort}${pathname}`,
|
|
{
|
|
headers: {
|
|
'X-client-header1': 'client123',
|
|
'X-CLIENT-HEADER2': '123client',
|
|
},
|
|
}
|
|
);
|
|
const spans = memoryExporter.getFinishedSpans();
|
|
const [incomingSpan, outgoingSpan] = spans;
|
|
|
|
assert.strictEqual(spans.length, 2);
|
|
|
|
assert.deepStrictEqual(
|
|
incomingSpan.attributes['http.request.header.x_client_header2'],
|
|
['123client']
|
|
);
|
|
|
|
assert.deepStrictEqual(
|
|
incomingSpan.attributes['http.response.header.x_server_header2'],
|
|
['123server']
|
|
);
|
|
|
|
assert.strictEqual(
|
|
incomingSpan.attributes['http.request.header.x_client_header1'],
|
|
undefined
|
|
);
|
|
|
|
assert.strictEqual(
|
|
incomingSpan.attributes['http.response.header.x_server_header1'],
|
|
undefined
|
|
);
|
|
|
|
assert.deepStrictEqual(
|
|
outgoingSpan.attributes['http.request.header.x_client_header1'],
|
|
['client123']
|
|
);
|
|
assert.deepStrictEqual(
|
|
outgoingSpan.attributes['http.response.header.x_server_header1'],
|
|
['server123']
|
|
);
|
|
|
|
assert.strictEqual(
|
|
outgoingSpan.attributes['http.request.header.x_client_header2'],
|
|
undefined
|
|
);
|
|
|
|
assert.strictEqual(
|
|
outgoingSpan.attributes['http.response.header.x_server_header2'],
|
|
undefined
|
|
);
|
|
});
|
|
});
|
|
});
|