dd-trace 5.106.0 → 5.108.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/index.d.ts +20 -1
- package/package.json +9 -11
- package/packages/datadog-core/src/storage.js +47 -48
- package/packages/datadog-esbuild/index.js +6 -1
- package/packages/datadog-instrumentations/src/ai.js +12 -3
- package/packages/datadog-instrumentations/src/body-parser.js +5 -2
- package/packages/datadog-instrumentations/src/connect.js +3 -2
- package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
- package/packages/datadog-instrumentations/src/cucumber.js +7 -0
- package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
- package/packages/datadog-instrumentations/src/express-session.js +12 -11
- package/packages/datadog-instrumentations/src/express.js +24 -20
- package/packages/datadog-instrumentations/src/fastify.js +18 -6
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
- package/packages/datadog-instrumentations/src/http/client.js +9 -12
- package/packages/datadog-instrumentations/src/http/server.js +30 -16
- package/packages/datadog-instrumentations/src/http2/client.js +15 -12
- package/packages/datadog-instrumentations/src/http2/server.js +15 -8
- package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
- package/packages/datadog-instrumentations/src/jest.js +143 -73
- package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
- package/packages/datadog-instrumentations/src/multer.js +3 -2
- package/packages/datadog-instrumentations/src/mysql2.js +34 -0
- package/packages/datadog-instrumentations/src/net.js +8 -6
- package/packages/datadog-instrumentations/src/openai.js +19 -7
- package/packages/datadog-instrumentations/src/pg.js +19 -0
- package/packages/datadog-instrumentations/src/router.js +12 -10
- package/packages/datadog-instrumentations/src/vitest.js +29 -4
- package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
- package/packages/datadog-plugin-cucumber/src/index.js +2 -0
- package/packages/datadog-plugin-cypress/src/support.js +31 -1
- package/packages/datadog-plugin-http/src/client.js +0 -3
- package/packages/datadog-plugin-http/src/server.js +11 -1
- package/packages/datadog-plugin-mocha/src/index.js +2 -0
- package/packages/datadog-plugin-pg/src/index.js +10 -0
- package/packages/dd-trace/src/aiguard/index.js +34 -15
- package/packages/dd-trace/src/aiguard/sdk.js +34 -3
- package/packages/dd-trace/src/aiguard/tags.js +6 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +3 -2
- package/packages/dd-trace/src/appsec/iast/index.js +3 -2
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +2 -1
- package/packages/dd-trace/src/appsec/reporter.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +14 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -1
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +5 -9
- package/packages/dd-trace/src/config/parsers.js +8 -0
- package/packages/dd-trace/src/config/supported-configurations.json +20 -6
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/datastreams/writer.js +1 -2
- package/packages/dd-trace/src/debugger/config.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
- package/packages/dd-trace/src/debugger/index.js +1 -2
- package/packages/dd-trace/src/dogstatsd.js +2 -3
- package/packages/dd-trace/src/encode/0.4.js +49 -41
- package/packages/dd-trace/src/encode/agentless-json.js +5 -1
- package/packages/dd-trace/src/encode/tags-processors.js +14 -0
- package/packages/dd-trace/src/exporters/agent/index.js +1 -2
- package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
- package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
- package/packages/dd-trace/src/exporters/common/request.js +26 -0
- package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
- package/packages/dd-trace/src/llmobs/sdk.js +4 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
- package/packages/dd-trace/src/llmobs/tagger.js +5 -3
- package/packages/dd-trace/src/llmobs/util.js +54 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
- package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
- package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
- package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
- package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +3 -2
- package/packages/dd-trace/src/opentracing/span.js +23 -18
- package/packages/dd-trace/src/opentracing/tracer.js +16 -12
- package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
- package/packages/dd-trace/src/priority_sampler.js +6 -5
- package/packages/dd-trace/src/profiling/config.js +3 -2
- package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
- package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
- package/packages/dd-trace/src/proxy.js +13 -10
- package/packages/dd-trace/src/remote_config/index.js +1 -2
- package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
- package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
- package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
- package/packages/dd-trace/src/span_format.js +33 -25
- package/packages/dd-trace/src/span_stats.js +1 -1
- package/packages/dd-trace/src/startup-log.js +1 -2
- package/packages/dd-trace/src/telemetry/send-data.js +1 -1
- package/packages/dd-trace/src/tracer.js +1 -1
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/dd-trace/src/agent/url.js +0 -28
- package/scripts/preinstall.js +0 -34
|
@@ -10,7 +10,6 @@ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
|
|
|
10
10
|
const { getIsAzureFunction } = require('../serverless')
|
|
11
11
|
const { getAzureTagsFromMetadata, getAzureAppMetadata, getAzureFunctionMetadata } = require('../azure_metadata')
|
|
12
12
|
const { getEnvironmentVariable } = require('../config/helper')
|
|
13
|
-
const { getAgentUrl } = require('../agent/url')
|
|
14
13
|
const { isACFActive } = require('../../../datadog-core/src/storage')
|
|
15
14
|
|
|
16
15
|
const { AgentExporter } = require('./exporters/agent')
|
|
@@ -54,7 +53,7 @@ class Config {
|
|
|
54
53
|
this.pprofPrefix = options.DD_PROFILING_PPROF_PREFIX
|
|
55
54
|
this.v8ProfilerBugWorkaroundEnabled = options.DD_PROFILING_V8_PROFILER_BUG_WORKAROUND
|
|
56
55
|
|
|
57
|
-
this.url =
|
|
56
|
+
this.url = options.url
|
|
58
57
|
|
|
59
58
|
this.libraryInjected = !!options.DD_INJECTION_ENABLED
|
|
60
59
|
|
|
@@ -87,6 +86,7 @@ class Config {
|
|
|
87
86
|
|
|
88
87
|
this.timelineEnabled = options.DD_PROFILING_TIMELINE_ENABLED
|
|
89
88
|
this.timelineSamplingEnabled = options.DD_INTERNAL_PROFILING_TIMELINE_SAMPLING_ENABLED
|
|
89
|
+
this.allocationProfilingEnabled = options.DD_PROFILING_ALLOCATION_ENABLED
|
|
90
90
|
this.codeHotspotsEnabled = options.DD_PROFILING_CODEHOTSPOTS_ENABLED
|
|
91
91
|
this.cpuProfilingEnabled = options.DD_PROFILING_CPU_ENABLED
|
|
92
92
|
this.heapSamplingInterval = options.DD_PROFILING_HEAP_SAMPLING_INTERVAL
|
|
@@ -140,6 +140,7 @@ class Config {
|
|
|
140
140
|
|
|
141
141
|
get systemInfoReport () {
|
|
142
142
|
const report = {
|
|
143
|
+
allocationProfilingEnabled: this.allocationProfilingEnabled,
|
|
143
144
|
asyncContextFrameEnabled: this.asyncContextFrameEnabled,
|
|
144
145
|
codeHotspotsEnabled: this.codeHotspotsEnabled,
|
|
145
146
|
cpuProfilingEnabled: this.cpuProfilingEnabled,
|
|
@@ -66,12 +66,11 @@ class GCDecorator {
|
|
|
66
66
|
constructor (stringTable) {
|
|
67
67
|
this.stringTable = stringTable
|
|
68
68
|
this.reasonLabelKey = stringTable.dedup('gc reason')
|
|
69
|
+
this.kindLabelKey = stringTable.dedup('gc type')
|
|
69
70
|
this.kindLabels = []
|
|
70
71
|
this.reasonLabels = []
|
|
71
72
|
this.flagObj = {}
|
|
72
73
|
|
|
73
|
-
const kindLabelKey = stringTable.dedup('gc type')
|
|
74
|
-
|
|
75
74
|
// Create labels for all GC performance flags and kinds of GC
|
|
76
75
|
for (const [key, value] of Object.entries(constants)) {
|
|
77
76
|
if (key.startsWith('NODE_PERFORMANCE_GC_FLAGS_')) {
|
|
@@ -79,20 +78,43 @@ class GCDecorator {
|
|
|
79
78
|
} else if (key.startsWith('NODE_PERFORMANCE_GC_')) {
|
|
80
79
|
// It's a constant for a kind of GC
|
|
81
80
|
const kind = key.slice(20).toLowerCase()
|
|
82
|
-
this.kindLabels[value] = labelFromStr(stringTable, kindLabelKey, kind)
|
|
81
|
+
this.kindLabels[value] = labelFromStr(stringTable, this.kindLabelKey, kind)
|
|
83
82
|
}
|
|
84
83
|
}
|
|
84
|
+
|
|
85
|
+
// V8's young-generation collector emits GC events with kind 2, but Node.js
|
|
86
|
+
// doesn't expose a matching NODE_PERFORMANCE_GC_* constant for it, so we map it
|
|
87
|
+
// explicitly. The collector was renamed from Minor Mark-Compact to Minor
|
|
88
|
+
// Mark-Sweep in the V8 version that shipped with Node 22. See equivalent
|
|
89
|
+
// mapping in runtime_metrics.js.
|
|
90
|
+
const minorMarkGCKind = 2
|
|
91
|
+
if (this.kindLabels[minorMarkGCKind] === undefined) {
|
|
92
|
+
const { NODE_MAJOR } = require('../../../../../version')
|
|
93
|
+
const minorGCLabel = NODE_MAJOR >= 22 ? 'minor_mark_sweep' : 'minor_mark_compact'
|
|
94
|
+
this.kindLabels[minorMarkGCKind] = labelFromStr(stringTable, this.kindLabelKey, minorGCLabel)
|
|
95
|
+
}
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
decorateSample (sampleInput, item) {
|
|
88
99
|
const { kind, flags } = item.detail
|
|
89
|
-
sampleInput.label.push(this.
|
|
100
|
+
sampleInput.label.push(this.getKindLabel(kind))
|
|
90
101
|
const reasonLabel = this.getReasonLabel(flags)
|
|
91
102
|
if (reasonLabel) {
|
|
92
103
|
sampleInput.label.push(reasonLabel)
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
|
|
107
|
+
getKindLabel (kind) {
|
|
108
|
+
let kindLabel = this.kindLabels[kind]
|
|
109
|
+
if (kindLabel === undefined) {
|
|
110
|
+
// Gracefully handle GC kinds we don't have a label for (e.g. a value
|
|
111
|
+
// introduced by a future Node.js/V8 version).
|
|
112
|
+
kindLabel = labelFromStr(this.stringTable, this.kindLabelKey, `unknown_${kind}`)
|
|
113
|
+
this.kindLabels[kind] = kindLabel
|
|
114
|
+
}
|
|
115
|
+
return kindLabel
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
getReasonLabel (flags) {
|
|
97
119
|
if (flags === 0) {
|
|
98
120
|
return null
|
|
@@ -13,12 +13,14 @@ class NativeSpaceProfiler {
|
|
|
13
13
|
#mapper
|
|
14
14
|
#oomMonitoring
|
|
15
15
|
#pprof
|
|
16
|
+
#allocationProfilingEnabled = false
|
|
16
17
|
#samplingInterval = 512 * 1024
|
|
17
18
|
#started = false
|
|
18
19
|
|
|
19
20
|
constructor (options = {}) {
|
|
20
21
|
// TODO: Remove default value. It is only used in testing.
|
|
21
22
|
this.#samplingInterval = options.heapSamplingInterval || 512 * 1024
|
|
23
|
+
this.#allocationProfilingEnabled = options.allocationProfilingEnabled
|
|
22
24
|
this.#oomMonitoring = options.oomMonitoring || {}
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -31,7 +33,7 @@ class NativeSpaceProfiler {
|
|
|
31
33
|
|
|
32
34
|
this.#mapper = mapper
|
|
33
35
|
this.#pprof = require('@datadog/pprof')
|
|
34
|
-
this.#pprof.heap.start(this.#samplingInterval, STACK_DEPTH)
|
|
36
|
+
this.#pprof.heap.start(this.#samplingInterval, STACK_DEPTH, this.#allocationProfilingEnabled)
|
|
35
37
|
if (this.#oomMonitoring.enabled) {
|
|
36
38
|
const strategies = this.#oomMonitoring.exportStrategies
|
|
37
39
|
this.#pprof.heap.monitorOutOfMemory(
|
|
@@ -195,6 +195,19 @@ class Tracer extends NoopProxy {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// OTel logs/metrics pipelines must be initialized BEFORE runtimeMetrics.start so that
|
|
199
|
+
// when the OTLP runtime metrics module calls metrics.getMeterProvider(), it gets the
|
|
200
|
+
// real provider, otherwise instruments register on the noop provider and never export.
|
|
201
|
+
if (config.DD_LOGS_OTEL_ENABLED) {
|
|
202
|
+
const { initializeOpenTelemetryLogs } = require('./opentelemetry/logs')
|
|
203
|
+
initializeOpenTelemetryLogs(config)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (config.DD_METRICS_OTEL_ENABLED) {
|
|
207
|
+
const { initializeOpenTelemetryMetrics } = require('./opentelemetry/metrics')
|
|
208
|
+
initializeOpenTelemetryMetrics(config)
|
|
209
|
+
}
|
|
210
|
+
|
|
198
211
|
if (config.runtimeMetrics.enabled) {
|
|
199
212
|
runtimeMetrics.start(config)
|
|
200
213
|
}
|
|
@@ -224,16 +237,6 @@ class Tracer extends NoopProxy {
|
|
|
224
237
|
}
|
|
225
238
|
}
|
|
226
239
|
|
|
227
|
-
if (config.DD_LOGS_OTEL_ENABLED) {
|
|
228
|
-
const { initializeOpenTelemetryLogs } = require('./opentelemetry/logs')
|
|
229
|
-
initializeOpenTelemetryLogs(config)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (config.DD_METRICS_OTEL_ENABLED) {
|
|
233
|
-
const { initializeOpenTelemetryMetrics } = require('./opentelemetry/metrics')
|
|
234
|
-
initializeOpenTelemetryMetrics(config)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
240
|
if (config.isTestDynamicInstrumentationEnabled) {
|
|
238
241
|
const getDynamicInstrumentationClient = require('./ci-visibility/dynamic-instrumentation')
|
|
239
242
|
// We instantiate the client but do not start the Worker here. The worker is started lazily
|
|
@@ -8,7 +8,6 @@ const { getExtraServices } = require('../service-naming/extra-services')
|
|
|
8
8
|
const getGitMetadata = require('../git_metadata')
|
|
9
9
|
const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags')
|
|
10
10
|
const tagger = require('../tagger')
|
|
11
|
-
const { getAgentUrl } = require('../agent/url')
|
|
12
11
|
const processTags = require('../process-tags')
|
|
13
12
|
const Scheduler = require('./scheduler')
|
|
14
13
|
const { UNACKNOWLEDGED, ACKNOWLEDGED, ERROR } = require('./apply_states')
|
|
@@ -32,7 +31,7 @@ class RemoteConfig {
|
|
|
32
31
|
constructor (config) {
|
|
33
32
|
const pollInterval = Math.floor(config.remoteConfig.pollInterval * 1000)
|
|
34
33
|
|
|
35
|
-
this.url =
|
|
34
|
+
this.url = config.url
|
|
36
35
|
|
|
37
36
|
tagger.add(config.tags, {
|
|
38
37
|
'_dd.rc.client_id': clientId,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { DogStatsDClient, MetricsAggregationClient } = require('../dogstatsd')
|
|
4
|
+
const processTags = require('../process-tags')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds the aggregating DogStatsD client used to emit DD-proprietary tracer
|
|
8
|
+
* metrics (runtime.node.*, datadog.tracer.*). Shared by both runtime-metrics
|
|
9
|
+
* paths (DogStatsD and OTLP) so their client construction can't drift apart.
|
|
10
|
+
*
|
|
11
|
+
* Process tags are applied here, not via config/generateClientConfig, so they only
|
|
12
|
+
* reach this bounded set of runtime metrics. Putting them on the global tags would
|
|
13
|
+
* also tag user-facing custom metrics, inflating their cardinality (and billing).
|
|
14
|
+
*
|
|
15
|
+
* @param {import('../config/config-base')} config - Tracer configuration
|
|
16
|
+
* @returns {MetricsAggregationClient}
|
|
17
|
+
*/
|
|
18
|
+
function createMetricsClient (config) {
|
|
19
|
+
const clientConfig = DogStatsDClient.generateClientConfig(config)
|
|
20
|
+
|
|
21
|
+
if (config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED) {
|
|
22
|
+
for (const tag of processTags.tagsArray) {
|
|
23
|
+
clientConfig.tags.push(tag)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return new MetricsAggregationClient(new DogStatsDClient(clientConfig))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { createMetricsClient }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const log = require('../log')
|
|
4
|
+
|
|
3
5
|
let runtimeMetrics
|
|
4
6
|
|
|
5
7
|
const noop = runtimeMetrics = {
|
|
@@ -20,11 +22,19 @@ module.exports = {
|
|
|
20
22
|
start (config) {
|
|
21
23
|
if (!config?.runtimeMetrics.enabled) return
|
|
22
24
|
|
|
23
|
-
runtimeMetrics =
|
|
25
|
+
runtimeMetrics = config.DD_METRICS_OTEL_ENABLED
|
|
26
|
+
? require('./otlp_runtime_metrics')
|
|
27
|
+
: require('./runtime_metrics')
|
|
24
28
|
|
|
25
29
|
Object.setPrototypeOf(module.exports, runtimeMetrics)
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
try {
|
|
32
|
+
runtimeMetrics.start(config)
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// Unwind whatever managed to register so a partial init doesn't leak into the next start().
|
|
35
|
+
runtimeMetrics.stop()
|
|
36
|
+
log.error('Failed to start runtime metrics', err)
|
|
37
|
+
}
|
|
28
38
|
},
|
|
29
39
|
|
|
30
40
|
stop () {
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const v8 = require('node:v8')
|
|
4
|
+
const process = require('node:process')
|
|
5
|
+
const { performance, monitorEventLoopDelay, PerformanceObserver, constants } = require('node:perf_hooks')
|
|
6
|
+
const { metrics } = require('@opentelemetry/api')
|
|
7
|
+
const log = require('../log')
|
|
8
|
+
const { createMetricsClient } = require('./client')
|
|
9
|
+
|
|
10
|
+
const METER_NAME = 'datadog.runtime_metrics'
|
|
11
|
+
|
|
12
|
+
const ATTR_ELU_STATE_IDLE = { 'nodejs.eventloop.state': 'idle' }
|
|
13
|
+
const ATTR_ELU_STATE_ACTIVE = { 'nodejs.eventloop.state': 'active' }
|
|
14
|
+
|
|
15
|
+
// Pre-allocated `{ 'v8js.gc.type': <type> }` attribute objects so the observer
|
|
16
|
+
// doesn't allocate a new one per entry under GC pressure.
|
|
17
|
+
const ATTR_GC_TYPE_MINOR = { 'v8js.gc.type': 'minor' }
|
|
18
|
+
const ATTR_GC_TYPE_MAJOR = { 'v8js.gc.type': 'major' }
|
|
19
|
+
const ATTR_GC_TYPE_INCREMENTAL = { 'v8js.gc.type': 'incremental' }
|
|
20
|
+
const ATTR_GC_TYPE_WEAKCB = { 'v8js.gc.type': 'weakcb' }
|
|
21
|
+
|
|
22
|
+
// Kind 2 is V8's MinorMarkSweep (Node 20+) and not exposed via perf_hooks.constants.
|
|
23
|
+
const GC_ATTR_BY_KIND = new Map([
|
|
24
|
+
[constants.NODE_PERFORMANCE_GC_MINOR, ATTR_GC_TYPE_MINOR],
|
|
25
|
+
[2, ATTR_GC_TYPE_MINOR],
|
|
26
|
+
[constants.NODE_PERFORMANCE_GC_MAJOR, ATTR_GC_TYPE_MAJOR],
|
|
27
|
+
[constants.NODE_PERFORMANCE_GC_INCREMENTAL, ATTR_GC_TYPE_INCREMENTAL],
|
|
28
|
+
[constants.NODE_PERFORMANCE_GC_WEAKCB, ATTR_GC_TYPE_WEAKCB],
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
let meter = null
|
|
32
|
+
let eventLoopHistogram = null
|
|
33
|
+
let gcObserver = null
|
|
34
|
+
let lastElu = null
|
|
35
|
+
|
|
36
|
+
// Cache `{ 'v8js.heap.space.name': <name> }` per V8 space name to avoid per-scrape allocations.
|
|
37
|
+
const HEAP_SPACE_ATTR_CACHE = new Map()
|
|
38
|
+
function getHeapSpaceAttr (name) {
|
|
39
|
+
let attr = HEAP_SPACE_ATTR_CACHE.get(name)
|
|
40
|
+
if (!attr) {
|
|
41
|
+
attr = { 'v8js.heap.space.name': name }
|
|
42
|
+
HEAP_SPACE_ATTR_CACHE.set(name, attr)
|
|
43
|
+
}
|
|
44
|
+
return attr
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// getMeter() returns a cached meter, so without tracking what we registered we'd
|
|
48
|
+
// stack callbacks every time start() runs.
|
|
49
|
+
const registeredCallbacks = []
|
|
50
|
+
const registeredBatchCallbacks = []
|
|
51
|
+
|
|
52
|
+
// DD-proprietary tracer metrics (runtime.node.spans.*, datadog.tracer.*) have no OTel
|
|
53
|
+
// equivalent; keep a DogStatsD client so OTLP-path customers don't lose them.
|
|
54
|
+
let client = null
|
|
55
|
+
let flushInterval = null
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
/**
|
|
59
|
+
* @param {import('../config/config-base')} config - Tracer configuration
|
|
60
|
+
*/
|
|
61
|
+
start (config) {
|
|
62
|
+
this.stop()
|
|
63
|
+
|
|
64
|
+
client = createMetricsClient(config)
|
|
65
|
+
flushInterval = setInterval(() => {
|
|
66
|
+
client.flush()
|
|
67
|
+
}, config.DD_RUNTIME_METRICS_FLUSH_INTERVAL ?? 10_000)
|
|
68
|
+
flushInterval.unref?.()
|
|
69
|
+
|
|
70
|
+
meter = metrics.getMeterProvider().getMeter(METER_NAME)
|
|
71
|
+
|
|
72
|
+
const trackEventLoop = config.runtimeMetrics.eventLoop !== false
|
|
73
|
+
const trackGc = config.runtimeMetrics.gc !== false
|
|
74
|
+
if (trackEventLoop) {
|
|
75
|
+
eventLoopHistogram = monitorEventLoopDelay({ resolution: 4 })
|
|
76
|
+
eventLoopHistogram.enable()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const heapUsed = createHeapInstrument('v8js.memory.heap.used', 'V8 heap memory used.')
|
|
80
|
+
const heapLimit = createHeapInstrument('v8js.memory.heap.limit', 'V8 heap memory total available size.')
|
|
81
|
+
const heapSpaceAvailable = createHeapInstrument(
|
|
82
|
+
'v8js.memory.heap.space.available_size', 'V8 heap space available size.')
|
|
83
|
+
const heapSpacePhysical = createHeapInstrument(
|
|
84
|
+
'v8js.memory.heap.space.physical_size', 'V8 heap space physical size.')
|
|
85
|
+
const heapSpaceSize = createHeapInstrument(
|
|
86
|
+
'v8js.memory.heap.space.size', 'Total heap memory size pre-allocated for a heap space.')
|
|
87
|
+
|
|
88
|
+
registerBatchCallback(
|
|
89
|
+
(result) => {
|
|
90
|
+
const stats = v8.getHeapStatistics()
|
|
91
|
+
result.observe(heapLimit, stats.heap_size_limit)
|
|
92
|
+
|
|
93
|
+
const spaces = v8.getHeapSpaceStatistics()
|
|
94
|
+
for (let i = 0; i < spaces.length; i++) {
|
|
95
|
+
const space = spaces[i]
|
|
96
|
+
const attr = getHeapSpaceAttr(space.space_name)
|
|
97
|
+
result.observe(heapUsed, space.space_used_size, attr)
|
|
98
|
+
result.observe(heapSpaceAvailable, space.space_available_size, attr)
|
|
99
|
+
result.observe(heapSpacePhysical, space.physical_space_size, attr)
|
|
100
|
+
result.observe(heapSpaceSize, space.space_size, attr)
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[heapUsed, heapLimit, heapSpaceAvailable, heapSpacePhysical, heapSpaceSize]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const activeResource = meter.createObservableGauge('v8js.resource.active', {
|
|
107
|
+
unit: '{resource}',
|
|
108
|
+
description: 'Gauge of the active resources that are currently keeping the event loop alive.',
|
|
109
|
+
})
|
|
110
|
+
registerCallback((result) => {
|
|
111
|
+
const counts = new Map()
|
|
112
|
+
// Stable since Node 22.16; available on 18+ as experimental.
|
|
113
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
114
|
+
for (const resource of process.getActiveResourcesInfo()) {
|
|
115
|
+
counts.set(resource, (counts.get(resource) ?? 0) + 1)
|
|
116
|
+
}
|
|
117
|
+
for (const [type, count] of counts) {
|
|
118
|
+
result.observe(count, { 'v8js.resource.type': type })
|
|
119
|
+
}
|
|
120
|
+
}, activeResource)
|
|
121
|
+
|
|
122
|
+
// Spec wants nodejs.eventloop.delay.* in seconds; perf_hooks gives nanoseconds.
|
|
123
|
+
// Match @opentelemetry/instrumentation-runtime-node EventLoopDelayCollector: one batch
|
|
124
|
+
// callback, guard on sample count, emit, then reset so each interval is independent.
|
|
125
|
+
if (trackEventLoop) {
|
|
126
|
+
const delayMin = createDelayGauge('nodejs.eventloop.delay.min', 'Event loop minimum delay.')
|
|
127
|
+
const delayMax = createDelayGauge('nodejs.eventloop.delay.max', 'Event loop maximum delay.')
|
|
128
|
+
const delayMean = createDelayGauge('nodejs.eventloop.delay.mean', 'Event loop mean delay.')
|
|
129
|
+
const delayStddev = createDelayGauge('nodejs.eventloop.delay.stddev', 'Event loop standard deviation delay.')
|
|
130
|
+
const delayP50 = createDelayGauge('nodejs.eventloop.delay.p50', 'Event loop 50th percentile delay.')
|
|
131
|
+
const delayP90 = createDelayGauge('nodejs.eventloop.delay.p90', 'Event loop 90th percentile delay.')
|
|
132
|
+
const delayP99 = createDelayGauge('nodejs.eventloop.delay.p99', 'Event loop 99th percentile delay.')
|
|
133
|
+
|
|
134
|
+
registerBatchCallback((result) => {
|
|
135
|
+
const h = eventLoopHistogram
|
|
136
|
+
if (!h || h.count < 5) return
|
|
137
|
+
result.observe(delayMin, h.min / 1e9)
|
|
138
|
+
result.observe(delayMax, h.max / 1e9)
|
|
139
|
+
result.observe(delayMean, h.mean / 1e9)
|
|
140
|
+
result.observe(delayStddev, h.stddev / 1e9)
|
|
141
|
+
result.observe(delayP50, h.percentile(50) / 1e9)
|
|
142
|
+
result.observe(delayP90, h.percentile(90) / 1e9)
|
|
143
|
+
result.observe(delayP99, h.percentile(99) / 1e9)
|
|
144
|
+
h.reset()
|
|
145
|
+
}, [delayMin, delayMax, delayMean, delayStddev, delayP50, delayP90, delayP99])
|
|
146
|
+
|
|
147
|
+
if (performance.eventLoopUtilization) {
|
|
148
|
+
// Baseline so the first observation isn't 1.0.
|
|
149
|
+
lastElu = performance.eventLoopUtilization()
|
|
150
|
+
|
|
151
|
+
const eluTime = meter.createObservableCounter('nodejs.eventloop.time', {
|
|
152
|
+
unit: 's',
|
|
153
|
+
description: 'Cumulative duration of time the event loop has been in each state.',
|
|
154
|
+
})
|
|
155
|
+
registerCallback((result) => {
|
|
156
|
+
const elu = performance.eventLoopUtilization()
|
|
157
|
+
result.observe(elu.idle / 1000, ATTR_ELU_STATE_IDLE)
|
|
158
|
+
result.observe(elu.active / 1000, ATTR_ELU_STATE_ACTIVE)
|
|
159
|
+
}, eluTime)
|
|
160
|
+
|
|
161
|
+
const eluGauge = meter.createObservableGauge('nodejs.eventloop.utilization', {
|
|
162
|
+
unit: '1',
|
|
163
|
+
description: 'Event loop utilization.',
|
|
164
|
+
})
|
|
165
|
+
registerCallback((result) => {
|
|
166
|
+
const current = performance.eventLoopUtilization()
|
|
167
|
+
const idle = current.idle - lastElu.idle
|
|
168
|
+
const active = current.active - lastElu.active
|
|
169
|
+
lastElu = current
|
|
170
|
+
const total = idle + active
|
|
171
|
+
result.observe(total > 0 ? active / total : 0)
|
|
172
|
+
}, eluGauge)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (trackGc) {
|
|
177
|
+
const gcHistogram = meter.createHistogram('v8js.gc.duration', {
|
|
178
|
+
unit: 's',
|
|
179
|
+
description: 'Garbage collection duration.',
|
|
180
|
+
})
|
|
181
|
+
gcObserver = new PerformanceObserver(list => {
|
|
182
|
+
const entries = list.getEntries()
|
|
183
|
+
for (let i = 0; i < entries.length; i++) {
|
|
184
|
+
const entry = entries[i]
|
|
185
|
+
const attr = GC_ATTR_BY_KIND.get(entry.detail?.kind ?? entry.kind)
|
|
186
|
+
if (attr === undefined) continue
|
|
187
|
+
gcHistogram.record(entry.duration / 1000, attr)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
gcObserver.observe({ type: 'gc' })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
log.debug('Started OTLP runtime metrics with OTel-native naming (v8js.*, nodejs.*)')
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @returns {void}
|
|
198
|
+
*/
|
|
199
|
+
stop () {
|
|
200
|
+
if (eventLoopHistogram) {
|
|
201
|
+
eventLoopHistogram.disable()
|
|
202
|
+
eventLoopHistogram = null
|
|
203
|
+
}
|
|
204
|
+
gcObserver?.disconnect()
|
|
205
|
+
gcObserver = null
|
|
206
|
+
for (let i = 0; i < registeredCallbacks.length; i++) {
|
|
207
|
+
const [callback, instrument] = registeredCallbacks[i]
|
|
208
|
+
instrument.removeCallback(callback)
|
|
209
|
+
}
|
|
210
|
+
registeredCallbacks.length = 0
|
|
211
|
+
if (meter) {
|
|
212
|
+
for (let i = 0; i < registeredBatchCallbacks.length; i++) {
|
|
213
|
+
const [callback, instruments] = registeredBatchCallbacks[i]
|
|
214
|
+
meter.removeBatchObservableCallback(callback, instruments)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
registeredBatchCallbacks.length = 0
|
|
218
|
+
meter = null
|
|
219
|
+
lastElu = null
|
|
220
|
+
if (flushInterval) {
|
|
221
|
+
clearInterval(flushInterval)
|
|
222
|
+
flushInterval = null
|
|
223
|
+
}
|
|
224
|
+
client = null
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// Tied to @datadog/native-metrics which the OTLP path doesn't enable; noop with expected shape.
|
|
228
|
+
track () { return { finish () {} } },
|
|
229
|
+
|
|
230
|
+
boolean (name, value, tag) {
|
|
231
|
+
client?.boolean(name, value, tag)
|
|
232
|
+
},
|
|
233
|
+
histogram (name, value, tag) {
|
|
234
|
+
client?.histogram(name, value, tag)
|
|
235
|
+
},
|
|
236
|
+
count (name, count, tag, monotonic = false) {
|
|
237
|
+
client?.count(name, count, tag, monotonic)
|
|
238
|
+
},
|
|
239
|
+
gauge (name, value, tag) {
|
|
240
|
+
client?.gauge(name, value, tag)
|
|
241
|
+
},
|
|
242
|
+
increment (name, tag, monotonic) {
|
|
243
|
+
this.count(name, 1, tag, monotonic)
|
|
244
|
+
},
|
|
245
|
+
decrement (name, tag) {
|
|
246
|
+
this.count(name, -1, tag)
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {Function} callback
|
|
252
|
+
* @param {object} instrument
|
|
253
|
+
*/
|
|
254
|
+
function registerCallback (callback, instrument) {
|
|
255
|
+
instrument.addCallback(callback)
|
|
256
|
+
registeredCallbacks.push([callback, instrument])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {Function} callback
|
|
261
|
+
* @param {Array} instruments
|
|
262
|
+
*/
|
|
263
|
+
function registerBatchCallback (callback, instruments) {
|
|
264
|
+
meter.addBatchObservableCallback(callback, instruments)
|
|
265
|
+
registeredBatchCallbacks.push([callback, instruments])
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {string} name
|
|
270
|
+
* @param {string} description
|
|
271
|
+
* @returns {object}
|
|
272
|
+
*/
|
|
273
|
+
function createHeapInstrument (name, description) {
|
|
274
|
+
return meter.createObservableUpDownCounter(name, { unit: 'By', description })
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {string} name
|
|
279
|
+
* @param {string} description
|
|
280
|
+
* @returns {object}
|
|
281
|
+
*/
|
|
282
|
+
function createDelayGauge (name, description) {
|
|
283
|
+
return meter.createObservableGauge(name, { unit: 's', description })
|
|
284
|
+
}
|
|
@@ -6,11 +6,9 @@ const v8 = require('v8')
|
|
|
6
6
|
const os = require('os')
|
|
7
7
|
const process = require('process')
|
|
8
8
|
const { performance, PerformanceObserver, monitorEventLoopDelay } = require('perf_hooks')
|
|
9
|
-
const { DogStatsDClient, MetricsAggregationClient } = require('../dogstatsd')
|
|
10
9
|
const log = require('../log')
|
|
11
|
-
|
|
12
10
|
const { NODE_MAJOR } = require('../../../../version')
|
|
13
|
-
const
|
|
11
|
+
const { createMetricsClient } = require('./client')
|
|
14
12
|
|
|
15
13
|
const eventLoopDelayResolution = 4
|
|
16
14
|
|
|
@@ -37,18 +35,11 @@ module.exports = {
|
|
|
37
35
|
this.stop()
|
|
38
36
|
// The agent expects a flush every ten seconds, so this is for tests only.
|
|
39
37
|
const flushIntervalMs = config.DD_RUNTIME_METRICS_FLUSH_INTERVAL
|
|
40
|
-
const clientConfig = DogStatsDClient.generateClientConfig(config)
|
|
41
|
-
|
|
42
|
-
if (config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED) {
|
|
43
|
-
for (const tag of processTags.tagsArray) {
|
|
44
|
-
clientConfig.tags.push(tag)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
38
|
|
|
48
39
|
const trackEventLoop = config.runtimeMetrics.eventLoop !== false
|
|
49
40
|
const trackGc = config.runtimeMetrics.gc !== false
|
|
50
41
|
|
|
51
|
-
client =
|
|
42
|
+
client = createMetricsClient(config)
|
|
52
43
|
|
|
53
44
|
if (trackGc) {
|
|
54
45
|
startGCObserver()
|
|
@@ -26,7 +26,11 @@ function resolveServiceSource (span, tracerService) {
|
|
|
26
26
|
|
|
27
27
|
if (currentService === tracerService) {
|
|
28
28
|
if (existingSource === undefined) return
|
|
29
|
-
|
|
29
|
+
// Clear by assigning undefined rather than deleting: `delete` on the plain
|
|
30
|
+
// `_tags` object drops it into dictionary (slow) mode, so the per-span
|
|
31
|
+
// extractTags scan that follows pays the slow path. The encode loop skips
|
|
32
|
+
// undefined values, so the emitted meta is unchanged.
|
|
33
|
+
spanContext.setTag(SVC_SRC_KEY, undefined)
|
|
30
34
|
return
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -48,9 +48,10 @@ const { IGNORE_OTEL_ERROR } = constants
|
|
|
48
48
|
* @property {Array} links
|
|
49
49
|
* @property {Array<SpanEvent> | undefined} span_events
|
|
50
50
|
*
|
|
51
|
-
* @typedef {object} SpanEvent
|
|
51
|
+
* @typedef {object} SpanEvent Raw span event as stored on the span; the encoder
|
|
52
|
+
* layer derives `time_unix_nano` from `startTime` via `eventTimeNano`.
|
|
52
53
|
* @property {string} name
|
|
53
|
-
* @property {number}
|
|
54
|
+
* @property {number} startTime Milliseconds with sub-millisecond precision.
|
|
54
55
|
* @property {Record<string, string>} [attributes]
|
|
55
56
|
*/
|
|
56
57
|
|
|
@@ -88,7 +89,6 @@ function formatSpan (span) {
|
|
|
88
89
|
metrics: {},
|
|
89
90
|
start: Math.round(span._startTime * 1e6),
|
|
90
91
|
duration: Math.round(span._duration * 1e6),
|
|
91
|
-
links: [],
|
|
92
92
|
span_events: undefined,
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -112,24 +112,32 @@ function setSingleSpanIngestionTags (formattedSpan, options) {
|
|
|
112
112
|
* @param {import('./opentracing/span')} span
|
|
113
113
|
*/
|
|
114
114
|
function extractSpanLinks (formattedSpan, span) {
|
|
115
|
-
|
|
115
|
+
const links = span._links
|
|
116
|
+
if (!links?.length) {
|
|
116
117
|
return
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
// Build the `_dd.span_links` JSON directly. The trace / span ids are decimal
|
|
120
|
+
// strings (no escaping); attributes are pre-sanitized to a string map and
|
|
121
|
+
// `undefined` when empty, so they only need a presence check. Avoids the
|
|
122
|
+
// throwaway array of formatted-link objects the previous `map` allocated and
|
|
123
|
+
// the second walk `JSON.stringify` does over them.
|
|
124
|
+
let serialized = '['
|
|
125
|
+
for (let i = 0; i < links.length; i++) {
|
|
126
|
+
if (i > 0) serialized += ','
|
|
127
|
+
const { context, attributes } = links[i]
|
|
128
|
+
serialized += `{"trace_id":"${context.toTraceId(true)}","span_id":"${context.toSpanId(true)}"`
|
|
129
|
+
if (attributes !== undefined) {
|
|
130
|
+
serialized += `,"attributes":${JSON.stringify(attributes)}`
|
|
122
131
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
formattedLink.attributes = attributes
|
|
132
|
+
if (context?._sampling?.priority >= 0) {
|
|
133
|
+
serialized += `,"flags":${context._sampling.priority > 0 ? 1 : 0}`
|
|
126
134
|
}
|
|
127
|
-
if (context?.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
135
|
+
if (context?._tracestate) {
|
|
136
|
+
serialized += `,"tracestate":${JSON.stringify(context._tracestate.toString())}`
|
|
137
|
+
}
|
|
138
|
+
serialized += '}'
|
|
139
|
+
}
|
|
140
|
+
serialized += ']'
|
|
133
141
|
if (serialized.length > MAX_META_VALUE_LENGTH) {
|
|
134
142
|
serialized = `${serialized.slice(0, MAX_META_VALUE_LENGTH)}...`
|
|
135
143
|
}
|
|
@@ -137,6 +145,12 @@ function extractSpanLinks (formattedSpan, span) {
|
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
/**
|
|
148
|
+
* Hand the raw `_events` array to the encoder layer instead of copying it into
|
|
149
|
+
* reshaped `{ name, time_unix_nano, attributes }` objects. Each encoder derives
|
|
150
|
+
* `time_unix_nano` from `event.startTime` via `eventTimeNano` and drops empty
|
|
151
|
+
* attribute objects itself, so the per-event allocation here is pure waste on
|
|
152
|
+
* every event-bearing span.
|
|
153
|
+
*
|
|
140
154
|
* @param {FormattedSpan} formattedSpan
|
|
141
155
|
* @param {import('./opentracing/span')} span
|
|
142
156
|
*/
|
|
@@ -144,13 +158,7 @@ function extractSpanEvents (formattedSpan, span) {
|
|
|
144
158
|
if (!span._events?.length) {
|
|
145
159
|
return
|
|
146
160
|
}
|
|
147
|
-
formattedSpan.span_events = span._events
|
|
148
|
-
return {
|
|
149
|
-
name: event.name,
|
|
150
|
-
time_unix_nano: Math.round(event.startTime * 1e6),
|
|
151
|
-
attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined,
|
|
152
|
-
}
|
|
153
|
-
})
|
|
161
|
+
formattedSpan.span_events = span._events
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
function extractTags (formattedSpan, span) {
|
|
@@ -168,7 +176,7 @@ function extractTags (formattedSpan, span) {
|
|
|
168
176
|
metrics[MEASURED] = 1
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
const tracerService = span.tracer().
|
|
179
|
+
const tracerService = span.tracer().serviceLower
|
|
172
180
|
if (tags['service.name']?.toLowerCase() !== tracerService) {
|
|
173
181
|
span.setTag(BASE_SERVICE, tracerService)
|
|
174
182
|
|
|
@@ -190,7 +190,7 @@ class SpanStatsProcessor {
|
|
|
190
190
|
if (!this.enabled) return
|
|
191
191
|
if (!span.metrics[TOP_LEVEL_KEY] && !span.metrics[MEASURED]) return
|
|
192
192
|
|
|
193
|
-
const spanEndNs = span.
|
|
193
|
+
const spanEndNs = span.start + span.duration
|
|
194
194
|
const bucketTime = spanEndNs - (spanEndNs % this.bucketSizeNs)
|
|
195
195
|
|
|
196
196
|
this.buckets.forTime(bucketTime)
|