Skip to content

Commit 4f85c4a

Browse files
fix(id): drop pending telemetry state and re-add RC client-id on MicroVM identity refresh
Buffered/aggregated telemetry recorded before a MicroVM snapshot would otherwise export or flush under every clone's refreshed identity instead of being dropped with the rest of the pre-clone state. Reset it as part of the identity-refresh path, in each of the affected subsystems: - dogstatsd: MetricsAggregationClient drops pending counters/gauges/ histograms when the wrapped client's tags actually change - agentless exporter: Writer#resetPendingBatch() discards the pending encoded trace batch - OTLP logs: BatchLogRecordProcessor#resetPendingState() discards queued log records and clears the batch timer - OTLP metrics: PeriodicMetricReader#resetPendingState() discards queued measurements and rebases sync Counter/Histogram cumulative state - span stats: SpanStatsProcessor replaces its bucket map - runtime metrics: rebase CPU/event-loop/ELU sampler baselines so the next collection reports a delta since the resume, not one spanning the snapshot pause Also fixes a separate identity-refresh gap: an RC lib-config update rebuilds config.tags from tracked sources (config/remote_config.js's tracing_tags transformer), dropping the directly-set _dd.rc.client_id key. refreshIdentity()'s guard only wrote the refreshed value back when the tag was already present, so once that sequence happened, config.tags (and the DogStatsD/OTLP tags built from it) permanently lost _dd.rc.client_id after an identity refresh, even though the RC client's own id field kept updating correctly. Gate the write on the RC client existing instead, and write it unconditionally in that case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 6cac3ff commit 4f85c4a

20 files changed

Lines changed: 314 additions & 14 deletions

File tree

