Skip to content
Merged
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
343c772
first attempt at otel logs support
mabdinur Sep 17, 2025
b3a2973
lint files and centralize configs
mabdinur Sep 18, 2025
2c0d528
add tests
mabdinur Sep 18, 2025
6c359ce
clean up yarn.lock
mabdinur Sep 18, 2025
7dc1480
clean up logs
mabdinur Sep 19, 2025
2c60dfc
add back log exporter logic
mabdinur Sep 19, 2025
dabe98f
clean up docs
mabdinur Sep 19, 2025
b7ae33d
clean up docs and fix otlp protocol
mabdinur Sep 19, 2025
f73d425
revert readme docs
mabdinur Sep 19, 2025
543805a
add new otel files
mabdinur Sep 19, 2025
c63e20e
working version
mabdinur Sep 19, 2025
d4d8aac
fmy
mabdinur Sep 19, 2025
9514e68
fmt
mabdinur Sep 19, 2025
df7cbca
Update packages/dd-trace/src/config_defaults.js
mabdinur Sep 19, 2025
2c0a5a5
fix tests
mabdinur Sep 19, 2025
f109cf3
allow any version of logs api, let opentelemetry api determine the ve…
mabdinur Sep 19, 2025
0c29478
add otlp payload tests
mabdinur Sep 21, 2025
8b1ce46
add telemetry metrics
mabdinur Sep 22, 2025
432ec89
some other clean ups
mabdinur Sep 22, 2025
036776a
simplify tests
mabdinur Sep 22, 2025
8abbe95
use agent hostname to resolve otlp endpoints
mabdinur Sep 22, 2025
e9456e3
clean up initalization
mabdinur Sep 22, 2025
9164c6f
Merge branch 'master' into munir/otlp-logs-support
mabdinur Sep 22, 2025
4bc66fc
parse additional otlp headers
mabdinur Sep 22, 2025
d505cab
clean up component args
mabdinur Sep 22, 2025
0f2bd21
clean up docs
mabdinur Sep 22, 2025
3af5080
clean up component args
mabdinur Sep 22, 2025
9578887
remove addLogProcessor, init provider with a processor
mabdinur Sep 22, 2025
c7c15d8
support trace-log correlation
mabdinur Sep 23, 2025
f6f58fe
clean up registering provider in tests, and rename exporter arg
mabdinur Sep 24, 2025
434fbc4
first round of clean ups from PR review
mabdinur Sep 25, 2025
3963ed6
clean ups part 2
mabdinur Sep 25, 2025
6d10314
make things private and clean up tests
mabdinur Sep 25, 2025
11161f3
clean up tests
mabdinur Sep 26, 2025
266c936
clean up yarn file
mabdinur Sep 26, 2025
d63a4ed
remove unused configs
mabdinur Sep 26, 2025
b98cd76
fix context issues
mabdinur Sep 26, 2025
9955d5e
nother round of clean ups
mabdinur Sep 26, 2025
a9ddbcc
group payloads by instrumentation scope
mabdinur Sep 29, 2025
d9d9dfb
fix typing
mabdinur Sep 29, 2025
351ca2f
address review comments
mabdinur Sep 29, 2025
b99ba1d
add better typing, and better support for sending schemaurl
mabdinur Sep 29, 2025
07ec605
Merge branch 'master' into munir/otlp-logs-support
mabdinur Sep 30, 2025
a1f8cf7
revert instrumentationScope change to span
mabdinur Sep 30, 2025
09fedbc
Merge branch 'master' into munir/otlp-logs-support
mabdinur Sep 30, 2025
9ff13fd
review comments
mabdinur Oct 1, 2025
0b8e71d
clean up protobuf loader file
mabdinur Oct 1, 2025
46d1385
Merge branch 'master' into munir/otlp-logs-support
mabdinur Oct 3, 2025
5410486
lint
mabdinur Oct 3, 2025
55e86e9
move protos to same dir, this will set up metrics work
mabdinur Oct 3, 2025
311500c
clean up throws
mabdinur Oct 6, 2025
07ab61c
update protos
mabdinur Oct 6, 2025
f25c7a5
disable log injection when otel logs support is enabled
mabdinur Oct 6, 2025
9f716d1
update configurations to pass telemetry system tests
mabdinur Oct 6, 2025
ea532c2
remove useless import
mabdinur Oct 6, 2025
e695249
add more tests
mabdinur Oct 6, 2025
557b6d5
provide fix for failing system test
mabdinur Oct 6, 2025
266a239
add test case for noop logger and fix mocking for remote config
mabdinur Oct 6, 2025
df5d6b4
add test case for noop logger and fix mocking for remote config
mabdinur Oct 6, 2025
2763296
Merge branch 'master' into munir/otlp-logs-support
mabdinur Oct 6, 2025
e995e0c
fix encoding for doubles, remove unused shutdown code, test getLogger…
mabdinur Oct 6, 2025
e20668e
remove unused timer and shutdown logic, simplify loggerprovider regis…
mabdinur Oct 6, 2025
3f8eab1
fix comment
mabdinur Oct 7, 2025
148d47b
clean up how otel endpoint configs are loaded
mabdinur Oct 7, 2025
9ac86bb
address Ayans comments
mabdinur Oct 8, 2025
7f4fbff
add integration tests
mabdinur Oct 9, 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
2 changes: 2 additions & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ require,@datadog/pprof,Apache license 2.0,Copyright 2019 Google Inc.
require,@datadog/sketches-js,Apache license 2.0,Copyright 2020 Datadog Inc.
require,@datadog/wasm-js-rewriter,Apache license 2.0,Copyright 2018 Datadog Inc.
require,@opentelemetry/api,Apache license 2.0,Copyright OpenTelemetry Authors
require,@opentelemetry/api-logs,Apache license 2.0,Copyright OpenTelemetry Authors
require,@opentelemetry/core,Apache license 2.0,Copyright OpenTelemetry Authors
require,@opentelemetry/resources,Apache license 2.0,Copyright OpenTelemetry Authors
require,@isaacs/ttlcache,ISC,Copyright (c) 2022-2023 - Isaac Z. Schlueter and Contributors
require,crypto-randomuuid,MIT,Copyright 2021 Node.js Foundation and contributors
require,dc-polyfill,MIT,Copyright 2023 Datadog Inc.
Expand Down
38 changes: 38 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,44 @@ The following attributes are available to override Datadog-specific options:
* `resource.name`: The resource name to be used for this span. The operation name will be used if this is not provided.
* `span.type`: The span type to be used for this span. Will fallback to `custom` if not provided.

