opentelemetry-auto-instrume.../plugins/node/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts
2024-08-08 12:04:12 +08:00

408 lines
13 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 { SpanKind, Span, context, propagation } from '@opentelemetry/api';
import {
HTTPFLAVORVALUES_HTTP_1_1,
NETTRANSPORTVALUES_IP_TCP,
SEMATTRS_HTTP_FLAVOR,
SEMATTRS_HTTP_HOST,
SEMATTRS_NET_TRANSPORT,
} from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import * as url from 'url';
import { HttpInstrumentation } from '../../src/http';
import { assertSpan } from '../utils/assertSpan';
import * as utils from '../utils/utils';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
import { httpRequest } from '../utils/httpRequest';
import { DummyPropagation } from '../utils/DummyPropagation';
import { Socket } from 'net';
import { sendRequestTwice } from '../utils/rawRequest';
const protocol = 'http';
const serverPort = 32345;
const hostname = 'localhost';
const memoryExporter = new InMemorySpanExporter();
const customAttributeFunction = (span: Span): void => {
span.setAttribute('span kind', SpanKind.CLIENT);
};
describe('HttpInstrumentation Integration tests', () => {
let mockServerPort = 0;
let mockServer: http.Server;
const sockets: Array<Socket> = [];
before(done => {
mockServer = http.createServer((req, res) => {
if (req.url === '/timeout') {
setTimeout(() => {
res.end();
}, 1000);
}
res.statusCode = 200;
res.setHeader('content-type', 'application/json');
res.write(
JSON.stringify({
success: true,
})
);
res.end();
});
mockServer.listen(0, () => {
const addr = mockServer.address();
if (addr == null) {
done(new Error('unexpected addr null'));
return;
}
if (typeof addr === 'string') {
done(new Error(`unexpected addr ${addr}`));
return;
}
if (addr.port <= 0) {
done(new Error('Could not get port'));
return;
}
mockServerPort = addr.port;
done();
});
});
after(done => {
sockets.forEach(s => s.destroy());
mockServer.close(done);
});
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
propagation.setGlobalPropagator(new DummyPropagation());
context.setGlobalContextManager(new AsyncHooksContextManager().enable());
});
after(() => {
context.disable();
propagation.disable();
});
describe('enable()', () => {
before(function (done) {
// mandatory
if (process.env.CI) {
done();
return;
}
utils.checkInternet(isConnected => {
if (!isConnected) {
this.skip();
// don't disturb people
}
done();
});
});
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
instrumentation.setTracerProvider(provider);
beforeEach(() => {
memoryExporter.reset();
});
before(() => {
const ignoreConfig = [
`${protocol}://${hostname}:${serverPort}/ignored/string`,
/\/ignored\/regexp$/i,
(url: string) => url.endsWith('/ignored/function'),
];
instrumentation.setConfig({
ignoreIncomingPaths: ignoreConfig,
ignoreOutgoingUrls: ignoreConfig,
applyCustomAttributesOnSpan: customAttributeFunction,
});
instrumentation.enable();
});
after(() => {
instrumentation.disable();
});
it('should create a rootSpan for GET requests and add propagation headers', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const result = await httpRequest.get(
`${protocol}://localhost:${mockServerPort}/?query=test`
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
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 httpRequest.get(
new url.URL(`${protocol}://localhost:${mockServerPort}/?query=test`)
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should create a valid rootSpan with propagation headers for GET requests if URL and options are used', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const result = await httpRequest.get(
new url.URL(`${protocol}://localhost:${mockServerPort}/?query=test`),
{
headers: { 'x-foo': 'foo' },
}
);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(result.reqHeaders['x-foo'], 'foo');
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_FLAVOR],
HTTPFLAVORVALUES_HTTP_1_1
);
assert.strictEqual(
span.attributes[SEMATTRS_NET_TRANSPORT],
NETTRANSPORTVALUES_IP_TCP
);
assertSpan(span, SpanKind.CLIENT, validations);
});
it('custom attributes should show up on client spans', async () => {
const result = await httpRequest.get(
`${protocol}://localhost:${mockServerPort}/`
);
const spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT);
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should not mutate given headers object when adding propagation headers', async () => {
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const headers = { 'x-foo': 'foo' };
const result = await httpRequest.get(
new url.URL(`${protocol}://localhost:${mockServerPort}/?query=test`),
{ headers }
);
assert.deepStrictEqual(headers, { 'x-foo': 'foo' });
assert.ok(result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
assert.ok(result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
});
it('should create a span for GET requests and add propagation headers with Expect headers', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const options = Object.assign(
{ headers: { Expect: '100-continue' } },
url.parse(`${protocol}://localhost:${mockServerPort}/`)
);
const result = await httpRequest.get(options);
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
const validations = {
hostname: 'localhost',
httpStatusCode: 200,
httpMethod: 'GET',
pathname: '/',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assertSpan(span, SpanKind.CLIENT, validations);
});
for (const headers of [
{ 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: get(url, options, callback) and following headers: ${JSON.stringify(
headers
)}`, done => {
let validations: {
hostname: string;
httpStatusCode: number;
httpMethod: string;
pathname: string;
reqHeaders: http.OutgoingHttpHeaders;
resHeaders: http.IncomingHttpHeaders;
};
let data = '';
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
const options = { headers };
const req = http.get(
`${protocol}://localhost:${mockServerPort}/`,
options,
(resp: http.IncomingMessage) => {
const res = resp as unknown as http.IncomingMessage & {
req: http.IncomingMessage;
};
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
validations = {
hostname: 'localhost',
httpStatusCode: 301,
httpMethod: 'GET',
pathname: '/',
resHeaders: resp.headers,
/* tslint:disable:no-any */
reqHeaders: (res.req as any).getHeaders
? (res.req as any).getHeaders()
: (res.req as any)._headers,
/* tslint:enable:no-any */
};
});
}
);
req.on('close', () => {
const spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.ok(data);
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);
done();
});
});
}
it('should work for multiple active requests in keep-alive mode', async () => {
await sendRequestTwice(hostname, mockServerPort);
const spans = memoryExporter.getFinishedSpans();
const span = spans.find((s: any) => s.kind === SpanKind.SERVER);
assert.ok(span);
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
});
it('should have correct spans even when request timeout', async () => {
let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0);
try {
await httpRequest.get(
`${protocol}://localhost:${mockServerPort}/timeout`,
{ timeout: 1 }
);
} catch (err: any) {
assert.ok(err.message.startsWith('timeout'));
}
spans = memoryExporter.getFinishedSpans();
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(
span.attributes[SEMATTRS_HTTP_HOST],
`localhost:${mockServerPort}`
);
});
});
});