packages/dd-trace/src/dogstatsd.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,21 @@ class DogStatsDClient {
6161
* before the swap.
6262
*
6363
* @param {string[]} tags - DogStatsD-formatted tags (e.g. `['key:value']`)
64+
* @returns {boolean} True if the tag prefix actually changed (and buffered lines were dropped)
6465
*/
6566
updateTags (tags) {
6667
const tagsPrefix = tags.length ? `|#${tags.join(',')}` : ''
6768

6869
this._tags = tags
6970

70-
if (tagsPrefix === this.#tagsPrefix) return
71+
if (tagsPrefix === this.#tagsPrefix) return false
7172

7273
this.#tagsPrefix = tagsPrefix
7374
this._queue = []
7475
this._buffer = ''
7576
this._offset = 0
77+
78+
return true
7679
}
7780

7881
increment (stat, value, tags) {
@@ -241,11 +244,16 @@ class MetricsAggregationClient {
241244
}
242245

243246
/**
244-
* Recomputes the wrapped client's cached tags (e.g. after a MicroVM clone resume).
247+
* Recomputes the wrapped client's cached tags (e.g. after a MicroVM clone resume). Pending
248+
* counters/gauges/histograms were aggregated under the old identity, so they're reset along
249+
* with the client's buffered lines — but only if the tags actually changed, so a no-op resume
250+
* doesn't discard in-flight aggregation for nothing.
245251
* @param {string[]} tags - DogStatsD-formatted tags (e.g. `['key:value']`)
246252
*/
247253
updateTags (tags) {
248-
this._client.updateTags(tags)
254+
if (this._client.updateTags(tags)) {
255+
this.reset()
256+
}
249257
}
250258

251259
flush () {

packages/dd-trace/src/exporters/agentless/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
const { URL } = require('node:url')
44
const os = require('node:os')
55

6+
const { channel } = require('dc-polyfill')
7+
68
const log = require('../../log')
79
const { entityId } = require('../common/docker')
810
const tracerVersion = require('../../../../../package.json').version
911
const Writer = require('./writer')
1012
const { computeIntakeUrl } = require('./intake')
1113

14+
const identityRefreshChannel = channel('datadog:identity:refresh')
15+
1216
/**
1317
* Agentless exporter for APM trace intake.
1418
* Sends traces directly to the Datadog intake without requiring a local agent.
@@ -56,6 +60,10 @@ class AgentlessExporter {
5660
metadata,
5761
})
5862

63+
// Drop traces buffered before a MicroVM snapshot instead of flushing them under every
64+
// clone's identity. No stop() hook here, same as this exporter's other process-lifetime state.
65+
identityRefreshChannel.subscribe(() => this._writer.resetPendingBatch())
66+
5967
const ddTrace = globalThis[Symbol.for('dd-trace')]
6068
if (ddTrace?.beforeExitHandlers) {
6169
ddTrace.beforeExitHandlers.add(this.flush.bind(this))

packages/dd-trace/src/exporters/common/writer.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ class Writer {
7070
setUrl (url) {
7171
this._url = url
7272
}
73+
74+
/**
75+
* Discards the pending encoded batch, e.g. on a MicroVM clone resume so traces buffered
76+
* before the snapshot don't flush under every clone's identity.
77+
* @returns {void}
78+
*/
79+
resetPendingBatch () {
80+
this._encoder.reset()
81+
}
7382
}
7483

7584
module.exports = Writer

packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ class BatchLogRecordProcessor {
6060
this.#export()
6161
}
6262

63+
/**
64+
* Discards queued log records, e.g. on a MicroVM clone resume so records buffered before the
65+
* snapshot don't export under every clone's identity.
66+
* @returns {void}
67+
*/
68+
resetPendingState () {
69+
this.#logRecords = []
70+
this.#clearTimer()
71+
}
72+
6373
/**
6474
* Starts the batch timeout timer.
6575
* @private

packages/dd-trace/src/opentelemetry/logs/index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
'use strict'
22

3+
const { channel } = require('dc-polyfill')
4+
35
const { buildResourceAttributes, registerResourceAttributeRefresh } = require('../resource-attributes')
46

57
/**
68
* @typedef {import('../../config')} Config
79
*/
810

11+
const identityRefreshChannel = channel('datadog:identity:refresh')
12+
13+
// Replaces the previous subscription on each call, so restarting doesn't accumulate listeners.
14+
let unsubscribeLogsPendingStateReset = null
15+
916
/**
1017
* OpenTelemetry Logs Implementation for `dd-trace-js`
1118
*
@@ -59,6 +66,13 @@ function initializeOpenTelemetryLogs (config) {
5966
loggerProvider.register()
6067

6168
registerResourceAttributeRefresh(exporter, () => buildResourceAttributes(config))
69+
70+
// Drop log records queued before a MicroVM snapshot instead of exporting them under every
71+
// clone's identity.
72+
unsubscribeLogsPendingStateReset?.()
73+
const onIdentityRefresh = () => processor.resetPendingState()
74+
identityRefreshChannel.subscribe(onIdentityRefresh)
75+
unsubscribeLogsPendingStateReset = () => identityRefreshChannel.unsubscribe(onIdentityRefresh)
6276
}
6377

6478
module.exports = {

packages/dd-trace/src/opentelemetry/metrics/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const os = require('os')
44

55
const { metrics } = require('@opentelemetry/api')
6+
const { channel } = require('dc-polyfill')
67

78
const { VERSION } = require('../../../../../version')
89
const processTags = require('../../process-tags')
@@ -18,6 +19,11 @@ const OtlpHttpMetricExporter = require('./otlp_http_metric_exporter')
1819
* @typedef {import('../../config')} Config
1920
*/
2021

22+
const identityRefreshChannel = channel('datadog:identity:refresh')
23+
24+
// Replaces the previous subscription on each call, so restarting doesn't accumulate listeners.
25+
let unsubscribeMetricsPendingStateReset = null
26+
2127
/**
2228
* @file OpenTelemetry Metrics Implementation for dd-trace-js
2329
*
@@ -64,6 +70,13 @@ function initializeOpenTelemetryMetrics (config) {
6470
metrics.setGlobalMeterProvider(meterProvider)
6571

6672
registerResourceAttributeRefresh(exporter, () => buildGeneralResourceAttributes(config))
73+
74+
// Drop measurements queued before a MicroVM snapshot instead of exporting them under every
75+
// clone's identity.
76+
unsubscribeMetricsPendingStateReset?.()
77+
const onIdentityRefresh = () => reader.resetPendingState()
78+
identityRefreshChannel.subscribe(onIdentityRefresh)
79+
unsubscribeMetricsPendingStateReset = () => identityRefreshChannel.unsubscribe(onIdentityRefresh)
6780
}
6881

6982
function buildResourceAttributes (tags, { reportHostname, otelSemanticsEnabled, service, env, serviceVersion } = {}) {

packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,25 @@ class PeriodicMetricReader {
207207
this.#collectAndExport()
208208
}
209209

210+
/**
211+
* Discards queued measurements and sync-instrument cumulative state, e.g. on a MicroVM clone
212+
* resume, so measurements recorded before the snapshot don't export under every clone's identity.
213+
*
214+
* Only clears `#lastExportedState` entries that have a matching `#cumulativeState` entry (sync
215+
* Counter/Histogram delta baselines). An ObservableCounter's baseline lives only in
216+
* `#lastExportedState` — clearing it too would turn its next export into an absolute reading
217+
* instead of a delta.
218+
* @returns {void}
219+
*/
220+
resetPendingState () {
221+
this.#measurements = []
222+
223+
for (const key of this.#cumulativeState.keys()) {
224+
this.#lastExportedState.delete(key)
225+
}
226+
this.#cumulativeState.clear()
227+
}
228+
210229
/**
211230
* Shuts down the reader and stops periodic collection.
212231
* @returns {void}

packages/dd-trace/src/remote_config/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,10 +601,11 @@ function getTagsString (config, repositoryUrl, commitSHA) {
601601
*/
602602
function refreshIdentity (config) {
603603
clientId = uuid()
604-
if (config.tags['_dd.rc.client_id']) {
605-
config.tags['_dd.rc.client_id'] = clientId
606-
}
607604
if (client !== undefined) {
605+
// Written unconditionally: an RC lib-config update replaces `config.tags` with a fresh
606+
// object (see `config/remote_config.js`'s `tracing_tags` transformer), dropping this
607+
// directly-set key even though a client already exists.
608+
config.tags['_dd.rc.client_id'] = clientId
608609
client.id = clientId
609610
client.client_tracer.runtime_id = config.tags['runtime-id']
610611
const { commitSHA, repositoryUrl } = getGitMetadata(config)

packages/dd-trace/src/runtime_metrics/client.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,15 @@ function createMetricsClient (config) {
4848
*
4949
* @param {MetricsAggregationClient} client - The client returned by `createMetricsClient()`
5050
* @param {import('../config/config-base')} config - Tracer configuration
51+
* @param {() => void} [onRefresh] - Called after the tag update, e.g. to rebase sampler
52+
* baselines (CPU usage, event-loop delay) that would otherwise span the snapshot pause
5153
* @returns {() => void} Unsubscribe function; call it from the owning module's `stop()`
5254
*/
53-
function subscribeToIdentityRefresh (client, config) {
54-
const onIdentityRefresh = () => client.updateTags(buildClientConfig(config).tags)
55+
function subscribeToIdentityRefresh (client, config, onRefresh) {
56+
const onIdentityRefresh = () => {
57+
client.updateTags(buildClientConfig(config).tags)
58+
onRefresh?.()
59+
}
5560
identityRefreshChannel.subscribe(onIdentityRefresh)
5661
return () => identityRefreshChannel.unsubscribe(onIdentityRefresh)
5762
}

packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ module.exports = {
6363
this.stop()
6464

6565
client = createMetricsClient(config)
66-
unsubscribeIdentityRefresh = subscribeToIdentityRefresh(client, config)
66+
unsubscribeIdentityRefresh = subscribeToIdentityRefresh(client, config, resetSamplerBaselines)
6767
flushInterval = setInterval(() => {
6868
client.flush()
6969
}, config.DD_RUNTIME_METRICS_FLUSH_INTERVAL ?? 10_000)
@@ -251,6 +251,20 @@ module.exports = {
251251
},
252252
}
253253

254+
/**
255+
* Rebases the event-loop-delay and ELU sampler baselines to the current reading, e.g. on a
256+
* MicroVM clone resume, so the next collection reports the delta since the resume instead of
257+
* one spanning the snapshot pause.
258+
* @returns {void}
259+
*/
260+
function resetSamplerBaselines () {
261+
eventLoopHistogram?.reset()
262+
263+
if (lastElu !== null) {
264+
lastElu = performance.eventLoopUtilization()
265+
}
266+
}
267+
254268
/**
255269
* @param {Function} callback
256270
* @param {object} instrument

0 commit comments

Comments
 (0)