Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
OlivierAlbertini marked this conversation as resolved.
Show resolved Hide resolved
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',
OlivierAlbertini marked this conversation as resolved.
Show resolved Hide resolved
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
158 changes: 157 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,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(),
};
};
Loading