Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ecb9c67
fix(serverless): retain telemetry on Vercel
wconti27 Aug 12, 2026
3071c5d
fix(serverless): retain active telemetry exports
wconti27 Aug 13, 2026
2cd52d1
refactor(serverless): isolate Vercel lifecycle adapter
wconti27 Aug 13, 2026
a8011be
docs(otlp): clarify logger flush registration
wconti27 Aug 13, 2026
c8eb4e2
docs(serverless): clarify telemetry flush registry
wconti27 Aug 13, 2026
f077780
test(otlp): cover multi-batch log flushing
wconti27 Aug 13, 2026
295ba2d
refactor(serverless): bound flushing in tracer
wconti27 Aug 13, 2026
fa7c7dd
fix(serverless): retain span stats and bounded log batches
wconti27 Aug 13, 2026
b4dc847
test(span-stats): cover in-flight export flush
wconti27 Aug 13, 2026
49242e9
fix(serverless): wait for Vercel response completion
wconti27 Aug 13, 2026
7754861
fix(serverless): order Vercel telemetry flushing
wconti27 Aug 13, 2026
6d1e78a
fix(exporters): clean up failed flush records
wconti27 Aug 13, 2026
af34546
fix(serverless): retain outer Vercel telemetry
wconti27 Aug 18, 2026
7caaca0
fix(exporters): retain in-flight traces after flush failure
wconti27 Aug 18, 2026
7370141
fix(serverless): retain telemetry on Vercel
wconti27 Aug 12, 2026
dcb9d69
fix(serverless): retain active telemetry exports
wconti27 Aug 13, 2026
59de657
refactor(serverless): isolate Vercel lifecycle adapter
wconti27 Aug 13, 2026
99a4454
docs(otlp): clarify logger flush registration
wconti27 Aug 13, 2026
f5c3d82
docs(serverless): clarify telemetry flush registry
wconti27 Aug 13, 2026
6791765
test(otlp): cover multi-batch log flushing
wconti27 Aug 13, 2026
94b7729
refactor(serverless): bound flushing in tracer
wconti27 Aug 13, 2026
ab88223
fix(serverless): retain span stats and bounded log batches
wconti27 Aug 13, 2026
54ba4d3
test(span-stats): cover in-flight export flush
wconti27 Aug 13, 2026
199cebb
fix(serverless): wait for Vercel response completion
wconti27 Aug 13, 2026
e299783
fix(serverless): order Vercel telemetry flushing
wconti27 Aug 13, 2026
3ff70e5
fix(exporters): clean up failed flush records
wconti27 Aug 13, 2026
614e2b1
fix(serverless): retain outer Vercel telemetry
wconti27 Aug 18, 2026
98cf0f9
fix(exporters): retain in-flight traces after flush failure
wconti27 Aug 18, 2026
5a7fa90
fix(serverless): retain runtime metrics and span stats
wconti27 Aug 18, 2026
2360f69
merge: retain PR history after rebase
wconti27 Aug 18, 2026
1bb364d
Merge branch 'master' into conti/vercel-waituntil-flush-callback
wconti27 Aug 18, 2026
859506d
fix(serverless): retain all Vercel telemetry flushes
wconti27 Aug 18, 2026
27b2693
fix(serverless): retain remaining Vercel telemetry
wconti27 Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
/packages/dd-trace/src/lambda/ @DataDog/serverless-aws @DataDog/apm-serverless
/packages/dd-trace/src/azure_metadata.js @DataDog/apm-serverless
/packages/dd-trace/src/serverless.js @DataDog/apm-serverless
/packages/dd-trace/src/serverless/vercel.js @DataDog/apm-serverless
/packages/dd-trace/src/flush.js @DataDog/apm-serverless @DataDog/apm-sdk-capabilities-js
/packages/dd-trace/test/lambda/ @DataDog/serverless-aws @DataDog/apm-serverless
/packages/dd-trace/test/azure_metadata.spec.js @DataDog/apm-serverless
/packages/dd-trace/test/serverless.spec.js @DataDog/apm-serverless
Expand Down
14 changes: 14 additions & 0 deletions packages/datadog-plugin-http2/test/server.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { setImmediate } = require('node:timers/promises')