<h3 id="opentelemetry-logs">OpenTelemetry Logs</h3>

dd-trace-js includes experimental support for OpenTelemetry logs, designed as a drop-in replacement for the OpenTelemetry SDK. This support is primarily intended for logging libraries rather than direct user configuration. Enable it by setting `DD_LOGS_OTEL_ENABLED=true` and use the [OpenTelemetry Logs API](https://open-telemetry.github.io/opentelemetry-js/modules/_opentelemetry_api-logs.html) to emit structured log data:

```javascript
require('dd-trace').init()
const { logs } = require('@opentelemetry/api-logs')
const express = require('express')

const app = express()
const logger = logs.getLogger('my-service', '1.0.0')

app.get('/api/users/:id', (req, res) => {
logger.emit({
severityText: 'INFO',
severityNumber: 9,
body: `Processing user request for ID: ${req.params.id}`,
})
res.json({ id: req.params.id, name: 'John Doe' })
})

app.listen(3000)
```

#### Supported Configuration

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_BSP_MAX_EXPORT_BATCH_SIZE` - Maximum logs per batch (default: `512`)

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

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

<h3 id="tracer-settings">Tracer settings</h3>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@
"@datadog/wasm-js-rewriter": "4.0.1",
"@isaacs/ttlcache": "^1.4.1",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"@opentelemetry/api-logs": "<1.0.0",
"@opentelemetry/core": ">=1.14.0 <1.31.0",
"@opentelemetry/resources": ">=1.0.0 <1.10.0",
"crypto-randomuuid": "^1.0.0",
"dc-polyfill": "^0.1.10",
"ignore": "^7.0.5",
Expand Down
43 changes: 42 additions & 1 deletion packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ class Config {
DD_INSTRUMENTATION_TELEMETRY_ENABLED,
DD_INSTRUMENTATION_CONFIG_ID,
DD_LOGS_INJECTION,
DD_LOGS_OTEL_ENABLED,
DD_LANGCHAIN_SPAN_CHAR_LIMIT,
DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE,
DD_LLMOBS_AGENTLESS_ENABLED,
Expand Down Expand Up @@ -635,7 +636,17 @@ class Config {
OTEL_RESOURCE_ATTRIBUTES,
OTEL_SERVICE_NAME,
OTEL_TRACES_SAMPLER,
OTEL_TRACES_SAMPLER_ARG
OTEL_TRACES_SAMPLER_ARG,
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
OTEL_EXPORTER_OTLP_LOGS_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
} = getEnvironmentVariables()

const tags = {}
Expand All @@ -649,6 +660,23 @@ class Config {
tagger.add(tags, DD_TRACE_TAGS)
tagger.add(tags, DD_TRACE_GLOBAL_TAGS)

this._setBoolean(env, 'otelLogsEnabled', isTrue(DD_LOGS_OTEL_ENABLED))
// Set OpenTelemetry logs configuration with specific _LOGS_ vars taking precedence over generic _EXPORTERS_ vars
if (OTEL_EXPORTER_OTLP_ENDPOINT) {
// Only set if there's a custom URL, otherwise let calc phase handle the default
this._setString(env, 'otelUrl', OTEL_EXPORTER_OTLP_ENDPOINT)
}
if (OTEL_EXPORTER_OTLP_ENDPOINT || OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) {
this._setString(env, 'otelLogsUrl', OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || env.otelUrl)
}
this._setString(env, 'otelHeaders', OTEL_EXPORTER_OTLP_HEADERS)
this._setString(env, 'otelLogsHeaders', OTEL_EXPORTER_OTLP_LOGS_HEADERS || env.otelHeaders)
this._setString(env, 'otelProtocol', OTEL_EXPORTER_OTLP_PROTOCOL)
this._setString(env, 'otelLogsProtocol', OTEL_EXPORTER_OTLP_LOGS_PROTOCOL || env.otelProtocol)
env.otelTimeout = maybeInt(OTEL_EXPORTER_OTLP_TIMEOUT)
env.otelLogsTimeout = maybeInt(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) || env.otelTimeout
env.otelLogsBatchTimeout = maybeInt(OTEL_BSP_SCHEDULE_DELAY)
env.otelLogsMaxExportBatchSize = maybeInt(OTEL_BSP_MAX_EXPORT_BATCH_SIZE)
this._setBoolean(
env,
'apmTracingEnabled',
Expand Down Expand Up @@ -1154,7 +1182,20 @@ class Config {
calc.testManagementAttemptToFixRetries = maybeInt(DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES) ?? 20
this._setBoolean(calc, 'isImpactedTestsEnabled', !isFalse(DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED))
}

// Disable log injection when OTEL logs are enabled
// OTEL logs and DD log injection are mutually exclusive
if (this._env.otelLogsEnabled) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we add a unit test for this scenario?

this._setBoolean(calc, 'logInjection', false)
}

calc['dogstatsd.hostname'] = this._getHostname()

// Compute OTLP logs URL to send payloads to the active Datadog Agent
const agentHostname = this._getHostname()
calc.otelLogsUrl = `http://${agentHostname}:4318`
calc.otelUrl = `http://${agentHostname}:4318`

this._setBoolean(calc, 'isGitUploadEnabled',
calc.isIntelligentTestRunnerEnabled && !isFalse(this._isCiVisibilityGitUploadEnabled()))
this._setBoolean(calc, 'spanComputePeerService', this._getSpanComputePeerService())
Expand Down
11 changes: 11 additions & 0 deletions packages/dd-trace/src/config_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ module.exports = {
isTestManagementEnabled: false,
isImpactedTestsEnabled: false,
logInjection: true,
otelLogsEnabled: false,
otelUrl: undefined,
otelLogsUrl: undefined, // Will be computed using agent host
otelHeaders: undefined,
otelLogsHeaders: '',
otelProtocol: 'http/protobuf',
otelLogsProtocol: 'http/protobuf',
otelLogsTimeout: 10_000,
otelTimeout: 10_000,
otelLogsBatchTimeout: 5000,
otelLogsMaxExportBatchSize: 512,
lookup: undefined,
inferredProxyServicesEnabled: false,
memcachedCommandEnabled: false,
Expand Down
100 changes: 100 additions & 0 deletions packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
'use strict'

/**
* @typedef {import('@opentelemetry/api-logs').LogRecord} LogRecord
* @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope
*/

/**
* BatchLogRecordProcessor processes log records in batches for efficient export to Datadog Agent.
*
* This implementation follows the OpenTelemetry JavaScript SDK BatchLogRecordProcessor:
* https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_sdk-logs.BatchLogRecordProcessor.html
*
* @class BatchLogRecordProcessor
*/
class BatchLogRecordProcessor {
#logRecords
#timer
#batchTimeout
#maxExportBatchSize

/**
* Creates a new BatchLogRecordProcessor instance.
*
* @param {OtlpHttpLogExporter} exporter - Log processor for exporting batches to Datadog Agent
* @param {number} batchTimeout - Timeout in milliseconds for batch processing
* @param {number} maxExportBatchSize - Maximum number of log records per batch
*/
constructor (exporter, batchTimeout, maxExportBatchSize) {
this.exporter = exporter
this.#batchTimeout = batchTimeout
this.#maxExportBatchSize = maxExportBatchSize
this.#logRecords = []
this.#timer = null
}

/**
* Processes a single log record.
*
* @param {LogRecord} logRecord - The enriched log record with trace correlation and metadata
* @param {InstrumentationScope} instrumentationScope - The instrumentation library
*/
onEmit (logRecord, instrumentationScope) {
// Store the log record (already enriched by Logger.emit)
this.#logRecords.push({ ...logRecord, instrumentationScope })

if (this.#logRecords.length >= this.#maxExportBatchSize) {
this.#export()
} else if (this.#logRecords.length === 1) {
this.#startTimer()
}
}

/**
* Forces an immediate flush of all pending log records.
* @returns {undefined} Promise that resolves when flush is complete
*/
forceFlush () {
this.#export()
}

/**
* Starts the batch timeout timer.
* @private
*/
#startTimer () {
if (this.#timer) {
return
}

this.#timer = setTimeout(() => {
this.#export()
}, this.#batchTimeout)
}

/**
* Exports the current batch of log records.
* @private
*/
#export () {
const logRecords = this.#logRecords.slice(0, this.#maxExportBatchSize)
this.#logRecords = this.#logRecords.slice(this.#maxExportBatchSize)

this.#clearTimer()
this.exporter.export(logRecords, () => {})
}

