dd-trace
Version:
Datadog APM tracing client for JavaScript
184 lines (137 loc) • 4.65 kB
JavaScript
'use strict'
const web = require('../../dd-trace/src/plugins/util/web')
const WebPlugin = require('../../datadog-plugin-web/src')
const analyticsSampler = require('../../dd-trace/src/analytics_sampler')
const { storage } = require('../../datadog-core')
const { COMPONENT } = require('../../dd-trace/src/constants')
class RouterPlugin extends WebPlugin {
static id = 'router'
#storeStacks = new WeakMap()
#contexts = new WeakMap()
constructor (...args) {
super(...args)
this.addSub(`apm:${this.constructor.id}:middleware:enter`, ({ req, name, route }) => {
const childOf = this.#getActive(req) || this.#getStoreSpan()
if (!childOf) return
const span = this.#getMiddlewareSpan(name, childOf)
const context = this.#createContext(req, route, childOf)
if (childOf !== span) {
context.middleware.push(span)
}
const store = storage('legacy').getStore()
let storeStack = this.#storeStacks.get(req)
if (!storeStack) {
storeStack = []
this.#storeStacks.set(req, storeStack)
}
storeStack.push(store)
this.enter(span, store)
web.patch(req)
web.setRoute(req, context.route)
})
this.addSub(`apm:${this.constructor.id}:middleware:next`, ({ req }) => {
const context = this.#contexts.get(req)
if (!context) return
context.stack.pop()
})
this.addSub(`apm:${this.constructor.id}:middleware:finish`, ({ req }) => {
const context = this.#contexts.get(req)
if (!context || context.middleware.length === 0) return
context.middleware.pop().finish()
})
this.addSub(`apm:${this.constructor.id}:middleware:exit`, ({ req }) => {
const storeStack = this.#storeStacks.get(req)
const savedStore = storeStack && storeStack.pop()
if (storeStack && storeStack.length === 0) {
this.#storeStacks.delete(req)
}
const span = savedStore && savedStore.span
this.enter(span, savedStore)
})
this.addSub(`apm:${this.constructor.id}:middleware:error`, ({ req, error }) => {
web.addError(req, error)
if (!this.config.middleware) return
const span = this.#getActive(req)
if (!span) return
span.setTag('error', error)
})
this.addSub('apm:http:server:request:finish', ({ req }) => {
const context = this.#contexts.get(req)
if (!context) return
let span
while ((span = context.middleware.pop())) {
span.finish()
}
})
}
#getActive (req) {
const context = this.#contexts.get(req)
if (!context) return
if (context.middleware.length === 0) return context.span
return context.middleware.at(-1)
}
#getStoreSpan () {
const store = storage('legacy').getStore()
return store && store.span
}
#getMiddlewareSpan (name, childOf) {
if (this.config.middleware === false) {
return childOf
}
const span = this.tracer.startSpan(`${this.constructor.id}.middleware`, {
childOf,
integrationName: this.constructor.id,
tags: {
[COMPONENT]: this.constructor.id,
'resource.name': name || '<anonymous>',
},
})
analyticsSampler.sample(span, this.config.measured)
return span
}
#createContext (req, route, span) {
let context = this.#contexts.get(req)
if (!route || route === '/' || route === '*') {
route = ''
}
if (context) {
context.stack.push(route)
route = context.stack.join('')
// Longer route is more likely to be the actual route handler route.
if (isMoreSpecificThan(route, context.route)) {
context.route = route
}
} else {
context = {
span,
stack: [route],
route,
middleware: [],
}
this.#contexts.set(req, context)
}
return context
}
}
function isMoreSpecificThan (routeA, routeB) {
// Concrete paths beat catch-all wildcards (`/*splat`, `/api/*`) on the same
// request so that `/foo/bar` wins over `/foo/*splat` regardless of length.
if (routeA && routeB) {
const aWild = hasWildcard(routeA)
const bWild = hasWildcard(routeB)
if (aWild !== bWild) return !aWild
}
if (!routeIsRegex(routeA) && routeIsRegex(routeB)) {
return true
}
return routeA.length > routeB.length
}
function routeIsRegex (route) {
return route.includes('(/')
}
function hasWildcard (route) {
// RegExp routes are encoded as `(/.../)` and may legitimately contain `*`,
// so only treat plain string patterns as wildcards.
return !routeIsRegex(route) && route.includes('*')
}
module.exports = RouterPlugin