signalfx-tracing
Version:
Provides auto-instrumentation for JavaScript libraries and frameworks
316 lines (251 loc) • 8.4 kB
JavaScript
const url = require('url')
const semver = require('semver')
const opentracing = require('opentracing')
const log = require('../../log')
const constants = require('../../constants')
const tags = require('../../../ext/tags')
const kinds = require('../../../ext/kinds')
const formats = require('../../../ext/formats')
const urlFilter = require('../util/urlfilter')
const analyticsSampler = require('../../analytics_sampler')
const Reference = opentracing.Reference
const HTTP_HEADERS = formats.HTTP_HEADERS
const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE
const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS
const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS
const SPAN_KIND = tags.SPAN_KIND
const CLIENT = kinds.CLIENT
const REFERENCE_CHILD_OF = opentracing.REFERENCE_CHILD_OF
const REFERENCE_NOOP = constants.REFERENCE_NOOP
function patch (http, methodName, tracer, config) {
config = normalizeConfig(tracer, config)
this.wrap(http, methodName, fn => makeRequestTrace(fn))
function makeRequestTrace (request) {
return function requestTrace () {
const args = normalizeArgs.apply(null, arguments)
const uri = args.uri
const options = args.options
let callback = args.callback
const method = (options.method || 'GET').toUpperCase()
const scope = tracer.scope()
const childOf = scope.active()
let references
if (config.filter(uri)) {
references = (childOf !== null) ? [ new Reference(REFERENCE_CHILD_OF, childOf) ] : []
} else {
references = [ new Reference(REFERENCE_NOOP, childOf) ]
}
const span = tracer.startSpan('http.request', {
references,
tags: {
[SPAN_KIND]: CLIENT,
'service.name': getServiceName(tracer, config, options),
'resource.name': method,
'span.type': 'http',
'http.method': method,
'http.url': uri
}
})
if (!hasAmazonSignature(options)) {
tracer.inject(span, HTTP_HEADERS, options.headers)
}
analyticsSampler.sample(span, config.analytics)
callback = scope.bind(callback, childOf)
const req = scope.bind(request, span).call(this, options, callback)
const emit = req.emit
req.emit = function (eventName, arg) {
switch (eventName) {
case 'response': {
const res = arg
scope.bind(res)
span.setTag(HTTP_STATUS_CODE, res.statusCode)
addResponseHeaders(res, span, config)
if (!config.validateStatus(res.statusCode)) {
span.setTag('error', 'true')
}
res.on('end', () => finish(req, res, span, config))
break
}
case 'error':
addError(span, arg)
case 'abort': // eslint-disable-line no-fallthrough
case 'close': // eslint-disable-line no-fallthrough
finish(req, null, span, config)
}
return emit.apply(this, arguments)
}
scope.bind(req)
return req
}
}
function finish (req, res, span, config) {
addRequestHeaders(req, span, config)
config.hooks.request(span, req, res)
span.finish()
}
function addError (span, error) {
span.addTags({
'sfx.error.kind': error.name,
'sfx.error.message': error.message,
'sfx.error.stack': error.stack
})
return error
}
function addRequestHeaders (req, span, config) {
config.headers.forEach(key => {
const value = req.getHeader(key)
if (value) {
span.setTag(`${HTTP_REQUEST_HEADERS}.${key}`, value)
}
})
}
function addResponseHeaders (res, span, config) {
config.headers.forEach(key => {
const value = res.headers[key]
if (value) {
span.setTag(`${HTTP_RESPONSE_HEADERS}.${key}`, value)
}
})
}
function extractUrl (options) {
const uri = options
const agent = options.agent || http.globalAgent
return typeof uri === 'string' ? uri : url.format({
protocol: options.protocol || agent.protocol,
hostname: options.hostname || options.host || 'localhost',
port: options.port,
pathname: options.path || options.pathname || '/'
})
}
function normalizeArgs (inputURL, inputOptions, callback) {
let options = typeof inputURL === 'string' ? url.parse(inputURL) : Object.assign({}, inputURL)
options.headers = options.headers || {}
if (typeof inputOptions === 'function') {
callback = inputOptions
} else if (typeof inputOptions === 'object') {
options = Object.assign(options, inputOptions)
}
const uri = extractUrl(options)
return { uri, options, callback }
}
}
function getHost (options) {
if (typeof options === 'string') {
return url.parse(options).host
}
const hostname = options.hostname || options.host || 'localhost'
const port = options.port
return [hostname, port].filter(val => val).join(':')
}
function getServiceName (tracer, config, options) {
if (config.splitByDomain) {
return getHost(options)
} else if (config.service) {
return config.service
}
return `${tracer._service}-http-client`
}
function hasAmazonSignature (options) {
if (!options) {
return false
}
if (options.headers) {
const headers = Object.keys(options.headers)
for (let i = 0; i < headers.length; i++) {
const header = headers[i].toLowerCase()
if (header === 'x-amz-signature') {
return true
}
if (header === 'authorization' && hasAmazonHmac([].concat(options.headers[headers[i]]))) {
return true
}
}
}
return options.path && options.path.toLowerCase().indexOf('x-amz-signature=') !== -1
}
function hasAmazonHmac (fields) {
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
if (typeof field === 'string' && field.startsWith('AWS4-HMAC-SHA256')) {
return true
}
}
return false
}
function unpatch (http) {
this.unwrap(http, 'request')
this.unwrap(http, 'get')
}
function getStatusValidator (config) {
if (typeof config.validateStatus === 'function') {
return config.validateStatus
} else if (config.hasOwnProperty('validateStatus')) {
log.error('Expected `validateStatus` to be a function.')
}
return code => code < 500 || code >= 600
}
function getFilter (tracer, config) {
config = Object.assign({}, config, {
exclude: [`${tracer._url.href}`].concat(config.exclude || [])
})
return urlFilter.getFilter(config)
}
function normalizeConfig (tracer, config) {
config = config.client || config
const validateStatus = getStatusValidator(config)
const filter = getFilter(tracer, config)
const headers = getHeaders(config)
const hooks = getHooks(config)
return Object.assign({}, config, {
validateStatus,
filter,
headers,
hooks
})
}
function getHeaders (config) {
if (!Array.isArray(config.headers)) return []
return config.headers
.filter(key => typeof key === 'string')
.map(key => key.toLowerCase())
}
function getHooks (config) {
const noop = () => {}
const request = (config.hooks && config.hooks.request) || noop
return { request }
}
module.exports = [
{
name: 'http',
patch: function (http, tracer, config) {
if (config.client === false) return
patch.call(this, http, 'request', tracer, config)
if (semver.satisfies(process.version, '>=8')) {
/**
* In newer Node versions references internal to modules, such as `http(s).get` calling `http(s).request`, do
* not use externally patched versions, which is why we need to also patch `get` here separately.
*/
patch.call(this, http, 'get', tracer, config)
}
},
unpatch
},
{
name: 'https',
patch: function (http, tracer, config) {
if (config.client === false) return
if (semver.satisfies(process.version, '>=9')) {
patch.call(this, http, 'request', tracer, config)
patch.call(this, http, 'get', tracer, config)
} else {
/**
* Below Node v9 the `https` module invokes `http.request`, which would end up counting requests twice.
* So rather then patch the `https` module, we ensure the `http` module is patched and we count only there.
*/
require('http')
}
},
unpatch
}
]