/**
* Clears the batch timeout timer.
* @private
*/
#clearTimer () {
if (this.#timer) {
clearTimeout(this.#timer)
this.#timer = null
}
}
}

module.exports = BatchLogRecordProcessor
36 changes: 36 additions & 0 deletions packages/dd-trace/src/opentelemetry/logs/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict'

/**
* @fileoverview OpenTelemetry Logs Implementation for dd-trace-js
*
* This package provides a custom OpenTelemetry Logs implementation that integrates
* with the Datadog tracing library. It includes all necessary components for
* emitting, processing, and exporting log records via OTLP (OpenTelemetry Protocol).
*
* Key Components:
* - LoggerProvider: Main entry point for creating loggers
* - Logger: Provides methods to emit log records
* - BatchLogRecordProcessor: Processes log records in batches for efficient export
* - OtlpHttpLogExporter: Exports log records via OTLP over HTTP
* - OtlpTransformer: Transforms log records 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 LoggerProvider = require('./logger_provider')
const Logger = require('./logger')
const BatchLogRecordProcessor = require('./batch_log_processor')
const OtlpHttpLogExporter = require('./otlp_http_log_exporter')
const OtlpTransformer = require('./otlp_transformer')

module.exports = {
LoggerProvider,
Logger,
BatchLogRecordProcessor,
OtlpHttpLogExporter,
OtlpTransformer
}
77 changes: 77 additions & 0 deletions packages/dd-trace/src/opentelemetry/logs/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use strict'

const { sanitizeAttributes } = require('@opentelemetry/core')
const { context } = require('@opentelemetry/api')
const packageVersion = require('../../../../../package.json').version
/**
* @typedef {import('@opentelemetry/api-logs').LogRecord} LogRecord
* @typedef {import('@opentelemetry/api').SpanContext} SpanContext
* @typedef {import('@opentelemetry/api').Attributes} Attributes
* @typedef {import('@opentelemetry/resources').Resource} Resource
* @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope
*/

