dd-trace
Version:
Datadog APM tracing client for JavaScript
470 lines (366 loc) • 11.7 kB
JavaScript
'use strict'
const uniq = require('lodash.uniq')
const analyticsSampler = require('../../analytics_sampler')
const FORMAT_HTTP_HEADERS = require('opentracing').FORMAT_HTTP_HEADERS
const log = require('../../log')
const tags = require('../../../../../ext/tags')
const types = require('../../../../../ext/types')
const kinds = require('../../../../../ext/kinds')
const urlFilter = require('./urlfilter')
const { incomingHttpRequestEnd } = require('../../appsec/gateway/channels')
const WEB = types.WEB
const SERVER = kinds.SERVER
const RESOURCE_NAME = tags.RESOURCE_NAME
const SERVICE_NAME = tags.SERVICE_NAME
const SPAN_TYPE = tags.SPAN_TYPE
const SPAN_KIND = tags.SPAN_KIND
const ERROR = tags.ERROR
const HTTP_METHOD = tags.HTTP_METHOD
const HTTP_URL = tags.HTTP_URL
const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE
const HTTP_ROUTE = tags.HTTP_ROUTE
const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS
const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS
const MANUAL_DROP = tags.MANUAL_DROP
const HTTP2_HEADER_AUTHORITY = ':authority'
const HTTP2_HEADER_SCHEME = ':scheme'
const HTTP2_HEADER_PATH = ':path'
const web = {
// Ensure the configuration has the correct structure and defaults.
normalizeConfig (config) {
config = config.server || config
const headers = getHeadersToRecord(config)
const validateStatus = getStatusValidator(config)
const hooks = getHooks(config)
const filter = urlFilter.getFilter(config)
const middleware = getMiddlewareSetting(config)
return Object.assign({}, config, {
headers,
validateStatus,
hooks,
filter,
middleware
})
},
// Start a span and activate a scope for a request.
instrument (tracer, config, req, res, name, callback) {
this.patch(req)
const span = startSpan(tracer, config, req, res, name)
if (!config.filter(req.url)) {
span.setTag(MANUAL_DROP, true)
}
if (config.service) {
span.setTag(SERVICE_NAME, config.service)
}
analyticsSampler.sample(span, config.measured, true)
if (!req._datadog.instrumented) {
wrapEnd(req)
wrapEvents(req)
req._datadog.instrumented = true
}
return callback && tracer.scope().activate(span, () => callback(span))
},
// Reactivate the request scope in case it was changed by a middleware.
reactivate (req, fn) {
return reactivate(req, fn)
},
// Add a route segment that will be used for the resource name.
enterRoute (req, path) {
if (typeof path === 'string') {
req._datadog.paths.push(path)
}
},
// Remove the current route segment.
exitRoute (req) {
req._datadog.paths.pop()
},
// Start a new middleware span and activate a new scope with the span.
wrapMiddleware (req, middleware, name, fn) {
if (!this.active(req)) return fn()
const tracer = req._datadog.tracer
const childOf = this.active(req)
const config = req._datadog.config
if (config.middleware === false) return this.bindAndWrapMiddlewareErrors(fn, req, tracer, childOf)
const span = tracer.startSpan(name, { childOf })
analyticsSampler.sample(span, config.measured)
span.addTags({
[RESOURCE_NAME]: middleware._name || middleware.name || '<anonymous>'
})
req._datadog.middleware.push(span)
return tracer.scope().activate(span, fn)
},
// catch errors and apply to active span
bindAndWrapMiddlewareErrors (fn, req, tracer, activeSpan) {
try {
return tracer.scope().bind(fn, activeSpan).apply(this, arguments)
} catch (e) {
web.addError(req, e) // TODO: remove when error formatting is moved to Span
throw e
}
},
// Finish the active middleware span.
finish (req, error) {
if (!this.active(req)) return
const span = req._datadog.middleware.pop()
if (span) {
if (error) {
span.addTags({
'error.type': error.name,
'error.msg': error.message,
'error.stack': error.stack
})
}
span.finish()
}
},
// Register a callback to run before res.end() is called.
beforeEnd (req, callback) {
req._datadog.beforeEnd.push(callback)
},
// Prepare the request for instrumentation.
patch (req) {
if (req._datadog) return
if (req.stream && req.stream._datadog) {
req._datadog = req.stream._datadog
return
}
req._datadog = {
span: null,
paths: [],
middleware: [],
beforeEnd: [],
config: {}
}
},
// Return the request root span.
root (req) {
return req._datadog ? req._datadog.span : null
},
// Return the active span.
active (req) {
if (!req._datadog) return null
if (req._datadog.middleware.length === 0) return req._datadog.span || null
return req._datadog.middleware.slice(-1)[0]
},
// Extract the parent span from the headers and start a new span as its child
startChildSpan (tracer, name, headers) {
const childOf = tracer.scope().active() || tracer.extract(FORMAT_HTTP_HEADERS, headers)
const span = tracer.startSpan(name, { childOf })
return span
},
// Validate a request's status code and then add error tags if necessary
addStatusError (req, statusCode) {
const span = req._datadog.span
const error = req._datadog.error
if (!req._datadog.config.validateStatus(statusCode)) {
span.setTag(ERROR, error || true)
}
},
// Add an error to the request
addError (req, error) {
if (error instanceof Error) {
req._datadog.error = req._datadog.error || error
}
}
}
function startSpan (tracer, config, req, res, name) {
req._datadog.config = config
let span
if (req._datadog.span) {
req._datadog.span.context()._name = name
span = req._datadog.span
} else {
span = web.startChildSpan(tracer, name, req.headers)
}
configureDatadogObject(tracer, span, req, res)
return span
}
function configureDatadogObject (tracer, span, req, res) {
const ddObj = req._datadog
ddObj.tracer = tracer
ddObj.span = span
ddObj.res = res
}
function finish (req, res) {
if (req._datadog.finished && !req.stream) return
addRequestTags(req)
addResponseTags(req)
req._datadog.config.hooks.request(req._datadog.span, req, res)
addResourceTag(req)
req._datadog.span.finish()
req._datadog.finished = true
}
function finishMiddleware (req, res) {
if (req._datadog.finished) return
let span
while ((span = req._datadog.middleware.pop())) {
span.finish()
}
}
function wrapEnd (req) {
const scope = req._datadog.tracer.scope()
const res = req._datadog.res
const end = res.end
res.writeHead = wrapWriteHead(req)
res._datadog_end = function () {
for (const beforeEnd of req._datadog.beforeEnd) {
beforeEnd()
}
finishMiddleware(req, res)
if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.publish({ req, res })
const returnValue = end.apply(res, arguments)
finish(req, res)
return returnValue
}
Object.defineProperty(res, 'end', {
configurable: true,
get () {
return this._datadog_end
},
set (value) {
this._datadog_end = scope.bind(value, req._datadog.span)
}
})
}
function wrapWriteHead (req) {
const res = req._datadog.res
const writeHead = res.writeHead
return function (statusCode, statusMessage, headers) {
headers = typeof statusMessage === 'string' ? headers : statusMessage
headers = Object.assign(res.getHeaders(), headers)
if (req.method.toLowerCase() === 'options' && isOriginAllowed(req, headers)) {
addAllowHeaders(req, headers)
}
return writeHead.apply(this, arguments)
}
}
function addAllowHeaders (req, headers) {
const res = req._datadog.res
const allowHeaders = splitHeader(headers['access-control-allow-headers'])
const requestHeaders = splitHeader(req.headers['access-control-request-headers'])
const contextHeaders = [
'x-datadog-origin',
'x-datadog-parent-id',
'x-datadog-sampled', // Deprecated, but still accept it in case it's sent.
'x-datadog-sampling-priority',
'x-datadog-trace-id'
]
for (const header of contextHeaders) {
if (~requestHeaders.indexOf(header)) {
allowHeaders.push(header)
}
}
if (allowHeaders.length > 0) {
res.setHeader('access-control-allow-headers', uniq(allowHeaders).join(','))
}
}
function isOriginAllowed (req, headers) {
const origin = req.headers['origin']
const allowOrigin = headers['access-control-allow-origin']
return origin && (allowOrigin === '*' || allowOrigin === origin)
}
function splitHeader (str) {
return typeof str === 'string' ? str.split(/\s*,\s*/) : []
}
function wrapEvents (req) {
const scope = req._datadog.tracer.scope()
const res = req._datadog.res
scope.bind(res, req._datadog.span)
}
function reactivate (req, fn) {
return req._datadog
? req._datadog.tracer.scope().activate(req._datadog.span, fn)
: fn()
}
function addRequestTags (req) {
const url = extractURL(req)
const span = req._datadog.span
span.addTags({
[HTTP_URL]: url.split('?')[0],
[HTTP_METHOD]: req.method,
[SPAN_KIND]: SERVER,
[SPAN_TYPE]: WEB
})
addHeaders(req)
}
function addResponseTags (req) {
const span = req._datadog.span
const res = req._datadog.res
if (req._datadog.paths.length > 0) {
span.setTag(HTTP_ROUTE, req._datadog.paths.join(''))
}
span.addTags({
[HTTP_STATUS_CODE]: res.statusCode
})
web.addStatusError(req, res.statusCode)
}
function addResourceTag (req) {
const span = req._datadog.span
const tags = span.context()._tags
if (tags['resource.name']) return
const resource = [req.method, tags[HTTP_ROUTE]]
.filter(val => val)
.join(' ')
span.setTag(RESOURCE_NAME, resource)
}
function addHeaders (req) {
const span = req._datadog.span
req._datadog.config.headers.forEach(key => {
const reqHeader = req.headers[key]
const resHeader = req._datadog.res.getHeader(key)
if (reqHeader) {
span.setTag(`${HTTP_REQUEST_HEADERS}.${key}`, reqHeader)
}
if (resHeader) {
span.setTag(`${HTTP_RESPONSE_HEADERS}.${key}`, resHeader)
}
})
}
function extractURL (req) {
const headers = req.headers
if (req.stream) {
return `${headers[HTTP2_HEADER_SCHEME]}://${headers[HTTP2_HEADER_AUTHORITY]}${headers[HTTP2_HEADER_PATH]}`
} else {
const protocol = getProtocol(req)
return `${protocol}://${req.headers['host']}${req.originalUrl || req.url}`
}
}
function getProtocol (req) {
if (req.socket && req.socket.encrypted) return 'https'
if (req.connection && req.connection.encrypted) return 'https'
return 'http'
}
function getHeadersToRecord (config) {
if (Array.isArray(config.headers)) {
try {
return config.headers.map(key => key.toLowerCase())
} catch (err) {
log.error(err)
}
} else if (config.hasOwnProperty('headers')) {
log.error('Expected `headers` to be an array of strings.')
}
return []
}
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
}
function getHooks (config) {
const noop = () => {}
const request = (config.hooks && config.hooks.request) || noop
return { request }
}
function getMiddlewareSetting (config) {
if (config && typeof config.middleware === 'boolean') {
return config.middleware
} else if (config && config.hasOwnProperty('middleware')) {
log.error('Expected `middleware` to be a boolean.')
}
return true
}
module.exports = web