Skip to content
Open
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a851ce1
add otlp metrics protos
mabdinur Oct 14, 2025
181762c
feat: add OTLP metrics proto definitions and reorganize directory
mabdinur Oct 14, 2025
e98ba66
refactor: extract common OTLP logic into base classes
mabdinur Oct 14, 2025
9787d56
Merge branch 'munir/otlp-refactor-base-classes' into munir/otlp-metri…
mabdinur Oct 14, 2025
52a81ee
fix logs
mabdinur Oct 14, 2025
00a859b
Merge branch 'munir/otlp-infrastructure' into munir/otlp-refactor-bas…
mabdinur Oct 14, 2025
2144f7c
increase test coverage
mabdinur Oct 14, 2025
73b2127
feat(metrics): add support for otel metrics provider
mabdinur Oct 14, 2025
9a1b0b2
feat(metrics): add support for otlp configurations
mabdinur Oct 14, 2025
5c9bd42
updates to pass system tests
mabdinur Oct 16, 2025
2267339
add temperolality support and clean up implementation
mabdinur Oct 16, 2025
8ed35ff
add support for encoding attributes
mabdinur Oct 16, 2025
2e7062f
Merge branch 'munir/otlp-refactor-base-classes' into munir/otlp-metri…
mabdinur Oct 16, 2025
029155e
Merge branch 'munir/otlp-metrics-support' into munir/otlp-add-meter-p…
mabdinur Oct 16, 2025
272ea27
Merge branch 'munir/otlp-add-meter-provider' into munir/add-otel-metr…
mabdinur Oct 16, 2025
f859b4b
Merge branch 'master' into munir/otlp-metrics-support
mabdinur Oct 17, 2025
3923982
simplify tests
mabdinur Oct 17, 2025
463baf1
use real values in tests
mabdinur Oct 17, 2025
11e76ac
Merge branch 'munir/otlp-metrics-support' into munir/otlp-add-meter-p…
mabdinur Oct 20, 2025
e60a8a3
Merge branch 'munir/otlp-add-meter-provider' into munir/add-otel-metr…
mabdinur Oct 20, 2025
9841b8a
do not encode numbers as strings
mabdinur Oct 22, 2025
1dda3b4
use enum in transformation
mabdinur Oct 20, 2025
8d7c684
remove unneed fields
mabdinur Oct 22, 2025
fec1a7d
Merge branch 'munir/otlp-metrics-support' into munir/otlp-add-meter-p…
mabdinur Oct 23, 2025
a0e2878
add better temporality support and include encoding for async metric …
mabdinur Oct 23, 2025
af5d2e1
improve test coverage for scope attributes
mabdinur Oct 23, 2025
c94d94e
Merge branch 'munir/otlp-metrics-support' into munir/otlp-add-meter-p…
mabdinur Oct 23, 2025
24241c6
simplify stubs in tests
mabdinur Oct 23, 2025
d743d5c
validate scope attributes
mabdinur Oct 27, 2025
c431bd7
ruben comments
mabdinur Oct 27, 2025
790835e
Merge branch 'munir/otlp-metrics-support' into munir/otlp-add-meter-p…
mabdinur Oct 27, 2025
c7c1fb3
fix broken number test
mabdinur Oct 27, 2025
e6d8332
clean up tests
mabdinur Oct 27, 2025
6201c19
clean up instruments and update tests
mabdinur Oct 28, 2025
83a943b
simplify meter
mabdinur Oct 28, 2025
394f598
use constants and implement missing callbacks
mabdinur Oct 28, 2025
cf1b8e6
move otlp_transformer changes over
mabdinur Oct 28, 2025
d24e282
Merge branch 'munir/otlp-metrics-support' into munir/otlp-add-meter-p…
mabdinur Oct 28, 2025
76ee397
Merge branch 'munir/otlp-add-meter-provider' into munir/add-otel-metr…
mabdinur Oct 28, 2025
408bfd7
clean up private/public fields
mabdinur Oct 28, 2025
1722dec
avoid redefining constant
mabdinur Oct 28, 2025
c191762
Merge branch 'munir/otlp-add-meter-provider' into munir/add-otel-metr…
mabdinur Oct 29, 2025
c6fb9af
clean up tests
mabdinur Oct 30, 2025
f995693
update typing
mabdinur Oct 30, 2025
880c8e1
linting clean ups
mabdinur Oct 30, 2025
4d79bd5
Merge remote-tracking branch 'origin/master' into munir/add-otel-metr…
mabdinur Nov 3, 2025
822e4dc
Merge branch 'master' into munir/add-otel-metrics-configs
mabdinur Nov 3, 2025
f908778
first round of changes from cr
mabdinur Nov 7, 2025
dbd3d72
limit number of metrics in each batch
mabdinur Nov 7, 2025
7eace72
round 3 changes
mabdinur Nov 7, 2025
a0063f2
address more comments part 4
mabdinur Nov 7, 2025
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
58 changes: 58 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,64 @@ The Datadog SDK supports many of the configurations supported by the OpenTelemet

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/).

