Skip to content

Commit e6f19e0

Browse files
committed
chore: Record subscriber usage metric
# Conflicts: # test/versioned/express/ignoring.test.js # Conflicts: # test/versioned/express/ignoring.test.js
1 parent a84fa74 commit e6f19e0

File tree

10 files changed

+663
-8
lines changed

10 files changed

+663
-8
lines changed

lib/metrics/names.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,8 @@ const FEATURES = {
353353
SOURCE_MAPS: `${SUPPORTABILITY.FEATURES}/EnableSourceMaps`,
354354
CERTIFICATES: SUPPORTABILITY.FEATURES + '/Certificates',
355355
INSTRUMENTATION: {
356-
ON_REQUIRE: SUPPORTABILITY.FEATURES + '/Instrumentation/OnRequire'
356+
ON_REQUIRE: SUPPORTABILITY.FEATURES + '/Instrumentation/OnRequire',
357+
SUBSCRIBER_USED: SUPPORTABILITY.FEATURES + '/Instrumentation/SubscriberUsed'
357358
}
358359
}
359360

lib/subscribers/base.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
// eslint-disable-next-line n/no-unsupported-features/node-builtins
88
const { tracingChannel } = require('node:diagnostics_channel')
99
const cat = require('#agentlib/util/cat.js')
10+
const recordSupportabilityMetric = require('./record-supportability-metric.js')
11+
1012
// Used for the `traceCallback` work.
1113
// This can be removed when we add true support into orchestrion
1214
const makeCall = (fn) => (...args) => fn.call(...args)
@@ -60,6 +62,8 @@ const ArrayPrototypeSplice = makeCall(Array.prototype.splice)
6062
* -1 means last argument.
6163
*/
6264
class Subscriber {
65+
#usageMetricRecorded = false
66+
6367
/**
6468
* @param {SubscriberParams} params the subscriber constructor params
6569
*/
@@ -211,6 +215,14 @@ class Subscriber {
211215
* @returns {Context} either new context or existing
212216
*/
213217
handler(data, ctx) {
218+
if (this.#usageMetricRecorded === false) {
219+
recordSupportabilityMetric({
220+
agent: this.agent,
221+
moduleName: this.packageName,
222+
moduleVersion: data.moduleVersion
223+
})
224+
this.#usageMetricRecorded = true
225+
}
214226
return ctx
215227
}
216228

lib/subscribers/dc-base.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
*/
55

66
'use strict'
7+
78
// eslint-disable-next-line n/no-unsupported-features/node-builtins
89
const dc = require('node:diagnostics_channel')
10+
const recordSupportabilityMetric = require('./record-supportability-metric.js')
11+
const resolvePackageVersion = require('./resolve-package-version.js')
912

1013
/**
1114
* The baseline parameters available to all subscribers.
@@ -34,6 +37,8 @@ const dc = require('node:diagnostics_channel')
3437
* This is the same string one would pass to the `require` function.
3538
*/
3639
class Subscriber {
40+
#usageMetricRecorded = false
41+
3742
/**
3843
* @param {SubscriberParams} params to function
3944
*/
@@ -46,7 +51,7 @@ class Subscriber {
4651

4752
set channels(channels) {
4853
if (!Array.isArray(channels)) {
49-
throw new Error('channels must be a collection of with propertiesof channel and hook')
54+
throw new Error('channels must be a collection of objects with properties channel and hook')
5055
}
5156
this._channels = channels
5257
}
@@ -73,18 +78,43 @@ class Subscriber {
7378

7479
subscribe() {
7580
for (let index = 0; index < this.channels.length; index++) {
81+
const chan = this.channels[index]
7682
const { hook, channel } = this.channels[index]
7783
const boundHook = hook.bind(this)
78-
dc.subscribe(channel, boundHook)
79-
this.channels[index].boundHook = boundHook
84+
chan.boundHook = boundHook
85+
chan.eventHandler = (message, name) => {
86+
this.#supportability()
87+
boundHook(message, name)
88+
}
89+
dc.subscribe(channel, chan.eventHandler)
8090
}
8191
}
8292

8393
unsubscribe() {
8494
for (let index = 0; index < this.channels.length; index++) {
85-
const { channel, boundHook } = this.channels[index]
86-
dc.unsubscribe(channel, boundHook)
95+
const { channel, eventHandler } = this.channels[index]
96+
dc.unsubscribe(channel, eventHandler)
97+
}
98+
}
99+
100+
/**
101+
* Since this class subscribes to tracing channels natively published by
102+
* target modules, we do not get the package metadata that Orchestrion
103+
* provides in its channel events. So we have to try and find the package
104+
* manifest and get the version out of it in order to record our
105+
* supportability metric.
106+
*/
107+
#supportability() {
108+
if (this.#usageMetricRecorded === true) {
109+
return
87110
}
111+
const version = resolvePackageVersion(this.id)
112+
recordSupportabilityMetric({
113+
agent: this.agent,
114+
moduleName: this.id,
115+
moduleVersion: version
116+
})
117+
this.#usageMetricRecorded = true
88118
}
89119
}
90120

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
module.exports = recordSupportabilityMetric
9+
10+
const {
11+
FEATURES: {
12+
INSTRUMENTATION: { SUBSCRIBER_USED }
13+
}
14+
} = require('#agentlib/metrics/names.js')
15+
16+
function recordSupportabilityMetric({
17+
agent,
18+
moduleName,
19+
moduleVersion = 'unknown'
20+
} = {}) {
21+
const metric = agent.metrics.getOrCreateMetric(
22+
`${SUBSCRIBER_USED}/${moduleName}/${moduleVersion}`
23+
)
24+
metric.incrementCallCount()
25+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const path = require('node:path')
9+
const defaultLogger = require('#agentlib/logger.js').child({
10+
component: 'resolve-module-version'
11+
})
12+
13+
module.exports = resolveModuleVersion
14+
15+
/**
16+
* Given a module name, attempt to read the version string from its
17+
* associated package manifest. If the module is a built-in, or one that has
18+
* been bundled with Node.js (e.g. `undici`), a package manifest will not be
19+
* available. In this case, the string "unknown" will be returned.
20+
*
21+
* @param {string} moduleSpecifier What would be passed to `resolve()`.
22+
* @param {object} [deps] Optional dependencies.
23+
* @param {object} [deps.logger] Agent logger instance.
24+
* @param {Function} [deps.req] Node.js require function.
25+
*
26+
* @returns {string} The version string from the package manifest or "unknown".
27+
*/
28+
function resolveModuleVersion(moduleSpecifier, {
29+
logger = defaultLogger,
30+
req = require
31+
} = {}) {
32+
let pkgPath
33+
try {
34+
pkgPath = req.resolve(moduleSpecifier)
35+
} catch {
36+
logger.warn(
37+
{ moduleSpecifier },
38+
'Could not resolve module path. Possibly a built-in or Node.js bundled module.'
39+
)
40+
return 'unknown'
41+
}
42+
43+
const cwd = process.cwd()
44+
let reachedCwd = false
45+
let pkg
46+
let base = path.dirname(pkgPath)
47+
do {
48+
try {
49+
pkgPath = path.join(base, 'package.json')
50+
pkg = req(pkgPath)
51+
} catch {
52+
base = path.resolve(path.join(base, '..'))
53+
if (base === cwd) {
54+
reachedCwd = true
55+
} else if (reachedCwd === true) {
56+
// We reached the supposed app root, attempted to load a manifest
57+
// file in that location, and still couldn't find one. So we give up.
58+
pkg = {}
59+
}
60+
}
61+
} while (!pkg)
62+
63+
const version = pkg.version ?? 'unknown'
64+
logger.trace({ moduleSpecifier, version }, 'Resolved package version.')
65+
return version
66+
}

0 commit comments

Comments
 (0)