Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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 @@ -61,6 +61,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
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
31 changes: 29 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,41 @@
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) {
this._writer.append(payload)
this._writer.flush()
this.#flush(done)
}

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
9 changes: 8 additions & 1 deletion packages/dd-trace/src/opentelemetry/logs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ const os = require('os')
* @package
*/

const { registerTelemetryFlusher } = require('../../flush')
const LoggerProvider = require('./logger_provider')
const BatchLogRecordProcessor = require('./batch_log_processor')
const OtlpHttpLogExporter = require('./otlp_http_log_exporter')

let unregisterTelemetryFlusher

/**
* Initializes OpenTelemetry Logs support
* @param {import('../../config/config-base')} config - Tracer configuration instance
Expand Down Expand Up @@ -77,8 +80,12 @@ function initializeOpenTelemetryLogs (config) {
// Create logger provider with processor for Datadog Agent export
const loggerProvider = new LoggerProvider({ processor })

// Register the logger provider globally with OpenTelemetry API
// Expose this provider to application calls through the OpenTelemetry Logs API.
loggerProvider.register()
// Remove the old provider callback so lifecycle retention flushes only this global provider.
unregisterTelemetryFlusher?.()
// Include final log batches in lifecycle retention with trace delivery.
unregisterTelemetryFlusher = registerTelemetryFlusher(done => loggerProvider.forceFlush(done))
}

module.exports = {
Expand Down
11 changes: 7 additions & 4 deletions packages/dd-trace/src/opentelemetry/logs/logger_provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,15 @@ class LoggerProvider {

/**
* Forces a flush of all pending log records.
* @returns {undefined} Promise that resolves when flush is n ssue cncomplete
* @param {Function} [done] Called after all pending log exports complete
*/
forceFlush () {
if (!this.isShutdown) {
return this.processor?.forceFlush()
forceFlush (done) {
if (this.isShutdown || !this.processor) {
done?.()
return
}

this.processor.forceFlush(done)
}

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/dd-trace/src/opentelemetry/metrics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ const { metrics } = require('@opentelemetry/api')

const { VERSION } = require('../../../../../version')
const processTags = require('../../process-tags')
const { registerTelemetryFlusher } = require('../../flush')
const MeterProvider = require('./meter_provider')
const PeriodicMetricReader = require('./periodic_metric_reader')
const OtlpHttpMetricExporter = require('./otlp_http_metric_exporter')

let unregisterTelemetryFlusher

/**
* @typedef {import('../../config')} Config
*/
Expand Down Expand Up @@ -76,6 +79,10 @@ function initializeOpenTelemetryMetrics (config) {

const meterProvider = new MeterProvider({ reader })
metrics.setGlobalMeterProvider(meterProvider)
// Remove the old provider callback so lifecycle retention flushes only this global provider.
unregisterTelemetryFlusher?.()
// Include the final metric collection and export in lifecycle retention.
unregisterTelemetryFlusher = registerTelemetryFlusher(done => meterProvider.forceFlush(done))
}

function buildResourceAttributes (tags, { reportHostname, otelSemanticsEnabled, service, env, serviceVersion } = {}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ class MeterProvider {
}
return meter
}

/**
* @param {Function} [done] Called after the metric export completes
*/
forceFlush (done) {
if (this.reader) this.reader.forceFlush(done)
else done?.()
}
}

module.exports = MeterProvider
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ class OtlpHttpMetricExporter extends OtlpHttpExporterBase {
*
* @param {Map<string, AggregatedMetric>} metrics - Map of metric data to export
*
* @returns {void}
* @param {Function} [done] Called after the HTTP export completes
*/
export (metrics) {
export (metrics, done) {
if (metrics.size === 0) {
done?.({ code: 0 })
return
}

Expand All @@ -56,6 +57,7 @@ class OtlpHttpMetricExporter extends OtlpHttpExporterBase {
if (result.code === 0) {
this.recordTelemetry('otel.metrics_export_successes', 1, additionalTags)
}
done?.(result)
})
}
}
Expand Down
Loading