<h3 id="opentelemetry-metrics">OpenTelemetry Metrics</h3>

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 (default: `http://localhost:4318/v1/metrics`)
- `OTEL_EXPORTER_OTLP_METRICS_HEADERS` - Optional headers in JSON format for metrics (default: `{}`)
- `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` - OTLP protocol for metrics (default: `http/protobuf`)
- `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` - Request timeout in milliseconds for metrics (default: `10000`)
- `OTEL_METRIC_EXPORT_INTERVAL` - Metric export interval in milliseconds (default: `10000`)
- `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` - Temporality preference (default: `DELTA`)
- `OTEL_METRIC_EXPORT_TIMEOUT` - [NOT YET SUPPORTED] Time to export metrics including retries.

Metrics are collected periodically and exported via OTLP over HTTP. The protocol can be configured using `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` or `OTEL_EXPORTER_OTLP_PROTOCOL` environment variables. Supported protocols are `http/protobuf` (default) and `http/json`. All metrics use delta aggregation temporality to match Datadog's data model. For complete OTLP exporter configuration options, see the [OpenTelemetry OTLP Exporter documentation](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text is totally fine, I think it would just be more straight forward in case the individual configuration has all entries listed right away instead of having a separate section with a longer text that describes that.
I would therefore just inline this content into the above variables besides the parts that apply across multiple envs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I got rid of this paragraph and inlined it with the configs.

<h2 id="advanced-configuration">Advanced Configuration</h2>

<h3 id="tracer-settings">Tracer settings</h3>
Expand Down
33 changes: 31 additions & 2 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -570,12 +571,19 @@ 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_METRIC_EXPORT_INTERVAL
} = source

const tags = {}
Expand Down Expand Up @@ -606,6 +614,26 @@ class Config {
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 otelMetricsExporter = String(OTEL_METRICS_EXPORTER).toLowerCase() !== 'none'
this.#setBoolean(target, 'otelMetricsEnabled', DD_METRICS_OTEL_ENABLED && otelMetricsExporter)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have some documentation about the none exporter deactivating the metrics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's documented here but we can definetly do a better job here: https://docs.datadoghq.com/opentelemetry/config/environment_variable_support/ 😅

// 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: these will mess with telemetry values for now, while that is an issue in lots of places and it will be resolved in another PR where we fix the telemetry (the issue is that the property will be defined by either another property or the env and that can not be differentiated for the telemetry being defined like that).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep an eye on this

this.#setString(target, 'otelMetricsProtocol', OTEL_EXPORTER_OTLP_METRICS_PROTOCOL || target.otelProtocol)
target.otelMetricsTimeout = maybeInt(OTEL_EXPORTER_OTLP_METRICS_TIMEOUT) || target.otelTimeout
target.otelMetricsExportTimeout = maybeInt(OTEL_METRIC_EXPORT_TIMEOUT)
target.otelMetricsExportInterval = maybeInt(OTEL_METRIC_EXPORT_INTERVAL)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is zero allowed for any of these values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh good question. For timeouts 0 corresponds to no timeout (which is handled by http.request). For intervals and batch sizes zero is not allowed and should be ignored. I updated the code to handle this case.

// 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',
Expand Down Expand Up @@ -1200,9 +1228,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',
Expand Down
8 changes: 8 additions & 0 deletions packages/dd-trace/src/config_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ module.exports = {
otelTimeout: 10_000,
otelLogsBatchTimeout: 5000,
otelLogsMaxExportBatchSize: 512,
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These values are not documented as far as I know. Should we list them in the API.md?

Copy link
Contributor Author

@mabdinur mabdinur Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config is documented in api docs but it's very high level. I will update the api docs to include this link: https://opentelemetry.io/docs/specs/otel/metrics/sdk_exporters/otlp/#additional-environment-variable-configuration

lookup: undefined,
inferredProxyServicesEnabled: false,
memcachedCommandEnabled: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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)
}
}

Expand Down
16 changes: 8 additions & 8 deletions packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

/**
Expand All @@ -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)
}

/**
Expand All @@ -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()) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}))
}
}
Expand Down
28 changes: 28 additions & 0 deletions packages/dd-trace/src/opentelemetry/metrics/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'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]

module.exports = {
METRIC_TYPES,
TEMPORALITY,
DEFAULT_HISTOGRAM_BUCKETS
}
81 changes: 81 additions & 0 deletions packages/dd-trace/src/opentelemetry/metrics/index.js
Original file line number Diff line number Diff line change
@@ -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) {
// Build resource attributes
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
)

const meterProvider = new MeterProvider({ reader })

meterProvider.register()
}

module.exports = {
MeterProvider,
initializeOpenTelemetryMetrics
}
Loading