const { afterEach, beforeEach, describe, it } = require('mocha')
const sinon = require('sinon')
const { channel } = require('dc-polyfill')

const agent = require('../../dd-trace/test/plugins/agent')
const web = require('../../dd-trace/src/plugins/util/web')
Expand Down Expand Up @@ -309,6 +310,19 @@ describe('Plugin', () => {
rawExpectedSchema.server
)

it('publishes a close response event', async () => {
const emit = sinon.spy()
const emitChannel = channel('apm:http2:server:response:emit')
emitChannel.subscribe(emit)

try {
await request(http2, `http://localhost:${port}/user`)
sinon.assert.calledWithMatch(emit, { eventName: 'close' })
} finally {
emitChannel.unsubscribe(emit)
}
})

it('should do automatic instrumentation', done => {
agent
.assertFirstTraceSpan({
Expand Down
42 changes: 28 additions & 14 deletions packages/dd-trace/src/dogstatsd.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,23 @@ class DogStatsDClient {
this._add(stat, value, TYPE_HISTOGRAM, tags)
}

flush () {
flush (done) {
const queue = this._enqueue()

if (queue.length === 0) return
if (queue.length === 0) return done?.()
Comment thread
wconti27 marked this conversation as resolved.
Outdated
Comment thread
wconti27 marked this conversation as resolved.
Outdated

log.debug('Flushing %s metrics via %s', queue.length, this._httpOptions ? 'HTTP' : 'UDP')

this._queue = []

if (this._httpOptions) {
this._sendHttp(queue)
this._sendHttp(queue, done)
} else {
this._sendUdp(queue)
this._sendUdp(queue, done)
}
}

_sendHttp (queue) {
_sendHttp (queue, done) {
const buffer = Buffer.concat(queue)
request(buffer, this._httpOptions, (err) => {
if (err) {
Expand All @@ -95,32 +95,46 @@ class DogStatsDClient {
// options. Either way, we can give UDP a try.
this._httpOptions = undefined
}
this._sendUdp(queue)
this._sendUdp(queue, done)
} else {
done?.()
}
})
}

_sendUdp (queue) {
_sendUdp (queue, done) {
// dgram resolves the local address via the instrumented dns.lookup when it
// binds on first send; the noop store keeps that self-traffic off the trace.
legacyStorage.run({ noop: true }, () => {
if (this._family === 0) {
this.#lookup(this._host, (error, address, family) => {
if (error) return log.error('DogStatsDClient: Host not found', error)
this._sendUdpFromQueue(queue, address, family)
if (error) {
log.error('DogStatsDClient: Host not found', error)
return done?.()
}
this._sendUdpFromQueue(queue, address, family, done)
})
} else {
this._sendUdpFromQueue(queue, this._host, this._family)
this._sendUdpFromQueue(queue, this._host, this._family, done)
}
})
}

_sendUdpFromQueue (queue, address, family) {
_sendUdpFromQueue (queue, address, family, done) {
const socket = family === 6 ? this._udp6 : this._udp4
let pending = queue.length
const complete = () => {
if (--pending === 0) done?.()
}

for (const buffer of queue) {
log.debug('Sending to DogStatsD: %s', buffer)
socket.send(buffer, 0, buffer.length, this._port, address)
try {
socket.send(buffer, 0, buffer.length, this._port, address, complete)
} catch (error) {
log.error('DogStatsDClient: UDP error sending metrics', error)
complete()
}
}
}

Expand Down Expand Up @@ -212,12 +226,12 @@ class MetricsAggregationClient {
this.reset()
}

flush () {
flush (done) {
this._captureCounters()
this._captureGauges()
this._captureHistograms()

this._client.flush()
this._client.flush(done)
Comment thread
wconti27 marked this conversation as resolved.
}

reset () {
Expand Down
38 changes: 35 additions & 3 deletions packages/dd-trace/src/exporters/agent/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const Writer = require('./writer')

class AgentExporter {
#timer
#activeFlushes = new Set()

constructor (config, prioritySampler) {
this._config = config
Expand Down Expand Up @@ -44,10 +45,10 @@ class AgentExporter {
const { flushInterval } = this._config

if (flushInterval === 0) {
this._writer.flush()
this.#flush()
} else if (this.#timer === undefined) {
this.#timer = setTimeout(() => {
this._writer.flush()
this.#flush()
this.#timer = undefined
}, flushInterval)
this.#timer.unref?.()
Expand All @@ -57,7 +58,38 @@ class AgentExporter {
flush (done = () => {}) {
clearTimeout(this.#timer)
this.#timer = undefined
this._writer.flush(done)

// Snapshot before the boundary flush so a failed encoding cannot cause a
// Vercel lifecycle flush to abandon exports that were already in flight.
let activeFlushes = [...this.#activeFlushes]
Comment thread
wconti27 marked this conversation as resolved.
try {
this.#flush()
} catch (error) {
log.error('Failed to flush traces: %s', error.message)
}
activeFlushes = [...new Set([...activeFlushes, ...this.#activeFlushes])]
if (activeFlushes.length === 0) return done()

let pending = activeFlushes.length
const complete = () => {
if (--pending === 0) done()
}
for (const flush of activeFlushes) flush.callbacks.push(complete)
}

#flush () {
const flush = { callbacks: [] }
this.#activeFlushes.add(flush)
const complete = () => {
this.#activeFlushes.delete(flush)
for (const callback of flush.callbacks) callback()
}
try {
this._writer.flush(complete)
} catch (error) {
complete()
throw error
}
}
}

Expand Down
46 changes: 44 additions & 2 deletions packages/dd-trace/src/exporters/span-stats/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,56 @@
const { Writer } = require('./writer')

class SpanStatsExporter {
#activeFlushes = new Set()

constructor (config) {
this._url = config.url
this._writer = new Writer({ url: this._url })
}

export (payload) {
export (payload, done) {
if (done) {
const activeFlushes = [...this.#activeFlushes]
let pending = activeFlushes.length + 1
const complete = () => {
if (--pending === 0) done()
}
for (const flush of activeFlushes) flush.callbacks.push(complete)
this._writer.append(payload)
try {
this.#flush(complete)
Comment thread
wconti27 marked this conversation as resolved.
} catch {
// `#flush` has notified the boundary request; keep waiting for prior exports.
Comment thread
wconti27 marked this conversation as resolved.
}
return
}
this._writer.append(payload)
this._writer.flush()
this.#flush()
}

flush (done) {
const activeFlushes = [...this.#activeFlushes]
let pending = activeFlushes.length + 1
const complete = () => {
if (--pending === 0) done?.()
}
for (const flush of activeFlushes) flush.callbacks.push(complete)
this.#flush(complete)
}

#flush (done) {
const flush = { callbacks: done ? [done] : [] }
this.#activeFlushes.add(flush)
const complete = () => {
this.#activeFlushes.delete(flush)
for (const callback of flush.callbacks) callback()
}
try {
this._writer.flush(complete)
} catch (error) {
complete()
throw error
}
}
}

Expand Down
87 changes: 87 additions & 0 deletions packages/dd-trace/src/flush.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict'

const log = require('./log')

/**
* @typedef {(done: () => void) => void | Promise<void>} TelemetryFlusher
*/

/** @type {Set<TelemetryFlusher>} */
const telemetryFlushers = new Set()

/**
* Registers a configured telemetry pipeline so serverless lifecycle retention
* waits for its final export alongside trace delivery.
* @param {TelemetryFlusher} flusher
* @returns {() => void} Removes this pipeline when its provider is replaced.
*/
function registerTelemetryFlusher (flusher) {
telemetryFlushers.add(flusher)
// Avoid retaining a replaced provider or flushing it alongside the new one.
return () => telemetryFlushers.delete(flusher)
}

/**
* Flushes the trace exporter and every registered telemetry pipeline.
* @param {{
* _exporter?: { flush?: TelemetryFlusher },
* _processor?: { _stats?: { forceFlush?: TelemetryFlusher } }
* }|undefined} tracer
* @param {() => void} [done]
* @param {{ timeout?: number }} [options]
*/
function flushAll (tracer, done, options) {
const traceExporter = tracer?._exporter
const traceFlusher = traceExporter?.flush
const spanStatsFlusher = tracer?._processor?._stats?.forceFlush
// TODO: Include DSM after DataStreamsProcessor exposes a completion-aware flush API.
let pending = telemetryFlushers.size +
(typeof traceFlusher === 'function' ? 1 : 0) +
(typeof spanStatsFlusher === 'function' ? 1 : 0)
Comment thread
wconti27 marked this conversation as resolved.
Outdated
Comment thread
wconti27 marked this conversation as resolved.
Outdated
let completed = false
let timeout

const finish = () => {
if (completed) return
completed = true
clearTimeout(timeout)
done?.()
}
const complete = () => {
if (--pending === 0) finish()
}

if (pending === 0) return finish()
if (options?.timeout) {
timeout = setTimeout(() => {
log.warn('Timed out waiting for telemetry flush after %dms', options.timeout)
finish()
}, options.timeout)
}

const flush = flusher => {
let flushed = false
const onFlushed = error => {
if (flushed) return
flushed = true
if (error) log.error('Error flushing telemetry pipeline:', error)
complete()
}
try {
const result = flusher(onFlushed)
result?.then(onFlushed, error => onFlushed(error))
} catch (error) {
onFlushed(error)
}
}

if (typeof traceFlusher === 'function') {
flush(done => traceFlusher.call(traceExporter, done))
Comment thread
wconti27 marked this conversation as resolved.
Outdated
Comment thread
wconti27 marked this conversation as resolved.
Outdated
}
if (typeof spanStatsFlusher === 'function') {
flush(done => spanStatsFlusher.call(tracer._processor._stats, done))
}
for (const flusher of telemetryFlushers) flush(flusher)
Comment thread
wconti27 marked this conversation as resolved.
}

module.exports = { flushAll, registerTelemetryFlusher }
31 changes: 28 additions & 3 deletions packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,34 @@ class BatchLogRecordProcessor {

/**
* Forces an immediate flush of all pending log records.
* @returns {undefined} Promise that resolves when flush is complete
* @param {Function} [done] Called after all pending log exports complete
*/
forceFlush () {
this.#export()
forceFlush (done) {
this.#clearTimer()
// Flush only records present at this boundary. New records belong to the
// later request that produced them and must not extend this lifecycle flush.
const logRecords = this.#logRecords
this.#logRecords = []
let pending = 2
const complete = () => {
if (--pending === 0) done?.()
}

// Join exports already active at this boundary before draining this snapshot.
if (typeof this.exporter.flush === 'function') this.exporter.flush(complete)
else complete()

const flushNext = () => {
if (logRecords.length === 0) {
complete()
return
}

// Drain the boundary snapshot one batch at a time.
const batch = logRecords.splice(0, this.#maxExportBatchSize)
this.exporter.export(batch, flushNext)
}
flushNext()
}

/**
Expand All @@ -79,6 +103,7 @@ class BatchLogRecordProcessor {
* @private
*/
#export () {
if (this.#logRecords.length === 0) return
const logRecords = this.#logRecords.slice(0, this.#maxExportBatchSize)
this.#logRecords = this.#logRecords.slice(this.#maxExportBatchSize)

Expand Down
Loading
Loading