dd-trace
Version:
Datadog APM tracing client for JavaScript
132 lines (119 loc) • 4.08 kB
JavaScript
const { AsyncLocalStorage } = require('async_hooks')
/**
* `AsyncLocalStorage` with a `getHandle()` escape hatch: a span stashes the
* active handle at creation (see opentracing/span.js) so a later context can
* recover its store without holding the store itself.
*
* Under AsyncContextFrame — Node's default ALS backend on every release that
* ships it — the active value lives in the context frame and is never written
* to the async resource, so the store is held directly and the handle is the
* store. Pre-ACF `async_hooks` instead pinned the value onto the resource
* object, where it was visible to logging and retained with the resource
* graph; the `!isACFActive` block below restores the original WeakMap-handle
* indirection that kept the real store off the resource on those runtimes.
*
* @template T
* @typedef {Record<string, T>} Store
*/
class DatadogStorage extends AsyncLocalStorage {
/**
* Passthrough to the real `getStore()`. A span stashes this handle and feeds
* it back to `getStore(handle)` later. Identical in both modes: under ACF the
* handle is the store; without ACF it is the WeakMap key.
*
* @returns {Store<unknown>}
*/
getHandle () {
return super.getStore()
}
/**
* @param {Store<unknown>} [handle] A handle from `getHandle()`; defaults to
* the active one. Under ACF the handle is the store, so it is returned as-is.
* @returns {Store<unknown> | undefined}
* @override
*/
getStore (handle) {
return handle ?? super.getStore()
}
}
// To handle all versions always correct, feature detect AsyncContextFrame and
// fallback to manual approach if not active. With ACF `run` delegates to
// `enterWith`, without ACF `run` does not.
const isACFActive = (() => {
let active = false
const als = new AsyncLocalStorage()
als.enterWith = () => { active = true }
als.run(1, () => {})
als.disable()
return active
})()
if (!isACFActive) {
const superGetStore = AsyncLocalStorage.prototype.getStore
const superEnterWith = AsyncLocalStorage.prototype.enterWith
// Without ACF, ALS writes the entered value onto the async resource. Keep the
// real store off the resource by entering a small handle and mapping it to
// the store through a WeakMap, then reversing the lookup on read.
const stores = new WeakMap()
/**
* @param {Store<unknown>} [store]
*/
DatadogStorage.prototype.enterWith = function enterWith (store) {
const handle = { noop: store?.noop }
stores.set(handle, store)
superEnterWith.call(this, handle)
}
/**
* @param {object} [handle]
* @returns {Store<unknown> | undefined}
*/
DatadogStorage.prototype.getStore = function getStore (handle) {
if (!handle) {
handle = superGetStore.call(this)
}
if (handle) {
return stores.get(handle)
}
}
/**
* Without ACF, `super.run()` does not delegate to `enterWith()`, so the
* WeakMap handle is never created and `getStore()` would miss. Drive the
* handle path manually and restore the prior handle on the way out.
*
* @template R
* @template {unknown[]} TArgs
* @param {Store<unknown>} store
* @param {(...args: TArgs) => R} fn
* @param {TArgs} args
* @returns {R}
* @override
*/
DatadogStorage.prototype.run = function run (store, fn, ...args) {
const prior = superGetStore.call(this)
this.enterWith(store)
try {
return Reflect.apply(fn, null, args)
} finally {
superEnterWith.call(this, prior)
}
}
}
/**
* For convenience, we use the `storage` function as a registry of namespaces
* corresponding to DatadogStorage instances. This lets us have separate
* storages for separate purposes.
* @type {Map<string, DatadogStorage>}
*/
const storages = Object.create(null)
/**
*
* @param {string} namespace The namespace to use
* @returns {DatadogStorage}
*/
function storage (namespace) {
if (!storages[namespace]) {
storages[namespace] = new DatadogStorage()
}
return storages[namespace]
}
module.exports = { storage, isACFActive }