Skip to content

Instantly share code, notes, and snippets.

@pauldraper
Last active March 15, 2019 12:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pauldraper/5f7055b2d24d042c58625bfebfd2331f to your computer and use it in GitHub Desktop.
Save pauldraper/5f7055b2d24d042c58625bfebfd2331f to your computer and use it in GitHub Desktop.
OpenTracing for Node.js http
import { EventEmitter } from 'events';
import { globalTracer, Span } from 'opentracing';
/**
* All events from the emitter share the same span.
*/
export function activateEventEmitter(span: Span, emitter: EventEmitter) {
const { emit } = emitter;
emitter.emit = function(this: any) {
const args = arguments;
return globalTracer()
.spanManager()
.activate(span, () => emit.apply(this, args));
};
}
import http = require('http');
import {
childOf,
globalTracer,
FORMAT_HTTP_HEADERS,
Reference,
Span,
Tags,
} from 'opentracing';
import { activateEventEmitter } from './events';
import { Socket } from 'net';
/**
* Globally instruments the HTTP module.
*
* Must be invoked prior to other modules loading it.
*/
export function instrument() {
const { createServer } = http;
http.createServer = function(this: any) {
const server = createServer.apply(this, arguments);
instrumentServer(server);
return server;
};
http.get = instrumentClientFunction(http.get);
http.request = instrumentClientFunction(http.request);
}
export function instrumentClientFunction(
f: typeof http.request,
name: string = 'http-request',
) {
return function(this: any) {
const tracer = globalTracer();
const span = tracer.startSpan(name, {
tags: {
[Tags.COMPONENT]: 'http.Client',
[Tags.SPAN_KIND]: Tags.SPAN_KIND_RPC_CLIENT,
},
});
return tracer.spanManager().activate(span, () => {
let options: http.RequestOptions;
let url: URL | undefined;
if (typeof arguments[1] === 'string') {
options = arguments[2] || {};
url = new URL(arguments[1]);
} else if (arguments[1] instanceof URL) {
options = arguments[2] || {};
url = arguments[1];
} else {
options = arguments[1] || {};
}
if (!options.headers) {
options.headers = {};
}
tracer.inject(span, FORMAT_HTTP_HEADERS, options.headers);
const request = f.apply(this, arguments);
activateEventEmitter(span, request);
tagClientRequest(span, url, options, request);
return request;
});
};
}
/**
* Instruments the HTTP Server
*/
export function instrumentServer(
server: http.Server,
name: string = 'http-response',
) {
const emit = server.emit;
server.emit = function(name: string) {
switch (name) {
case 'request':
const request: http.ServerRequest = arguments[1];
const response: http.ServerResponse = arguments[2];
const args = arguments;
const tracer = globalTracer();
const references: Reference[] = [];
const parent = tracer.extract(FORMAT_HTTP_HEADERS, request.headers);
if (parent) {
references.push(childOf(parent));
}
const span = tracer.startSpan(name, {
references,
tags: {
[Tags.COMPONENT]: 'http.Server',
[Tags.SPAN_KIND]: Tags.SPAN_KIND_RPC_SERVER,
},
});
activateEventEmitter(span, request);
activateEventEmitter(span, response);
tagServerRequest(span, request);
tagServerResponse(span, response);
return tracer.spanManager().activate(span, function(this: any) {
return emit.apply(this, args);
});
}
return emit.apply(this, arguments);
};
}
export function tagClientRequest(
span: Span,
url: URL | undefined,
options: http.RequestOptions,
request: http.ClientRequest,
) {
span.setTag(Tags.PEER_ADDRESS, url ? url.host : options.host);
span.setTag(Tags.PEER_HOSTNAME, url ? url.hostname : options.hostname);
span.setTag(Tags.HTTP_METHOD, options.method || 'GET');
const {
'content-length': contentLength,
'content-type': contentType,
} = request.getHeaders();
if (contentLength) {
span.setTag('http.request.contentLength', +contentLength);
}
if (contentType) {
span.setTag('http.request.contentType', contentType);
}
let finished = false;
function finish() {
if (!finished) {
span.finish();
}
}
request.once('abort', function() {
span.log({
event: 'abort',
});
span.setTag(Tags.ERROR, true);
finish();
});
request.once('socket', (socket: Socket) => {
if (socket.remoteAddress) {
switch (socket.remoteFamily) {
case 'IPV4':
span.setTag(Tags.PEER_HOST_IPV4, socket.remoteAddress);
break;
case 'IPV6':
span.setTag(Tags.PEER_HOST_IPV6, socket.remoteAddress);
break;
}
} else {
socket.once('lookup', function(this: Socket) {
span.log({
event: 'dns',
});
});
socket.once('connect', function(this: Socket) {
span.log({
event: 'connect',
});
switch (this.remoteFamily) {
case 'IPV4':
span.setTag(Tags.PEER_HOST_IPV4, this.remoteAddress);
break;
case 'IPV6':
span.setTag(Tags.PEER_HOST_IPV6, this.remoteAddress);
break;
}
});
socket.once('secureConnect', function(this: Socket) {
span.log({ event: 'secure' });
});
}
});
request.once('readable', function() {
span.log({ event: 'response.firstByte' });
});
request.once('response', function(response: http.IncomingMessage) {
span.log({ event: 'response' });
span.setTag(Tags.HTTP_STATUS_CODE, response.statusCode);
span.setTag('http.status_phrase', response.statusMessage);
const {'content-type': contentType} = response.headers;
if (typeof contentType === 'string') {
span.setTag('http.response.content_type', contentType);
}
response.once('end', finish);
});
request.once('timeout', function() {
span.log({ event: 'timeout' });
span.setTag(Tags.ERROR, true);
finish();
});
request.on('error', error => {
span.log({
event: 'error',
message: error.message,
stack: error.stack,
});
span.setTag(Tags.ERROR, true);
finish();
});
}
/**
* Add tags to the request
*/
export function tagServerRequest(span: Span, request: http.ServerRequest) {
span.setTag(Tags.HTTP_METHOD, request.method);
span.setTag(Tags.HTTP_URL, request.url);
const {
accept,
'content-length': contentLength,
'content-type': contentType,
host,
'user-agent': userAgent,
'x-forwarded-for': xForwardedFor,
'x-forwarded-port': xForwardedPort,
'x-forwarded-proto': xForwardedProto,
} = request.headers;
if (typeof accept === 'string') {
span.setTag('http.request.accept', accept);
}
if (typeof contentLength === 'string') {
span.setTag('http.request.contentLength', +contentLength);
}
if (typeof contentType === 'string') {
span.setTag('http.request.contentType', contentType);
}
if (typeof host === 'string') {
span.setTag('http.request.host', host);
}
if (typeof userAgent === 'string') {
span.setTag('http.request.userAgent', userAgent);
}
if (xForwardedFor) {
const original =
typeof xForwardedFor === 'string' ? xForwardedFor : xForwardedFor[0];
const tag = /\d+\.\d+\.\d+\.\d+/.test(original)
? Tags.PEER_HOST_IPV4
: Tags.PEER_HOST_IPV6;
span.setTag(tag, request.connection.remoteAddress);
} else {
switch (request.connection.remoteFamily) {
case 'IPV4':
span.setTag(Tags.PEER_HOST_IPV4, request.connection.remoteAddress);
break;
case 'IPV6':
span.setTag(Tags.PEER_HOST_IPV6, request.connection.remoteAddress);
break;
}
}
if (xForwardedPort) {
const original =
typeof xForwardedPort === 'string' ? xForwardedPort : xForwardedPort[0];
span.setTag(Tags.PEER_PORT, +original);
} else {
span.setTag(Tags.PEER_PORT, request.connection.remotePort);
}
if (xForwardedProto) {
const original =
typeof xForwardedProto === 'string'
? xForwardedProto
: xForwardedProto[0];
span.setTag('net.protocol', original);
} else {
span.setTag('net.protocol', 'http');
}
request.once('abort', function() {
span.log({ event: 'abort' });
});
request.once('timeout', function() {
span.log({ event: 'timeout' });
});
}
export function tagServerResponse(span: Span, response: http.ServerResponse) {
const { writeHead } = response;
response.writeHead = function(this: any) {
span.setTag(Tags.HTTP_STATUS_CODE, response.statusCode);
span.setTag('http.status_phrase', response.statusMessage);
const {
'content-length': contentLength,
'content-type': contentType,
} = response.getHeaders();
if (
typeof contentLength === 'number' ||
typeof contentLength === 'string'
) {
span.setTag('http.response.contentLength', +contentLength);
}
if (typeof contentType === 'string') {
span.setTag('http.response.contentType', contentType);
}
span.log({ event: 'http.response.headers' });
return writeHead.apply(this, arguments);
};
response.once('finish', () => span.finish());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment