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 | | [`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 | | [`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 | | [`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 ## Useful links
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/> - For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
- For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js> - 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 { export enum AttributeNames {
HTTP_HOSTNAME = 'http.hostname', HTTP_HOST = 'http.host',
COMPONENT = 'component', COMPONENT = 'component',
HTTP_METHOD = 'http.method', HTTP_METHOD = 'http.method',
HTTP_PATH = 'http.path', HTTP_TARGET = 'http.target',
HTTP_ROUTE = 'http.route', HTTP_ROUTE = 'http.route',
HTTP_URL = 'http.url', HTTP_URL = 'http.url',
HTTP_STATUS_CODE = 'http.status_code', HTTP_STATUS_CODE = 'http.status_code',
HTTP_STATUS_TEXT = 'http.status_text', 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 // NOT ON OFFICIAL SPEC
HTTP_ERROR_NAME = 'http.error_name', HTTP_ERROR_NAME = 'http.error_name',
HTTP_ERROR_MESSAGE = 'http.error_message', HTTP_ERROR_MESSAGE = 'http.error_message',

View File

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

View File

@ -57,10 +57,18 @@ export interface HttpCustomAttributeFunction {
): void; ): 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 { export interface HttpPluginConfig extends PluginConfig {
/** Not trace all incoming requests that match paths */
ignoreIncomingPaths?: IgnoreMatcher[]; ignoreIncomingPaths?: IgnoreMatcher[];
/** Not trace all outgoing requests that match urls */
ignoreOutgoingUrls?: IgnoreMatcher[]; ignoreOutgoingUrls?: IgnoreMatcher[];
/** Function for adding custom attributes */
applyCustomAttributesOnSpan?: HttpCustomAttributeFunction; applyCustomAttributesOnSpan?: HttpCustomAttributeFunction;
/** The primary server name of the matched virtual host. */
serverName?: string;
} }
export interface Err extends Error { export interface Err extends Error {

View File

@ -14,17 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
import { Status, CanonicalCode, Span } from '@opentelemetry/types'; import { Status, CanonicalCode, Span, Attributes } from '@opentelemetry/types';
import { import {
RequestOptions, RequestOptions,
IncomingMessage, IncomingMessage,
ClientRequest, ClientRequest,
IncomingHttpHeaders, IncomingHttpHeaders,
OutgoingHttpHeaders, OutgoingHttpHeaders,
ServerResponse,
} from 'http'; } from 'http';
import { IgnoreMatcher, Err, ParsedRequestOptions } from './types'; import { IgnoreMatcher, Err, ParsedRequestOptions } from './types';
import { AttributeNames } from './enums/AttributeNames'; import { AttributeNames } from './enums/AttributeNames';
import * as url from 'url'; import * as url from 'url';
import { Socket } from 'net';
export const OT_REQUEST_HEADER = 'x-opentelemetry-outgoing-request'; export const OT_REQUEST_HEADER = 'x-opentelemetry-outgoing-request';
/** /**
@ -280,3 +282,157 @@ export const isValidOptionsType = (options: unknown): boolean => {
export const isOpenTelemetryRequest = (options: RequestOptions) => { export const isOpenTelemetryRequest = (options: RequestOptions) => {
return !!(options && options.headers && options.headers[OT_REQUEST_HEADER]); 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 protocol = 'http';
const hostname = 'localhost'; const hostname = 'localhost';
const pathname = '/test'; const pathname = '/test';
const serverName = 'my.server.name';
const memoryExporter = new InMemorySpanExporter(); const memoryExporter = new InMemorySpanExporter();
const httpTextFormat = new DummyPropagation(); const httpTextFormat = new DummyPropagation();
const logger = new NoopLogger(); const logger = new NoopLogger();
@ -140,6 +141,14 @@ describe('HttpPlugin', () => {
assert.strictEqual(spans.length, 2); assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations); assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, 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 () => { it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
@ -176,6 +185,7 @@ describe('HttpPlugin', () => {
(url: string) => url.endsWith(`/ignored/function`), (url: string) => url.endsWith(`/ignored/function`),
], ],
applyCustomAttributesOnSpan: customAttributeFunction, applyCustomAttributesOnSpan: customAttributeFunction,
serverName,
}; };
plugin.enable(http, tracer, tracer.logger, config); plugin.enable(http, tracer, tracer.logger, config);
server = http.createServer((request, response) => { server = http.createServer((request, response) => {
@ -204,7 +214,13 @@ describe('HttpPlugin', () => {
it('should generate valid spans (client side and server side)', async () => { it('should generate valid spans (client side and server side)', async () => {
const result = await httpRequest.get( 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 spans = memoryExporter.getFinishedSpans();
const [incomingSpan, outgoingSpan] = spans; const [incomingSpan, outgoingSpan] = spans;
@ -216,11 +232,36 @@ describe('HttpPlugin', () => {
resHeaders: result.resHeaders, resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders, reqHeaders: result.reqHeaders,
component: plugin.component, component: plugin.component,
serverName,
}; };
assert.strictEqual(spans.length, 2); assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations); assert.strictEqual(
assertSpan(outgoingSpan, SpanKind.CLIENT, validations); 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 () => { it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
@ -545,7 +586,7 @@ describe('HttpPlugin', () => {
const [span] = spans; const [span] = spans;
assert.strictEqual(spans.length, 1); assert.strictEqual(spans.length, 1);
assert.strictEqual(span.status.code, CanonicalCode.ABORTED); 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, SimpleSpanProcessor,
} from '@opentelemetry/tracing'; } from '@opentelemetry/tracing';
import { HttpPluginConfig } from '../../src/types'; import { HttpPluginConfig } from '../../src/types';
import { AttributeNames } from '../../src/enums/AttributeNames';
const protocol = 'http'; const protocol = 'http';
const serverPort = 32345; const serverPort = 32345;
const hostname = 'localhost'; const hostname = 'localhost';
@ -141,7 +142,7 @@ describe('HttpPlugin Integration tests', () => {
assertSpan(span, SpanKind.CLIENT, validations); 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(); let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0); assert.strictEqual(spans.length, 0);
@ -166,6 +167,11 @@ describe('HttpPlugin Integration tests', () => {
assert.strictEqual(spans.length, 1); assert.strictEqual(spans.length, 1);
assert.ok(span.name.indexOf('GET /') >= 0); assert.ok(span.name.indexOf('GET /') >= 0);
assert.strictEqual(result.reqHeaders['x-foo'], 'foo'); 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); assertSpan(span, SpanKind.CLIENT, validations);
}); });

View File

@ -35,6 +35,7 @@ export const assertSpan = (
reqHeaders?: http.OutgoingHttpHeaders; reqHeaders?: http.OutgoingHttpHeaders;
path?: string | null; path?: string | null;
forceStatus?: Status; forceStatus?: Status;
serverName?: string;
component: string; component: string;
} }
) => { ) => {
@ -53,16 +54,12 @@ export const assertSpan = (
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE], span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
span.status.message span.status.message
); );
assert.strictEqual(
span.attributes[AttributeNames.HTTP_HOSTNAME],
validations.hostname
);
assert.strictEqual( assert.strictEqual(
span.attributes[AttributeNames.HTTP_METHOD], span.attributes[AttributeNames.HTTP_METHOD],
validations.httpMethod validations.httpMethod
); );
assert.strictEqual( assert.strictEqual(
span.attributes[AttributeNames.HTTP_PATH], span.attributes[AttributeNames.HTTP_TARGET],
validations.path || validations.pathname validations.path || validations.pathname
); );
assert.strictEqual( assert.strictEqual(
@ -79,12 +76,6 @@ export const assertSpan = (
utils.parseResponseStatus(validations.httpStatusCode) 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(span.endTime, 'must be finished');
assert.ok(hrTimeToNanoseconds(span.duration), 'must have positive duration'); 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 (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); assert.strictEqual(span.parentSpanId, DummyPropagation.SPAN_CONTEXT_KEY);
} else if (validations.reqHeaders) { } else if (validations.reqHeaders) {
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);

View File

@ -45,6 +45,7 @@ let server: https.Server;
const serverPort = 32345; const serverPort = 32345;
const protocol = 'https'; const protocol = 'https';
const hostname = 'localhost'; const hostname = 'localhost';
const serverName = 'my.server.name';
const pathname = '/test'; const pathname = '/test';
const memoryExporter = new InMemorySpanExporter(); const memoryExporter = new InMemorySpanExporter();
const httpTextFormat = new DummyPropagation(); const httpTextFormat = new DummyPropagation();
@ -153,6 +154,14 @@ describe('HttpsPlugin', () => {
assert.strictEqual(spans.length, 2); assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations); assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, 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 () => { it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
@ -189,6 +198,7 @@ describe('HttpsPlugin', () => {
(url: string) => url.endsWith(`/ignored/function`), (url: string) => url.endsWith(`/ignored/function`),
], ],
applyCustomAttributesOnSpan: customAttributeFunction, applyCustomAttributesOnSpan: customAttributeFunction,
serverName,
}; };
plugin.enable( plugin.enable(
(https as unknown) as Http, (https as unknown) as Http,
@ -230,7 +240,13 @@ describe('HttpsPlugin', () => {
it('should generate valid spans (client side and server side)', async () => { it('should generate valid spans (client side and server side)', async () => {
const result = await httpsRequest.get( 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 spans = memoryExporter.getFinishedSpans();
const [incomingSpan, outgoingSpan] = spans; const [incomingSpan, outgoingSpan] = spans;
@ -242,11 +258,37 @@ describe('HttpsPlugin', () => {
resHeaders: result.resHeaders, resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders, reqHeaders: result.reqHeaders,
component: plugin.component, component: plugin.component,
serverName,
}; };
assert.strictEqual(spans.length, 2); assert.strictEqual(spans.length, 2);
assertSpan(incomingSpan, SpanKind.SERVER, validations); assert.strictEqual(
assertSpan(outgoingSpan, SpanKind.CLIENT, validations); 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 () => { it(`should not trace requests with '${OT_REQUEST_HEADER}' header`, async () => {
@ -571,7 +613,7 @@ describe('HttpsPlugin', () => {
const [span] = spans; const [span] = spans;
assert.strictEqual(spans.length, 1); assert.strictEqual(spans.length, 1);
assert.strictEqual(span.status.code, CanonicalCode.ABORTED); 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 { 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 { SpanKind, Span } from '@opentelemetry/types';
import * as assert from 'assert'; import * as assert from 'assert';
import * as http from 'http'; import * as http from 'http';
@ -143,7 +147,7 @@ describe('HttpsPlugin Integration tests', () => {
assertSpan(span, SpanKind.CLIENT, validations); 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(); let spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 0); assert.strictEqual(spans.length, 0);
@ -168,6 +172,11 @@ describe('HttpsPlugin Integration tests', () => {
assert.strictEqual(spans.length, 1); assert.strictEqual(spans.length, 1);
assert.ok(span.name.indexOf('GET /') >= 0); assert.ok(span.name.indexOf('GET /') >= 0);
assert.strictEqual(result.reqHeaders['x-foo'], 'foo'); 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); assertSpan(span, SpanKind.CLIENT, validations);
}); });

View File

@ -18,12 +18,12 @@ import { SpanKind, Status } from '@opentelemetry/types';
import { hrTimeToNanoseconds } from '@opentelemetry/core'; import { hrTimeToNanoseconds } from '@opentelemetry/core';
import * as assert from 'assert'; import * as assert from 'assert';
import * as http from 'http'; import * as http from 'http';
import { DummyPropagation } from './DummyPropagation';
import { ReadableSpan } from '@opentelemetry/tracing';
import { import {
AttributeNames, AttributeNames,
parseResponseStatus, parseResponseStatus,
} from '@opentelemetry/plugin-http'; } from '@opentelemetry/plugin-http';
import { DummyPropagation } from './DummyPropagation';
import { ReadableSpan } from '@opentelemetry/tracing';
export const assertSpan = ( export const assertSpan = (
span: ReadableSpan, span: ReadableSpan,
@ -37,6 +37,7 @@ export const assertSpan = (
reqHeaders?: http.OutgoingHttpHeaders; reqHeaders?: http.OutgoingHttpHeaders;
path?: string | null; path?: string | null;
forceStatus?: Status; forceStatus?: Status;
serverName?: string;
component: string; component: string;
} }
) => { ) => {
@ -55,16 +56,12 @@ export const assertSpan = (
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE], span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
span.status.message span.status.message
); );
assert.strictEqual(
span.attributes[AttributeNames.HTTP_HOSTNAME],
validations.hostname
);
assert.strictEqual( assert.strictEqual(
span.attributes[AttributeNames.HTTP_METHOD], span.attributes[AttributeNames.HTTP_METHOD],
validations.httpMethod validations.httpMethod
); );
assert.strictEqual( assert.strictEqual(
span.attributes[AttributeNames.HTTP_PATH], span.attributes[AttributeNames.HTTP_TARGET],
validations.path || validations.pathname validations.path || validations.pathname
); );
assert.strictEqual( assert.strictEqual(
@ -80,12 +77,6 @@ export const assertSpan = (
validations.forceStatus || parseResponseStatus(validations.httpStatusCode) 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(span.endTime, 'must be finished');
assert.ok(hrTimeToNanoseconds(span.duration), 'must have positive duration'); 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 (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); assert.strictEqual(span.parentSpanId, DummyPropagation.SPAN_CONTEXT_KEY);
} else if (validations.reqHeaders) { } else if (validations.reqHeaders) {
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);