UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

183 lines (163 loc) 6.82 kB
'use strict' // Shimmer required: NATS consumer paths need argument modification — the user's // `opts.callback` is wrapped before being handed to SubscriptionImpl, and the // returned subscription's async iterator is wrapped so iterator-style consumers // get receive events. Orchestrion can only wrap method calls, not arguments // or returned iterables. const shimmer = require('../../datadog-shimmer') const { addHook, channel } = require('./helpers/instrument') const publishStartCh = channel('apm:nats:publish:start') const publishFinishCh = channel('apm:nats:publish:finish') const publishErrorCh = channel('apm:nats:publish:error') const consumeStartCh = channel('apm:nats:consume:start') const consumeFinishCh = channel('apm:nats:consume:finish') const consumeErrorCh = channel('apm:nats:consume:error') // Tracks connections that are currently inside a `request`/`requestMany` call // so the nested `this.publish(...)` they issue short-circuits without creating // a second producer span (the outer request wrap already created one and // injected headers — the inner publish would double-count it). A WeakSet avoids // changing the shape of the user's connection object. const requestsInFlight = new WeakSet() // Captured from the `lib/headers.js` hook below. The nats-core package always // imports `./headers` from `lib/nats.js`, so by the time we wrap `publish` the // reference is set. No defensive checks needed at call sites. let createHeaders addHook({ name: '@nats-io/nats-core', versions: ['>=3.0.0'], file: 'lib/headers.js' }, exports => { createHeaders = exports.headers return exports }) // transport-node re-exports nats-core internals — the passthrough hook ensures // the package name is registered so `withVersions('nats', '@nats-io/transport-node', ...)` // can resolve it in plugin tests. addHook({ name: '@nats-io/transport-node', versions: ['>=3.0.0'] }, exports => exports) function wrapSyncProducer (original, type) { return function (subject, data, options) { if (!publishStartCh.hasSubscribers) { return original.apply(this, arguments) } const opts = { ...options } const ctx = { type, subject, data, options: opts, connection: this, createHeaders } return publishStartCh.runStores(ctx, () => { try { return original.call(this, subject, data, opts) } catch (err) { ctx.error = err publishErrorCh.publish(ctx) throw err } finally { publishFinishCh.publish(ctx) } }) } } // publish is also wrapped by `wrapSyncProducer`, but request/requestMany call // `this.publish(...)` internally. Set a marker on the connection so the inner // publish wrap short-circuits — see `wrapPublish`. function wrapAsyncProducer (original, type) { return function (subject, data, options) { if (!publishStartCh.hasSubscribers) { return original.apply(this, arguments) } const opts = { ...options } const ctx = { type, subject, data, options: opts, connection: this, createHeaders } return publishStartCh.runStores(ctx, () => { requestsInFlight.add(this) let promise try { // `request`/`requestMany` never throw synchronously — they wrap their own // input validation in a try/catch that returns `Promise.reject`. promise = original.call(this, subject, data, opts) } finally { // The nested `this.publish(...)` runs during the synchronous body of // request/requestMany, so clearing the marker as soon as the call // returns is sufficient — the promise resolution happens later. requestsInFlight.delete(this) } return Promise.resolve(promise).then( result => { ctx.result = result publishFinishCh.publish(ctx) return result }, err => { ctx.error = err publishErrorCh.publish(ctx) publishFinishCh.publish(ctx) throw err } ) }) } } function wrapPublish (original) { const wrapped = wrapSyncProducer(original, 'publish') return function (subject, data, options) { // Called from inside request/requestMany — the outer wrap already produced // a span and injected headers; running the inner wrap would double-count. if (requestsInFlight.has(this)) { return original.apply(this, arguments) } return wrapped.apply(this, arguments) } } function wrapSubscribeCallback (userCallback, subject, connection) { return function (err, message) { if (!message || err) { return userCallback.call(this, err, message) } const ctx = { subject, message, connection } return consumeStartCh.runStores(ctx, () => { try { return userCallback.call(this, err, message) } catch (e) { ctx.error = e consumeErrorCh.publish(ctx) throw e } finally { consumeFinishCh.publish(ctx) } }) } } // Iterator-style consumers don't expose a delivery callback we can wrap, so // the consume span represents the moment of receipt only — it starts and // finishes before the value is yielded to user code, and the user's loop // body is not parented under the span. function wrapAsyncIteratorFactory (asyncIterator, subject, connection) { return function () { const iterator = asyncIterator.apply(this, arguments) iterator.next = shimmer.wrapCallback(iterator.next, next => function () { return next.apply(this, arguments).then(result => { if (result && !result.done && result.value) { const ctx = { subject, message: result.value, connection } consumeStartCh.runStores(ctx, () => { consumeFinishCh.publish(ctx) }) } return result }) }) return iterator } } addHook({ name: '@nats-io/nats-core', versions: ['>=3.0.0'], file: 'lib/nats.js' }, exports => { const proto = exports.NatsConnectionImpl.prototype shimmer.wrap(proto, 'publish', wrapPublish) shimmer.wrap(proto, 'request', request => wrapAsyncProducer(request, 'request')) shimmer.wrap(proto, 'requestMany', requestMany => wrapAsyncProducer(requestMany, 'requestMany')) shimmer.wrap(proto, 'subscribe', subscribe => function (subject, opts) { if (!consumeStartCh.hasSubscribers) { return subscribe.apply(this, arguments) } const userOpts = opts ?? {} if (typeof userOpts.callback === 'function') { arguments[1] = { ...userOpts, callback: wrapSubscribeCallback(userOpts.callback, subject, this) } return subscribe.apply(this, arguments) } const sub = subscribe.apply(this, arguments) shimmer.wrap(sub, Symbol.asyncIterator, asyncIterator => wrapAsyncIteratorFactory(asyncIterator, subject, this)) return sub }) return exports })