diff --git a/docs/API.md b/docs/API.md
index eba59b30c4a..724c75cc8c2 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -408,14 +408,74 @@ app.listen(3000)
The Datadog SDK supports many of the configurations supported by the OpenTelemetry SDK. The following environment variables are supported:
- `DD_LOGS_OTEL_ENABLED` - Enable OpenTelemetry logs (default: `false`)
-- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` - OTLP endpoint URL for logs (default: `http://localhost:4318`)
-- `OTEL_EXPORTER_OTLP_LOGS_HEADERS` - Optional headers in JSON format for logs (default: `{}`)
-- `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` - OTLP protocol for logs (default: `http/protobuf`)
-- `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` - Request timeout in milliseconds for logs (default: `10000`)
-- `OTEL_BSP_SCHEDULE_DELAY` - Batch timeout in milliseconds (default: `5000`)
+- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` - OTLP endpoint URL for logs. Falls back to `OTEL_EXPORTER_OTLP_ENDPOINT` with `/v1/logs` appended (default: `http://localhost:4318/v1/logs`)
+- `OTEL_EXPORTER_OTLP_LOGS_HEADERS` - Optional headers for logs in JSON format. Falls back to `OTEL_EXPORTER_OTLP_HEADERS` (default: `{}`)
+- `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` - OTLP protocol for logs. Options: `http/protobuf`, `http/json`. Falls back to `OTEL_EXPORTER_OTLP_PROTOCOL` (default: `http/protobuf`)
+- `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` - Request timeout in milliseconds for logs. Falls back to `OTEL_EXPORTER_OTLP_TIMEOUT` (default: `10000`)
+- `OTEL_BSP_SCHEDULE_DELAY` - Batch export delay in milliseconds (default: `5000`)
- `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` - Maximum logs per batch (default: `512`)
+- `OTEL_BSP_MAX_QUEUE_SIZE` - Maximum logs to queue before dropping (default: `2048`)
-Logs are exported via OTLP over HTTP. The protocol can be configured using `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` or `OTEL_EXPORTER_OTLP_PROTOCOL` environment variables. Supported protocols are `http/protobuf` (default) and `http/json`. For complete OTLP exporter configuration options, see the [OpenTelemetry OTLP Exporter documentation](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/).
+For complete OTLP exporter configuration options, see the [OpenTelemetry OTLP Exporter documentation](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/).
+
+
OpenTelemetry Metrics
+
+dd-trace-js includes experimental support for OpenTelemetry metrics, designed as a drop-in replacement for the OpenTelemetry Metrics SDK. This lightweight implementation is fully compliant with the OpenTelemetry Metrics API and integrates with the existing OTLP export infrastructure. Enable it by setting `DD_METRICS_OTEL_ENABLED=true` and use the [OpenTelemetry Metrics API](https://open-telemetry.github.io/opentelemetry-js/modules/_opentelemetry_api.html) to record metric data:
+
+```javascript
+require('dd-trace').init()
+const { metrics } = require('@opentelemetry/api')
+
+const meter = metrics.getMeter('my-service', '1.0.0')
+
+// Counter - monotonically increasing values
+const requestCounter = meter.createCounter('http.requests', {
+ description: 'Total HTTP requests',
+ unit: 'requests'
+})
+requestCounter.add(1, { method: 'GET', status: 200 })
+
+// Histogram - distribution of values
+const durationHistogram = meter.createHistogram('http.duration', {
+ description: 'HTTP request duration',
+ unit: 'ms'
+})
+durationHistogram.record(145, { route: '/api/users' })
+
+// UpDownCounter - can increase and decrease
+const connectionCounter = meter.createUpDownCounter('active.connections', {
+ description: 'Active connections',
+ unit: 'connections'
+})
+connectionCounter.add(1) // New connection
+connectionCounter.add(-1) // Connection closed
+
+// ObservableGauge - asynchronous observations
+const cpuGauge = meter.createObservableGauge('system.cpu.usage', {
+ description: 'CPU usage percentage',
+ unit: 'percent'
+})
+cpuGauge.addCallback((result) => {
+ const cpuUsage = process.cpuUsage()
+ result.observe(cpuUsage.system / 1000000, { core: '0' })
+})
+```
+
+#### Supported Configuration
+
+The Datadog SDK supports many of the configurations supported by the OpenTelemetry SDK. The following environment variables are supported:
+
+- `DD_METRICS_OTEL_ENABLED` - Enable OpenTelemetry metrics (default: `false`)
+- `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` - OTLP endpoint URL for metrics. Falls back to `OTEL_EXPORTER_OTLP_ENDPOINT` with `/v1/metrics` appended (default: `http://localhost:4318/v1/metrics`)
+- `OTEL_EXPORTER_OTLP_METRICS_HEADERS` - Optional headers for metrics in JSON format. Falls back to `OTEL_EXPORTER_OTLP_HEADERS` (default: `{}`)
+- `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` - OTLP protocol for metrics. Options: `http/protobuf`, `http/json`. Falls back to `OTEL_EXPORTER_OTLP_PROTOCOL` (default: `http/protobuf`)
+- `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` - Request timeout in milliseconds for metrics. Falls back to `OTEL_EXPORTER_OTLP_TIMEOUT` (default: `10000`)
+- `OTEL_METRIC_EXPORT_INTERVAL` - Metric export interval in milliseconds (default: `10000`)
+- `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` - Aggregation temporality preference. Options: `CUMULATIVE`, `DELTA`, `LOWMEMORY` (default: `DELTA`). See [OpenTelemetry spec](https://opentelemetry.io/docs/specs/otel/metrics/sdk_exporters/otlp/#additional-environment-variable-configuration) for details
+- `OTEL_BSP_MAX_QUEUE_SIZE` - Maximum metrics to queue before dropping (default: `2048`)
+- `OTEL_METRIC_EXPORT_TIMEOUT` - [NOT YET SUPPORTED] Time to export metrics including retries
+
+For complete OTLP exporter configuration options, see the [OpenTelemetry OTLP Exporter documentation](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/).
Advanced Configuration
diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js
index fd02d3766ad..bd06c07373b 100644
--- a/packages/dd-trace/src/config.js
+++ b/packages/dd-trace/src/config.js
@@ -476,6 +476,7 @@ class Config {
DD_INSTRUMENTATION_CONFIG_ID,
DD_LOGS_INJECTION,
DD_LOGS_OTEL_ENABLED,
+ DD_METRICS_OTEL_ENABLED,
DD_LANGCHAIN_SPAN_CHAR_LIMIT,
DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE,
DD_LLMOBS_AGENTLESS_ENABLED,
@@ -570,12 +571,20 @@ class Config {
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
+ OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
+ OTEL_EXPORTER_OTLP_METRICS_HEADERS,
+ OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
+ OTEL_EXPORTER_OTLP_METRICS_TIMEOUT,
+ OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE,
+ OTEL_METRIC_EXPORT_TIMEOUT,
OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_ENDPOINT,
OTEL_EXPORTER_OTLP_HEADERS,
OTEL_EXPORTER_OTLP_TIMEOUT,
OTEL_BSP_SCHEDULE_DELAY,
- OTEL_BSP_MAX_EXPORT_BATCH_SIZE
+ OTEL_BSP_MAX_EXPORT_BATCH_SIZE,
+ OTEL_BSP_MAX_QUEUE_SIZE,
+ OTEL_METRIC_EXPORT_INTERVAL
} = source
const tags = {}
@@ -602,10 +611,40 @@ class Config {
this.#setString(target, 'otelLogsHeaders', OTEL_EXPORTER_OTLP_LOGS_HEADERS || target.otelHeaders)
this.#setString(target, 'otelProtocol', OTEL_EXPORTER_OTLP_PROTOCOL)
this.#setString(target, 'otelLogsProtocol', OTEL_EXPORTER_OTLP_LOGS_PROTOCOL || target.otelProtocol)
- target.otelTimeout = maybeInt(OTEL_EXPORTER_OTLP_TIMEOUT)
- target.otelLogsTimeout = maybeInt(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) || target.otelTimeout
- target.otelLogsBatchTimeout = maybeInt(OTEL_BSP_SCHEDULE_DELAY)
- target.otelLogsMaxExportBatchSize = maybeInt(OTEL_BSP_MAX_EXPORT_BATCH_SIZE)
+ const otelTimeout = nonNegInt(OTEL_EXPORTER_OTLP_TIMEOUT, 'OTEL_EXPORTER_OTLP_TIMEOUT')
+ if (otelTimeout !== undefined) {
+ target.otelTimeout = otelTimeout
+ }
+ const otelLogsTimeout = nonNegInt(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, 'OTEL_EXPORTER_OTLP_LOGS_TIMEOUT')
+ target.otelLogsTimeout = otelLogsTimeout === undefined ? target.otelTimeout : otelLogsTimeout
+ const otelBatchTimeout = nonNegInt(OTEL_BSP_SCHEDULE_DELAY, 'OTEL_BSP_SCHEDULE_DELAY', false)
+ if (otelBatchTimeout !== undefined) {
+ target.otelBatchTimeout = otelBatchTimeout
+ }
+ target.otelMaxExportBatchSize = nonNegInt(OTEL_BSP_MAX_EXPORT_BATCH_SIZE, 'OTEL_BSP_MAX_EXPORT_BATCH_SIZE', false)
+ target.otelMaxQueueSize = nonNegInt(OTEL_BSP_MAX_QUEUE_SIZE, 'OTEL_BSP_MAX_QUEUE_SIZE', false)
+
+ const otelMetricsExporter = !OTEL_METRICS_EXPORTER || OTEL_METRICS_EXPORTER.toLowerCase() !== 'none'
+ this.#setBoolean(target, 'otelMetricsEnabled', DD_METRICS_OTEL_ENABLED && otelMetricsExporter)
+ // Set OpenTelemetry metrics configuration with specific _METRICS_ vars
+ // taking precedence over generic _EXPORTERS_ vars
+ if (OTEL_EXPORTER_OTLP_ENDPOINT || OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) {
+ this.#setString(target, 'otelMetricsUrl', OTEL_EXPORTER_OTLP_METRICS_ENDPOINT || target.otelUrl)
+ }
+ this.#setString(target, 'otelMetricsHeaders', OTEL_EXPORTER_OTLP_METRICS_HEADERS || target.otelHeaders)
+ this.#setString(target, 'otelMetricsProtocol', OTEL_EXPORTER_OTLP_METRICS_PROTOCOL || target.otelProtocol)
+ const otelMetricsTimeout = nonNegInt(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, 'OTEL_EXPORTER_OTLP_METRICS_TIMEOUT')
+ target.otelMetricsTimeout = otelMetricsTimeout === undefined ? target.otelTimeout : otelMetricsTimeout
+ target.otelMetricsExportTimeout = nonNegInt(OTEL_METRIC_EXPORT_TIMEOUT, 'OTEL_METRIC_EXPORT_TIMEOUT')
+ target.otelMetricsExportInterval = nonNegInt(OTEL_METRIC_EXPORT_INTERVAL, 'OTEL_METRIC_EXPORT_INTERVAL', false)
+
+ // Parse temporality preference (default to DELTA for Datadog)
+ if (OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE) {
+ const temporalityPref = OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE.toUpperCase()
+ if (['DELTA', 'CUMULATIVE', 'LOWMEMORY'].includes(temporalityPref)) {
+ this.#setString(target, 'otelMetricsTemporalityPreference', temporalityPref)
+ }
+ }
this.#setBoolean(
target,
'apmTracingEnabled',
@@ -1200,9 +1239,10 @@ class Config {
calc['dogstatsd.hostname'] = this.#getHostname()
- // Compute OTLP logs URL to send payloads to the active Datadog Agent
+ // Compute OTLP logs and metrics URLs to send payloads to the active Datadog Agent
const agentHostname = this.#getHostname()
calc.otelLogsUrl = `http://${agentHostname}:${DEFAULT_OTLP_PORT}`
+ calc.otelMetricsUrl = `http://${agentHostname}:${DEFAULT_OTLP_PORT}/v1/metrics`
calc.otelUrl = `http://${agentHostname}:${DEFAULT_OTLP_PORT}`
this.#setBoolean(calc, 'isGitUploadEnabled',
@@ -1491,6 +1531,16 @@ function maybeFloat (number) {
return Number.isNaN(parsed) ? undefined : parsed
}
+function nonNegInt (value, envVarName, allowZero = true) {
+ if (value === undefined) return
+ const parsed = Number.parseInt(value)
+ if (Number.isNaN(parsed) || parsed < 0 || (parsed === 0 && !allowZero)) {
+ log.warn(`Invalid value ${parsed} for ${envVarName}. Using default value.`)
+ return
+ }
+ return parsed
+}
+
function getAgentUrl (url, options) {
if (url) return new URL(url)
diff --git a/packages/dd-trace/src/config_defaults.js b/packages/dd-trace/src/config_defaults.js
index a7492209c46..201f93830b5 100644
--- a/packages/dd-trace/src/config_defaults.js
+++ b/packages/dd-trace/src/config_defaults.js
@@ -138,8 +138,17 @@ module.exports = {
otelLogsProtocol: 'http/protobuf',
otelLogsTimeout: 10_000,
otelTimeout: 10_000,
- otelLogsBatchTimeout: 5000,
- otelLogsMaxExportBatchSize: 512,
+ otelBatchTimeout: 5000,
+ otelMaxExportBatchSize: 512,
+ otelMaxQueueSize: 2048,
+ otelMetricsEnabled: false,
+ otelMetricsUrl: undefined, // Will be computed using agent host
+ otelMetricsHeaders: '',
+ otelMetricsProtocol: 'http/protobuf',
+ otelMetricsTimeout: 10_000,
+ otelMetricsExportTimeout: 7500,
+ otelMetricsExportInterval: 10_000,
+ otelMetricsTemporalityPreference: 'DELTA', // DELTA, CUMULATIVE, or LOWMEMORY
lookup: undefined,
inferredProxyServicesEnabled: false,
memcachedCommandEnabled: false,
diff --git a/packages/dd-trace/src/opentelemetry/logs/index.js b/packages/dd-trace/src/opentelemetry/logs/index.js
index 76a91e05a7b..6b4054da781 100644
--- a/packages/dd-trace/src/opentelemetry/logs/index.js
+++ b/packages/dd-trace/src/opentelemetry/logs/index.js
@@ -70,8 +70,8 @@ function initializeOpenTelemetryLogs (config) {
// Create batch processor for exporting logs to Datadog Agent
const processor = new BatchLogRecordProcessor(
exporter,
- config.otelLogsBatchTimeout,
- config.otelLogsMaxExportBatchSize
+ config.otelBatchTimeout,
+ config.otelMaxExportBatchSize
)
// Create logger provider with processor for Datadog Agent export
diff --git a/packages/dd-trace/src/opentelemetry/logs/logger.js b/packages/dd-trace/src/opentelemetry/logs/logger.js
index 57eb344fee0..7f210206bd5 100644
--- a/packages/dd-trace/src/opentelemetry/logs/logger.js
+++ b/packages/dd-trace/src/opentelemetry/logs/logger.js
@@ -2,7 +2,8 @@
const { sanitizeAttributes } = require('@opentelemetry/core')
const { context } = require('@opentelemetry/api')
-const packageVersion = require('../../../../../package.json').version
+const { VERSION: packageVersion } = require('../../../../../version')
+
/**
* @typedef {import('@opentelemetry/api-logs').LogRecord} LogRecord
* @typedef {import('@opentelemetry/api').SpanContext} SpanContext
diff --git a/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js b/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js
index 29a8ed45901..3d157e85b0b 100644
--- a/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js
+++ b/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js
@@ -11,7 +11,7 @@ const OtlpTransformer = require('./otlp_transformer')
/**
* OtlpHttpLogExporter exports log records via OTLP over HTTP.
*
- * This implementation follows the OTLP HTTP specification:
+ * This implementation follows the OTLP HTTP v1.7.0 specification:
* https://opentelemetry.io/docs/specs/otlp/#otlphttp
*
* @class OtlpHttpLogExporter
@@ -37,6 +37,8 @@ class OtlpHttpLogExporter extends OtlpHttpExporterBase {
*
* @param {LogRecord[]} logRecords - Array of enriched log records to export
* @param {Function} resultCallback - Callback function for export result
+ *
+ * @returns {void}
*/
export (logRecords, resultCallback) {
if (logRecords.length === 0) {
@@ -45,8 +47,8 @@ class OtlpHttpLogExporter extends OtlpHttpExporterBase {
}
const payload = this.transformer.transformLogRecords(logRecords)
- this._sendPayload(payload, resultCallback)
- this._recordTelemetry('otel.log_records', logRecords.length)
+ this.sendPayload(payload, resultCallback)
+ this.recordTelemetry('otel.log_records', logRecords.length)
}
}
diff --git a/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js b/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js
index 4e2fc97a4fc..3120604103f 100644
--- a/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js
+++ b/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js
@@ -40,7 +40,7 @@ const SEVERITY_MAP = {
/**
* OtlpTransformer transforms log records to OTLP format.
*
- * This implementation follows the OTLP Logs Data Model specification:
+ * This implementation follows the OTLP Logs v1.7.0 Data Model specification:
* https://opentelemetry.io/docs/specs/otlp/#log-data-model
*
* @class OtlpTransformer
@@ -80,12 +80,12 @@ class OtlpTransformer extends OtlpTransformerBase {
const logsData = {
resourceLogs: [{
- resource: this._transformResource(),
+ resource: this.transformResource(),
scopeLogs: this.#transformScope(logRecords),
}]
}
- return this._serializeToProtobuf(protoLogsService, logsData)
+ return this.serializeToProtobuf(protoLogsService, logsData)
}
/**
@@ -97,11 +97,11 @@ class OtlpTransformer extends OtlpTransformerBase {
#transformToJson (logRecords) {
const logsData = {
resourceLogs: [{
- resource: this._transformResource(),
+ resource: this.transformResource(),
scopeLogs: this.#transformScope(logRecords)
}]
}
- return this._serializeToJson(logsData)
+ return this.serializeToJson(logsData)
}
/**
@@ -111,7 +111,7 @@ class OtlpTransformer extends OtlpTransformerBase {
* @private
*/
#transformScope (logRecords) {
- const groupedRecords = this._groupByInstrumentationScope(logRecords)
+ const groupedRecords = this.groupByInstrumentationScope(logRecords)
const scopeLogs = []
for (const records of groupedRecords.values()) {
@@ -159,7 +159,7 @@ class OtlpTransformer extends OtlpTransformerBase {
}
if (logRecord.attributes) {
- result.attributes = this._transformAttributes(logRecord.attributes)
+ result.attributes = this.transformAttributes(logRecord.attributes)
}
if (spanContext?.traceFlags !== undefined) {
@@ -240,7 +240,7 @@ class OtlpTransformer extends OtlpTransformerBase {
kvlistValue: {
values: Object.entries(body).map(([key, value]) => ({
key,
- value: this._transformAnyValue(value)
+ value: this.transformAnyValue(value)
}))
}
}
diff --git a/packages/dd-trace/src/opentelemetry/metrics/constants.js b/packages/dd-trace/src/opentelemetry/metrics/constants.js
new file mode 100644
index 00000000000..098dc1dfdc4
--- /dev/null
+++ b/packages/dd-trace/src/opentelemetry/metrics/constants.js
@@ -0,0 +1,33 @@
+'use strict'
+
+// Metric type constants
+const METRIC_TYPES = {
+ HISTOGRAM: 'histogram',
+ COUNTER: 'counter',
+ UPDOWNCOUNTER: 'updowncounter',
+ OBSERVABLECOUNTER: 'observable-counter',
+ OBSERVABLEUPDOWNCOUNTER: 'observable-updowncounter',
+ GAUGE: 'gauge'
+}
+
+// Temporality constants
+const TEMPORALITY = {
+ DELTA: 'DELTA',
+ CUMULATIVE: 'CUMULATIVE',
+ GAUGE: 'GAUGE',
+ LOWMEMORY: 'LOWMEMORY'
+}
+
+// Default histogram bucket boundaries (in milliseconds for latency metrics)
+const DEFAULT_HISTOGRAM_BUCKETS = [0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10_000]
+
+// Maximum number of measurements to queue before dropping
+// This is an arbitrary limit to prevent memory exhaustion
+const DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE = 524_288
+
+module.exports = {
+ METRIC_TYPES,
+ TEMPORALITY,
+ DEFAULT_HISTOGRAM_BUCKETS,
+ DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE
+}
diff --git a/packages/dd-trace/src/opentelemetry/metrics/index.js b/packages/dd-trace/src/opentelemetry/metrics/index.js
new file mode 100644
index 00000000000..9d5bffd7a93
--- /dev/null
+++ b/packages/dd-trace/src/opentelemetry/metrics/index.js
@@ -0,0 +1,81 @@
+'use strict'
+
+const os = require('os')
+
+/**
+ * @typedef {import('../../config')} Config
+ */
+
+/**
+ * @fileoverview OpenTelemetry Metrics Implementation for dd-trace-js
+ *
+ * This package provides a custom OpenTelemetry Metrics implementation that integrates
+ * with the Datadog tracing library. It includes all necessary components for
+ * creating instruments, recording measurements, and exporting metrics via OTLP.
+ *
+ * Key Components:
+ * - MeterProvider: Main entry point for creating meters
+ * - Meter: Provides methods to create metric instruments
+ * - Instruments: Gauge, Counter, UpDownCounter, ObservableGauge, ObservableCounter, ObservableUpDownCounter, Histogram
+ * - PeriodicMetricReader: Collects and exports instruments (metrics) at regular intervals
+ * - OtlpHttpMetricExporter: Exports instruments (metrics) via OTLP over HTTP
+ * - OtlpTransformer: Transforms instruments (metrics) to OTLP format
+ *
+ * This is a custom implementation to avoid pulling in the full OpenTelemetry SDK,
+ * based on OTLP Protocol v1.7.0. It supports both protobuf and JSON serialization
+ * formats and integrates with Datadog's configuration system.
+ *
+ * @package
+ */
+
+const MeterProvider = require('./meter_provider')
+const PeriodicMetricReader = require('./periodic_metric_reader')
+const OtlpHttpMetricExporter = require('./otlp_http_metric_exporter')
+
+/**
+ * Initializes OpenTelemetry Metrics support
+ * @param {Config} config - Tracer configuration instance
+ */
+function initializeOpenTelemetryMetrics (config) {
+ const resourceAttributes = {
+ 'service.name': config.service,
+ 'service.version': config.version,
+ 'deployment.environment': config.env
+ }
+
+ if (config.tags) {
+ const filteredTags = { ...config.tags }
+ delete filteredTags.service
+ delete filteredTags.version
+ delete filteredTags.env
+ Object.assign(resourceAttributes, filteredTags)
+ }
+
+ if (config.reportHostname) {
+ resourceAttributes['host.name'] = os.hostname()
+ }
+
+ const exporter = new OtlpHttpMetricExporter(
+ config.otelMetricsUrl,
+ config.otelMetricsHeaders,
+ config.otelMetricsTimeout,
+ config.otelMetricsProtocol,
+ resourceAttributes
+ )
+
+ const reader = new PeriodicMetricReader(
+ exporter,
+ config.otelMetricsExportInterval,
+ config.otelMetricsTemporalityPreference,
+ config.otelMaxQueueSize
+ )
+
+ const meterProvider = new MeterProvider({ reader })
+
+ meterProvider.register()
+}
+
+module.exports = {
+ MeterProvider,
+ initializeOpenTelemetryMetrics
+}
diff --git a/packages/dd-trace/src/opentelemetry/metrics/instruments.js b/packages/dd-trace/src/opentelemetry/metrics/instruments.js
new file mode 100644
index 00000000000..e789a74f7d3
--- /dev/null
+++ b/packages/dd-trace/src/opentelemetry/metrics/instruments.js
@@ -0,0 +1,221 @@
+'use strict'
+
+const { sanitizeAttributes } = require('@opentelemetry/core')
+const { METRIC_TYPES } = require('./constants')
+
+/**
+ * @typedef {import('@opentelemetry/api').Attributes} Attributes
+ * @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope
+ */
+
+/**
+ * @typedef {Object} Measurement
+ * @property {string} name - Instrument name
+ * @property {string} description - Instrument description
+ * @property {string} unit - Measurement unit
+ * @property {InstrumentationScope} instrumentationScope - Instrumentation scope
+ * @property {string} type - Metric type from METRIC_TYPES
+ * @property {number} value - Measured value
+ * @property {Attributes} attributes - Sanitized metric attributes
+ * @property {number} timestamp - Timestamp in nanoseconds
+ */
+
+/**
+ * Base class for all metric instruments.
+ *
+ * @private
+ */
+class Instrument {
+ /**
+ * Creates a new instrument instance.
+ *
+ * @param {string} name - Instrument name (e.g., 'http.request.duration')
+ * @param {Object} options - Instrument configuration options
+ * @param {string} [options.description] - Human-readable description of the instrument
+ * @param {string} [options.unit] - Unit of measurement (e.g., 'ms', 'bytes', '1')
+ * @param {InstrumentationScope} instrumentationScope - Instrumentation scope for this instrument
+ * @param {Object} reader - Metric reader for recording measurements
+ */
+ constructor (name, options, instrumentationScope, reader) {
+ this.name = name
+ this.description = options.description ?? ''
+ this.unit = options.unit ?? ''
+ this.valueType = options.valueType // currently ignored, TODO: add support for ValueType
+ this.advice = options.advice // currently ignored, TODO: add support for MetricAdvice
+ this.instrumentationScope = instrumentationScope
+ this.reader = reader
+ }
+
+ /**
+ * Creates a measurement object for recording metric values.
+ * @param {string} type - Metric type from METRIC_TYPES
+ * @param {number} value - Numeric value to record
+ * @param {Attributes} attributes - Key-value pairs for metric dimensions
+ * @returns {Measurement} Measurement object with metadata and timestamp
+ */
+ createMeasurement (type, value, attributes) {
+ return {
+ name: this.name,
+ description: this.description,
+ unit: this.unit,
+ instrumentationScope: this.instrumentationScope,
+ type,
+ value,
+ attributes: sanitizeAttributes(attributes),
+ timestamp: Number(process.hrtime.bigint())
+ }
+ }
+}
+
+/**
+ * Implementation of the OpenTelemetry Counter interface:
+ * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Counter.html
+ * @class Counter
+ */
+class Counter extends Instrument {
+ add (value, attributes = {}) {
+ if (value < 0) return
+ this.reader?.record(this.createMeasurement(METRIC_TYPES.COUNTER, value, attributes))
+ }
+}
+
+/**
+ * Implementation of the OpenTelemetry UpDownCounter interface:
+ * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.UpDownCounter.html
+ * @class UpDownCounter
+ */
+class UpDownCounter extends Instrument {
+ add (value, attributes = {}) {
+ this.reader?.record(this.createMeasurement(METRIC_TYPES.UPDOWNCOUNTER, value, attributes))
+ }
+}
+
+/**
+ * Implementation of the OpenTelemetry Histogram interface:
+ * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Histogram.html
+ * @class Histogram
+ */
+class Histogram extends Instrument {
+ record (value, attributes = {}) {
+ if (value < 0) return
+ this.reader?.record(this.createMeasurement(METRIC_TYPES.HISTOGRAM, value, attributes))
+ }
+}
+
+/**
+ * Implementation of the OpenTelemetry Gauge interface:
+ * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Gauge.html
+ * @class Gauge
+ */
+class Gauge extends Instrument {
+ record (value, attributes = {}) {
+ this.reader?.record(this.createMeasurement(METRIC_TYPES.GAUGE, value, attributes))
+ }
+}
+
+/**
+ * Base class for observable (asynchronous) instruments.
+ * Implementation of the OpenTelemetry Observable interface:
+ * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Observable.html
+ * @private
+ */
+class ObservableInstrument extends Instrument {
+ #callbacks = []
+ #type
+
+ constructor (name, options, instrumentationScope, reader, type) {
+ super(name, options, instrumentationScope, reader)
+ this.#type = type
+ }
+
+ /**
+ * Adds a callback to invoke during metric collection.
+ *
+ * @param {Function} callback - Receives an ObservableResult to record observations
+ */
+ addCallback (callback) {
+ if (typeof callback !== 'function') return
+ this.#callbacks.push(callback)
+ this.reader?.registerObservableInstrument(this)
+ }
+
+ /**
+ * Removes a callback.
+ *
+ * @param {Function} callback - The callback to remove
+ */
+ removeCallback (callback) {
+ const index = this.#callbacks.indexOf(callback)
+ if (index !== -1) {
+ this.#callbacks.splice(index, 1)
+ }
+ }
+
+ /**
+ * Collects observations from all callbacks. Errors are silently ignored.
+ *
+ * @returns {Array} Array of measurements
+ */
+ collect () {
+ const observations = []
+ const observableResult = {
+ observe: (value, attributes = {}) => {
+ observations.push(this.createMeasurement(this.#type, value, attributes))
+ }
+ }
+
+ for (const callback of this.#callbacks) {
+ try {
+ callback(observableResult)
+ } catch {
+ // Ignore callback errors per OpenTelemetry spec to prevent disruption
+ // Errors are swallowed as callbacks should not break metric collection
+ }
+ }
+
+ return observations
+ }
+}
+
+/**
+ * Implementation of the OpenTelemetry ObservableGauge interface:
+ * https://open-telemetry.github.io/opentelemetry-js/types/_opentelemetry_api._opentelemetry_api.ObservableGauge.html
+ * @class ObservableGauge
+ */
+class ObservableGauge extends ObservableInstrument {
+ constructor (name, options, instrumentationScope, reader) {
+ super(name, options, instrumentationScope, reader, METRIC_TYPES.GAUGE)
+ }
+}
+
+/**
+ * Implementation of the OpenTelemetry ObservableCounter interface:
+ * https://open-telemetry.github.io/opentelemetry-js/types/_opentelemetry_api._opentelemetry_api.ObservableCounter.html
+ * @class ObservableCounter
+ */
+class ObservableCounter extends ObservableInstrument {
+ constructor (name, options, instrumentationScope, reader) {
+ super(name, options, instrumentationScope, reader, METRIC_TYPES.OBSERVABLECOUNTER)
+ }
+}
+
+/**
+ * Implementation of the OpenTelemetry ObservableUpDownCounter interface:
+ * https://open-telemetry.github.io/opentelemetry-js/types/_opentelemetry_api._opentelemetry_api.ObservableUpDownCounter.html
+ * @class ObservableUpDownCounter
+ */
+class ObservableUpDownCounter extends ObservableInstrument {
+ constructor (name, options, instrumentationScope, reader) {
+ super(name, options, instrumentationScope, reader, METRIC_TYPES.OBSERVABLEUPDOWNCOUNTER)
+ }
+}
+
+module.exports = {
+ Counter,
+ UpDownCounter,
+ Histogram,
+ Gauge,
+ ObservableGauge,
+ ObservableCounter,
+ ObservableUpDownCounter
+}
diff --git a/packages/dd-trace/src/opentelemetry/metrics/meter.js b/packages/dd-trace/src/opentelemetry/metrics/meter.js
new file mode 100644
index 00000000000..a06d4ecb829
--- /dev/null
+++ b/packages/dd-trace/src/opentelemetry/metrics/meter.js
@@ -0,0 +1,171 @@
+'use strict'
+
+const { VERSION: packageVersion } = require('../../../../../version')
+const {
+ Counter, UpDownCounter, Histogram, Gauge, ObservableGauge, ObservableCounter, ObservableUpDownCounter
+} = require('./instruments')
+const log = require('../../log')
+const { METRIC_TYPES } = require('./constants')
+
+/**
+ * @typedef {import('@opentelemetry/api').MetricOptions} MetricOptions
+ * @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope
+ */
+
+/**
+ * Meter provides methods to create metric instruments.
+ *
+ * This implementation follows the OpenTelemetry JavaScript API Meter:
+ * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Meter.html
+ *
+ * @class Meter
+ */
+class Meter {
+ #instrumentationScope
+ #instruments = new Map()
+ /**
+ * Creates a new Meter instance.
+ *
+ * @param {MeterProvider} meterProvider - Parent meter provider
+ * @param {InstrumentationScope} instrumentationScope - Instrumentation scope information
+ * @param {string} [instrumentationScope.name] - Meter name (defaults to 'dd-trace-js')
+ * @param {string} [instrumentationScope.version] - Meter version (defaults to tracer version)
+ * @param {string} [instrumentationScope.schemaUrl] - Schema URL
+ * @param {Object} [instrumentationScope.attributes] - Attributes for the instrumentation scope
+ */
+ constructor (
+ meterProvider,
+ { name = 'dd-trace-js', version = packageVersion, schemaUrl = '', attributes = {} } = {}
+ ) {
+ this.meterProvider = meterProvider
+ this.#instrumentationScope = {
+ name,
+ version,
+ schemaUrl,
+ attributes,
+ }
+ }
+
+ /**
+ * Gets an existing instrument or creates a new one if it doesn't exist.
+ * Instruments are cached by type and normalized (lowercase) name.
+ *
+ * @private
+ * @param {string} name - Instrument name (will be normalized to lowercase)
+ * @param {string} type - Instrument type (e.g., 'counter', 'histogram', 'gauge')
+ * @param {Function} InstrumentClass - Constructor for the instrument type
+ * @param {MetricOptions} [options] - Instrument options (description, unit, etc.)
+ * @returns {Instrument} The instrument instance (new or cached)
+ */
+ #getOrCreateInstrument (name, type, InstrumentClass, options) {
+ const normalizedName = name.toLowerCase()
+ const key = `${type}:${normalizedName}`
+ let instrument = this.#instruments.get(key)
+ if (!instrument) {
+ instrument = new InstrumentClass(
+ normalizedName, options, this.#instrumentationScope, this.meterProvider.reader
+ )
+ this.#instruments.set(key, instrument)
+ }
+ return instrument
+ }
+
+ /**
+ * Creates a Counter instrument.
+ *
+ * @param {string} name - Instrument name (case-insensitive)
+ * @param {MetricOptions} [options] - Instrument options
+ * @returns {Counter} Counter instrument
+ */
+ createCounter (name, options = {}) {
+ return this.#getOrCreateInstrument(name, METRIC_TYPES.COUNTER, Counter, options)
+ }
+
+ /**
+ * Creates an UpDownCounter instrument.
+ *
+ * @param {string} name - Instrument name
+ * @param {MetricOptions} [options] - Instrument options
+ * @returns {UpDownCounter} UpDownCounter instrument
+ */
+ createUpDownCounter (name, options = {}) {
+ return this.#getOrCreateInstrument(name, METRIC_TYPES.UPDOWNCOUNTER, UpDownCounter, options)
+ }
+
+ /**
+ * Creates a Histogram instrument.
+ *
+ * @param {string} name - Instrument name (case-insensitive)
+ * @param {MetricOptions} [options] - Instrument options
+ * @returns {Histogram} Histogram instrument
+ */
+ createHistogram (name, options = {}) {
+ return this.#getOrCreateInstrument(name, METRIC_TYPES.HISTOGRAM, Histogram, options)
+ }
+
+ /**
+ * Creates a Gauge instrument.
+ *
+ * @param {string} name - Instrument name (case-insensitive)
+ * @param {MetricOptions} [options] - Instrument options
+ * @returns {Gauge} Gauge instrument
+ */
+ createGauge (name, options = {}) {
+ return this.#getOrCreateInstrument(name, METRIC_TYPES.GAUGE, Gauge, options)
+ }
+
+ /**
+ * Creates an ObservableGauge instrument.
+ *
+ * @param {string} name - Instrument name (case-insensitive)
+ * @param {MetricOptions} [options] - Instrument options
+ * @returns {ObservableGauge} ObservableGauge instrument
+ */
+ createObservableGauge (name, options = {}) {
+ return this.#getOrCreateInstrument(name, METRIC_TYPES.OBSERVABLEGAUGE, ObservableGauge, options)
+ }
+
+ /**
+ * Creates an ObservableCounter instrument.
+ *
+ * @param {string} name - Instrument name (case-insensitive)
+ * @param {MetricOptions} [options] - Instrument options
+ * @returns {ObservableCounter} ObservableCounter instrument
+ */
+ createObservableCounter (name, options = {}) {
+ return this.#getOrCreateInstrument(name, METRIC_TYPES.OBSERVABLECOUNTER, ObservableCounter, options)
+ }
+
+ /**
+ * Creates an ObservableUpDownCounter instrument.
+ *
+ * @param {string} name - Instrument name (case-insensitive)
+ * @param {MetricOptions} [options] - Instrument options
+ * @returns {ObservableUpDownCounter} ObservableUpDownCounter instrument
+ */
+ createObservableUpDownCounter (name, options = {}) {
+ return this.#getOrCreateInstrument(name, METRIC_TYPES.OBSERVABLEUPDOWNCOUNTER, ObservableUpDownCounter, options)
+ }
+
+ /**
+ * Adds a batch observable callback (not implemented).
+ *
+ * @param {Function} callback - Batch observable callback
+ * @param {Array} observables - Array of observable instruments
+ */
+ addBatchObservableCallback (callback, observables) {
+ log.warn('addBatchObservableCallback is not implemented')
+ }
+
+ /**
+ * Removes a batch observable callback (not implemented).
+ *
+ * @param {Function} callback - Batch observable callback
+ * @param {Array} observables - Array of observable instruments
+ */
+ removeBatchObservableCallback (callback, observables) {
+ log.warn('removeBatchObservableCallback is not implemented')
+ }
+}
+
+module.exports = Meter
diff --git a/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js b/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js
new file mode 100644
index 00000000000..cf037bffcb1
--- /dev/null
+++ b/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js
@@ -0,0 +1,113 @@
+'use strict'
+
+const { metrics } = require('@opentelemetry/api')
+const Meter = require('./meter')
+const log = require('../../log')
+const { context } = require('@opentelemetry/api')
+const ContextManager = require('../context_manager')
+
+/**
+ * @typedef {import('@opentelemetry/api').Meter} Meter
+ * @typedef {import('@opentelemetry/api').MeterOptions} MeterOptions
+ * @typedef {import('./periodic_metric_reader')} PeriodicMetricReader
+ */
+
+/**
+ * MeterProvider is the main entry point for creating meters with a single reader for Datadog Agent export.
+ *
+ * This implementation follows the OpenTelemetry JavaScript API MeterProvider interface:
+ * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.MeterProvider.html
+ *
+ * @class MeterProvider
+ * @implements {import('@opentelemetry/api').MeterProvider}
+ */
+class MeterProvider {
+ #meters = new Map()
+ #contextManager = new ContextManager()
+ /**
+ * Creates a new MeterProvider instance with a single reader for Datadog Agent export.
+ *
+ * @param {MeterOptions} [options] - MeterProvider options
+ * @param {PeriodicMetricReader} [options.reader] - Single MetricReader instance for
+ * exporting metrics to Datadog Agent
+ */
+ constructor (options = {}) {
+ this.reader = options.reader
+ this.isShutdown = false
+ }
+
+ /**
+ * Gets or creates a meter instance.
+ *
+ * @param {string} name - Meter name (case-insensitive)
+ * @param {string} [version] - Meter version
+ * @param {MeterOptions} [options] - Additional options
+ * @returns {Meter} Meter instance
+ */
+ getMeter (name, version = '', { schemaUrl = '' } = {}) {
+ if (this.isShutdown) {
+ return this.#createNoOpMeter()
+ }
+ const normalizedName = name.toLowerCase()
+ const key = `${normalizedName}@${version}@${schemaUrl}`
+ let meter = this.#meters.get(key)
+ if (!meter) {
+ meter = new Meter(this, { name: normalizedName, version, schemaUrl })
+ this.#meters.set(key, meter)
+ }
+ return meter
+ }
+
+ /**
+ * Registers this meter provider as the global provider.
+ */
+ register () {
+ if (this.isShutdown) {
+ log.warn('Cannot register after shutdown')
+ return
+ }
+ // Set context manager (may be needed for future trace/metrics correlation)
+ context.setGlobalContextManager(this.#contextManager)
+ metrics.setGlobalMeterProvider(this)
+ }
+
+ /**
+ * Forces a flush of all pending metrics.
+ * @returns {void}
+ */
+ forceFlush () {
+ if (!this.isShutdown && this.reader) this.reader.forceFlush()
+ }
+
+ /**
+ * Shuts down the meter provider and all associated readers.
+ * @returns {void}
+ */
+ shutdown () {
+ if (!this.isShutdown) {
+ this.isShutdown = true
+ if (this.reader) {
+ this.reader.shutdown()
+ }
+ }
+ }
+
+ /**
+ * Creates a no-op meter for use when the provider is shutdown.
+ * @returns {Meter} A no-op meter instance
+ * @private
+ */
+ #createNoOpMeter () {
+ return {
+ createCounter: () => ({ add: () => {} }),
+ createUpDownCounter: () => ({ add: () => {} }),
+ createHistogram: () => ({ record: () => {} }),
+ createGauge: () => ({ record: () => {} }),
+ createObservableGauge: () => ({ addCallback: () => {} }),
+ createObservableCounter: () => ({ addCallback: () => {} }),
+ createObservableUpDownCounter: () => ({ addCallback: () => {} })
+ }
+ }
+}
+
+module.exports = MeterProvider
diff --git a/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js b/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js
new file mode 100644
index 00000000000..90a732573c1
--- /dev/null
+++ b/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js
@@ -0,0 +1,65 @@
+'use strict'
+
+const OtlpHttpExporterBase = require('../otlp/otlp_http_exporter_base')
+const OtlpTransformer = require('./otlp_transformer')
+
+/**
+ * @typedef {import('@opentelemetry/resources').Resource} Resource
+ * @typedef {import('./periodic_metric_reader').AggregatedMetric} AggregatedMetric
+ */
+
+/**
+ * OtlpHttpMetricExporter exports metrics via OTLP over HTTP.
+ *
+ * @class OtlpHttpMetricExporter
+ */
+class OtlpHttpMetricExporter extends OtlpHttpExporterBase {
+ /**
+ * Creates a new OtlpHttpMetricExporter instance.
+ *
+ * @param {string} url - OTLP endpoint URL
+ * @param {string} headers - Additional HTTP headers as comma-separated key=value string
+ * @param {number} timeout - Request timeout in milliseconds
+ * @param {string} protocol - OTLP protocol (http/protobuf or http/json)
+ * @param {Resource} resource - Resource attributes
+ */
+ constructor (url, headers, timeout, protocol, resource) {
+ super(url, headers, timeout, protocol, '/v1/metrics', 'metrics')
+ this.transformer = new OtlpTransformer(resource, protocol)
+ }
+
+ /**
+ * Exports metrics via OTLP over HTTP.
+ *
+ * @param {Map} metrics - Map of metric data to export
+ * @param {Function} resultCallback - Callback function for export result
+ *
+ * @returns {void}
+ */
+ export (metrics, resultCallback) {
+ if (metrics.size === 0) {
+ resultCallback({ code: 0 })
+ return
+ }
+
+ let dataPointCount = 0
+ for (const metric of metrics.values()) {
+ if (metric.dataPointMap) {
+ dataPointCount += metric.dataPointMap.size
+ }
+ }
+
+ const additionalTags = [`points:${dataPointCount}`]
+ this.recordTelemetry('otel.metrics_export_attempts', 1, additionalTags)
+
+ const payload = this.transformer.transformMetrics(metrics.values())
+ this.sendPayload(payload, (result) => {
+ if (result.code === 0) {
+ this.recordTelemetry('otel.metrics_export_successes', 1, additionalTags)
+ }
+ resultCallback(result)
+ })
+ }
+}
+
+module.exports = OtlpHttpMetricExporter
diff --git a/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js b/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js
new file mode 100644
index 00000000000..6043eb370ae
--- /dev/null
+++ b/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js
@@ -0,0 +1,251 @@
+'use strict'
+
+const OtlpTransformerBase = require('../otlp/otlp_transformer_base')
+const { getProtobufTypes } = require('../otlp/protobuf_loader')
+const { METRIC_TYPES, TEMPORALITY } = require('./constants')
+
+const { protoAggregationTemporality } = getProtobufTypes()
+const AGGREGATION_TEMPORALITY_DELTA = protoAggregationTemporality.values.AGGREGATION_TEMPORALITY_DELTA
+const AGGREGATION_TEMPORALITY_CUMULATIVE = protoAggregationTemporality.values.AGGREGATION_TEMPORALITY_CUMULATIVE
+
+/**
+ * @typedef {import('./periodic_metric_reader').AggregatedMetric} AggregatedMetric
+ * @typedef {import('./periodic_metric_reader').NumberDataPoint} NumberDataPoint
+ * @typedef {import('./periodic_metric_reader').HistogramDataPoint} HistogramDataPoint
+ */
+
+/**
+ * OtlpTransformer transforms metrics to OTLP format.
+ *
+ * This implementation follows the OTLP Metrics v1.7.0 Data Model specification:
+ * https://opentelemetry.io/docs/specs/otlp/#metrics-data-model
+ *
+ * @class OtlpTransformer
+ * @extends OtlpTransformerBase
+ */
+class OtlpTransformer extends OtlpTransformerBase {
+ /**
+ * Creates a new OtlpTransformer instance.
+ *
+ * @param {import('@opentelemetry/api').Attributes} resourceAttributes - Resource attributes
+ * @param {string} protocol - OTLP protocol (http/protobuf or http/json)
+ */
+ constructor (resourceAttributes, protocol) {
+ super(resourceAttributes, protocol, 'metrics')
+ }
+
+ /**
+ * Transforms metrics to OTLP format based on the configured protocol.
+ * @param {Iterable} metrics - Iterable of metric data to transform
+ * @returns {Buffer} Transformed metrics in the appropriate format
+ */
+ transformMetrics (metrics) {
+ if (this.protocol === 'http/json') {
+ return this.#transformToJson(metrics)
+ }
+ return this.#transformToProtobuf(metrics)
+ }
+
+ /**
+ * Transforms metrics to protobuf format.
+ * @param {Iterable} metrics - Iterable of metrics to transform
+ * @returns {Buffer} Protobuf-encoded metrics
+ * @private
+ */
+ #transformToProtobuf (metrics) {
+ const { protoMetricsService } = getProtobufTypes()
+
+ const metricsData = {
+ resourceMetrics: [{
+ resource: this.transformResource(),
+ scopeMetrics: this.#transformScope(metrics),
+ }]
+ }
+
+ return this.serializeToProtobuf(protoMetricsService, metricsData)
+ }
+
+ /**
+ * Transforms metrics to JSON format.
+ * @param {Array} metrics - Array of metrics to transform
+ * @returns {Buffer} JSON-encoded metrics
+ * @private
+ */
+ #transformToJson (metrics) {
+ const metricsData = {
+ resourceMetrics: [{
+ resource: this.transformResource(),
+ scopeMetrics: this.#transformScope(metrics, true)
+ }]
+ }
+ return this.serializeToJson(metricsData)
+ }
+
+ /**
+ * Creates scope metrics grouped by instrumentation scope.
+ * @param {Iterable} metrics - Iterable of metrics to transform
+ * @param {boolean} isJson - Whether to format for JSON output
+ * @returns {Array