Skip to content

Commit 86a3bde

Browse files
mabdinurjuan-fernandez
authored andcommitted
Add support for OpenTelemetry Logs API (#6465)
* first attempt at otel logs support * lint files and centralize configs * add tests * clean up yarn.lock * clean up logs * add back log exporter logic * clean up docs * clean up docs and fix otlp protocol * revert readme docs * add new otel files * working version * fmy * fmt * Update packages/dd-trace/src/config_defaults.js * fix tests * allow any version of logs api, let opentelemetry api determine the version * add otlp payload tests * add telemetry metrics * some other clean ups * simplify tests * use agent hostname to resolve otlp endpoints * clean up initalization * parse additional otlp headers * clean up component args * clean up docs * clean up component args * remove addLogProcessor, init provider with a processor * support trace-log correlation * clean up registering provider in tests, and rename exporter arg * first round of clean ups from PR review * clean ups part 2 * make things private and clean up tests * clean up tests * clean up yarn file * remove unused configs * fix context issues * nother round of clean ups * group payloads by instrumentation scope * fix typing * address review comments * add better typing, and better support for sending schemaurl * revert instrumentationScope change to span * review comments * clean up protobuf loader file * lint * move protos to same dir, this will set up metrics work * clean up throws * update protos * disable log injection when otel logs support is enabled * update configurations to pass telemetry system tests * remove useless import * add more tests * provide fix for failing system test * add test case for noop logger and fix mocking for remote config * add test case for noop logger and fix mocking for remote config * fix encoding for doubles, remove unused shutdown code, test getLogger params * remove unused timer and shutdown logic, simplify loggerprovider register and improve tests * fix comment * clean up how otel endpoint configs are loaded * address Ayans comments * add integration tests
1 parent fef1ae6 commit 86a3bde

24 files changed

+2235
-2
lines changed

.github/workflows/platform.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ jobs:
285285
runs-on: ubuntu-latest
286286
steps:
287287
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
288+
- uses: ./.github/actions/testagent/start
288289
- uses: ./.github/actions/node
289290
with:
290291
version: ${{ matrix.version }}

LICENSE-3rdparty.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ require,@datadog/pprof,Apache license 2.0,Copyright 2019 Google Inc.
77
require,@datadog/sketches-js,Apache license 2.0,Copyright 2020 Datadog Inc.
88
require,@datadog/wasm-js-rewriter,Apache license 2.0,Copyright 2018 Datadog Inc.
99
require,@opentelemetry/api,Apache license 2.0,Copyright OpenTelemetry Authors
10+
require,@opentelemetry/api-logs,Apache license 2.0,Copyright OpenTelemetry Authors
1011
require,@opentelemetry/core,Apache license 2.0,Copyright OpenTelemetry Authors
12+
require,@opentelemetry/resources,Apache license 2.0,Copyright OpenTelemetry Authors
1113
require,@isaacs/ttlcache,ISC,Copyright (c) 2022-2023 - Isaac Z. Schlueter and Contributors
1214
require,crypto-randomuuid,MIT,Copyright 2021 Node.js Foundation and contributors
1315
require,dc-polyfill,MIT,Copyright 2023 Datadog Inc.

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ services:
180180
image: ghcr.io/datadog/dd-apm-test-agent/ddapm-test-agent:v1.33.1
181181
ports:
182182
- "127.0.0.1:9126:9126"
183+
- "127.0.0.1:4318:4318"
183184
environment:
184185
- LOG_LEVEL=DEBUG
185186
- TRACE_LANGUAGE=javascript

docs/API.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,44 @@ The following attributes are available to override Datadog-specific options:
379379
* `resource.name`: The resource name to be used for this span. The operation name will be used if this is not provided.
380380
* `span.type`: The span type to be used for this span. Will fallback to `custom` if not provided.
381381

382+
<h3 id="opentelemetry-logs">OpenTelemetry Logs</h3>
383+
384+
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:
385+
386+
```javascript
387+
require('dd-trace').init()
388+
const { logs } = require('@opentelemetry/api-logs')
389+
const express = require('express')
390+
391+
const app = express()
392+
const logger = logs.getLogger('my-service', '1.0.0')
393+
394+
app.get('/api/users/:id', (req, res) => {
395+
logger.emit({
396+
severityText: 'INFO',
397+
severityNumber: 9,
398+
body: `Processing user request for ID: ${req.params.id}`,
399+
})
400+
res.json({ id: req.params.id, name: 'John Doe' })
401+
})
402+
403+
app.listen(3000)
404+
```
405+
406+
#### Supported Configuration
407+
408+
The Datadog SDK supports many of the configurations supported by the OpenTelemetry SDK. The following environment variables are supported:
409+
410+
- `DD_LOGS_OTEL_ENABLED` - Enable OpenTelemetry logs (default: `false`)
411+
- `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` - OTLP endpoint URL for logs (default: `http://localhost:4318`)
412+
- `OTEL_EXPORTER_OTLP_LOGS_HEADERS` - Optional headers in JSON format for logs (default: `{}`)
413+
- `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` - OTLP protocol for logs (default: `http/protobuf`)
414+
- `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` - Request timeout in milliseconds for logs (default: `10000`)
415+
- `OTEL_BSP_SCHEDULE_DELAY` - Batch timeout in milliseconds (default: `5000`)
416+
- `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` - Maximum logs per batch (default: `512`)
417+
418+
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/).
419+
382420
<h2 id="advanced-configuration">Advanced Configuration</h2>
383421

