newrelic 14.1.2 → 14.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NEWS.md +28 -0
- package/THIRD_PARTY_NOTICES.md +15 -15
- package/index.js +2 -2
- package/lib/collector/http-agents.js +5 -2
- package/lib/config/default.js +162 -139
- package/lib/config/index.js +22 -2
- package/lib/instrumentations.js +0 -1
- package/lib/profiling/index.js +2 -1
- package/lib/profiling/profilers/cpu.js +225 -3
- package/lib/subscriber-configs.js +1 -0
- package/lib/subscribers/base.js +14 -7
- package/lib/subscribers/db.js +11 -2
- package/lib/subscribers/mongodb/bulk.js +79 -0
- package/lib/subscribers/mongodb/collection.js +113 -0
- package/lib/subscribers/mongodb/config.js +72 -0
- package/lib/subscribers/mongodb/cursor.js +76 -0
- package/lib/subscribers/mongodb/db.js +101 -0
- package/lib/subscribers/mongodb/utils/admin-commands.js +19 -0
- package/lib/subscribers/mongodb/utils/get-host-details.js +85 -0
- package/lib/subscribers/mongodb/wrapper.js +109 -0
- package/lib/subscribers/resolve-package-version.js +42 -12
- package/lib/transaction/tracecontext.js +6 -19
- package/lib/transaction/tracer/index.js +16 -0
- package/package.json +4 -4
- package/lib/instrumentation/mongodb/common.js +0 -272
- package/lib/instrumentation/mongodb/constants.js +0 -87
- package/lib/instrumentation/mongodb/v4-mongo.js +0 -75
- package/lib/instrumentation/mongodb.js +0 -46
|
@@ -5,15 +5,68 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict'
|
|
7
7
|
const BaseProfiler = require('./base')
|
|
8
|
+
const semver = require('semver')
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* (CPED) Key under which each segment caches its sample context, so the same
|
|
12
|
+
* span always maps to the same object identity.
|
|
13
|
+
*/
|
|
14
|
+
const PROFILING_CONTEXT = Symbol('NewRelicProfilingContext')
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* (non-CPED) Empty holder for when no span is active. Frozen so it can't be
|
|
18
|
+
* mutated in place — pprof reads holders by reference, so we swap in a fresh one
|
|
19
|
+
* per sample.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_CONTEXT = Object.freeze({ current: {} })
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Produces a wall time stack profile via `pprof`.
|
|
25
|
+
* The native sampler fires at a fixed rate against whatever JS is currently on
|
|
26
|
+
* the stack, and each sample is tagged with the active transaction's trace/span
|
|
27
|
+
* ids. We capture those ids by hooking the agent's `AsyncLocalStorage`; pprof
|
|
28
|
+
* then carries them per sample via CPED when `AsyncContextFrame` is active, or
|
|
29
|
+
* in a holder we swap per sample otherwise.
|
|
30
|
+
*
|
|
31
|
+
* The wall-clock (or just wall) time for a function measures the time elapsed
|
|
32
|
+
* between entering and exiting a function. Wall time includes all wait time,
|
|
33
|
+
* including that for locks and thread synchronization.
|
|
34
|
+
*/
|
|
9
35
|
class CpuProfiler extends BaseProfiler {
|
|
10
36
|
#pprof
|
|
37
|
+
#tracer
|
|
11
38
|
#durationMillis
|
|
12
39
|
#intervalMicros = (1e3 / 99) * 1000 // samples at 99hz(99 times per second)
|
|
13
|
-
|
|
40
|
+
#kSampleCount
|
|
41
|
+
#useCPED
|
|
42
|
+
/**
|
|
43
|
+
* (non-CPED) `pprof` captures this holder by reference at sample time, so
|
|
44
|
+
* we keep `current` pointed at the active transaction's trace/span ids.
|
|
45
|
+
*/
|
|
46
|
+
#context = DEFAULT_CONTEXT
|
|
47
|
+
#state
|
|
48
|
+
#lastSampleCount = 0
|
|
49
|
+
/**
|
|
50
|
+
* The agent's `AsyncLocalStorage` instance.
|
|
51
|
+
*/
|
|
52
|
+
#store
|
|
53
|
+
// Saved ALS methods, restored by `#unhookContext` on stop.
|
|
54
|
+
#origRun
|
|
55
|
+
#origEnterWith
|
|
56
|
+
/**
|
|
57
|
+
* (non-CPED) Async hook that refreshes context on async resumption.
|
|
58
|
+
*/
|
|
59
|
+
#asyncHook
|
|
60
|
+
|
|
61
|
+
constructor({ logger, samplingInterval, tracer }) {
|
|
14
62
|
super({ logger })
|
|
15
63
|
this.#pprof = require('@datadog/pprof')
|
|
64
|
+
this.#kSampleCount = this.#pprof.time.constants.kSampleCount
|
|
65
|
+
this.#tracer = tracer
|
|
66
|
+
this.#store = tracer._contextManager._asyncLocalStorage
|
|
16
67
|
this.#durationMillis = samplingInterval
|
|
68
|
+
// CPED requires AsyncContextFrame which is on by default with node >=24
|
|
69
|
+
this.#useCPED = semver.satisfies(process.versions.node, '>=24.0.0')
|
|
17
70
|
}
|
|
18
71
|
|
|
19
72
|
start() {
|
|
@@ -25,8 +78,21 @@ class CpuProfiler extends BaseProfiler {
|
|
|
25
78
|
this.logger.trace(`Starting CpuProfiler, sample every ${this.#intervalMicros}hz for ${this.#durationMillis} ms.`)
|
|
26
79
|
this.#pprof.time.start({
|
|
27
80
|
durationMillis: this.#durationMillis,
|
|
28
|
-
intervalMicros: this.#intervalMicros
|
|
81
|
+
intervalMicros: this.#intervalMicros,
|
|
82
|
+
// needed for trace_id + span_id label
|
|
83
|
+
withContexts: true,
|
|
84
|
+
// carry the sample context per async-context-frame when ACF is active
|
|
85
|
+
useCPED: this.#useCPED
|
|
29
86
|
})
|
|
87
|
+
|
|
88
|
+
if (this.#useCPED) {
|
|
89
|
+
this.#hookEnterWith()
|
|
90
|
+
} else {
|
|
91
|
+
this.#state = this.#pprof.time.getState()
|
|
92
|
+
this.#resetContext()
|
|
93
|
+
this.#hookRun()
|
|
94
|
+
this.#hookAsyncResume()
|
|
95
|
+
}
|
|
30
96
|
}
|
|
31
97
|
|
|
32
98
|
stop() {
|
|
@@ -35,13 +101,169 @@ class CpuProfiler extends BaseProfiler {
|
|
|
35
101
|
return
|
|
36
102
|
}
|
|
37
103
|
|
|
104
|
+
// Restore the wrapped ALS methods before stopping.
|
|
105
|
+
this.#unhookContext()
|
|
38
106
|
this.#pprof.time.stop(false)
|
|
39
107
|
}
|
|
40
108
|
|
|
41
109
|
async collect() {
|
|
42
|
-
|
|
110
|
+
if (!this.#useCPED) {
|
|
111
|
+
// Flush the active span before sampling restarts.
|
|
112
|
+
this.#updateContext()
|
|
113
|
+
}
|
|
114
|
+
const profile = this.#pprof.time.stop(true, this.#generateLabels)
|
|
115
|
+
if (!this.#useCPED) {
|
|
116
|
+
// `stop(true)` restarts sampling, so start a fresh holder.
|
|
117
|
+
this.#resetContext()
|
|
118
|
+
}
|
|
43
119
|
return this.#pprof.encode(profile)
|
|
44
120
|
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Callback that returns the `span_id` and `trace_id`
|
|
124
|
+
* labels active when each sample was taken.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} params params from pprof
|
|
127
|
+
* @param {object} params.context the captured time profile node context
|
|
128
|
+
* @returns {object} label set applied to the pprof sample
|
|
129
|
+
*/
|
|
130
|
+
#generateLabels = ({ context }) => {
|
|
131
|
+
const capturedContext = context?.context
|
|
132
|
+
// With CPED the captured value is the label itself; without it,
|
|
133
|
+
// the label lives in the holder's `current`.
|
|
134
|
+
const { traceId, spanId } = (this.#useCPED ? capturedContext : capturedContext?.current) ?? {}
|
|
135
|
+
// A span id only exists when a trace id does, so trace_id anchors the label set:
|
|
136
|
+
// no trace id means no active span, and span_id is added only when present.
|
|
137
|
+
if (!traceId) {
|
|
138
|
+
return {}
|
|
139
|
+
}
|
|
140
|
+
return spanId ? { span_id: spanId, trace_id: traceId } : { trace_id: traceId }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Refreshes the pprof sample context with the active span. With CPED, the label
|
|
145
|
+
* is written straight into the current async-context-frame; otherwise a fresh
|
|
146
|
+
* holder is swapped in once a sample has landed (so earlier samples keep their
|
|
147
|
+
* ids) before reassigning its `current`.
|
|
148
|
+
*/
|
|
149
|
+
#updateContext = () => {
|
|
150
|
+
if (this.#useCPED) {
|
|
151
|
+
this.#updateCpedContext()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { 'trace.id': traceId, 'span.id': spanId } = this.#tracer.agent.getLinkingMetadata(true)
|
|
156
|
+
const spanContext = { traceId, spanId }
|
|
157
|
+
const sampleCount = this.#state?.[this.#kSampleCount]
|
|
158
|
+
if (sampleCount !== this.#lastSampleCount || this.#context === DEFAULT_CONTEXT) {
|
|
159
|
+
this.#lastSampleCount = sampleCount
|
|
160
|
+
this.#context = { current: spanContext }
|
|
161
|
+
this.#pprof.time.setContext(this.#context)
|
|
162
|
+
} else {
|
|
163
|
+
this.#context.current = spanContext
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* (CPED) Installs the active span's context into the current async-context-frame.
|
|
169
|
+
* Each segment's `{ traceId, spanId }` is built once and cached on the segment
|
|
170
|
+
* (its ids never change), and `setContext` is only called when the frame isn't
|
|
171
|
+
* already holding that object, skipping the redundant native write pprof would
|
|
172
|
+
* otherwise do every time a frame is entered or restored.
|
|
173
|
+
*/
|
|
174
|
+
#updateCpedContext() {
|
|
175
|
+
const segment = this.#tracer.getSegment()
|
|
176
|
+
let spanContext = Object.freeze({})
|
|
177
|
+
if (segment) {
|
|
178
|
+
spanContext = segment[PROFILING_CONTEXT]
|
|
179
|
+
if (spanContext === undefined) {
|
|
180
|
+
const { 'trace.id': traceId, 'span.id': spanId } = this.#tracer.agent.getLinkingMetadata(true)
|
|
181
|
+
spanContext = segment[PROFILING_CONTEXT] = { traceId, spanId }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (this.#pprof.time.getContext() !== spanContext) {
|
|
186
|
+
this.#pprof.time.setContext(spanContext)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* (non-CPED) Points the pprof time profiler at a fresh `#context`
|
|
192
|
+
* and resyncs the sample counter.
|
|
193
|
+
*/
|
|
194
|
+
#resetContext() {
|
|
195
|
+
this.#context = DEFAULT_CONTEXT
|
|
196
|
+
this.#lastSampleCount = this.#state?.[this.#kSampleCount] ?? 0
|
|
197
|
+
this.#pprof.time.setContext(this.#context)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* (CPED) Wraps `enterWith` on the agent's `AsyncLocalStorage`. Since `run`
|
|
202
|
+
* delegates to `enterWith` under ACF, this one hook covers every context
|
|
203
|
+
* switch; we then `setContext` the active span into the current frame, which
|
|
204
|
+
* pprof propagates to continuations and restores on unwind.
|
|
205
|
+
*/
|
|
206
|
+
#hookEnterWith() {
|
|
207
|
+
const self = this
|
|
208
|
+
this.#origEnterWith = this.#store.enterWith
|
|
209
|
+
const origEnterWith = this.#origEnterWith
|
|
210
|
+
this.#store.enterWith = function (store) {
|
|
211
|
+
const retVal = origEnterWith.call(this, store)
|
|
212
|
+
self.#updateContext()
|
|
213
|
+
return retVal
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* (non-CPED) Wraps `run` on the agent's `AsyncLocalStorage` to refresh the
|
|
219
|
+
* holder on `run` entry and exit (so a sibling doesn't inherit a returned
|
|
220
|
+
* child's span). Wraps the instance, not the prototype.
|
|
221
|
+
*/
|
|
222
|
+
#hookRun() {
|
|
223
|
+
const self = this
|
|
224
|
+
this.#origRun = this.#store.run
|
|
225
|
+
const origRun = this.#origRun
|
|
226
|
+
this.#store.run = function (store, callback, ...args) {
|
|
227
|
+
function wrapped(...cbArgs) {
|
|
228
|
+
self.#updateContext()
|
|
229
|
+
return callback.apply(this, cbArgs)
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
return origRun.call(this, store, wrapped, ...args)
|
|
233
|
+
} finally {
|
|
234
|
+
self.#updateContext()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* (non-CPED) Async resumptions (timer, promise, I/O) restore the active store
|
|
241
|
+
* without a `run` call, leaving the holder frozen at the last `run`'s span. A
|
|
242
|
+
* `before` hook refreshes it so those samples get the right span.
|
|
243
|
+
*/
|
|
244
|
+
#hookAsyncResume() {
|
|
245
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
246
|
+
const { createHook } = require('async_hooks')
|
|
247
|
+
this.#asyncHook = createHook({ before: () => this.#updateContext() })
|
|
248
|
+
this.#asyncHook.enable()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Restores the original `run`/`enterWith` and disables the async hook so none
|
|
253
|
+
* fire once the profiler has stopped.
|
|
254
|
+
*/
|
|
255
|
+
#unhookContext() {
|
|
256
|
+
if (this.#origRun) {
|
|
257
|
+
this.#store.run = this.#origRun
|
|
258
|
+
this.#origRun = null
|
|
259
|
+
}
|
|
260
|
+
if (this.#origEnterWith) {
|
|
261
|
+
this.#store.enterWith = this.#origEnterWith
|
|
262
|
+
this.#origEnterWith = null
|
|
263
|
+
}
|
|
264
|
+
this.#asyncHook?.disable()
|
|
265
|
+
this.#asyncHook = null
|
|
266
|
+
}
|
|
45
267
|
}
|
|
46
268
|
|
|
47
269
|
module.exports = CpuProfiler
|
|
@@ -32,6 +32,7 @@ const subscribers = {
|
|
|
32
32
|
...require('./subscribers/langgraph/config'),
|
|
33
33
|
...require('./subscribers/mcp-sdk/config'),
|
|
34
34
|
...require('./subscribers/memcached/config'),
|
|
35
|
+
...require('./subscribers/mongodb/config'),
|
|
35
36
|
...require('./subscribers/mysql/config'),
|
|
36
37
|
...require('./subscribers/mysql2/config'),
|
|
37
38
|
...require('./subscribers/nestjs/config'),
|
package/lib/subscribers/base.js
CHANGED
|
@@ -188,11 +188,12 @@ class Subscriber {
|
|
|
188
188
|
* @param {object} [params.recorder] - Optional recorder for the segment
|
|
189
189
|
* @param {Context} params.ctx - The context containing the parent segment and
|
|
190
190
|
* transaction
|
|
191
|
+
* @param {object} [params.attributes] See {@link #addAttributes}.
|
|
191
192
|
*
|
|
192
193
|
* @returns {AsyncContext} - The updated context with the new segment or
|
|
193
194
|
* existing context if segment creation fails
|
|
194
195
|
*/
|
|
195
|
-
createSegment({ name, recorder, ctx }) {
|
|
196
|
+
createSegment({ name, recorder, ctx, attributes = null }) {
|
|
196
197
|
const parent = ctx?.segment
|
|
197
198
|
|
|
198
199
|
if (this.shouldCreateSegment(parent) === false) {
|
|
@@ -212,7 +213,7 @@ class Subscriber {
|
|
|
212
213
|
segment.shimId = this.packageName
|
|
213
214
|
segment.start()
|
|
214
215
|
this.logger.trace('Created segment %s, returning new context', name)
|
|
215
|
-
this.addAttributes(segment)
|
|
216
|
+
this.addAttributes(segment, attributes)
|
|
216
217
|
return ctx.enterSegment({ segment })
|
|
217
218
|
} else {
|
|
218
219
|
this.logger.trace('Failed to create segment for %s, returning existing context', name)
|
|
@@ -221,13 +222,19 @@ class Subscriber {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
/**
|
|
224
|
-
* By default this is a no-op, but can be overridden by subclasses
|
|
225
|
-
*
|
|
225
|
+
* By default, this is a no-op, but can be overridden by subclasses.
|
|
226
|
+
*
|
|
227
|
+
* @param {Segment} segment The segment to which attributes will be added
|
|
228
|
+
* @param {object} [attributes] A set of attributes to add to the
|
|
229
|
+
* segment. Some subclasses, e.g. the {@link DbSubscriber} subclass, have
|
|
230
|
+
* a mechanism to supply these as an object property
|
|
231
|
+
* (`subscriber.parameters`). But there are cases where the attributes must
|
|
232
|
+
* be calculated during the transaction, and are thus easier to pass through
|
|
233
|
+
* via this parameter.
|
|
234
|
+
*
|
|
226
235
|
* @returns {void}
|
|
227
236
|
*/
|
|
228
|
-
addAttributes(segment) {
|
|
229
|
-
|
|
230
|
-
}
|
|
237
|
+
addAttributes(segment, attributes) {}
|
|
231
238
|
|
|
232
239
|
/**
|
|
233
240
|
* Not all subscribers need to change the context on an event.
|
package/lib/subscribers/db.js
CHANGED
|
@@ -14,6 +14,8 @@ const urltils = require('../util/urltils')
|
|
|
14
14
|
* - `database_name`: The name of the database.
|
|
15
15
|
* - `port_path_or_id`: The database port, path, or ID.
|
|
16
16
|
* @property {string} system The database system being used (e.g., MySQL, MongoDB).
|
|
17
|
+
*
|
|
18
|
+
* @class DbSubscriber
|
|
17
19
|
*/
|
|
18
20
|
class DbSubscriber extends Subscriber {
|
|
19
21
|
/**
|
|
@@ -50,9 +52,16 @@ class DbSubscriber extends Subscriber {
|
|
|
50
52
|
/**
|
|
51
53
|
* Adds `this.parameters` to the active segment.
|
|
52
54
|
* @param {object} segment the current segment
|
|
55
|
+
* @param {object} [attributes] Attributes to add. Defaults to
|
|
56
|
+
* `this.parameters`. If both are defined, this value takes precedence.
|
|
53
57
|
*/
|
|
54
|
-
addAttributes(segment) {
|
|
55
|
-
|
|
58
|
+
addAttributes(segment, attributes = {}) {
|
|
59
|
+
const parameters = Object.assign(
|
|
60
|
+
{},
|
|
61
|
+
this.parameters ?? {},
|
|
62
|
+
attributes
|
|
63
|
+
)
|
|
64
|
+
for (let [key, value] of Object.entries(parameters)) {
|
|
56
65
|
if (this.instanceKeys.includes(key) && !this.instanceReporting) {
|
|
57
66
|
continue
|
|
58
67
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const Wrapper = require('./wrapper.js')
|
|
9
|
+
const getHostDetails = require('./utils/get-host-details.js')
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
DB: DB_METRIC_CONSTANTS,
|
|
13
|
+
MONGODB: MONGODB_METRIC_CONSTANTS
|
|
14
|
+
} = require('#agentlib/metrics/names.js')
|
|
15
|
+
|
|
16
|
+
const ADMIN_COMMANDS = require('./utils/admin-commands.js')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This list is a list of all methods we are interested in instrumenting,
|
|
20
|
+
* regardless of client version. As we iterate through the list we will perform
|
|
21
|
+
* a presence check for each method.
|
|
22
|
+
*
|
|
23
|
+
* @type {string[]}
|
|
24
|
+
*/
|
|
25
|
+
const CURSOR_METHODS = [
|
|
26
|
+
'execute'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
module.exports = class BulkSubscriber extends Wrapper {
|
|
30
|
+
constructor({ agent, logger }) {
|
|
31
|
+
super({
|
|
32
|
+
agent,
|
|
33
|
+
logger,
|
|
34
|
+
channelName: 'nr_bulk',
|
|
35
|
+
packageName: 'mongodb',
|
|
36
|
+
system: MONGODB_METRIC_CONSTANTS.PREFIX
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
end(data, ctx) {
|
|
41
|
+
const { self: bulkOperation, arguments: args } = data
|
|
42
|
+
const [collection] = args
|
|
43
|
+
const collectionName = collection.collectionName
|
|
44
|
+
const details = getHostDetails(collection)
|
|
45
|
+
this.parameters = {
|
|
46
|
+
host: details.host,
|
|
47
|
+
port_path_or_id: details.port_path_or_id,
|
|
48
|
+
database_name: details.database_name,
|
|
49
|
+
product: this.system
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const method of CURSOR_METHODS) {
|
|
53
|
+
if (typeof bulkOperation[method] !== 'function') {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.wrapDatabaseMethod(bulkOperation, method, {
|
|
58
|
+
getSegmentName: () => {
|
|
59
|
+
const operation = bulkOperation.isOrdered ? 'orderedBulk' : 'unorderedBulk'
|
|
60
|
+
return `${DB_METRIC_CONSTANTS.STATEMENT}/${this.system}/${collectionName}/${operation}/batch`
|
|
61
|
+
},
|
|
62
|
+
getRecorderContext: () => {
|
|
63
|
+
return {
|
|
64
|
+
operation: bulkOperation.isOrdered ? 'orderedBulk' : 'unorderedBulk',
|
|
65
|
+
collection: collectionName,
|
|
66
|
+
type: this.type
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
getSegmentAttributes: (method) => (
|
|
70
|
+
ADMIN_COMMANDS.includes(method)
|
|
71
|
+
? { database_name: 'admin' }
|
|
72
|
+
: {}
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return ctx
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const Wrapper = require('./wrapper.js')
|
|
9
|
+
const getHostDetails = require('./utils/get-host-details.js')
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
DB: DB_METRIC_CONSTANTS,
|
|
13
|
+
MONGODB: MONGODB_METRIC_CONSTANTS
|
|
14
|
+
} = require('#agentlib/metrics/names.js')
|
|
15
|
+
|
|
16
|
+
const ADMIN_COMMANDS = require('./utils/admin-commands.js')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This list is a list of all methods we are interested in instrumenting,
|
|
20
|
+
* regardless of client version. As we iterate through the list we will perform
|
|
21
|
+
* a presence check for each method.
|
|
22
|
+
*
|
|
23
|
+
* @type {string[]}
|
|
24
|
+
*/
|
|
25
|
+
const CURSOR_METHODS = [
|
|
26
|
+
'aggregate',
|
|
27
|
+
'bulkWrite',
|
|
28
|
+
'count',
|
|
29
|
+
'createIndex',
|
|
30
|
+
'deleteMany',
|
|
31
|
+
'deleteOne',
|
|
32
|
+
'distinct',
|
|
33
|
+
'drop',
|
|
34
|
+
'dropAllIndexes',
|
|
35
|
+
'dropIndex',
|
|
36
|
+
'ensureIndex',
|
|
37
|
+
'findAndModify',
|
|
38
|
+
'findAndRemove',
|
|
39
|
+
'findOne',
|
|
40
|
+
'findOneAndDelete',
|
|
41
|
+
'findOneAndReplace',
|
|
42
|
+
'findOneAndUpdate',
|
|
43
|
+
'geoHaystackSearch',
|
|
44
|
+
'geoNear',
|
|
45
|
+
'group',
|
|
46
|
+
'indexExists',
|
|
47
|
+
'indexInformation',
|
|
48
|
+
'indexes',
|
|
49
|
+
'insert',
|
|
50
|
+
'insertMany',
|
|
51
|
+
'insertOne',
|
|
52
|
+
'isCapped',
|
|
53
|
+
'mapReduce',
|
|
54
|
+
'options',
|
|
55
|
+
'parallelCollectionScan',
|
|
56
|
+
'reIndex',
|
|
57
|
+
'remove',
|
|
58
|
+
'rename',
|
|
59
|
+
'replaceOne',
|
|
60
|
+
'save',
|
|
61
|
+
'stats',
|
|
62
|
+
'update',
|
|
63
|
+
'updateMany',
|
|
64
|
+
'updateOne'
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
module.exports = class CollectionSubscriber extends Wrapper {
|
|
68
|
+
constructor({ agent, logger }) {
|
|
69
|
+
super({
|
|
70
|
+
agent,
|
|
71
|
+
logger,
|
|
72
|
+
channelName: 'nr_collection',
|
|
73
|
+
packageName: 'mongodb',
|
|
74
|
+
system: MONGODB_METRIC_CONSTANTS.PREFIX
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
end(data, ctx) {
|
|
79
|
+
const { self: dbClient, arguments: args } = data
|
|
80
|
+
const [dbInstance, collectionName] = args
|
|
81
|
+
const details = getHostDetails(dbInstance)
|
|
82
|
+
this.parameters = {
|
|
83
|
+
host: details.host,
|
|
84
|
+
port_path_or_id: details.port_path_or_id,
|
|
85
|
+
database_name: details.database_name,
|
|
86
|
+
product: this.system
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const method of CURSOR_METHODS) {
|
|
90
|
+
if (typeof dbClient[method] !== 'function') {
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.wrapDatabaseMethod(dbClient, method, {
|
|
95
|
+
getSegmentName: (method) => `${DB_METRIC_CONSTANTS.STATEMENT}/${this.system}/${collectionName}/${method}`,
|
|
96
|
+
getRecorderContext: (method) => {
|
|
97
|
+
return {
|
|
98
|
+
operation: method,
|
|
99
|
+
collection: collectionName,
|
|
100
|
+
type: this.type
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
getSegmentAttributes: (method) => (
|
|
104
|
+
ADMIN_COMMANDS.includes(method)
|
|
105
|
+
? { database_name: 'admin' }
|
|
106
|
+
: {}
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return ctx
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const modName = 'mongodb'
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
[modName]: [
|
|
12
|
+
{
|
|
13
|
+
path: './mongodb/db.js',
|
|
14
|
+
instrumentations: [{
|
|
15
|
+
module: {
|
|
16
|
+
name: modName,
|
|
17
|
+
filePath: 'lib/db.js',
|
|
18
|
+
versionRange: '>=4.1.4'
|
|
19
|
+
},
|
|
20
|
+
channelName: 'nr_db',
|
|
21
|
+
functionQuery: {
|
|
22
|
+
className: 'Db'
|
|
23
|
+
}
|
|
24
|
+
}]
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
path: './mongodb/cursor.js',
|
|
29
|
+
instrumentations: [{
|
|
30
|
+
module: {
|
|
31
|
+
name: modName,
|
|
32
|
+
filePath: 'lib/cursor/abstract_cursor.js',
|
|
33
|
+
versionRange: '>=4.1.4'
|
|
34
|
+
},
|
|
35
|
+
channelName: 'nr_cursor',
|
|
36
|
+
functionQuery: {
|
|
37
|
+
className: 'AbstractCursor'
|
|
38
|
+
}
|
|
39
|
+
}]
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
path: './mongodb/collection.js',
|
|
44
|
+
instrumentations: [{
|
|
45
|
+
module: {
|
|
46
|
+
name: modName,
|
|
47
|
+
filePath: 'lib/collection.js',
|
|
48
|
+
versionRange: '>=4.1.4'
|
|
49
|
+
},
|
|
50
|
+
channelName: 'nr_collection',
|
|
51
|
+
functionQuery: {
|
|
52
|
+
className: 'Collection'
|
|
53
|
+
}
|
|
54
|
+
}]
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
path: './mongodb/bulk.js',
|
|
59
|
+
instrumentations: [{
|
|
60
|
+
module: {
|
|
61
|
+
name: modName,
|
|
62
|
+
filePath: 'lib/bulk/common.js',
|
|
63
|
+
versionRange: '>=4.1.4'
|
|
64
|
+
},
|
|
65
|
+
channelName: 'nr_bulk',
|
|
66
|
+
functionQuery: {
|
|
67
|
+
className: 'BulkOperationBase'
|
|
68
|
+
}
|
|
69
|
+
}]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
const Wrapper = require('./wrapper.js')
|
|
9
|
+
const getHostDetails = require('./utils/get-host-details.js')
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
DB: DB_METRIC_CONSTANTS,
|
|
13
|
+
MONGODB: MONGODB_METRIC_CONSTANTS
|
|
14
|
+
} = require('#agentlib/metrics/names.js')
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This list is a list of all methods we are interested in instrumenting,
|
|
18
|
+
* regardless of client version. As we iterate through the list we will perform
|
|
19
|
+
* a presence check for each method.
|
|
20
|
+
*
|
|
21
|
+
* @type {string[]}
|
|
22
|
+
*/
|
|
23
|
+
const CURSOR_METHODS = [
|
|
24
|
+
'count',
|
|
25
|
+
'explain',
|
|
26
|
+
// `forEach` is for mongodb@4 through mongodb@7. It is slated to be
|
|
27
|
+
// removed in some version after 7.
|
|
28
|
+
'forEach',
|
|
29
|
+
'next',
|
|
30
|
+
'nextObject',
|
|
31
|
+
'toArray'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
module.exports = class CursorSubscriber extends Wrapper {
|
|
35
|
+
constructor({ agent, logger }) {
|
|
36
|
+
super({
|
|
37
|
+
agent,
|
|
38
|
+
logger,
|
|
39
|
+
channelName: 'nr_cursor',
|
|
40
|
+
packageName: 'mongodb',
|
|
41
|
+
system: MONGODB_METRIC_CONSTANTS.PREFIX
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
end(data, ctx) {
|
|
46
|
+
const { self: dbClient, arguments: args } = data
|
|
47
|
+
const [mongoClient, dbNamespace] = args
|
|
48
|
+
const { collection } = dbNamespace
|
|
49
|
+
const details = getHostDetails(mongoClient)
|
|
50
|
+
this.parameters = {
|
|
51
|
+
host: details.host,
|
|
52
|
+
port_path_or_id: details.port_path_or_id,
|
|
53
|
+
database_name: dbNamespace.db,
|
|
54
|
+
product: this.system
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const method of CURSOR_METHODS) {
|
|
58
|
+
if (typeof dbClient[method] !== 'function') {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.wrapDatabaseMethod(dbClient, method, {
|
|
63
|
+
getSegmentName: (method) => `${DB_METRIC_CONSTANTS.STATEMENT}/${this.system}/${collection}/${method}`,
|
|
64
|
+
getRecorderContext: (method) => {
|
|
65
|
+
return {
|
|
66
|
+
operation: method,
|
|
67
|
+
collection,
|
|
68
|
+
type: this.type
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return ctx
|
|
75
|
+
}
|
|
76
|
+
}
|