UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

349 lines (277 loc) 10.6 kB
'use strict' const METHODS = [...require('http').METHODS.map(v => v.toLowerCase()), 'all'] const shimmer = require('../../datadog-shimmer') const { addHook, channel } = require('./helpers/instrument') const { getCompileToRegexp } = require('./path-to-regexp') const { getRouterMountPaths, joinPath, getLayerMatchers, setLayerMatchers, isAppMounted, setRouterMountPath, extractMountPaths, getRouteFullPaths, wrapRouteMethodsAndPublish, collectRoutesFromRouter, } = require('./helpers/router-helper') function isFastStar (layer, matchers) { return layer.regexp?.fast_star ?? matchers.hasStarPath } function isFastSlash (layer, matchers) { return layer.regexp?.fast_slash ?? matchers.hasSlashPath } // TODO: Move this function to a shared file between Express and Router /** * @param {string} name Channel namespace (`apm:<name>:middleware:*`). * @param {((pattern: string | RegExp) => RegExp | undefined) | undefined} compile * Host-resolved path-to-regexp compile adapter, or undefined when the host * instance ships no path-to-regexp. Captured here so each express/router * instance keeps the dialect it actually loaded. */ function createWrapRouterMethod (name, compile) { const enterChannel = channel(`apm:${name}:middleware:enter`) const exitChannel = channel(`apm:${name}:middleware:exit`) const finishChannel = channel(`apm:${name}:middleware:finish`) const errorChannel = channel(`apm:${name}:middleware:error`) const nextChannel = channel(`apm:${name}:middleware:next`) const routeAddedChannel = channel(`apm:${name}:route:added`) function wrapLayerHandle (layer, original) { original._name = original._name || layer.name return shimmer.wrapFunction(original, original => function (...args) { if (!enterChannel.hasSubscribers) return original.apply(this, args) const matchers = getLayerMatchers(layer) const lastIndex = args.length - 1 const name = original._name || original.name const req = args[args.length > 3 ? 1 : 0] const next = args[lastIndex] if (typeof next === 'function') { args[lastIndex] = wrapNext(req, next) } let route if (matchers?.length && !isFastStar(layer, matchers) && !isFastSlash(layer, matchers)) { if (matchers.length === 1) { // The host already matched this layer; the lone pattern is the route. route = matchers[0].path } else { for (const matcher of matchers) { if (matcher.regex?.test(layer.path)) { route = matcher.path break } } } } enterChannel.publish({ name, req, route, layer }) try { return original.apply(this, args) } catch (error) { errorChannel.publish({ req, error }) nextChannel.publish({ req }) finishChannel.publish({ req }) throw error } finally { exitChannel.publish({ req }) } }) } function wrapStack (layers, matchers) { for (const layer of layers) { if (layer.__handle) { // express-async-errors layer.__handle = wrapLayerHandle(layer, layer.__handle) } else { layer.handle = wrapLayerHandle(layer, layer.handle) } setLayerMatchers(layer, matchers) if (layer.route) { for (const method of METHODS) { if (typeof layer.route.stack === 'function') { layer.route.stack = [{ handle: layer.route.stack }] } layer.route[method] = wrapMethod(layer.route[method]) } } } } function wrapNext (req, originalNext) { // Per layer dispatch, N per request. `shimmer.wrapCallback` preserves // only `name` + `length`; see its JSDoc for the full contract. return shimmer.wrapCallback(originalNext, next => function (error) { if (error && error !== 'route' && error !== 'router') { errorChannel.publish({ req, error }) } nextChannel.publish({ req }) finishChannel.publish({ req }) next.apply(this, arguments) }) } function extractMatchers (fn) { const arg = Array.isArray(fn) ? fn.flat(Infinity) : [fn] if (typeof arg[0] === 'function') { return [] } if (arg.length === 1) { const pattern = arg[0] const path = pattern instanceof RegExp ? `(${pattern})` : pattern const matchers = [{ path }] matchers.hasStarPath = path === '*' matchers.hasSlashPath = path === '/' return matchers } // hasStarPath/hasSlashPath cache the lookups isFastStar/isFastSlash // would otherwise re-run on every request. let hasStarPath = false let hasSlashPath = false const matchers = arg.map(pattern => { const isRegExp = pattern instanceof RegExp const path = isRegExp ? `(${pattern})` : pattern if (path === '*') { hasStarPath = true } else if (path === '/') { hasSlashPath = true } return { path, regex: isRegExp ? pattern : compile?.(pattern), } }) matchers.hasStarPath = hasStarPath matchers.hasSlashPath = hasSlashPath return matchers } function wrapMethod (original) { return shimmer.wrapFunction(original, original => function methodWithTrace (...args) { let offset = 0 if (this.stack) { offset = Array.isArray(this.stack) ? this.stack.length : 1 } const router = original.apply(this, args) if (typeof this.stack === 'function') { this.stack = [{ handle: this.stack }] } if (routeAddedChannel.hasSubscribers) { routeAddedChannel.publish({ topOfStackFunc: methodWithTrace, layer: this.stack?.at(-1) }) } const fn = args[0] // Publish only if this router was mounted by app.use() (prevents early '/sub/...') if (routeAddedChannel.hasSubscribers && isAppMounted(this) && this.stack?.length > offset) { // Handle nested router mounting for 'use' method if (original.name === 'use' && args.length >= 2) { const { mountPaths, startIdx } = extractMountPaths(fn) if (mountPaths.length) { const parentPaths = getRouterMountPaths(this) for (let i = startIdx; i < args.length; i++) { const nestedRouter = args[i] if (!nestedRouter || typeof nestedRouter !== 'function') continue for (const parentPath of parentPaths) { for (const normalizedMountPath of mountPaths) { const fullMountPath = joinPath(parentPath, normalizedMountPath) if (fullMountPath === null) continue setRouterMountPath(nestedRouter, fullMountPath) collectRoutesFromRouter(nestedRouter, fullMountPath) } } } } } const mountPaths = getRouterMountPaths(this) if (mountPaths.length) { const layer = this.stack.at(-1) if (layer?.route) { const route = layer.route const fullPaths = mountPaths.flatMap(mountPath => getRouteFullPaths(route, mountPath)) wrapRouteMethodsAndPublish(route, fullPaths, (payload) => { routeAddedChannel.publish(payload) }) } } } if (this.stack?.length > offset) { wrapStack(this.stack.slice(offset), extractMatchers(fn)) } return router }) } return wrapMethod } addHook({ name: 'router', versions: ['>=1 <2'] }, Router => { const wrapRouterMethod = createWrapRouterMethod('router', getCompileToRegexp()) shimmer.wrap(Router.prototype, 'use', wrapRouterMethod) shimmer.wrap(Router.prototype, 'route', wrapRouterMethod) return Router }) const queryParserReadCh = channel('datadog:query:read:finish') addHook({ name: 'router', versions: ['>=2'] }, Router => { const wrapRouterMethod = createWrapRouterMethod('router', getCompileToRegexp()) const WrappedRouter = shimmer.wrapFunction(Router, function (originalRouter) { return function wrappedMethod (...args) { const router = originalRouter.apply(this, args) shimmer.wrap(router, 'handle', function wrapHandle (originalHandle) { return function wrappedHandle (req, res, next) { const abortController = new AbortController() if (queryParserReadCh.hasSubscribers && req) { queryParserReadCh.publish({ req, res, query: req.query, abortController }) if (abortController.signal.aborted) return } return originalHandle.apply(this, arguments) } }) return router } }) shimmer.wrap(WrappedRouter.prototype, 'use', wrapRouterMethod) shimmer.wrap(WrappedRouter.prototype, 'route', wrapRouterMethod) return WrappedRouter }) const routerParamStartCh = channel('datadog:router:param:start') const visitedParams = new WeakSet() function wrapHandleRequest (original) { return function wrappedHandleRequest (req, res, next) { if (routerParamStartCh.hasSubscribers && !visitedParams.has(req.params) && Object.keys(req.params).length) { visitedParams.add(req.params) const abortController = new AbortController() routerParamStartCh.publish({ req, res, params: req?.params, abortController, }) if (abortController.signal.aborted) return } return original.apply(this, arguments) } } addHook({ name: 'router', file: 'lib/layer.js', versions: ['>=2'], }, Layer => { shimmer.wrap(Layer.prototype, 'handleRequest', wrapHandleRequest) return Layer }) function wrapParam (original) { return function wrappedProcessParams (...args) { args[1] = shimmer.wrapFunction(args[1], (originalFn) => { return function wrappedFn (req, res) { if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) { visitedParams.add(req.params) const abortController = new AbortController() routerParamStartCh.publish({ req, res, params: req?.params, abortController, }) if (abortController.signal.aborted) return } return originalFn.apply(this, arguments) } }) return original.apply(this, args) } } addHook({ name: 'router', versions: ['>=2'], }, router => { shimmer.wrap(router.prototype, 'param', wrapParam) return router }) module.exports = { createWrapRouterMethod }