/**
* Logger provides methods to emit log records.
*
* This implementation follows the OpenTelemetry JavaScript API Logger:
* https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api-logs.Logger.html
*
* @class Logger
*/
class Logger {
#instrumentationScope

/**
* Creates a new Logger instance.
*
* @param {LoggerProvider} loggerProvider - Parent logger provider
* @param {InstrumentationScope} [instrumentationScope] - Instrumentation scope information (newer API)
* @param {Object} [instrumentationLibrary] - Instrumentation library information (legacy API) [DEPRECATED in v1.3.0]
* @param {InstrumentationScope} [instrumentationScope.name] - Library name (defaults to 'dd-trace-js')
* @param {InstrumentationScope} [instrumentationScope.version] - Library version (defaults to tracer version)
* @param {string} [instrumentationLibrary.name] - Library name (legacy, defaults to 'dd-trace-js')
* @param {string} [instrumentationLibrary.version] - Library version (legacy, defaults to tracer version)
*/
constructor (loggerProvider, instrumentationScope, instrumentationLibrary) {
this.loggerProvider = loggerProvider

// Support both newer instrumentationScope and legacy instrumentationLibrary
const scope = instrumentationScope || instrumentationLibrary
this.#instrumentationScope = {
name: scope?.name || 'dd-trace-js',
version: scope?.version || packageVersion,
schemaUrl: scope?.schemaUrl || '',
}
}

/**
* Emits a log record.
*
* @param {LogRecord} logRecord - The log record to emit
* @returns {void}
*/
emit (logRecord) {
if (this.loggerProvider.isShutdown || !this.loggerProvider.processor) {
return
}

if (logRecord.attributes) {
logRecord.attributes = sanitizeAttributes(logRecord.attributes)
}

// Note: timestamp is in nanoseconds (as defined by OpenTelemetry LogRecord API)
if (!logRecord.timestamp) {
logRecord.timestamp = Number(process.hrtime.bigint())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
logRecord.timestamp = Number(process.hrtime.bigint())
// The timestamp is handled in nanoseconds.
logRecord.timestamp = Number(process.hrtime.bigint())

Is it possible to rename the property to include that information?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense. I thought it might just be good to add the comment for clarity :)

}

if (!logRecord.context) {
// Store span context in the log record context for trace correlation
logRecord.context = context.active()
}

this.loggerProvider.processor.onEmit(logRecord, this.#instrumentationScope)
}
}

module.exports = Logger
Loading