Skip to content

Commit

Permalink
feat(plugin-http): add/modify attributes
Browse files Browse the repository at this point in the history
closes open-telemetry#373, open-telemetry#394

Signed-off-by: Olivier Albertini <[email protected]>
  • Loading branch information
OlivierAlbertini committed Dec 21, 2019
1 parent d8c8509 commit 01a89b7
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 96 deletions.
2 changes: 1 addition & 1 deletion packages/opentelemetry-plugin-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
18 changes: 15 additions & 3 deletions packages/opentelemetry-plugin-http/src/enums/AttributeNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
78 changes: 24 additions & 54 deletions packages/opentelemetry-plugin-http/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
Span,
SpanKind,
SpanOptions,
Attributes,
CanonicalCode,
Status,
} from '@opentelemetry/types';
Expand All @@ -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
Expand Down Expand Up @@ -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()');
Expand Down Expand Up @@ -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 || '/'
: '/';
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions packages/opentelemetry-plugin-http/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
157 changes: 156 additions & 1 deletion packages/opentelemetry-plugin-http/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
/**
Expand Down Expand Up @@ -280,3 +282,156 @@ 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 attributes: Attributes = {
[AttributeNames.NET_PEER_IP]: socket.remoteAddress,
[AttributeNames.NET_PEER_PORT]: socket.remotePort,
[AttributeNames.HTTP_HOST]: `${options.hostname}:${socket.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 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 userAgent = headers['user-agent'];
const ipsOrFalse = headers && headers['x-forwarded-for'];
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 (ipsOrFalse && typeof ipsOrFalse === 'string') {
attributes[AttributeNames.HTTP_CLIENT_IP] = ipsOrFalse.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;
return {
[AttributeNames.NET_HOST_IP]: socket.localAddress,
[AttributeNames.NET_HOST_PORT]: socket.remotePort,
[AttributeNames.NET_PEER_IP]: socket.remoteAddress,
[AttributeNames.NET_PEER_PORT]: socket.remotePort,
[AttributeNames.HTTP_STATUS_CODE]: statusCode,
[AttributeNames.HTTP_STATUS_TEXT]: (statusMessage || '').toUpperCase(),
};
};
Loading

0 comments on commit 01a89b7

Please sign in to comment.