384422
<h3 id="tracer-settings">Tracer settings</h3>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict'
2+
3+
const { assert } = require('chai')
4+
const { createSandbox } = require('./helpers')
5+
const http = require('http')
6+
7+
describe('OpenTelemetry Logs Integration', () => {
8+
let sandbox
9+
10+
beforeEach(async () => {
11+
sandbox = await createSandbox()
12+
})
13+
14+
afterEach(async () => {
15+
await sandbox.remove()
16+
})
17+
18+
it('should send OTLP logs to test agent and receive 200', (done) => {
19+
const payload = JSON.stringify({
20+
resourceLogs: [{
21+
scopeLogs: [{ logRecords: [{ body: { stringValue: 'test' }, timeUnixNano: String(Date.now() * 1000000) }] }]
22+
}]
23+
})
24+
25+
const req = http.request({
26+
hostname: '127.0.0.1',
27+
port: 4318,
28+
path: '/v1/logs',
29+
method: 'POST',
30+
headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length }
31+
}, (res) => {
32+
assert.strictEqual(res.statusCode, 200)
33+
done()
34+
})
35+
36+
req.on('error', done)
37+
req.write(payload)
38+
req.end()
39+
})
40+
41+
it('should receive 400 when sending protobuf to JSON endpoint', (done) => {
42+
const protobufPayload = Buffer.from([0x0a, 0x04, 0x08, 0x01, 0x12, 0x00])
43+
44+
const req = http.request({
45+
hostname: '127.0.0.1',
46+
port: 4318,
47+
path: '/v3/logs',
48+
method: 'POST',
49+
headers: { 'Content-Type': 'application/x-protobuf', 'Content-Length': protobufPayload.length }
50+
}, (res) => {
51+
// 404 Not Found - wrong path
52+
assert.strictEqual(res.statusCode, 404)
53+
done()
54+
})
55+
56+
req.on('error', done)
57+
req.write(protobufPayload)
58+
req.end()
59+
})
60+
})

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@
126126
"@datadog/wasm-js-rewriter": "4.0.1",
127127
"@isaacs/ttlcache": "^1.4.1",
128128
"@opentelemetry/api": ">=1.0.0 <1.10.0",
129+
"@opentelemetry/api-logs": "<1.0.0",
129130
"@opentelemetry/core": ">=1.14.0 <1.31.0",
131+
"@opentelemetry/resources": ">=1.0.0 <1.10.0",
130132
"crypto-randomuuid": "^1.0.0",
131133
"dc-polyfill": "^0.1.10",
132134
"ignore": "^7.0.5",

