From 9fd9ea3ca0120f51f0eac918aadd2a9064024494 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Wed, 26 Aug 2026 20:13:09 -0400 Subject: [PATCH 1/2] feat(opentelemetry): add metrics lifecycle methods --- docs/API.md | 25 ++ docs/test.ts | 3 + index.d.ts | 5 + index.d.v5.ts | 5 + .../src/opentelemetry/metrics/index.js | 2 +- .../opentelemetry/metrics/meter_provider.js | 18 +- .../metrics/periodic_metric_reader.js | 124 ++++-- .../otlp/otlp_http_exporter_base.js | 6 +- .../test/opentelemetry/metrics.spec.js | 360 ++++++++++++++++-- 9 files changed, 479 insertions(+), 69 deletions(-) diff --git a/docs/API.md b/docs/API.md index b8847d62e4..c40a671bee 100644 --- a/docs/API.md +++ b/docs/API.md @@ -517,6 +517,31 @@ cpuGauge.addCallback((result) => { }) ``` +Short-lived processes can wait for pending metrics to reach the configured OTLP endpoint before exiting: + +Set `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` to bound each export. The value is in milliseconds. + +```javascript +const meterProvider = metrics.getMeterProvider() + +await meterProvider.forceFlush() +``` + +Call `shutdown()` when the application is finished recording metrics. It performs one final export and then stops the +provider. Calls to `forceFlush()` after shutdown have no effect. + +```javascript +await meterProvider.shutdown() +``` + +For TypeScript, cast the provider to the Datadog implementation type: + +```typescript +import type { opentelemetry as DatadogOpenTelemetry } from 'dd-trace' + +const meterProvider = metrics.getMeterProvider() as DatadogOpenTelemetry.MeterProvider +``` + #### Supported Configuration The Datadog SDK supports many of the configurations supported by the OpenTelemetry SDK. The following environment variables are supported: diff --git a/docs/test.ts b/docs/test.ts index a014c2f616..ea0407964a 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -559,6 +559,9 @@ const provider: opentelemetry.TracerProvider = new tracer.TracerProvider(); provider.register(); const otelTracer: opentelemetry.Tracer = provider.getTracer("name", "version") +const otelMeterProvider = {} as opentelemetry.MeterProvider +promise = otelMeterProvider.forceFlush() +promise = otelMeterProvider.shutdown() // OTel supports several time input formats otelTracer.startSpan("name", { startTime: new Date() }) diff --git a/index.d.ts b/index.d.ts index 5822d42cf6..e6d8e3234c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3261,6 +3261,11 @@ declare namespace tracer { } export namespace opentelemetry { + export interface MeterProvider extends otel.MeterProvider { + forceFlush(): Promise; + shutdown(): Promise; + } + /** * A registry for creating named {@link Tracer}s. */ diff --git a/index.d.v5.ts b/index.d.v5.ts index b96d2f20f3..61adc1ee4d 100644 --- a/index.d.v5.ts +++ b/index.d.v5.ts @@ -3447,6 +3447,11 @@ declare namespace tracer { } export namespace opentelemetry { + export interface MeterProvider extends otel.MeterProvider { + forceFlush(): Promise; + shutdown(): Promise; + } + /** * A registry for creating named {@link Tracer}s. */ diff --git a/packages/dd-trace/src/opentelemetry/metrics/index.js b/packages/dd-trace/src/opentelemetry/metrics/index.js index ef665a4b3f..76eabb3e09 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/index.js +++ b/packages/dd-trace/src/opentelemetry/metrics/index.js @@ -79,7 +79,7 @@ function initializeOpenTelemetryMetrics (config) { const meterProvider = new MeterProvider({ reader }) metrics.setGlobalMeterProvider(meterProvider) // Include the final metric collection and export in lifecycle retention. - registerTelemetryFlusher(done => meterProvider.forceFlush(done)) + registerTelemetryFlusher(done => reader.forceFlush(done)) } /** diff --git a/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js b/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js index 53cfcb20a5..97e8ca524c 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +++ b/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js @@ -50,12 +50,18 @@ class MeterProvider { return meter } - /** - * @param {Function} [done] Called after the metric export completes - */ - forceFlush (done) { - if (this.reader) this.reader.forceFlush(done) - else done?.() + forceFlush () { + return new Promise((resolve, reject) => { + if (this.reader) this.reader.forceFlush(error => error ? reject(error) : resolve()) + else resolve() + }) + } + + shutdown () { + return new Promise((resolve, reject) => { + if (this.reader) this.reader.shutdown(error => error ? reject(error) : resolve()) + else resolve() + }) } } diff --git a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js index 3bdf1408f8..8164cdd17e 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +++ b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js @@ -8,6 +8,15 @@ const { const { ObservableInstrument } = require('./instruments') const { nowUnixNano } = require('./time') +function invokeLifecycleCallback (callback, error) { + if (!callback) return + try { + callback(error) + } catch (callbackError) { + log.error('Error completing OTLP metrics lifecycle:', callbackError) + } +} + /** * @typedef {import('@opentelemetry/api').Attributes} Attributes * @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope @@ -105,6 +114,11 @@ class PeriodicMetricReader { #exportInterval #aggregator #batchCallbacks = [] + #exportQueue = [] + #isExporting = false + #periodicExportPending = false + #shutdownCallbacks = [] + #shutdownComplete = false /** * Creates a new PeriodicMetricReader instance. @@ -202,42 +216,31 @@ class PeriodicMetricReader { forceFlush (done) { if (this.#isShutdown) { log.warn('PeriodicMetricReader is shutdown. %d measurement(s) were dropped', this.#droppedCount) - done?.() + invokeLifecycleCallback(done) return } - let pending = 2 - const complete = () => { - if (--pending === 0) done?.() - } - - // Snapshot requests already active before starting this flush's export. - try { - if (typeof this.exporter.flush === 'function') this.exporter.flush(complete) - else complete() - } catch (error) { - log.error('Error flushing OTLP metrics:', error) - complete() - } - try { - this.#collectAndExport(complete) - } catch (error) { - log.error('Error exporting OTLP metrics:', error) - complete() - } + this.#enqueueExport(done) } /** * Shuts down the reader and stops periodic collection. - * @returns {void} + * + * @param {Function} [done] Called after the final export and exporter shutdown complete */ - shutdown () { + shutdown (done) { if (this.#isShutdown) { - log.warn('PeriodicMetricReader is already shutdown') + if (this.#shutdownComplete) { + log.warn('PeriodicMetricReader is already shutdown') + invokeLifecycleCallback(done) + } else if (done) { + this.#shutdownCallbacks.push(done) + } return } this.#isShutdown = true + if (done) this.#shutdownCallbacks.push(done) this.#clearTimer() - this.forceFlush() + this.#enqueueExport(error => this.#shutdownExporter(error)) } /** @@ -248,7 +251,7 @@ class PeriodicMetricReader { if (this.#timer) return this.#timer = setInterval(() => { - this.#collectAndExport() + this.#schedulePeriodicExport() }, this.#exportInterval) this.#timer.unref?.() } @@ -264,6 +267,69 @@ class PeriodicMetricReader { } } + #schedulePeriodicExport () { + if (this.#periodicExportPending) return + + this.#periodicExportPending = true + this.#enqueueExport(error => { + this.#periodicExportPending = false + if (error) log.error('Error exporting OTLP metrics:', error) + }) + } + + #enqueueExport (callback) { + this.#exportQueue.push(callback) + this.#drainExportQueue() + } + + #drainExportQueue () { + if (this.#isExporting || this.#exportQueue.length === 0) return + + this.#isExporting = true + const callback = this.#exportQueue.shift() + let completed = false + const complete = error => { + if (completed) return + completed = true + this.#isExporting = false + try { + invokeLifecycleCallback(callback, error) + } finally { + queueMicrotask(() => this.#drainExportQueue()) + } + } + + try { + this.#collectAndExport(complete) + } catch (error) { + log.error('Error exporting OTLP metrics:', error) + complete(error) + } + } + + #shutdownExporter (exportError) { + let completed = false + const complete = shutdownError => { + if (completed) return + completed = true + this.#shutdownComplete = true + const error = exportError || shutdownError + const callbacks = this.#shutdownCallbacks + this.#shutdownCallbacks = [] + for (const callback of callbacks) { + invokeLifecycleCallback(callback, error) + } + } + + try { + if (typeof this.exporter.shutdown === 'function') this.exporter.shutdown(complete) + else complete() + } catch (error) { + log.error('Error shutting down OTLP metrics exporter:', error) + complete(error) + } + } + /** * Collects measurements and exports metrics. * @@ -322,7 +388,13 @@ class PeriodicMetricReader { this.#lastExportedState ) - this.exporter.export(metrics, callback) + this.exporter.export(metrics, result => { + if (result?.code === 1) { + callback?.(result.error || new Error('OTLP metrics export failed')) + } else { + callback?.() + } + }) } } diff --git a/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js b/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js index b5034396c7..9fcba7f9ad 100644 --- a/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +++ b/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js @@ -136,9 +136,9 @@ class OtlpHttpExporterBase { }) req.once('timeout', () => { - req.destroy() const error = new Error('Request timeout') complete({ code: 1, error }) + req.destroy() }) req.write(payload) @@ -172,7 +172,9 @@ class OtlpHttpExporterBase { this.telemetryTags[0] = `protocol:${this.#transport === https ? 'https' : 'http'}` } - shutdown () {} + shutdown (done) { + done?.() + } } module.exports = OtlpHttpExporterBase diff --git a/packages/dd-trace/test/opentelemetry/metrics.spec.js b/packages/dd-trace/test/opentelemetry/metrics.spec.js index f7d26b6fff..8f3e9efbb9 100644 --- a/packages/dd-trace/test/opentelemetry/metrics.spec.js +++ b/packages/dd-trace/test/opentelemetry/metrics.spec.js @@ -1,6 +1,7 @@ 'use strict' const assert = require('assert') +const { EventEmitter } = require('node:events') const http = require('http') const { format, inspect } = require('util') @@ -14,6 +15,7 @@ const { protoMetricsService } = require('../../src/opentelemetry/otlp/protobuf_l const { getConfigFresh } = require('../helpers/config') const { DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE } = require('../../src/opentelemetry/metrics/constants') const MeterProvider = require('../../src/opentelemetry/metrics/meter_provider') +const OtlpHttpMetricExporter = require('../../src/opentelemetry/metrics/otlp_http_metric_exporter') const PeriodicMetricReader = require('../../src/opentelemetry/metrics/periodic_metric_reader') /** @@ -663,56 +665,346 @@ describe('OpenTelemetry Meter Provider', () => { }) describe('Lifecycle', () => { - it('waits for an in-flight export during forceFlush', () => { + it('exposes Promise lifecycle methods on the global meter provider', async () => { + const callbacks = {} + const reader = { + forceFlush: done => { callbacks.forceFlush = done }, + shutdown: done => { callbacks.shutdown = done }, + } + const provider = new MeterProvider({ reader }) + + const flush = provider.forceFlush() + assert(flush instanceof Promise) + let flushed = false + flush.then(() => { flushed = true }) + await Promise.resolve() + assert.strictEqual(flushed, false) + callbacks.forceFlush() + await flush + + const shutdown = provider.shutdown() + assert(shutdown instanceof Promise) + callbacks.shutdown() + await shutdown + }) + + it('rejects the provider lifecycle Promise when export fails', async () => { + const failure = new Error('export failed') + const reader = { + forceFlush: done => { done(failure) }, + shutdown: done => { done(failure) }, + } + const provider = new MeterProvider({ reader }) + + await assert.rejects(provider.forceFlush(), failure) + await assert.rejects(provider.shutdown(), failure) + }) + + it('serializes concurrent forceFlush exports and waits for each HTTP outcome', async () => { const exports = [] - const flushes = [] - const reader = new PeriodicMetricReader({ - export: (metrics, done) => { exports.push(done) }, - flush: (done) => { flushes.push(done) }, - }, 60_000, 'DELTA', 1024) - const meter = new MeterProvider({ reader }).getMeter('test') - const firstDone = sinon.spy() - const done = sinon.spy() + const exporter = { + export: (batch, done) => { exports.push({ batch, done }) }, + shutdown: done => { done() }, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + const counter = provider.getMeter('test').createCounter('requests') + + counter.add(1) + const first = provider.forceFlush() + counter.add(2) + const second = provider.forceFlush() + + assert.strictEqual(exports.length, 1) + exports[0].done({ code: 0 }) + await first + assert.strictEqual(exports.length, 2) + exports[1].done({ code: 0 }) + await second + await provider.shutdown() + }) + + it('coalesces periodic collections while an export is in flight', async () => { + const clock = sinon.useFakeTimers() + const exports = [] + const exporter = { + export: (batch, done) => { exports.push(done) }, + shutdown: done => { done() }, + } + const reader = new PeriodicMetricReader(exporter, 100, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + const gauge = provider.getMeter('test').createObservableGauge('temperature') + gauge.addCallback(result => { result.observe(20) }) + + try { + await clock.tickAsync(300) + assert.strictEqual(exports.length, 1) + exports[0]({ code: 0 }) + await Promise.resolve() + assert.strictEqual(exports.length, 1) + + const shutdown = provider.shutdown() + assert.strictEqual(exports.length, 2) + exports[1]({ code: 0 }) + await shutdown + } finally { + clock.restore() + } + }) + + it('rejects forceFlush after the HTTP export reports a failure', async () => { + const failure = new Error('HTTP 500') + const exporter = { + export: (batch, done) => { done({ code: 1, error: failure }) }, + shutdown: done => { done() }, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + + provider.getMeter('test').createCounter('requests').add(1) - meter.createCounter('in-flight').add(1) - reader.forceFlush(firstDone) - flushes.shift()() - meter.createCounter('boundary').add(1) - reader.forceFlush(done) + await assert.rejects(provider.forceFlush(), failure) + await provider.shutdown() + }) + + it('advances the export queue once when an exporter calls back twice', async () => { + const exports = [] + const exporter = { + export: (batch, done) => { exports.push(done) }, + shutdown: done => { done() }, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + const counter = provider.getMeter('test').createCounter('requests') + + counter.add(1) + const first = provider.forceFlush() + counter.add(2) + const second = provider.forceFlush() - sinon.assert.notCalled(done) + exports[0]({ code: 0 }) + exports[0]({ code: 1, error: new Error('late request error') }) + await first assert.strictEqual(exports.length, 2) exports[1]({ code: 0 }) - sinon.assert.notCalled(done) - flushes[0]() - sinon.assert.calledOnce(done) + await second + await provider.shutdown() + }) + + it('continues draining exports when a completion callback throws', async () => { + const exports = [] + const exporter = { + export: (batch, done) => { exports.push(done) }, + shutdown: done => { done() }, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + const counter = provider.getMeter('test').createCounter('requests') + + counter.add(1) + reader.forceFlush(() => { throw new Error('callback failed') }) + counter.add(2) + const second = provider.forceFlush() + + exports[0]({ code: 0 }) + await Promise.resolve() + assert.strictEqual(exports.length, 2) + exports[1]({ code: 0 }) + await second + await provider.shutdown() + }) + + it('drains a large lifecycle queue without exhausting the call stack', async () => { + const exports = [] + const exporter = { + export: (batch, done) => { exports.push(done) }, + shutdown: sinon.spy(done => { done() }), + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + + provider.getMeter('test').createCounter('requests').add(1) + const first = provider.forceFlush() + const flushes = Array.from({ length: 5_000 }, () => provider.forceFlush()) + const final = provider.shutdown() + exports[0]({ code: 0 }) - reader.shutdown() + await Promise.all([first, ...flushes, final]) + sinon.assert.calledOnce(exporter.shutdown) }) - it('waits for an earlier export when the boundary export throws', () => { - let priorDone - const reader = new PeriodicMetricReader({ - export: sinon.stub().throws(new Error('encode failed')), - flush: done => { priorDone = done }, - }, 60_000, 'DELTA', 1024) - const meter = new MeterProvider({ reader }).getMeter('test') + it('notifies every shutdown callback when one throws', () => { + const exports = [] + const exporter = { + export: (batch, done) => { exports.push(done) }, + shutdown: done => { done() }, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + const secondDone = sinon.spy() + + provider.getMeter('test').createCounter('requests').add(1) + reader.shutdown(() => { throw new Error('callback failed') }) + reader.shutdown(secondDone) + + exports[0]({ code: 0 }) + sinon.assert.calledOnce(secondDone) + }) + + it('isolates callbacks after shutdown completes', async () => { + const exporter = { + export: sinon.spy(), + shutdown: done => { done() }, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) const done = sinon.spy() - meter.createCounter('boundary').add(1) - reader.forceFlush(done) + await provider.shutdown() - sinon.assert.notCalled(done) - priorDone() + reader.forceFlush(() => { throw new Error('callback failed') }) + reader.shutdown(() => { throw new Error('callback failed') }) + reader.shutdown(done) sinon.assert.calledOnce(done) - reader.shutdown() + }) + + it('waits for an in-flight export before performing the final shutdown export', async () => { + const exports = [] + const shutdown = sinon.spy(done => { done() }) + const exporter = { + export: (batch, done) => { exports.push({ batch, done }) }, + shutdown, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + const counter = provider.getMeter('test').createCounter('requests') + + counter.add(1) + const flush = provider.forceFlush() + counter.add(2) + const final = provider.shutdown() + + assert.strictEqual(exports.length, 1) + sinon.assert.notCalled(shutdown) + exports[0].done({ code: 0 }) + await flush + assert.strictEqual(exports.length, 2) + sinon.assert.notCalled(shutdown) + exports[1].done({ code: 0 }) + await final + sinon.assert.calledOnce(shutdown) + }) + + it('shuts down the exporter and rejects when the final export fails', async () => { + const failure = new Error('final export failed') + const shutdown = sinon.spy(done => { done() }) + const exporter = { + export: (batch, done) => { done({ code: 1, error: failure }) }, + shutdown, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + + provider.getMeter('test').createCounter('requests').add(1) + + await assert.rejects(provider.shutdown(), failure) + sinon.assert.calledOnce(shutdown) + }) + + it('treats repeated shutdown and forceFlush after shutdown as successful no-ops', async () => { + const exporter = { + export: sinon.spy(), + shutdown: sinon.spy(done => { done() }), + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + + await provider.shutdown() + await provider.shutdown() + await provider.forceFlush() + + sinon.assert.calledOnce(exporter.shutdown) + sinon.assert.notCalled(exporter.export) + }) + + it('does not contact the exporter when forceFlush has no data', async () => { + const exporter = { + export: sinon.spy(), + shutdown: done => { done() }, + } + const reader = new PeriodicMetricReader(exporter, 60_000, 'DELTA', 1024) + const provider = new MeterProvider({ reader }) + + await provider.forceFlush() + sinon.assert.notCalled(exporter.export) + await provider.shutdown() + sinon.assert.notCalled(exporter.export) + }) + + it('completes an HTTP export once when timeout is followed by a request error', () => { + const request = new EventEmitter() + request.write = sinon.spy() + request.end = sinon.spy() + request.destroy = () => { request.emit('error', new Error('socket closed')) } + httpStub = sinon.stub(http, 'request').returns(request) + const exporter = new OtlpHttpMetricExporter( + 'http://localhost:4318/v1/metrics', {}, 10, 'http/json', {} + ) + const done = sinon.spy() + + exporter.sendPayload(Buffer.from('{}'), done) + request.emit('timeout') + + sinon.assert.calledOnce(done) + assert.strictEqual(done.firstCall.args[0].code, 1) + assert.match(done.firstCall.args[0].error.message, /Request timeout/) + }) + + it('reports the HTTP response outcome to the export callback', () => { + const response = new EventEmitter() + response.statusCode = 500 + const request = new EventEmitter() + request.write = sinon.spy() + request.end = () => { response.emit('data', 'failed'); response.emit('end') } + request.destroy = sinon.spy() + httpStub = sinon.stub(http, 'request').callsFake((options, callback) => { + callback(response) + return request + }) + const exporter = new OtlpHttpMetricExporter( + 'http://localhost:4318/v1/metrics', {}, 10, 'http/json', {} + ) + const done = sinon.spy() + + exporter.sendPayload(Buffer.from('{}'), done) + + sinon.assert.calledOnce(done) + assert.strictEqual(done.firstCall.args[0].code, 1) + assert.match(done.firstCall.args[0].error.message, /HTTP 500: failed/) + }) + + it('reports a request error to the export callback', () => { + const failure = new Error('connection refused') + const request = new EventEmitter() + request.write = sinon.spy() + request.end = () => { request.emit('error', failure) } + request.destroy = sinon.spy() + httpStub = sinon.stub(http, 'request').returns(request) + const exporter = new OtlpHttpMetricExporter( + 'http://localhost:4318/v1/metrics', {}, 10, 'http/json', {} + ) + const done = sinon.spy() + + exporter.sendPayload(Buffer.from('{}'), done) + + sinon.assert.calledOnceWithExactly(done, { code: 1, error: failure }) }) it('handles shutdown gracefully', async () => { setupMetrics() const provider = metrics.getMeterProvider() - await provider.reader.shutdown() - await provider.reader.shutdown() // Second shutdown should be safe + await provider.shutdown() + await provider.shutdown() // Second shutdown should be safe }) it('handles forceFlush', async () => { @@ -725,7 +1017,7 @@ describe('OpenTelemetry Meter Provider', () => { meter.createCounter('test').add(1) const provider = metrics.getMeterProvider() - await provider.reader.forceFlush() + await provider.forceFlush() validator() }) From 471864e5b40c717a7cb041f06e3629bf0f5300fe Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 27 Aug 2026 09:05:08 -0400 Subject: [PATCH 2/2] test: keep MeterProvider type assertions side-effect free --- docs/test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/test.ts b/docs/test.ts index ea0407964a..440aebce93 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -560,8 +560,8 @@ provider.register(); const otelTracer: opentelemetry.Tracer = provider.getTracer("name", "version") const otelMeterProvider = {} as opentelemetry.MeterProvider -promise = otelMeterProvider.forceFlush() -promise = otelMeterProvider.shutdown() +const otelForceFlush: () => Promise = otelMeterProvider.forceFlush +const otelShutdown: () => Promise = otelMeterProvider.shutdown // OTel supports several time input formats otelTracer.startSpan("name", { startTime: new Date() })