Last active
March 15, 2019 12:37
-
-
Save pauldraper/5f7055b2d24d042c58625bfebfd2331f to your computer and use it in GitHub Desktop.
OpenTracing for Node.js http
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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