Skip to content

Commit 3d9b822

Browse files
feat(plugin-http): add/modify attributes (#643)
* feat(plugin-http): add/modify attributes closes #373, #394 Signed-off-by: Olivier Albertini <[email protected]> * fix: change remotePort to localPort refactor: remove useless checks test: add assertions Signed-off-by: Olivier Albertini <[email protected]> * test(plugin-https): sync with http plugin Signed-off-by: Olivier Albertini <[email protected]> Co-authored-by: Mayur Kale <[email protected]>
1 parent e859b8e commit 3d9b822

File tree

11 files changed

+383
-96
lines changed

11 files changed

+383
-96
lines changed

packages/opentelemetry-plugin-http/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Http plugin has few options available to choose from. You can set the following:
5454
| [`applyCustomAttributesOnSpan`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L52) | `HttpCustomAttributeFunction` | Function for adding custom attributes |
5555
| [`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 |
5656
| [`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 |
57-
57+
| [`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. |
5858
## Useful links
5959
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
6060
- For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js>

packages/opentelemetry-plugin-http/src/enums/AttributeNames.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,29 @@
1515
*/
1616

1717
/**
18-
* Attributes Names according to [OpenTelemetry attributes specs](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#semantic-conventions)
18+
* Attributes Names according to [OpenTelemetry attributes specs](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#common-attributes)
1919
*/
2020
export enum AttributeNames {
21-
HTTP_HOSTNAME = 'http.hostname',
21+
HTTP_HOST = 'http.host',
2222
COMPONENT = 'component',
2323
HTTP_METHOD = 'http.method',
24-
HTTP_PATH = 'http.path',
24+
HTTP_TARGET = 'http.target',
2525
HTTP_ROUTE = 'http.route',
2626
HTTP_URL = 'http.url',
2727
HTTP_STATUS_CODE = 'http.status_code',
2828
HTTP_STATUS_TEXT = 'http.status_text',
29+
HTTP_FLAVOR = 'http.flavor',
30+
NET_PEER_IP = 'net.peer.ip',
31+
NET_PEER_PORT = 'net.peer.port',
32+
NET_PEER_NAME = 'net.peer.name',
33+
NET_HOST_IP = 'net.host.ip',
34+
NET_HOST_PORT = 'net.host.port',
35+
NET_HOST_NAME = 'net.host.name',
36+
NET_TRANSPORT = 'net.transport',
37+
IP_TCP = 'IP.TCP',
38+
IP_UDP = 'IP.UDP',
39+
HTTP_SERVER_NAME = 'http.server_name',
40+
HTTP_CLIENT_IP = 'http.client_ip',
2941
// NOT ON OFFICIAL SPEC
3042
HTTP_ERROR_NAME = 'http.error_name',
3143
HTTP_ERROR_MESSAGE = 'http.error_message',

packages/opentelemetry-plugin-http/src/http.ts

Lines changed: 24 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
Span,
2020
SpanKind,
2121
SpanOptions,
22-
Attributes,
2322
CanonicalCode,
2423
Status,
2524
} from '@opentelemetry/types';
@@ -45,6 +44,7 @@ import {
4544
import { Format } from './enums/Format';
4645
import { AttributeNames } from './enums/AttributeNames';
4746
import * as utils from './utils';
47+
import { Socket } from 'net';
4848

4949
/**
5050
* Http instrumentation plugin for Opentelemetry
@@ -183,37 +183,24 @@ export class HttpPlugin extends BasePlugin<Http> {
183183
return (): ClientRequest => {
184184
this._logger.debug('makeRequestTrace by injecting context into header');
185185

186-
const host = options.hostname || options.host || 'localhost';
187-
const method = options.method ? options.method.toUpperCase() : 'GET';
188-
const headers = options.headers || {};
189-
const userAgent = headers['user-agent'];
190-
191-
span.setAttributes({
192-
[AttributeNames.HTTP_URL]: utils.getAbsoluteUrl(
193-
options,
194-
headers,
195-
`${this.component}:`
196-
),
197-
[AttributeNames.HTTP_HOSTNAME]: host,
198-
[AttributeNames.HTTP_METHOD]: method,
199-
[AttributeNames.HTTP_PATH]: options.path || '/',
186+
const hostname =
187+
options.hostname ||
188+
options.host?.replace(/^(.*)(\:[0-9]{1,5})/, '$1') ||
189+
'localhost';
190+
const attributes = utils.getOutgoingRequestAttributes(options, {
191+
component: this.component,
192+
hostname,
200193
});
201-
202-
if (userAgent !== undefined) {
203-
span.setAttribute(AttributeNames.HTTP_USER_AGENT, userAgent);
204-
}
194+
span.setAttributes(attributes);
205195

206196
request.on(
207197
'response',
208-
(
209-
response: IncomingMessage & { aborted?: boolean; req: ClientRequest }
210-
) => {
211-
if (response.statusCode) {
212-
span.setAttributes({
213-
[AttributeNames.HTTP_STATUS_CODE]: response.statusCode,
214-
[AttributeNames.HTTP_STATUS_TEXT]: response.statusMessage,
215-
});
216-
}
198+
(response: IncomingMessage & { aborted?: boolean }) => {
199+
const attributes = utils.getOutgoingRequestAttributesOnResponse(
200+
response,
201+
{ hostname }
202+
);
203+
span.setAttributes(attributes);
217204

218205
this._tracer.bind(response);
219206
this._logger.debug('outgoingRequest on response()');
@@ -280,7 +267,7 @@ export class HttpPlugin extends BasePlugin<Http> {
280267
}
281268

282269
const request = args[0] as IncomingMessage;
283-
const response = args[1] as ServerResponse;
270+
const response = args[1] as ServerResponse & { socket: Socket };
284271
const pathname = request.url
285272
? url.parse(request.url).pathname || '/'
286273
: '/';
@@ -301,8 +288,13 @@ export class HttpPlugin extends BasePlugin<Http> {
301288

302289
const propagation = plugin._tracer.getHttpTextFormat();
303290
const headers = request.headers;
291+
304292
const spanOptions: SpanOptions = {
305293
kind: SpanKind.SERVER,
294+
attributes: utils.getIncomingRequestAttributes(request, {
295+
component: plugin.component,
296+
serverName: plugin._config.serverName,
297+
}),
306298
};
307299

308300
const spanContext = propagation.extract(Format.HTTP, headers);
@@ -332,32 +324,10 @@ export class HttpPlugin extends BasePlugin<Http> {
332324
() => response.end.apply(this, arguments as any),
333325
true
334326
);
335-
const requestUrl = request.url ? url.parse(request.url) : null;
336-
const hostname = headers.host
337-
? headers.host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')
338-
: 'localhost';
339-
const userAgent = headers['user-agent'];
340-
341-
const attributes: Attributes = {
342-
[AttributeNames.HTTP_URL]: utils.getAbsoluteUrl(
343-
requestUrl,
344-
headers,
345-
`${plugin.component}:`
346-
),
347-
[AttributeNames.HTTP_HOSTNAME]: hostname,
348-
[AttributeNames.HTTP_METHOD]: method,
349-
[AttributeNames.HTTP_STATUS_CODE]: response.statusCode,
350-
[AttributeNames.HTTP_STATUS_TEXT]: response.statusMessage,
351-
};
352-
353-
if (requestUrl) {
354-
attributes[AttributeNames.HTTP_PATH] = requestUrl.path || '/';
355-
attributes[AttributeNames.HTTP_ROUTE] = requestUrl.pathname || '/';
356-
}
357327

358-
if (userAgent !== undefined) {
359-
attributes[AttributeNames.HTTP_USER_AGENT] = userAgent;
360-
}
328+
const attributes = utils.getIncomingRequestAttributesOnResponse(
329+
response
330+
);
361331

362332
span
363333
.setAttributes(attributes)

packages/opentelemetry-plugin-http/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,18 @@ export interface HttpCustomAttributeFunction {
5757
): void;
5858
}
5959

60+
/**
61+
* Options available for the HTTP Plugin (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-http#http-plugin-options))
62+
*/
6063
export interface HttpPluginConfig extends PluginConfig {
64+
/** Not trace all incoming requests that match paths */
6165
ignoreIncomingPaths?: IgnoreMatcher[];
66+
/** Not trace all outgoing requests that match urls */
6267
ignoreOutgoingUrls?: IgnoreMatcher[];
68+
/** Function for adding custom attributes */
6369
applyCustomAttributesOnSpan?: HttpCustomAttributeFunction;
70+
/** The primary server name of the matched virtual host. */
71+
serverName?: string;
6472
}
6573

6674
export interface Err extends Error {

packages/opentelemetry-plugin-http/src/utils.ts

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Status, CanonicalCode, Span } from '@opentelemetry/types';
17+
import { Status, CanonicalCode, Span, Attributes } from '@opentelemetry/types';
1818
import {
1919
RequestOptions,
2020
IncomingMessage,
2121
ClientRequest,
2222
IncomingHttpHeaders,
2323
OutgoingHttpHeaders,
24+
ServerResponse,
2425
} from 'http';
2526
import { IgnoreMatcher, Err, ParsedRequestOptions } from './types';
2627
import { AttributeNames } from './enums/AttributeNames';
2728
import * as url from 'url';
29+
import { Socket } from 'net';
2830

2931
export const OT_REQUEST_HEADER = 'x-opentelemetry-outgoing-request';
3032
/**
@@ -280,3 +282,157 @@ export const isValidOptionsType = (options: unknown): boolean => {
280282
export const isOpenTelemetryRequest = (options: RequestOptions) => {
281283
return !!(options && options.headers && options.headers[OT_REQUEST_HEADER]);
282284
};
285+
286+
/**
287+
* Returns outgoing request attributes scoped to the options passed to the request
288+
* @param {ParsedRequestOptions} requestOptions the same options used to make the request
289+
* @param {{ component: string, hostname: string }} options used to pass data needed to create attributes
290+
*/
291+
export const getOutgoingRequestAttributes = (
292+
requestOptions: ParsedRequestOptions,
293+
options: { component: string; hostname: string }
294+
): Attributes => {
295+
const host = requestOptions.host;
296+
const hostname =
297+
requestOptions.hostname ||
298+
host?.replace(/^(.*)(\:[0-9]{1,5})/, '$1') ||
299+
'localhost';
300+
const requestMethod = requestOptions.method;
301+
const method = requestMethod ? requestMethod.toUpperCase() : 'GET';
302+
const headers = requestOptions.headers || {};
303+
const userAgent = headers['user-agent'];
304+
305+
const attributes: Attributes = {
306+
[AttributeNames.HTTP_URL]: getAbsoluteUrl(
307+
requestOptions,
308+
headers,
309+
`${options.component}:`
310+
),
311+
[AttributeNames.HTTP_METHOD]: method,
312+
[AttributeNames.HTTP_TARGET]: requestOptions.path || '/',
313+
[AttributeNames.NET_PEER_NAME]: hostname,
314+
};
315+
316+
if (userAgent !== undefined) {
317+
attributes[AttributeNames.HTTP_USER_AGENT] = userAgent;
318+
}
319+
return attributes;
320+
};
321+
322+
/**
323+
* Returns attributes related to the kind of HTTP protocol used
324+
* @param {string} [kind] Kind of HTTP protocol used: "1.0", "1.1", "2", "SPDY" or "QUIC".
325+
*/
326+
export const getAttributesFromHttpKind = (kind?: string): Attributes => {
327+
const attributes: Attributes = {};
328+
if (kind) {
329+
attributes[AttributeNames.HTTP_FLAVOR] = kind;
330+
if (kind.toUpperCase() !== 'QUIC') {
331+
attributes[AttributeNames.NET_TRANSPORT] = AttributeNames.IP_TCP;
332+
} else {
333+
attributes[AttributeNames.NET_TRANSPORT] = AttributeNames.IP_UDP;
334+
}
335+
}
336+
return attributes;
337+
};
338+
339+
/**
340+
* Returns outgoing request attributes scoped to the response data
341+
* @param {IncomingMessage} response the response object
342+
* @param {{ hostname: string }} options used to pass data needed to create attributes
343+
*/
344+
export const getOutgoingRequestAttributesOnResponse = (
345+
response: IncomingMessage,
346+
options: { hostname: string }
347+
): Attributes => {
348+
const { statusCode, statusMessage, httpVersion, socket } = response;
349+
const { remoteAddress, remotePort } = socket;
350+
const attributes: Attributes = {
351+
[AttributeNames.NET_PEER_IP]: remoteAddress,
352+
[AttributeNames.NET_PEER_PORT]: remotePort,
353+
[AttributeNames.HTTP_HOST]: `${options.hostname}:${remotePort}`,
354+
};
355+
356+
if (statusCode) {
357+
attributes[AttributeNames.HTTP_STATUS_CODE] = statusCode;
358+
attributes[AttributeNames.HTTP_STATUS_TEXT] = (
359+
statusMessage || ''
360+
).toUpperCase();
361+
}
362+
363+
const httpKindAttributes = getAttributesFromHttpKind(httpVersion);
364+
return Object.assign(attributes, httpKindAttributes);
365+
};
366+
367+
/**
368+
* Returns incoming request attributes scoped to the request data
369+
* @param {IncomingMessage} request the request object
370+
* @param {{ component: string, serverName?: string }} options used to pass data needed to create attributes
371+
*/
372+
export const getIncomingRequestAttributes = (
373+
request: IncomingMessage,
374+
options: { component: string; serverName?: string }
375+
): Attributes => {
376+
const headers = request.headers;
377+
const userAgent = headers['user-agent'];
378+
const ips = headers['x-forwarded-for'];
379+
const method = request.method || 'GET';
380+
const httpVersion = request.httpVersion;
381+
const requestUrl = request.url ? url.parse(request.url) : null;
382+
const host = requestUrl?.host || headers.host;
383+
const hostname =
384+
requestUrl?.hostname ||
385+
host?.replace(/^(.*)(\:[0-9]{1,5})/, '$1') ||
386+
'localhost';
387+
const serverName = options.serverName;
388+
const attributes: Attributes = {
389+
[AttributeNames.HTTP_URL]: getAbsoluteUrl(
390+
requestUrl,
391+
headers,
392+
`${options.component}:`
393+
),
394+
[AttributeNames.HTTP_HOST]: host,
395+
[AttributeNames.NET_HOST_NAME]: hostname,
396+
[AttributeNames.HTTP_METHOD]: method,
397+
};
398+
399+
if (typeof ips === 'string') {
400+
attributes[AttributeNames.HTTP_CLIENT_IP] = ips.split(',')[0];
401+
}
402+
403+
if (typeof serverName === 'string') {
404+
attributes[AttributeNames.HTTP_SERVER_NAME] = serverName;
405+
}
406+
407+
if (requestUrl) {
408+
attributes[AttributeNames.HTTP_TARGET] = requestUrl.path || '/';
409+
attributes[AttributeNames.HTTP_ROUTE] = requestUrl.pathname || '/';
410+
}
411+
412+
if (userAgent !== undefined) {
413+
attributes[AttributeNames.HTTP_USER_AGENT] = userAgent;
414+
}
415+
416+
const httpKindAttributes = getAttributesFromHttpKind(httpVersion);
417+
return Object.assign(attributes, httpKindAttributes);
418+
};
419+
420+
/**
421+
* Returns incoming request attributes scoped to the response data
422+
* @param {(ServerResponse & { socket: Socket; })} response the response object
423+
*/
424+
export const getIncomingRequestAttributesOnResponse = (
425+
response: ServerResponse & { socket: Socket }
426+
): Attributes => {
427+
const { statusCode, statusMessage, socket } = response;
428+
const { localAddress, localPort, remoteAddress, remotePort } = socket;
429+
430+
return {
431+
[AttributeNames.NET_HOST_IP]: localAddress,
432+
[AttributeNames.NET_HOST_PORT]: localPort,
433+
[AttributeNames.NET_PEER_IP]: remoteAddress,
434+
[AttributeNames.NET_PEER_PORT]: remotePort,
435+
[AttributeNames.HTTP_STATUS_CODE]: statusCode,
436+
[AttributeNames.HTTP_STATUS_TEXT]: (statusMessage || '').toUpperCase(),
437+
};
438+
};

0 commit comments

Comments
 (0)