UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

254 lines (198 loc) 7.45 kB
'use strict' const shimmer = require('../../datadog-shimmer') const { createWrapRouterMethod } = require('./router') const { addHook, channel, tracingChannel } = require('./helpers/instrument') const { setRouterMountPath, markAppMounted, normalizeRoutePaths, wrapRouteMethodsAndPublish, extractMountPaths, hasRouterCycle, collectRoutesFromRouter } = require('./helpers/router-helper') const handleChannel = channel('apm:express:request:handle') const routeAddedChannel = channel('apm:express:route:added') function wrapHandle (handle) { return function handleWithTrace (req, res) { if (handleChannel.hasSubscribers) { handleChannel.publish({ req }) } return handle.apply(this, arguments) } } const wrapRouterMethod = createWrapRouterMethod('express') const responseJsonChannel = channel('datadog:express:response:json:start') function wrapResponseJson (json) { return function wrappedJson (obj) { if (responseJsonChannel.hasSubscribers) { // backward compat as express 4.x supports deprecated 3.x signature if (arguments.length === 2 && typeof arguments[1] !== 'number') { obj = arguments[1] } responseJsonChannel.publish({ req: this.req, res: this, body: obj }) } return json.apply(this, arguments) } } const responseRenderChannel = tracingChannel('datadog:express:response:render') function wrapResponseRender (render) { return function wrappedRender (view, options, callback) { if (!responseRenderChannel.start.hasSubscribers) { return render.apply(this, arguments) } const abortController = new AbortController() return responseRenderChannel.traceSync( function () { if (abortController.signal.aborted) { const error = abortController.signal.reason || new Error('Aborted') throw error } return render.apply(this, arguments) }, { req: this.req, view, options, abortController }, this, ...arguments ) } } function wrapAppAll (all) { return function wrappedAll (...args) { if (!routeAddedChannel.hasSubscribers) return all.apply(this, args) const path = args[0] const paths = normalizeRoutePaths(path) for (const p of paths) { routeAddedChannel.publish({ method: '*', path: p }) } return all.apply(this, args) } } // Wrap app.route() to instrument Route object function wrapAppRoute (route) { return function wrappedRoute (...args) { const routeObj = route.apply(this, args) if (!routeAddedChannel.hasSubscribers) return routeObj const path = args[0] const paths = normalizeRoutePaths(path) if (!paths.length) return routeObj wrapRouteMethodsAndPublish(routeObj, paths, ({ method, path }) => { routeAddedChannel.publish({ method, path }) }) return routeObj } } function wrapAppUse (use) { return function wrappedUse (...args) { if (!args.length) return use.call(this) // Get mount argument and use it to register each router against the exact paths Express will use. const { mountPaths, startIdx } = extractMountPaths(args[0]) const pathsToRegister = mountPaths.length ? mountPaths : ['/'] for (let i = startIdx; i < args.length; i++) { const router = args[i] if (!router || typeof router !== 'function') continue markAppMounted(router) // Avoid enumerating routes for routers that contain cycles. // Express will refuse those at runtime, but collecting them here could loop forever. let skipCollection = false if (routeAddedChannel.hasSubscribers) { skipCollection = hasRouterCycle(router) } for (const mountPath of pathsToRegister) { const normalizedMountPath = mountPath || '/' setRouterMountPath(router, normalizedMountPath) if (!skipCollection && routeAddedChannel.hasSubscribers) { collectRoutesFromRouter(router, normalizedMountPath) } } } return use.apply(this, args) } } addHook({ name: 'express', versions: ['>=4'], file: ['lib/express.js'] }, express => { shimmer.wrap(express.application, 'handle', wrapHandle) shimmer.wrap(express.application, 'all', wrapAppAll) shimmer.wrap(express.application, 'route', wrapAppRoute) shimmer.wrap(express.application, 'use', wrapAppUse) shimmer.wrap(express.response, 'json', wrapResponseJson) shimmer.wrap(express.response, 'jsonp', wrapResponseJson) shimmer.wrap(express.response, 'render', wrapResponseRender) return express }) // Express 5 does not rely on router in the same way as v4 and should not be instrumented anymore. // It would otherwise produce spans for router and express, and so duplicating them. // We now fall back to router instrumentation addHook({ name: 'express', versions: ['4'], file: 'lib/express.js' }, express => { shimmer.wrap(express.Router, 'use', wrapRouterMethod) shimmer.wrap(express.Router, 'route', wrapRouterMethod) return express }) const queryParserReadCh = channel('datadog:query:read:finish') function publishQueryParsedAndNext (req, res, next) { return shimmer.wrapFunction(next, next => function () { if (queryParserReadCh.hasSubscribers && req) { const abortController = new AbortController() const query = req.query queryParserReadCh.publish({ req, res, query, abortController }) if (abortController.signal.aborted) return } return next.apply(this, arguments) }) } addHook({ name: 'express', versions: ['4'], file: 'lib/middleware/query.js' }, query => { return shimmer.wrapFunction(query, query => function () { const queryMiddleware = query.apply(this, arguments) return shimmer.wrapFunction(queryMiddleware, queryMiddleware => function (req, res, next) { arguments[2] = publishQueryParsedAndNext(req, res, next) return queryMiddleware.apply(this, arguments) }) }) }) const processParamsStartCh = channel('datadog:express:process_params:start') function wrapProcessParamsMethod (requestPositionInArguments) { return function wrapProcessParams (original) { return function wrappedProcessParams () { if (processParamsStartCh.hasSubscribers) { const req = arguments[requestPositionInArguments] const abortController = new AbortController() processParamsStartCh.publish({ req, res: req?.res, abortController, params: req?.params }) if (abortController.signal.aborted) return } return original.apply(this, arguments) } } } addHook({ name: 'express', versions: ['>=4.0.0 <4.3.0'], file: ['lib/express.js'] }, express => { shimmer.wrap(express.Router, 'process_params', wrapProcessParamsMethod(1)) return express }) addHook({ name: 'express', versions: ['>=4.3.0 <5.0.0'], file: ['lib/express.js'] }, express => { shimmer.wrap(express.Router, 'process_params', wrapProcessParamsMethod(2)) return express }) const queryReadCh = channel('datadog:express:query:finish') addHook({ name: 'express', file: ['lib/request.js'], versions: ['>=5.0.0'] }, request => { shimmer.wrap(request, 'query', function (originalGet) { return function wrappedGet () { const query = originalGet.call(this) if (queryReadCh.hasSubscribers && query) { queryReadCh.publish({ query }) } return query } }) return request })