feat(plugin-http): add/modify attributes (#643)

* feat(plugin-http): add/modify attributes

closes #373, #394

Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>

* fix: change remotePort to localPort

refactor: remove useless checks
test: add assertions

Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>

* test(plugin-https): sync with http plugin

Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>

Co-authored-by: Mayur Kale <mayurkale@google.com>
This commit is contained in:
Olivier Albertini 2020-01-02 02:07:39 -05:00 committed by Mayur Kale
parent e859b8e930
commit 3d9b8228b5
11 changed files with 383 additions and 96 deletions

View File

@ -54,7 +54,7 @@ Http plugin has few options available to choose from. You can set the following:
| [`applyCustomAttributesOnSpan`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L52) | `HttpCustomAttributeFunction` | Function for adding custom attributes |
| [`ignoreIncomingPaths`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L28) | `IgnoreMatcher[]` | Http plugin will not trace all incoming requests that match paths |
| [`ignoreOutgoingUrls`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L28) | `IgnoreMatcher[]` | Http plugin will not trace all outgoing requests that match urls |
| [`serverName`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L28) | `string` | The primary server name of the matched virtual host. |
## Useful links
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
- For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js>

View File

@ -15,17 +15,29 @@
*/
/**
* Attributes Names according to [OpenTelemetry attributes specs](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#semantic-conventions)
* Attributes Names according to [OpenTelemetry attributes specs](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#common-attributes)
*/
export enum AttributeNames {
HTTP_HOSTNAME = 'http.hostname',
HTTP_HOST = 'http.host',
COMPONENT = 'component',
HTTP_METHOD = 'http.method',
HTTP_PATH = 'http.path',
HTTP_TARGET = 'http.target',
HTTP_ROUTE = 'http.route',
HTTP_URL = 'http.url',
HTTP_STATUS_CODE = 'http.status_code',
HTTP_STATUS_TEXT = 'http.status_text',
HTTP_FLAVOR = 'http.flavor',
NET_PEER_IP = 'net.peer.ip',
NET_PEER_PORT = 'net.peer.port',
NET_PEER_NAME = 'net.peer.name',
NET_HOST_IP = 'net.host.ip',
NET_HOST_PORT = 'net.host.port',
NET_HOST_NAME = 'net.host.name',
NET_TRANSPORT = 'net.transport',
IP_TCP = 'IP.TCP',
IP_UDP = 'IP.UDP',
HTTP_SERVER_NAME = 'http.server_name',
HTTP_CLIENT_IP = 'http.client_ip',
// NOT ON OFFICIAL SPEC
HTTP_ERROR_NAME = 'http.error_name',
HTTP_ERROR_MESSAGE = 'http.error_message',

View File

@ -19,7 +19,6 @@ import {
Span,
SpanKind,
SpanOptions,
Attributes,
CanonicalCode,
Status,
} from '@opentelemetry/types';
@ -45,6 +44,7 @@ import {
import { Format } from './enums/Format';
import { AttributeNames } from './enums/AttributeNames';
import * as utils from './utils';
import { Socket } from 'net';
/**
* Http instrumentation plugin for Opentelemetry
@ -183,37 +183,24 @@ export class HttpPlugin extends BasePlugin<Http> {
return (): ClientRequest => {
this._logger.debug('makeRequestTrace by injecting context into header');
const host = options.hostname || options.host || 'localhost';
const method = options.method ? options.method.toUpperCase() : 'GET';
const headers = options.headers || {};
const userAgent = headers['user-agent'];
span.setAttributes({
[AttributeNames.HTTP_URL]: utils.getAbsoluteUrl(
options,
headers,
`${this.component}:`
),
[AttributeNames.HTTP_HOSTNAME]: host,
[AttributeNames.HTTP_METHOD]: method,
[AttributeNames.HTTP_PATH]: options.path || '/',
const hostname =
options.hostname ||
options.host?.replace(/^(.*)(\:[0-9]{1,5})/, '$1') ||
'localhost';
const attributes = utils.getOutgoingRequestAttributes(options, {
component: this.component,
hostname,
});
if (userAgent !== undefined) {
span.setAttribute(AttributeNames.HTTP_USER_AGENT, userAgent);
}
span.setAttributes(attributes);
request.on(
'response',
(
response: IncomingMessage & { aborted?: boolean; req: ClientRequest }
) => {
if (response.statusCode) {
span.setAttributes({
[AttributeNames.HTTP_STATUS_CODE]: response.statusCode,
[AttributeNames.HTTP_STATUS_TEXT]: response.statusMessage,
});
}
(response: IncomingMessage & { aborted?: boolean }) => {
const attributes = utils.getOutgoingRequestAttributesOnResponse(
response,
{ hostname }
);
span.setAttributes(attributes);
this._tracer.bind(response);
this._logger.debug('outgoingRequest on response()');
@ -280,7 +267,7 @@ export class HttpPlugin extends BasePlugin<Http> {
}
const request = args[0] as IncomingMessage;
const response = args[1] as ServerResponse;
const response = args[1] as ServerResponse & { socket: Socket };
const pathname = request.url
? url.parse(request.url).pathname || '/'
: '/';
@ -301,8 +288,13 @@ export class HttpPlugin extends BasePlugin<Http> {
const propagation = plugin._tracer.getHttpTextFormat();
const headers = request.headers;
const spanOptions: SpanOptions = {
kind: SpanKind.SERVER,
attributes: utils.getIncomingRequestAttributes(request, {
component: plugin.component,
serverName: plugin._config.serverName,
}),
};
const spanContext = propagation.extract(Format.HTTP, headers);
@ -332,32 +324,10 @@ export class HttpPlugin extends BasePlugin<Http> {
() => response.end.apply(this, arguments as any),
true
);
const requestUrl = request.url ? url.parse(request.url) : null;
const hostname = headers.host
? headers.host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')
: 'localhost';
const userAgent = headers['user-agent'];
const attributes: Attributes = {
[AttributeNames.HTTP_URL]: utils.getAbsoluteUrl(
requestUrl,
headers,
`${plugin.component}:`
),
[AttributeNames.HTTP_HOSTNAME]: hostname,
[AttributeNames.HTTP_METHOD]: method,
[AttributeNames.HTTP_STATUS_CODE]: response.statusCode,
[AttributeNames.HTTP_STATUS_TEXT]: response.statusMessage,
};
if (requestUrl) {
attributes[AttributeNames.HTTP_PATH] = requestUrl.path || '/';
attributes[AttributeNames.HTTP_ROUTE] = requestUrl.pathname || '/';
}
if (userAgent !== undefined) {
attributes[AttributeNames.HTTP_USER_AGENT] = userAgent;
}
const attributes = utils.getIncomingRequestAttributesOnResponse(
response
);
span
.setAttributes(attributes)

View File

@ -57,10 +57,18 @@ export interface HttpCustomAttributeFunction {
): void;
}
/**
* Options available for the HTTP Plugin (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-http#http-plugin-options))
*/
export interface HttpPluginConfig extends PluginConfig {
/** Not trace all incoming requests that match paths */
ignoreIncomingPaths?: IgnoreMatcher[];
/** Not trace all outgoing requests that match urls */
ignoreOutgoingUrls?: IgnoreMatcher[];
/** Function for adding custom attributes */
applyCustomAttributesOnSpan?: HttpCustomAttributeFunction;
/** The primary server name of the matched virtual host. */
serverName?: string;
}
export interface Err extends Error {

View File

@ -14,17 +14,19 @@
* limitations under the License.
*/
import { Status, CanonicalCode, Span } from '@opentelemetry/types';
import { Status, CanonicalCode, Span, Attributes } from '@opentelemetry/types';
import {
RequestOptions,
IncomingMessage,
ClientRequest,
IncomingHttpHeaders,
OutgoingHttpHeaders,
ServerResponse,
} from 'http';
import { IgnoreMatcher, Err, ParsedRequestOptions } from './types';
import { AttributeNames } from './enums/AttributeNames';
import * as url from 'url';
import { Socket } from 'net';
export const OT_REQUEST_HEADER = 'x-opentelemetry-outgoing-request';
/**
@ -280,3 +282,157 @@ export const isValidOptionsType = (options: unknown): boolean => {
export const isOpenTelemetryRequest = (options: RequestOptions) => {
return !!(options && options.headers && options.headers[OT_REQUEST_HEADER]);
};
/**
* Returns outgoing request attributes scoped to the options passed to the request
* @param {ParsedRequestOptions} requestOptions the same options used to make the request
* @param {{ component: string, hostname: string }} options used to pass data needed to create attributes
*/
export const getOutgoingRequestAttributes = (
requestOptions: ParsedRequestOptions,
options: { component: string; hostname: string }
): Attributes => {
const host = requestOptions.host;
const hostname =
requestOptions.hostname ||
host?.replace(/^(.*)(\:[0-9]{1,5})/, '$1') ||
'localhost';
const requestMethod = requestOptions.method;
const method = requestMethod ? requestMethod.toUpperCase() : 'GET';
const headers = requestOptions.headers || {};
const userAgent = headers['user-agent'];
const attributes: Attributes = {
[AttributeNames.HTTP_URL]: getAbsoluteUrl(
requestOptions,
headers,
`${options.component}:`
),
[AttributeNames.HTTP_METHOD]: method,
[AttributeNames.HTTP_TARGET]: requestOptions.path || '/',
[AttributeNames.NET_PEER_NAME]: hostname,
};
if (userAgent !== undefined) {
attributes[AttributeNames.HTTP_USER_AGENT] = userAgent;
}
return attributes;
};
/**
* Returns attributes related to the kind of HTTP protocol used
* @param {string} [kind] Kind of HTTP protocol used: "1.0", "1.1", "2", "SPDY" or "QUIC".
*/
export const getAttributesFromHttpKind = (kind?: string): Attributes => {
const attributes: Attributes = {};
if (kind) {
attributes[AttributeNames.HTTP_FLAVOR] = kind;
if (kind.toUpperCase() !== 'QUIC') {
attributes[AttributeNames.NET_TRANSPORT] = AttributeNames.IP_TCP;
} else {
attributes[AttributeNames.NET_TRANSPORT] = AttributeNames.IP_UDP;
}
}
return attributes;
};
/**
* Returns outgoing request attributes scoped to the response data
* @param {IncomingMessage} response the response object
* @param {{ hostname: string }} options used to pass data needed to create attributes
*/
export const getOutgoingRequestAttributesOnResponse = (
response: IncomingMessage,
options: { hostname: string }
): Attributes => {
const { statusCode, statusMessage, httpVersion, socket } = response;
const { remoteAddress, remotePort } = socket;
const attributes: Attributes = {
[AttributeNames.NET_PEER_IP]: remoteAddress,
[AttributeNames.NET_PEER_PORT]: remotePort,
[AttributeNames.HTTP_HOST]: `${options.hostname}:${remotePort}`,
};
if (statusCode) {
attributes[AttributeNames.HTTP_STATUS_CODE] = statusCode;
attributes[AttributeNames.HTTP_STATUS_TEXT] = (
statusMessage || ''
).toUpperCase();
}
const httpKindAttributes = getAttributesFromHttpKind(httpVersion);
return Object.assign(attributes, httpKindAttributes);
};
/**
* Returns incoming request attributes scoped to the request data
* @param {IncomingMessage} request the request object
* @param {{ component: string, serverName?: string }} options used to pass data needed to create attributes
*/
export const getIncomingRequestAttributes = (
request: IncomingMessage,
options: { component: string; serverName?: string }
): Attributes => {
const headers = request.headers;
const userAgent = headers['user-agent'];
const ips = headers['x-forwarded-for'];
const method = request.method || 'GET';
const httpVersion = request.httpVersion;
const requestUrl = request.url ? url.parse(request.url) : null;
const host = requestUrl?.host || headers.host;
const hostname =
requestUrl?.hostname ||
host?.replace(/^(.*)(\:[0-9]{1,5})/, '$1') ||
'localhost';
const serverName = options.serverName;
const attributes: Attributes = {
[AttributeNames.HTTP_URL]: getAbsoluteUrl(
requestUrl,
headers,
`${options.component}:`
),
[AttributeNames.HTTP_HOST]: host,
[AttributeNames.NET_HOST_NAME]: hostname,
[AttributeNames.HTTP_METHOD]: method,
};
if (typeof ips === 'string') {
attributes[AttributeNames.HTTP_CLIENT_IP] = ips.split(',')[0];
}
if (typeof serverName === 'string') {
attributes[AttributeNames.HTTP_SERVER_NAME] = serverName;
}
if (requestUrl) {
attributes[AttributeNames.HTTP_TARGET] = requestUrl.path || '/';
attributes[AttributeNames.HTTP_ROUTE] = requestUrl.pathname || '/';
}
if (userAgent !== undefined) {
attributes[AttributeNames.HTTP_USER_AGENT] = userAgent;
}
const httpKindAttributes = getAttributesFromHttpKind(httpVersion);
return Object.assign(attributes, httpKindAttributes);
};
/**
* Returns incoming request attributes scoped to the response data
* @param {(ServerResponse & { socket: Socket; })} response the response object
*/
export const getIncomingRequestAttributesOnResponse = (
response: ServerResponse & { socket: Socket }
): Attributes => {
const { statusCode, statusMessage, socket } = response;
const { localAddress, localPort, remoteAddress, remotePort } = socket;
return {
[AttributeNames.NET_HOST_IP]: localAddress,
[AttributeNames.NET_HOST_PORT]: localPort,
[AttributeNames.NET_PEER_IP]: remoteAddress,
[AttributeNames.NET_PEER_PORT]: remotePort,
[AttributeNames.HTTP_STATUS_CODE]: statusCode,
[AttributeNames.HTTP_STATUS_TEXT]: (statusMessage || '').toUpperCase(),
};
};

View File

@ -41,6 +41,7 @@ const serverPort = 22345;
const protocol = 'http';
const hostname = 'localhost';
const pathname = '/test';
const serverName = 'my.server.name';
const memoryExporter = new InMemorySpanExporter();
const httpTextFormat = new DummyPropagation();
const logger = new NoopLogger();
@ -140,6 +141,14 @@ describe('HttpPlugin', () => {
assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
assert.strictEqual(
incomingSpan.attributes[AttributeNames.NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[AttributeNames.NET_PEER_PORT],
serverPort
);
});
it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
@ -176,6 +185,7 @@ describe('HttpPlugin', () => {
(url: string) => url.endsWith(`/ignored/function`),
],
applyCustomAttributesOnSpan: customAttributeFunction,
serverName,
};
plugin.enable(http, tracer, tracer.logger, config);
server = http.createServer((request, response) => {
@ -204,7 +214,13 @@ describe('HttpPlugin', () => {
it('should generate valid spans (client side and server side)', async () => {
const result = await httpRequest.get(
`${protocol}://${hostname}:${serverPort}${pathname}`
`${protocol}://${hostname}:${serverPort}${pathname}`,
{
headers: {
'x-forwarded-for': '<client>, <proxy1>, <proxy2>',
'user-agent': 'chrome',
},
}
);
const spans = memoryExporter.getFinishedSpans();
const [incomingSpan, outgoingSpan] = spans;
@ -216,11 +232,36 @@ describe('HttpPlugin', () => {
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: plugin.component,
serverName,
};
assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
assert.strictEqual(
incomingSpan.attributes[AttributeNames.HTTP_CLIENT_IP],
'<client>'
);
assert.strictEqual(
incomingSpan.attributes[AttributeNames.NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[AttributeNames.NET_PEER_PORT],
serverPort
);
[
{ span: incomingSpan, kind: SpanKind.SERVER },
{ span: outgoingSpan, kind: SpanKind.CLIENT },
].forEach(({ span, kind }) => {
assert.strictEqual(
span.attributes[AttributeNames.HTTP_FLAVOR],
'1.1'
);
assert.strictEqual(
span.attributes[AttributeNames.NET_TRANSPORT],
AttributeNames.IP_TCP
);
assertSpan(span, kind, validations);
});
});
it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
@ -545,7 +586,7 @@ describe('HttpPlugin', () => {
const [span] = spans;
assert.strictEqual(spans.length, 1);
assert.strictEqual(span.status.code, CanonicalCode.ABORTED);
assert.ok(Object.keys(span.attributes).length > 6);
assert.ok(Object.keys(span.attributes).length >= 6);
}
});

View File

@ -30,6 +30,7 @@ import {
SimpleSpanProcessor,
} from '@opentelemetry/tracing';
import { HttpPluginConfig } from '../../src/types';
import { AttributeNames } from '../../src/enums/AttributeNames';
const protocol = 'http';
const serverPort = 32345;
const hostname = 'localhost';
@ -141,7 +142,7 @@ describe('HttpPlugin Integration tests', () => {
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should create a rootSpan for GET requests and add propagation headers if URL and options are used', async () => {
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);
@ -166,6 +167,11 @@ describe('HttpPlugin Integration tests', () => {
assert.strictEqual(spans.length, 1);
assert.ok(span.name.indexOf('GET /') >= 0);
assert.strictEqual(result.reqHeaders['x-foo'], 'foo');
assert.strictEqual(span.attributes[AttributeNames.HTTP_FLAVOR], '1.1');
assert.strictEqual(
span.attributes[AttributeNames.NET_TRANSPORT],
AttributeNames.IP_TCP
);
assertSpan(span, SpanKind.CLIENT, validations);
});

View File

@ -35,6 +35,7 @@ export const assertSpan = (
reqHeaders?: http.OutgoingHttpHeaders;
path?: string | null;
forceStatus?: Status;
serverName?: string;
component: string;
}
) => {
@ -53,16 +54,12 @@ export const assertSpan = (
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
span.status.message
);
assert.strictEqual(
span.attributes[AttributeNames.HTTP_HOSTNAME],
validations.hostname
);
assert.strictEqual(
span.attributes[AttributeNames.HTTP_METHOD],
validations.httpMethod
);
assert.strictEqual(
span.attributes[AttributeNames.HTTP_PATH],
span.attributes[AttributeNames.HTTP_TARGET],
validations.path || validations.pathname
);
assert.strictEqual(
@ -79,12 +76,6 @@ export const assertSpan = (
utils.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');
@ -97,8 +88,40 @@ export const assertSpan = (
);
}
}
if (span.kind === SpanKind.CLIENT) {
assert.strictEqual(
span.attributes[AttributeNames.NET_PEER_NAME],
validations.hostname,
'must be consistent (PEER_NAME and hostname)'
);
assert.ok(span.attributes[AttributeNames.NET_PEER_IP], 'must have PEER_IP');
assert.ok(
span.attributes[AttributeNames.NET_PEER_PORT],
'must have PEER_PORT'
);
assert.ok(
(span.attributes[AttributeNames.HTTP_URL] as string).indexOf(
span.attributes[AttributeNames.NET_PEER_NAME] as string
) > -1,
'must be consistent'
);
}
if (span.kind === SpanKind.SERVER) {
if (validations.serverName) {
assert.strictEqual(
span.attributes[AttributeNames.HTTP_SERVER_NAME],
validations.serverName,
' must have serverName attribute'
);
assert.ok(
span.attributes[AttributeNames.NET_HOST_PORT],
'must have HOST_PORT'
);
assert.ok(
span.attributes[AttributeNames.NET_HOST_IP],
'must have HOST_IP'
);
}
assert.strictEqual(span.parentSpanId, DummyPropagation.SPAN_CONTEXT_KEY);
} else if (validations.reqHeaders) {
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);

View File

@ -45,6 +45,7 @@ let server: https.Server;
const serverPort = 32345;
const protocol = 'https';
const hostname = 'localhost';
const serverName = 'my.server.name';
const pathname = '/test';
const memoryExporter = new InMemorySpanExporter();
const httpTextFormat = new DummyPropagation();
@ -153,6 +154,14 @@ describe('HttpsPlugin', () => {
assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
assert.strictEqual(
incomingSpan.attributes[AttributeNames.NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[AttributeNames.NET_PEER_PORT],
serverPort
);
});
it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
@ -189,6 +198,7 @@ describe('HttpsPlugin', () => {
(url: string) => url.endsWith(`/ignored/function`),
],
applyCustomAttributesOnSpan: customAttributeFunction,
serverName,
};
plugin.enable(
(https as unknown) as Http,
@ -230,7 +240,13 @@ describe('HttpsPlugin', () => {
it('should generate valid spans (client side and server side)', async () => {
const result = await httpsRequest.get(
`${protocol}://${hostname}:${serverPort}${pathname}`
`${protocol}://${hostname}:${serverPort}${pathname}`,
{
headers: {
'x-forwarded-for': '<client>, <proxy1>, <proxy2>',
'user-agent': 'chrome',
},
}
);
const spans = memoryExporter.getFinishedSpans();
const [incomingSpan, outgoingSpan] = spans;
@ -242,11 +258,37 @@ describe('HttpsPlugin', () => {
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: plugin.component,
serverName,
};
assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
assert.strictEqual(
incomingSpan.attributes[AttributeNames.HTTP_CLIENT_IP],
'<client>'
);
assert.strictEqual(
incomingSpan.attributes[AttributeNames.NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[AttributeNames.NET_PEER_PORT],
serverPort
);
[
{ span: incomingSpan, kind: SpanKind.SERVER },
{ span: outgoingSpan, kind: SpanKind.CLIENT },
].forEach(({ span, kind }) => {
assert.strictEqual(
span.attributes[AttributeNames.HTTP_FLAVOR],
'1.1'
);
assert.strictEqual(
span.attributes[AttributeNames.NET_TRANSPORT],
AttributeNames.IP_TCP
);
assertSpan(span, kind, validations);
});
});
it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
@ -571,7 +613,7 @@ describe('HttpsPlugin', () => {
const [span] = spans;
assert.strictEqual(spans.length, 1);
assert.strictEqual(span.status.code, CanonicalCode.ABORTED);
assert.ok(Object.keys(span.attributes).length > 6);
assert.ok(Object.keys(span.attributes).length >= 6);
}
});

View File

@ -15,7 +15,11 @@
*/
import { NoopLogger } from '@opentelemetry/core';
import { HttpPluginConfig, Http } from '@opentelemetry/plugin-http';
import {
HttpPluginConfig,
Http,
AttributeNames,
} from '@opentelemetry/plugin-http';
import { SpanKind, Span } from '@opentelemetry/types';
import * as assert from 'assert';
import * as http from 'http';
@ -143,7 +147,7 @@ describe('HttpsPlugin Integration tests', () => {
assertSpan(span, SpanKind.CLIENT, validations);
});
it('should create a rootSpan for GET requests and add propagation headers if URL and options are used', async () => {
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);
@ -168,6 +172,11 @@ describe('HttpsPlugin Integration tests', () => {
assert.strictEqual(spans.length, 1);
assert.ok(span.name.indexOf('GET /') >= 0);
assert.strictEqual(result.reqHeaders['x-foo'], 'foo');
assert.strictEqual(span.attributes[AttributeNames.HTTP_FLAVOR], '1.1');
assert.strictEqual(
span.attributes[AttributeNames.NET_TRANSPORT],
AttributeNames.IP_TCP
);
assertSpan(span, SpanKind.CLIENT, validations);
});

View File

@ -18,12 +18,12 @@ 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,
@ -37,6 +37,7 @@ export const assertSpan = (
reqHeaders?: http.OutgoingHttpHeaders;
path?: string | null;
forceStatus?: Status;
serverName?: string;
component: string;
}
) => {
@ -55,16 +56,12 @@ export const assertSpan = (
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
span.status.message
);
assert.strictEqual(
span.attributes[AttributeNames.HTTP_HOSTNAME],
validations.hostname
);
assert.strictEqual(
span.attributes[AttributeNames.HTTP_METHOD],
validations.httpMethod
);
assert.strictEqual(
span.attributes[AttributeNames.HTTP_PATH],
span.attributes[AttributeNames.HTTP_TARGET],
validations.path || validations.pathname
);
assert.strictEqual(
@ -80,12 +77,6 @@ export const assertSpan = (
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');
@ -98,8 +89,37 @@ export const assertSpan = (
);
}
}
if (span.kind === SpanKind.CLIENT) {
assert.strictEqual(
span.attributes[AttributeNames.NET_PEER_NAME],
validations.hostname,
'must be consistent (PEER_NAME and hostname)'
);
assert.ok(span.attributes[AttributeNames.NET_PEER_IP], 'must have PEER_IP');
assert.ok(
span.attributes[AttributeNames.NET_PEER_PORT],
'must have PEER_PORT'
);
assert.ok(
(span.attributes[AttributeNames.HTTP_URL] as string).indexOf(
span.attributes[AttributeNames.NET_PEER_NAME] as string
) > -1,
'must be consistent'
);
}
if (span.kind === SpanKind.SERVER) {
if (validations.serverName) {
assert.strictEqual(
span.attributes[AttributeNames.HTTP_SERVER_NAME],
validations.serverName,
' must have serverName attribute'
);
}
assert.ok(
span.attributes[AttributeNames.NET_HOST_PORT],
'must have HOST_PORT'
);
assert.ok(span.attributes[AttributeNames.NET_HOST_IP], 'must have HOST_IP');
assert.strictEqual(span.parentSpanId, DummyPropagation.SPAN_CONTEXT_KEY);
} else if (validations.reqHeaders) {
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);