UNPKG

signalfx-tracing

Version:

Provides auto-instrumentation for JavaScript libraries and frameworks

474 lines (383 loc) 13.1 kB
'use strict' const pathToRegexp = require('path-to-regexp') const xregexp = require('xregexp') 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 platform = require('../../platform') const SpanContext = require('../../opentracing/span_context') const idToHex = require('../../utils').idToHex const HTTP = types.HTTP 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 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 expandRouteParameters = getExpandRouteParameters(config) const synthesizeRequestingContext = getSynthesizeRequestingContext(config) return Object.assign({}, config, { headers, validateStatus, hooks, filter, expandRouteParameters, synthesizeRequestingContext }) }, // 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) // TODO: replace this with a REFERENCE_NOOP after we split http/express/etc if (!config.filter(req.url)) { span.context()._sampling.drop = true } if (config.service) { span.setTag(SERVICE_NAME, config.service) } analyticsSampler.sample(span, config.analytics, true) wrapEnd(req) wrapEvents(req) if (config.enableServerTiming) { if (!res._sfx_serverTimingAdded) { res.setHeader('Server-Timing', traceParentHeader(span.context())) res.setHeader('Access-Control-Expose-Headers', 'Server-Timing') Object.defineProperty(res, '_sfx_serverTimingAdded', { value: 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) { 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 span = tracer.startSpan(name, { childOf }) span.addTags({ [RESOURCE_NAME]: middleware._name || middleware.name || '<anonymous>' }) analyticsSampler.sample(span, req._datadog.config.analytics) req._datadog.middleware.push(span) return tracer.scope().activate(span, fn) }, // 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({ 'sfx.error.kind': error.name, 'sfx.error.message': error.message, 'sfx.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 Object.defineProperty(req, '_datadog', { value: { span: null, paths: [], middleware: [], beforeEnd: [], childOfRequestingContext: false } }) }, // 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] } } function startSpan (tracer, config, req, res, name) { req._datadog.config = config if (req._datadog.span) { req._datadog.span.context()._name = name return req._datadog.span } let childOf = tracer.extract(FORMAT_HTTP_HEADERS, req.headers) if (!childOf) { childOf = synthesizedSpanContext(req) } else { req._datadog.childOfRequestingContext = true } const span = tracer.startSpan(name, { childOf }) req._datadog.tracer = tracer req._datadog.span = span req._datadog.res = res return span } function finish (req, res) { if (req._datadog.finished) return addRequestTags(req) addResponseTags(req) req._datadog.config.hooks.request(req._datadog.span, req, res) addResourceTag(req) revertSynthesizedContext(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 if (end === req._datadog.end) return let _end = req._datadog.end = res.end = function () { req._datadog.beforeEnd.forEach(beforeEnd => beforeEnd()) finishMiddleware(req, res) const returnValue = end.apply(this, arguments) finish(req, res) return returnValue } Object.defineProperty(res, 'end', { configurable: true, get () { return _end }, set (value) { _end = scope.bind(value, req._datadog.span) } }) } function wrapEvents (req) { const scope = req._datadog.tracer.scope() const res = req._datadog.res const on = res.on if (on === req._datadog.on) return req._datadog.on = scope.bind(res, req._datadog.span).on } function reactivate (req, fn) { return req._datadog.tracer.scope().activate(req._datadog.span, fn) } function addRequestTags (req) { const protocol = req.connection.encrypted ? 'https' : 'http' const url = `${protocol}://${req.headers['host']}${req.originalUrl || req.url}` const span = req._datadog.span span.addTags({ [HTTP_URL]: url.split('?')[0], [HTTP_METHOD]: req.method, [SPAN_KIND]: SERVER, [SPAN_TYPE]: HTTP }) 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 }) addStatusError(req) } function addResourceTag (req) { const span = req._datadog.span const tags = span.context()._tags if (tags['resource.name']) return const path = expandRouteParameters(tags[HTTP_ROUTE], req) const resource = [].concat(path) .filter(val => val) .join(' ') if (!resource) { const componentName = tags.component ? tags.component : 'handle' span.setTag(RESOURCE_NAME, `${componentName}.request`) } else { span.setTag(RESOURCE_NAME, resource) } } // Allows :routeParameters to be expanded by their request path value function expandRouteParameters (httpRoute, req) { let expandedPath = httpRoute // default w/o expansion const expansionRules = req._datadog.config.expandRouteParameters[httpRoute] if (expansionRules === undefined) { return expandedPath } const keys = [] const re = pathToRegexp(httpRoute, keys) // Account for routing-reduced paths const path = req.originalUrl.substring(0, req.originalUrl.indexOf(req.path) + req.path.length) const matches = re.exec(path) if (matches === null) { return expandedPath } const hits = matches.slice(1, keys.length + 1) for (let i = 0; i < keys.length; i++) { const key = keys[i] if (expansionRules[key.name] === true) { const replacePattern = `:${key.name}` const patternIndex = expandedPath.indexOf(replacePattern) // get substrings before and after :key.name const before = expandedPath.substring(0, patternIndex) let after = expandedPath.substring(patternIndex + replacePattern.length) // remove immediate capture group from after substring let capGroupMatches try { capGroupMatches = xregexp.matchRecursive(after, '\\(', '\\)') } catch (err) { // will throw if unbalanced parens in data (nothing we can do) capGroupMatches = [] } if (capGroupMatches.length >= 1) { // replace stripped outer parens from recursive match and remove from after substring const replacedGroup = `(${capGroupMatches[0]})` const replacedGroupIndex = after.indexOf(replacedGroup) after = after.substring(replacedGroupIndex + replacedGroup.length) } // recreate expanded path with truncated substring // set expandedPath to be replaced :key.name w/ value expandedPath = before + hits[i] + after } } return expandedPath } // Creates a new span context and sets its ids as `req.sfx.traceId` // and `req.sfx.spanId` for user access. function synthesizedSpanContext (req) { const traceId = platform.id() const spanContext = new SpanContext({ traceId, spanId: traceId }) Object.defineProperty(spanContext, 'isSynthesized', { value: true }) const resId = idToHex(traceId) Object.defineProperty(req, 'sfx', { value: { traceId: resId, spanId: resId } }) return spanContext } // Will remove the synthesized parent for any request without // `synthesizeRequestingContext` configured. Since the router // instrumentation only determines paths by the end of the // lifecycle, this must affect all spans that aren't the actual // child of propagated context. function revertSynthesizedContext (req) { if (req._datadog.childOfRequestingContext) { return } const span = req._datadog.span const tags = span.context()._tags const path = tags[HTTP_ROUTE] const synthesize = req._datadog.config.synthesizeRequestingContext[path] if (synthesize) { return } // "revert" synthesized context span.context()._parentId = null } 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 addStatusError (req) { if (!req._datadog.config.validateStatus(req._datadog.res.statusCode)) { req._datadog.span.setTag(ERROR, true) } } 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 getExpandRouteParameters (config) { if (typeof config.expandRouteParameters === 'object') { return config.expandRouteParameters } else if (config.hasOwnProperty('expandRouteParameters')) { log.error('Expected `expandRouteParameters` to be an object of paths to expansion rules') } return {} } function getSynthesizeRequestingContext (config) { if (typeof config.synthesizeRequestingContext === 'object') { return config.synthesizeRequestingContext } else if (config.hasOwnProperty('synthesizeRequestingContext')) { log.error('Expected `synthesizeRequestingContext` to be an object of paths to booleans') } return {} } function padTo128 (hexId) { const padded = '0000000000000000' + hexId return padded.slice(-32) } function traceParentHeader (spanContext) { // https://www.w3.org/TR/server-timing/ // https://www.w3.org/TR/trace-context/#traceparent-header return 'traceparent;desc="00-' + padTo128(idToHex(spanContext._traceId)) + '-' + idToHex(spanContext._spanId) + '-01"' } module.exports = web