packages/dd-trace/src/config.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ const VALID_PROPAGATION_BEHAVIOR_EXTRACT = new Set(['continue', 'restart', 'igno
6767

6868
const VALID_LOG_LEVELS = new Set(['debug', 'info', 'warn', 'error'])
6969

70+
const DEFAULT_OTLP_PORT = 4318
71+
7072
function getFromOtelSamplerMap (otelTracesSampler, otelTracesSamplerArg) {
7173
const OTEL_TRACES_SAMPLER_MAPPING = {
7274
always_on: '1.0',
@@ -554,6 +556,7 @@ class Config {
554556
DD_INSTRUMENTATION_TELEMETRY_ENABLED,
555557
DD_INSTRUMENTATION_CONFIG_ID,
556558
DD_LOGS_INJECTION,
559+
DD_LOGS_OTEL_ENABLED,
557560
DD_LANGCHAIN_SPAN_CHAR_LIMIT,
558561
DD_LANGCHAIN_SPAN_PROMPT_COMPLETION_SAMPLE_RATE,
559562
DD_LLMOBS_AGENTLESS_ENABLED,
@@ -635,7 +638,17 @@ class Config {
635638
OTEL_RESOURCE_ATTRIBUTES,
636639
OTEL_SERVICE_NAME,
637640
OTEL_TRACES_SAMPLER,
638-
OTEL_TRACES_SAMPLER_ARG
641+
OTEL_TRACES_SAMPLER_ARG,
642+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
643+
OTEL_EXPORTER_OTLP_LOGS_HEADERS,
644+
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
645+
OTEL_EXPORTER_OTLP_LOGS_TIMEOUT,
646+
OTEL_EXPORTER_OTLP_PROTOCOL,
647+
OTEL_EXPORTER_OTLP_ENDPOINT,
648+
OTEL_EXPORTER_OTLP_HEADERS,
649+
OTEL_EXPORTER_OTLP_TIMEOUT,
650+
OTEL_BSP_SCHEDULE_DELAY,
651+
OTEL_BSP_MAX_EXPORT_BATCH_SIZE
639652
} = getEnvironmentVariables()
640653

641654
const tags = {}
@@ -649,6 +662,23 @@ class Config {
649662
tagger.add(tags, DD_TRACE_TAGS)
650663
tagger.add(tags, DD_TRACE_GLOBAL_TAGS)
651664

665+
this._setBoolean(env, 'otelLogsEnabled', isTrue(DD_LOGS_OTEL_ENABLED))
666+
// Set OpenTelemetry logs configuration with specific _LOGS_ vars taking precedence over generic _EXPORTERS_ vars
667+
if (OTEL_EXPORTER_OTLP_ENDPOINT) {
668+
// Only set if there's a custom URL, otherwise let calc phase handle the default
669+
this._setString(env, 'otelUrl', OTEL_EXPORTER_OTLP_ENDPOINT)
670+
}
671+
if (OTEL_EXPORTER_OTLP_ENDPOINT || OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) {
672+
this._setString(env, 'otelLogsUrl', OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || env.otelUrl)
673+
}
674+
this._setString(env, 'otelHeaders', OTEL_EXPORTER_OTLP_HEADERS)
675+
this._setString(env, 'otelLogsHeaders', OTEL_EXPORTER_OTLP_LOGS_HEADERS || env.otelHeaders)
676+
this._setString(env, 'otelProtocol', OTEL_EXPORTER_OTLP_PROTOCOL)
677+
this._setString(env, 'otelLogsProtocol', OTEL_EXPORTER_OTLP_LOGS_PROTOCOL || env.otelProtocol)
678+
env.otelTimeout = maybeInt(OTEL_EXPORTER_OTLP_TIMEOUT)
679+
env.otelLogsTimeout = maybeInt(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) || env.otelTimeout
680+
env.otelLogsBatchTimeout = maybeInt(OTEL_BSP_SCHEDULE_DELAY)
681+
env.otelLogsMaxExportBatchSize = maybeInt(OTEL_BSP_MAX_EXPORT_BATCH_SIZE)
652682
this._setBoolean(
653683
env,
654684
'apmTracingEnabled',
@@ -1156,7 +1186,20 @@ class Config {
11561186
calc.testManagementAttemptToFixRetries = maybeInt(DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES) ?? 20
11571187
this._setBoolean(calc, 'isImpactedTestsEnabled', !isFalse(DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED))
11581188
}
1189+
1190+
// Disable log injection when OTEL logs are enabled
1191+
// OTEL logs and DD log injection are mutually exclusive
1192+
if (this._env.otelLogsEnabled) {
1193+
this._setBoolean(calc, 'logInjection', false)
1194+
}
1195+
11591196
calc['dogstatsd.hostname'] = this._getHostname()
1197+
1198+
// Compute OTLP logs URL to send payloads to the active Datadog Agent
1199+
const agentHostname = this._getHostname()
1200+
calc.otelLogsUrl = `http://${agentHostname}:${DEFAULT_OTLP_PORT}`
1201+
calc.otelUrl = `http://${agentHostname}:${DEFAULT_OTLP_PORT}`
1202+
11601203
this._setBoolean(calc, 'isGitUploadEnabled',
11611204
calc.isIntelligentTestRunnerEnabled && !isFalse(this._isCiVisibilityGitUploadEnabled()))
11621205
this._setBoolean(calc, 'spanComputePeerService', this._getSpanComputePeerService())

packages/dd-trace/src/config_defaults.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ module.exports = {
126126
isTestManagementEnabled: false,
127127
isImpactedTestsEnabled: false,
128128
logInjection: true,
129+
otelLogsEnabled: false,
130+
otelUrl: undefined,
131+
otelLogsUrl: undefined, // Will be computed using agent host
132+
otelHeaders: undefined,
133+
otelLogsHeaders: '',
134+
otelProtocol: 'http/protobuf',
135+
otelLogsProtocol: 'http/protobuf',
136+
otelLogsTimeout: 10_000,
137+
otelTimeout: 10_000,
138+
otelLogsBatchTimeout: 5000,
139+
otelLogsMaxExportBatchSize: 512,
129140
lookup: undefined,
130141
inferredProxyServicesEnabled: false,
131142
memcachedCommandEnabled: false,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use strict'
2+
3+
/**
4+
* @typedef {import('@opentelemetry/api-logs').LogRecord} LogRecord
5+
* @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope
6+
*/
7+
8+
/**
9+
* BatchLogRecordProcessor processes log records in batches for efficient export to Datadog Agent.
10+
*
11+
* This implementation follows the OpenTelemetry JavaScript SDK BatchLogRecordProcessor:
12+
* https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_sdk-logs.BatchLogRecordProcessor.html
13+
*
14+
* @class BatchLogRecordProcessor
15+
*/
16+
class BatchLogRecordProcessor {
17+
#logRecords
18+
#timer
19+
#batchTimeout
20+
#maxExportBatchSize
21+
22+
/**
23+
* Creates a new BatchLogRecordProcessor instance.
24+
*
25+
* @param {OtlpHttpLogExporter} exporter - Log processor for exporting batches to Datadog Agent
26+
* @param {number} batchTimeout - Timeout in milliseconds for batch processing
27+
* @param {number} maxExportBatchSize - Maximum number of log records per batch
28+
*/
29+
constructor (exporter, batchTimeout, maxExportBatchSize) {
30+
this.exporter = exporter
31+
this.#batchTimeout = batchTimeout
32+
this.#maxExportBatchSize = maxExportBatchSize
33+
this.#logRecords = []
34+
this.#timer = null
35+
}
36+
37+
/**
38+
* Processes a single log record.
39+
*
40+
* @param {LogRecord} logRecord - The enriched log record with trace correlation and metadata
41+
* @param {InstrumentationScope} instrumentationScope - The instrumentation library
42+
*/
43+
onEmit (logRecord, instrumentationScope) {
44+
// Store the log record (already enriched by Logger.emit)
45+
this.#logRecords.push({ ...logRecord, instrumentationScope })
46+
47+
if (this.#logRecords.length >= this.#maxExportBatchSize) {
48+
this.#export()
49+
} else if (this.#logRecords.length === 1) {
50+
this.#startTimer()
51+
}
52+
}
53+
54+
/**
55+
* Forces an immediate flush of all pending log records.
56+
* @returns {undefined} Promise that resolves when flush is complete
57+
*/
58+
forceFlush () {
59+
this.#export()
60+
}
61+
62+
/**
63+
* Starts the batch timeout timer.
64+
* @private
65+
*/
66+
#startTimer () {
67+
if (this.#timer) {
68+
return
69+
}
70+
71+
this.#timer = setTimeout(() => {
72+
this.#export()
73+
}, this.#batchTimeout)
74+
}
75+
76+
/**
77+
* Exports the current batch of log records.
78+
* @private
79+
*/
80+
#export () {
81+
const logRecords = this.#logRecords.slice(0, this.#maxExportBatchSize)
82+
this.#logRecords = this.#logRecords.slice(this.#maxExportBatchSize)
83+
84+
this.#clearTimer()
85+
this.exporter.export(logRecords, () => {})
86+
}
87+
88+
/**
89+
* Clears the batch timeout timer.
90+
* @private
91+
*/
92+
#clearTimer () {
93+
if (this.#timer) {
94+
clearTimeout(this.#timer)
95+
this.#timer = null
96+
}
97+
}
98+
}
99+
100+
module.exports = BatchLogRecordProcessor

0 commit comments

Comments
 (0)