newrelic 13.0.0 → 13.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 +92 -0
- package/README.md +2 -4
- package/THIRD_PARTY_NOTICES.md +429 -9
- package/esm-loader.mjs +9 -1
- package/index.js +38 -3
- package/lib/agent.js +8 -2
- package/lib/config/build-instrumentation-config.js +3 -0
- package/lib/config/default.js +1397 -1383
- package/lib/config/index.js +15 -19
- package/lib/context-manager/context.js +13 -4
- package/lib/harvester.js +11 -13
- package/lib/instrumentation/@google/genai.js +8 -3
- package/lib/instrumentation/@hapi/hapi.js +4 -6
- package/lib/instrumentation/@nestjs/core.js +12 -14
- package/lib/instrumentation/aws-sdk/v3/bedrock.js +2 -2
- package/lib/instrumentation/core/http-outbound.js +11 -26
- package/lib/instrumentation/kafkajs/producer.js +13 -19
- package/lib/instrumentation/openai.js +2 -2
- package/lib/instrumentation/undici.js +48 -40
- package/lib/instrumentations.js +14 -5
- package/lib/llm-events/aws-bedrock/utils.js +1 -1
- package/lib/metrics/names.js +8 -0
- package/lib/metrics/normalizer.js +1 -3
- package/lib/otel/context.js +18 -14
- package/lib/otel/logs/bootstrap-logs.js +84 -0
- package/lib/otel/logs/no-op-exporter.js +25 -0
- package/lib/otel/logs/normalize-timestamp.js +59 -0
- package/lib/otel/logs/proxying-provider.js +46 -0
- package/lib/otel/logs/severity-to-string.js +56 -0
- package/lib/otel/metrics/bootstrap-metrics.js +1 -1
- package/lib/otel/setup.js +21 -8
- package/lib/patch-module.js +70 -0
- package/lib/serverless/aws-lambda.js +1 -3
- package/lib/shim/webframework-shim/middleware-mounter.js +1 -3
- package/lib/shimmer.js +56 -8
- package/lib/subscriber-configs.js +17 -0
- package/lib/subscribers/application-logs.js +55 -0
- package/lib/subscribers/base.js +177 -0
- package/lib/subscribers/create-config.js +27 -0
- package/lib/subscribers/db-operation.js +21 -0
- package/lib/subscribers/db-query.js +54 -0
- package/lib/subscribers/db.js +57 -0
- package/lib/subscribers/elasticsearch/config.js +49 -0
- package/lib/subscribers/elasticsearch/elasticsearch.js +42 -0
- package/lib/subscribers/elasticsearch/opensearch.js +15 -0
- package/lib/subscribers/elasticsearch/transport.js +14 -0
- package/lib/subscribers/ioredis/config.js +37 -0
- package/lib/subscribers/ioredis/index.js +48 -0
- package/lib/subscribers/mcp-sdk/client-prompt.js +23 -0
- package/lib/subscribers/mcp-sdk/client-resource.js +24 -0
- package/lib/subscribers/mcp-sdk/client-tool.js +23 -0
- package/lib/subscribers/mcp-sdk/client.js +29 -0
- package/lib/subscribers/mcp-sdk/config.js +104 -0
- package/lib/subscribers/pino/config.js +20 -0
- package/lib/subscribers/pino/index.js +102 -0
- package/lib/transaction/index.js +4 -1
- package/lib/transaction/trace/aggregator.js +7 -9
- package/lib/util/camel-case.js +1 -3
- package/lib/util/get-package-version.js +34 -0
- package/lib/util/urltils.js +19 -13
- package/lib/w3c/tracestate.js +3 -3
- package/newrelic.js +10 -0
- package/package.json +9 -5
- package/lib/instrumentation/@elastic/elasticsearch.js +0 -62
- package/lib/instrumentation/@opensearch-project/opensearch.js +0 -66
- package/lib/instrumentation/ioredis.js +0 -50
- package/lib/instrumentation/pino/nr-hooks.js +0 -19
- package/lib/instrumentation/pino/pino.js +0 -168
package/lib/otel/context.js
CHANGED
|
@@ -9,9 +9,13 @@ const FakeSpan = require('./fake-span')
|
|
|
9
9
|
const Context = require('../context-manager/context')
|
|
10
10
|
|
|
11
11
|
module.exports = class OtelContext extends Context {
|
|
12
|
-
constructor(transaction, segment,
|
|
13
|
-
super(transaction, segment)
|
|
14
|
-
this._otelCtx =
|
|
12
|
+
constructor({ transaction, segment, otelContext, extras } = {}) {
|
|
13
|
+
super({ transaction, segment, extras })
|
|
14
|
+
this._otelCtx = otelContext ? new Map(otelContext) : new Map()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get otelCtx() {
|
|
18
|
+
return this._otelCtx
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/**
|
|
@@ -23,11 +27,11 @@ module.exports = class OtelContext extends Context {
|
|
|
23
27
|
* @param {Transaction} params.transaction active transaction
|
|
24
28
|
* @returns {OtelContext} a newly constructed context
|
|
25
29
|
*/
|
|
26
|
-
enterSegment({ segment, transaction = this.
|
|
30
|
+
enterSegment({ segment, transaction = this.transaction }) {
|
|
27
31
|
if (transaction?.agent?.otelSpanKey && segment) {
|
|
28
|
-
this.
|
|
32
|
+
this.otelCtx.set(transaction.agent.otelSpanKey, new FakeSpan(segment, transaction))
|
|
29
33
|
}
|
|
30
|
-
return new this.constructor(transaction, segment, this.
|
|
34
|
+
return new this.constructor({ transaction, segment, otelContext: this.otelCtx, extras: this.extras })
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -39,9 +43,9 @@ module.exports = class OtelContext extends Context {
|
|
|
39
43
|
*/
|
|
40
44
|
enterTransaction(transaction) {
|
|
41
45
|
if (transaction?.agent?.otelSpanKey) {
|
|
42
|
-
this.
|
|
46
|
+
this.otelCtx.set(transaction.agent.otelSpanKey, new FakeSpan(transaction.trace.root, transaction))
|
|
43
47
|
}
|
|
44
|
-
return new this.constructor(transaction, transaction?.trace?.root, this.
|
|
48
|
+
return new this.constructor({ transaction, segment: transaction?.trace?.root, otelContext: this.otelCtx, extras: this.extras })
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/**
|
|
@@ -52,7 +56,7 @@ module.exports = class OtelContext extends Context {
|
|
|
52
56
|
* @returns {*} The stored value.
|
|
53
57
|
*/
|
|
54
58
|
getValue(key) {
|
|
55
|
-
return this.
|
|
59
|
+
return this.otelCtx.get(key)
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
/**
|
|
@@ -69,12 +73,12 @@ module.exports = class OtelContext extends Context {
|
|
|
69
73
|
if (value[otelSynthesis] && value[otelSynthesis].segment && value[otelSynthesis].transaction) {
|
|
70
74
|
const { segment, transaction } = value[otelSynthesis]
|
|
71
75
|
segment.start()
|
|
72
|
-
ctx = new this.constructor(transaction, segment, this.
|
|
76
|
+
ctx = new this.constructor({ transaction, segment, otelContext: this.otelCtx, extras: this.extras })
|
|
73
77
|
} else {
|
|
74
|
-
ctx = new this.constructor(this.
|
|
78
|
+
ctx = new this.constructor({ transaction: this.transaction, segment: this.segment, otelContext: this.otelCtx, extras: this.extras })
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
ctx.
|
|
81
|
+
ctx.otelCtx.set(key, value)
|
|
78
82
|
return ctx
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -86,8 +90,8 @@ module.exports = class OtelContext extends Context {
|
|
|
86
90
|
* @returns {OtelContext} The context object.
|
|
87
91
|
*/
|
|
88
92
|
deleteValue(key) {
|
|
89
|
-
const ctx = new this.constructor(this._transaction, this._segment, this.
|
|
90
|
-
ctx.
|
|
93
|
+
const ctx = new this.constructor({ transaction: this._transaction, segment: this._segment, otelContext: this.otelCtx, extras: this.extras })
|
|
94
|
+
ctx.otelCtx.delete(key)
|
|
91
95
|
return ctx
|
|
92
96
|
}
|
|
93
97
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 = bootstrapOtelLogs
|
|
9
|
+
|
|
10
|
+
const logsApi = require('@opentelemetry/api-logs')
|
|
11
|
+
const logsSdk = require('@opentelemetry/sdk-logs')
|
|
12
|
+
const defaultLogger = require('#agentlib/logger.js').child({ component: 'otel-logs' })
|
|
13
|
+
const {
|
|
14
|
+
isApplicationLoggingEnabled,
|
|
15
|
+
isLogForwardingEnabled,
|
|
16
|
+
isMetricsEnabled,
|
|
17
|
+
incrementLoggingLinesMetrics
|
|
18
|
+
} = require('#agentlib/util/application-logging.js')
|
|
19
|
+
|
|
20
|
+
const NewRelicLoggerProvider = require('./proxying-provider.js')
|
|
21
|
+
const NoOpExporter = require('./no-op-exporter.js')
|
|
22
|
+
const normalizeTimestamp = require('./normalize-timestamp.js')
|
|
23
|
+
const severityToString = require('./severity-to-string.js')
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sets up the OTEL logging system and instruments it in such a fashion
|
|
27
|
+
* that logs are shipped to New Relic when logs forwarding is enabled.
|
|
28
|
+
*
|
|
29
|
+
* The API, as of 2025-07, is in development. We should expect changes that
|
|
30
|
+
* will require significant refactoring here. For example, we might need to
|
|
31
|
+
* accept instances of various components, e.g. record processors, in order
|
|
32
|
+
* for customer apps to work as they expect.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} params Factory function parameters.
|
|
35
|
+
* @param {Agent} params.agent The Node.js agent instance.
|
|
36
|
+
* @param {object} [params.logger] An agent logger instance.
|
|
37
|
+
*/
|
|
38
|
+
function bootstrapOtelLogs({ agent, logger = defaultLogger } = {}) {
|
|
39
|
+
if (isApplicationLoggingEnabled(agent.config) === false) {
|
|
40
|
+
logger.info('application logging disabled, skipping otel logs setup')
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
agent.metrics
|
|
45
|
+
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Logs')
|
|
46
|
+
.incrementCallCount()
|
|
47
|
+
|
|
48
|
+
const exporter = new NoOpExporter()
|
|
49
|
+
const processor = new logsSdk.BatchLogRecordProcessor(exporter)
|
|
50
|
+
const otelProvider = new logsSdk.LoggerProvider({
|
|
51
|
+
processors: [processor]
|
|
52
|
+
})
|
|
53
|
+
const provider = new NewRelicLoggerProvider({
|
|
54
|
+
agent,
|
|
55
|
+
provider: otelProvider,
|
|
56
|
+
emitHandler: nrEmitHandler
|
|
57
|
+
})
|
|
58
|
+
logsApi.logs.setGlobalLoggerProvider(provider)
|
|
59
|
+
|
|
60
|
+
function nrEmitHandler(record) {
|
|
61
|
+
const level = severityToString(record.severityNumber ?? 0)
|
|
62
|
+
if (isMetricsEnabled(agent.config) === true) {
|
|
63
|
+
incrementLoggingLinesMetrics(level, agent.metrics)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// TODO: if we decide to support local decorating, implement it here
|
|
67
|
+
|
|
68
|
+
if (isLogForwardingEnabled(agent.config, agent) === true) {
|
|
69
|
+
const meta = agent.getLinkingMetadata(true)
|
|
70
|
+
const timestamp = normalizeTimestamp(record.timestamp)
|
|
71
|
+
const logData = {
|
|
72
|
+
message: record.body,
|
|
73
|
+
level,
|
|
74
|
+
timestamp,
|
|
75
|
+
...record.attributes,
|
|
76
|
+
...meta
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
agent.logs.add(logData)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
agent.emit('otelLogsBootstrapped')
|
|
84
|
+
}
|
|
@@ -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
|
+
const { ExportResultCode } = require('@opentelemetry/core')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @see https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk-node.logs.LogRecordExporter.html
|
|
12
|
+
*/
|
|
13
|
+
class NewRelicNoOpExporter {
|
|
14
|
+
export(_, done) {
|
|
15
|
+
if (typeof done === 'function') {
|
|
16
|
+
done(ExportResultCode.SUCCESS)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
shutdown() {
|
|
21
|
+
return Promise.resolve()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = NewRelicNoOpExporter
|
|
@@ -0,0 +1,59 @@
|
|
|
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 = normalizeTimestamp
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
isTimeInputHrTime
|
|
12
|
+
} = require('@opentelemetry/core')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns a normalized epoch in milliseconds based upon the value provided
|
|
16
|
+
* to the logger's `emit` method (if it was provided). The emit method receives
|
|
17
|
+
* a `LogRecord` instance. In the upstream code, a `LogRecord` is supposed to
|
|
18
|
+
* have a nanosecond epoch delta. But we are receiving it from user code. So
|
|
19
|
+
* it is possible for the value to be a variety of different kinds.
|
|
20
|
+
*
|
|
21
|
+
* @see https://github.com/open-telemetry/opentelemetry-js/blob/99dde77/api/src/common/Time.ts#L39
|
|
22
|
+
* @see https://github.com/open-telemetry/opentelemetry-js/blob/99dde77/packages/opentelemetry-core/src/common/time.ts#L61-L83
|
|
23
|
+
*
|
|
24
|
+
* @param {number[]|number|Date|undefined} input The value from the logger.
|
|
25
|
+
*
|
|
26
|
+
* @returns {number}
|
|
27
|
+
*/
|
|
28
|
+
function normalizeTimestamp(input) {
|
|
29
|
+
if (typeof input === 'number' && input >= 10_000_000_000_000_000) {
|
|
30
|
+
// Looks like a nanosecond epoch delta.
|
|
31
|
+
return Math.floor(input / 1_000_000)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof input === 'number' && input >= 100_000_000_000_000) {
|
|
35
|
+
// Looks like a microsecond epoch delta.
|
|
36
|
+
return Math.floor(input / 1_000)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof input === 'number' && input >= 100_000_000_000) {
|
|
40
|
+
// Looks like a millisecond epoch delta.
|
|
41
|
+
return input
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isTimeInputHrTime(input) === true) {
|
|
45
|
+
// It's unclear why upstream tries to support a `process.hrtime()` value.
|
|
46
|
+
// Such tuples are relative to an arbitrary point in time, not the Unix
|
|
47
|
+
// epoch. So there's no way to determine an actual wall clock time and date
|
|
48
|
+
// from such input. For lack of a better solution, we'll return the
|
|
49
|
+
// current epoch in this case.
|
|
50
|
+
return Date.now()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Object.getPrototypeOf(input) === Date.prototype) {
|
|
54
|
+
return input.getTime()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// We don't know what they've given us, so just return the current time.
|
|
58
|
+
return Date.now()
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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 symLogPatched = Symbol('nr.log.patched')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provides a logger provider that proxies to another logger provider.
|
|
12
|
+
* This allows us to intercept loggers as they are retrieved so that we can
|
|
13
|
+
* patch their `emit` method to do what we need.
|
|
14
|
+
*
|
|
15
|
+
* @see https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api-logs.LoggerProvider.html
|
|
16
|
+
*/
|
|
17
|
+
class NewRelicLoggerProvider {
|
|
18
|
+
#provider
|
|
19
|
+
#emitHandler
|
|
20
|
+
|
|
21
|
+
constructor({ provider, emitHandler }) {
|
|
22
|
+
this.#provider = provider
|
|
23
|
+
this.#emitHandler = emitHandler
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getLogger(name, version, options) {
|
|
27
|
+
const logger = this.#provider.getLogger(name, version, options)
|
|
28
|
+
const emit = logger.emit
|
|
29
|
+
const handler = this.#emitHandler
|
|
30
|
+
|
|
31
|
+
// Logger has already been patched, so just return it.
|
|
32
|
+
if (logger[symLogPatched] === true) {
|
|
33
|
+
return logger
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logger.emit = function nrEmitWrapper(record) {
|
|
37
|
+
handler(record)
|
|
38
|
+
return emit.apply(this, arguments)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
logger[symLogPatched] = true
|
|
42
|
+
return logger
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = NewRelicLoggerProvider
|
|
@@ -0,0 +1,56 @@
|
|
|
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 = severityToString
|
|
9
|
+
|
|
10
|
+
const SEV_UNKNOWN = 'unknown'
|
|
11
|
+
const SEV_TRACE = 'trace'
|
|
12
|
+
const SEV_DEBUG = 'debug'
|
|
13
|
+
const SEV_INFO = 'info'
|
|
14
|
+
const SEV_WARN = 'warn'
|
|
15
|
+
const SEV_ERROR = 'error'
|
|
16
|
+
const SEV_FATAL = 'fatal'
|
|
17
|
+
const severityMap = new Map([
|
|
18
|
+
[0, SEV_UNKNOWN],
|
|
19
|
+
[1, SEV_TRACE],
|
|
20
|
+
[2, SEV_TRACE],
|
|
21
|
+
[3, SEV_TRACE],
|
|
22
|
+
[4, SEV_TRACE],
|
|
23
|
+
[5, SEV_DEBUG],
|
|
24
|
+
[6, SEV_DEBUG],
|
|
25
|
+
[7, SEV_DEBUG],
|
|
26
|
+
[8, SEV_DEBUG],
|
|
27
|
+
[9, SEV_INFO],
|
|
28
|
+
[10, SEV_INFO],
|
|
29
|
+
[11, SEV_INFO],
|
|
30
|
+
[12, SEV_INFO],
|
|
31
|
+
[13, SEV_WARN],
|
|
32
|
+
[14, SEV_WARN],
|
|
33
|
+
[15, SEV_WARN],
|
|
34
|
+
[16, SEV_WARN],
|
|
35
|
+
[17, SEV_ERROR],
|
|
36
|
+
[18, SEV_ERROR],
|
|
37
|
+
[19, SEV_ERROR],
|
|
38
|
+
[20, SEV_ERROR],
|
|
39
|
+
[21, SEV_FATAL],
|
|
40
|
+
[22, SEV_FATAL],
|
|
41
|
+
[23, SEV_FATAL],
|
|
42
|
+
[24, SEV_FATAL]
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Converts the OTEL severity number to a recognizable string.
|
|
47
|
+
*
|
|
48
|
+
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/c041658/specification/logs/data-model.md?plain=1#L292-L299
|
|
49
|
+
*
|
|
50
|
+
* @param {number} severityNumber The number to convert.
|
|
51
|
+
*
|
|
52
|
+
* @returns {string} The known string.
|
|
53
|
+
*/
|
|
54
|
+
function severityToString(severityNumber) {
|
|
55
|
+
return severityMap.get(severityNumber) ?? SEV_UNKNOWN
|
|
56
|
+
}
|
|
@@ -52,7 +52,7 @@ function bootstrapOtelMetrics(agent) {
|
|
|
52
52
|
|
|
53
53
|
const meter = getMeter.apply(provider, args)
|
|
54
54
|
const proto = Object.getPrototypeOf(meter)
|
|
55
|
-
const methods = Object.getOwnPropertyNames(proto).filter(name => name.startsWith('create'))
|
|
55
|
+
const methods = Object.getOwnPropertyNames(proto).filter((name) => name.startsWith('create'))
|
|
56
56
|
const originals = {}
|
|
57
57
|
for (const method of methods) {
|
|
58
58
|
originals[method] = meter[method]
|
package/lib/otel/setup.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
'use strict'
|
|
7
7
|
|
|
8
8
|
const opentelemetry = require('@opentelemetry/api')
|
|
9
|
+
const logsApi = require('@opentelemetry/api-logs')
|
|
9
10
|
const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base')
|
|
10
11
|
|
|
11
12
|
const NrSpanProcessor = require('./span-processor')
|
|
@@ -16,6 +17,7 @@ const defaultLogger = require('../logger').child({ component: 'opentelemetry-bri
|
|
|
16
17
|
const createOtelLogger = require('./logger')
|
|
17
18
|
const interceptSpanKey = require('./span-key-interceptor')
|
|
18
19
|
const bootstrapMetrics = require('./metrics/bootstrap-metrics')
|
|
20
|
+
const bootstrapLogs = require('./logs/bootstrap-logs')
|
|
19
21
|
|
|
20
22
|
function setupOtel(agent, logger = defaultLogger) {
|
|
21
23
|
if (agent.config.opentelemetry_bridge.enabled !== true) {
|
|
@@ -25,8 +27,18 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
25
27
|
return
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
// When bridge mode is enabled, our context manager must utilize the OTEL
|
|
31
|
+
// context manager. Otherwise, traces within, e.g. logs, will not be
|
|
32
|
+
// captured correctly. The internal OTEL logger also must be configured
|
|
33
|
+
// for the OTEL context manager to generate diagnostics messages that get
|
|
34
|
+
// delivered to the correct place.
|
|
35
|
+
createOtelLogger(logger, agent.config)
|
|
36
|
+
interceptSpanKey(agent)
|
|
37
|
+
opentelemetry.context.setGlobalContextManager(new ContextManager(agent))
|
|
38
|
+
opentelemetry.propagation.setGlobalPropagator(new TracePropagator(agent))
|
|
39
|
+
|
|
28
40
|
if (agent.config.opentelemetry_bridge.traces.enabled === true) {
|
|
29
|
-
bootstrapTraces(agent
|
|
41
|
+
bootstrapTraces(agent)
|
|
30
42
|
} else {
|
|
31
43
|
logger.debug('`opentelemetry_bridge.traces` is not enabled, skipping')
|
|
32
44
|
}
|
|
@@ -37,14 +49,18 @@ function setupOtel(agent, logger = defaultLogger) {
|
|
|
37
49
|
logger.debug('`opentelemetry_bridge.metrics` is not enabled, skipping')
|
|
38
50
|
}
|
|
39
51
|
|
|
52
|
+
if (agent.config.opentelemetry_bridge.logs.enabled === true) {
|
|
53
|
+
bootstrapLogs({ agent })
|
|
54
|
+
} else {
|
|
55
|
+
logger.debug('`opentelemetry_bridge.logs` is not enabled, skipping')
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
agent.metrics
|
|
41
59
|
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Setup')
|
|
42
60
|
.incrementCallCount()
|
|
43
61
|
}
|
|
44
62
|
|
|
45
|
-
function bootstrapTraces(agent
|
|
46
|
-
createOtelLogger(logger, agent.config)
|
|
47
|
-
|
|
63
|
+
function bootstrapTraces(agent) {
|
|
48
64
|
opentelemetry.trace.setGlobalTracerProvider(new BasicTracerProvider({
|
|
49
65
|
sampler: new NrSampler(),
|
|
50
66
|
spanProcessors: [new NrSpanProcessor(agent)],
|
|
@@ -53,10 +69,6 @@ function bootstrapTraces(agent, logger) {
|
|
|
53
69
|
}
|
|
54
70
|
}))
|
|
55
71
|
|
|
56
|
-
interceptSpanKey(agent)
|
|
57
|
-
opentelemetry.context.setGlobalContextManager(new ContextManager(agent))
|
|
58
|
-
opentelemetry.propagation.setGlobalPropagator(new TracePropagator(agent))
|
|
59
|
-
|
|
60
72
|
agent.metrics
|
|
61
73
|
.getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Traces')
|
|
62
74
|
.incrementCallCount()
|
|
@@ -71,6 +83,7 @@ function teardownOtel(agent) {
|
|
|
71
83
|
opentelemetry.context.disable()
|
|
72
84
|
opentelemetry.propagation.disable()
|
|
73
85
|
opentelemetry.diag.disable()
|
|
86
|
+
logsApi.logs.disable()
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
module.exports = {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { create } = require('@apm-js-collab/code-transformer')
|
|
7
|
+
const Module = require('node:module')
|
|
8
|
+
const parse = require('module-details-from-path')
|
|
9
|
+
const getPackageVersion = require('./util/get-package-version')
|
|
10
|
+
const logger = require('./logger').child({ component: 'ModulePatch' })
|
|
11
|
+
|
|
12
|
+
class ModulePatch {
|
|
13
|
+
constructor({ packages = new Set(), instrumentations = [] } = {}) {
|
|
14
|
+
this.packages = packages
|
|
15
|
+
this.instrumentator = create(instrumentations)
|
|
16
|
+
this.transformers = new Map()
|
|
17
|
+
this.resolve = Module._resolveFilename
|
|
18
|
+
this.compile = Module.prototype._compile
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Patches the Node.js module class methods that are responsible for resolving filePaths and compiling code.
|
|
23
|
+
* If a module is found that has an instrumentator, it will transform the code before compiling it
|
|
24
|
+
* with tracing channel methods.
|
|
25
|
+
*/
|
|
26
|
+
patch() {
|
|
27
|
+
const self = this
|
|
28
|
+
Module._resolveFilename = function wrappedResolveFileName() {
|
|
29
|
+
const resolvedName = self.resolve.apply(this, arguments)
|
|
30
|
+
const resolvedModule = parse(resolvedName)
|
|
31
|
+
if (resolvedModule && self.packages.has(resolvedModule.name)) {
|
|
32
|
+
const version = getPackageVersion(resolvedModule.basedir, resolvedModule.name)
|
|
33
|
+
const transformer = self.instrumentator.getTransformer(resolvedModule.name, version, resolvedModule.path)
|
|
34
|
+
if (transformer) {
|
|
35
|
+
self.transformers.set(resolvedName, transformer)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return resolvedName
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Module.prototype._compile = function wrappedCompile(...args) {
|
|
42
|
+
const [content, filename] = args
|
|
43
|
+
if (self.transformers.has(filename)) {
|
|
44
|
+
const transformer = self.transformers.get(filename)
|
|
45
|
+
try {
|
|
46
|
+
const transformedCode = transformer.transform(content, 'unknown')
|
|
47
|
+
args[0] = transformedCode
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error({ error }, `Error transforming module ${filename}`)
|
|
50
|
+
} finally {
|
|
51
|
+
transformer.free()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return self.compile.apply(this, args)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Clears all the transformers and restores the original Module methods that were wrapped.
|
|
61
|
+
* **Note**: This is intended to be used in testing only.
|
|
62
|
+
*/
|
|
63
|
+
unpatch() {
|
|
64
|
+
this.transformers.clear()
|
|
65
|
+
Module._resolveFilename = this.resolve
|
|
66
|
+
Module.prototype._compile = this.compile
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = ModulePatch
|
|
@@ -54,9 +54,7 @@ class AwsLambda {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
_detectEventType(event) {
|
|
57
|
-
const pathMatch = (obj, path) =>
|
|
58
|
-
return get(obj, path, null) !== null
|
|
59
|
-
}
|
|
57
|
+
const pathMatch = (obj, path) => get(obj, path, null) !== null
|
|
60
58
|
|
|
61
59
|
for (const typeInfo of Object.values(EVENT_SOURCE_INFO)) {
|
|
62
60
|
if (typeInfo.required_keys.every((path) => pathMatch(event, path))) {
|
|
@@ -34,9 +34,7 @@ function wrapMounter(spec, shim, fn, fnName) {
|
|
|
34
34
|
routeIdx = null
|
|
35
35
|
route = null
|
|
36
36
|
} else if (shim.isArray(route)) {
|
|
37
|
-
route = route.map((routeArg) =>
|
|
38
|
-
return shim._routeParser.call(this, shim, fn, fnName, routeArg)
|
|
39
|
-
})
|
|
37
|
+
route = route.map((routeArg) => shim._routeParser.call(this, shim, fn, fnName, routeArg))
|
|
40
38
|
} else {
|
|
41
39
|
route = shim._routeParser.call(this, shim, fn, fnName, route)
|
|
42
40
|
}
|
package/lib/shimmer.js
CHANGED
|
@@ -23,7 +23,9 @@ let pkgsToHook = []
|
|
|
23
23
|
const NAMES = require('./metrics/names')
|
|
24
24
|
const symbols = require('./symbols')
|
|
25
25
|
const { unsubscribe } = require('./instrumentation/undici')
|
|
26
|
-
const
|
|
26
|
+
const subscriptions = require('./subscriber-configs')
|
|
27
|
+
const createSubscriberConfigs = require('./subscribers/create-config')
|
|
28
|
+
const ModulePatch = require('./patch-module')
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Unwrapping is only likely to be used by test code, and is a fairly drastic
|
|
@@ -329,14 +331,32 @@ const shimmer = (module.exports = {
|
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
pkgsToHook = []
|
|
332
|
-
|
|
334
|
+
if (agent?.config?.opentelemetry_bridge?.enabled === true) {
|
|
335
|
+
// We must lazy load the method in order to support stripping of the
|
|
336
|
+
// `@opentelemetry` packages for slim builds.
|
|
337
|
+
require('./otel/setup').teardownOtel(agent)
|
|
338
|
+
}
|
|
333
339
|
unsubscribe()
|
|
340
|
+
if (this._subscribers) {
|
|
341
|
+
shimmer.teardownSubscribers()
|
|
342
|
+
}
|
|
343
|
+
if (this._modulePatch) {
|
|
344
|
+
this._modulePatch.unpatch()
|
|
345
|
+
}
|
|
334
346
|
},
|
|
335
347
|
|
|
336
348
|
bootstrapInstrumentation: function bootstrapInstrumentation(agent) {
|
|
349
|
+
const subscriberConfigs = createSubscriberConfigs(subscriptions)
|
|
350
|
+
this._modulePatch = new ModulePatch(subscriberConfigs)
|
|
351
|
+
this._modulePatch.patch()
|
|
337
352
|
shimmer.registerCoreInstrumentation(agent)
|
|
338
353
|
shimmer.registerThirdPartyInstrumentation(agent)
|
|
339
|
-
|
|
354
|
+
shimmer.setupSubscribers(agent)
|
|
355
|
+
if (agent?.config?.opentelemetry_bridge?.enabled === true) {
|
|
356
|
+
// We must lazy load the method in order to support stripping of the
|
|
357
|
+
// `@opentelemetry` packages for slim builds.
|
|
358
|
+
require('./otel/setup').setupOtel(agent)
|
|
359
|
+
}
|
|
340
360
|
},
|
|
341
361
|
|
|
342
362
|
registerInstrumentation: function registerInstrumentation(opts) {
|
|
@@ -371,6 +391,35 @@ const shimmer = (module.exports = {
|
|
|
371
391
|
|
|
372
392
|
registeredInstrumentations: new InstrumentationTracker(),
|
|
373
393
|
|
|
394
|
+
setupSubscribers: function setupSubscribers(agent) {
|
|
395
|
+
this._subscribers = {}
|
|
396
|
+
for (const subscriberConfigList of Object.values(subscriptions)) {
|
|
397
|
+
for (const subscriberConfig of subscriberConfigList) {
|
|
398
|
+
const Subscriber = require(`./subscribers/${subscriberConfig.path}`)
|
|
399
|
+
const subscriber = new Subscriber({ agent, logger })
|
|
400
|
+
if (subscriber.enabled === false) {
|
|
401
|
+
logger.debug(
|
|
402
|
+
'Skipping subscriber %s because it is disabled in the config',
|
|
403
|
+
subscriber.id
|
|
404
|
+
)
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
subscriber.enable()
|
|
409
|
+
subscriber.subscribe()
|
|
410
|
+
this._subscribers[subscriber.id] = subscriber
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
teardownSubscribers: function teardownSubscribers() {
|
|
416
|
+
for (const subscriber of Object.values(this._subscribers)) {
|
|
417
|
+
subscriber.disable()
|
|
418
|
+
subscriber.unsubscribe()
|
|
419
|
+
}
|
|
420
|
+
this._subscribers = {}
|
|
421
|
+
},
|
|
422
|
+
|
|
374
423
|
/**
|
|
375
424
|
* NOT FOR USE IN PRODUCTION CODE
|
|
376
425
|
*
|
|
@@ -432,8 +481,7 @@ const shimmer = (module.exports = {
|
|
|
432
481
|
isInstrumented(moduleName, resolvedName) {
|
|
433
482
|
const allItems = shimmer.registeredInstrumentations.getAllByName(moduleName)
|
|
434
483
|
const items = allItems.filter(
|
|
435
|
-
(item) =>
|
|
436
|
-
item.instrumentation.resolvedName === resolvedName && item.meta.instrumented === true
|
|
484
|
+
(item) => item.instrumentation.resolvedName === resolvedName && item.meta.instrumented === true
|
|
437
485
|
)
|
|
438
486
|
return items.length === allItems.length
|
|
439
487
|
},
|
|
@@ -669,12 +717,12 @@ function _firstPartyInstrumentation(agent, fileName, shim, nodule, moduleName) {
|
|
|
669
717
|
* to load the module), as well as the `onRequire` and `onError` hooks to
|
|
670
718
|
* attach to the module.
|
|
671
719
|
*
|
|
672
|
-
* @param {object} agent
|
|
720
|
+
* @param {object} agent agent instance
|
|
673
721
|
* @param {object} nodule The newly loaded module.
|
|
674
722
|
* @param {string} name The simple name used to load the module.
|
|
675
723
|
* @param {string} resolvedName The full file system path to the module.
|
|
676
|
-
* @param {object} [esmResolver]
|
|
677
|
-
* @returns {*|Object|undefined}
|
|
724
|
+
* @param {object} [esmResolver] If the module was loaded via ESM
|
|
725
|
+
* @returns {*|Object|undefined} The instrumented module, or the original
|
|
678
726
|
* @private
|
|
679
727
|
*/
|
|
680
728
|
function _postLoad(agent, nodule, name, resolvedName, esmResolver) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
// The expected export of these files is:
|
|
9
|
+
// 'package-name': [ { path: 'subscriberPath', instrumentations: [] }, ... ]
|
|
10
|
+
const subscribers = {
|
|
11
|
+
...require('./subscribers/elasticsearch/config'),
|
|
12
|
+
...require('./subscribers/ioredis/config'),
|
|
13
|
+
...require('./subscribers/mcp-sdk/config'),
|
|
14
|
+
...require('./subscribers/pino/config'),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = subscribers
|