- 
                Notifications
    You must be signed in to change notification settings 
- Fork 351
Add support for OpenTelemetry Logs API #6465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 64 commits
343c772
              b3a2973
              2c0d528
              6c359ce
              7dc1480
              2c60dfc
              dabe98f
              b7ae33d
              f73d425
              543805a
              c63e20e
              d4d8aac
              9514e68
              df7cbca
              2c0a5a5
              f109cf3
              0c29478
              8b1ce46
              432ec89
              036776a
              8abbe95
              e9456e3
              9164c6f
              4bc66fc
              d505cab
              0f2bd21
              3af5080
              9578887
              c7c15d8
              f6f58fe
              434fbc4
              3963ed6
              6d10314
              11161f3
              266c936
              d63a4ed
              b98cd76
              9955d5e
              a9ddbcc
              d9d9dfb
              351ca2f
              b99ba1d
              07ec605
              a1f8cf7
              09fedbc
              9ff13fd
              0b8e71d
              46d1385
              5410486
              55e86e9
              311500c
              07ab61c
              f25c7a5
              9f716d1
              ea532c2
              e695249
              557b6d5
              266a239
              df5d6b4
              2763296
              e995e0c
              e20668e
              3f8eab1
              148d47b
              9ac86bb
              7f4fbff
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -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, | ||
|  | @@ -635,7 +636,17 @@ class Config { | |
| OTEL_RESOURCE_ATTRIBUTES, | ||
| OTEL_SERVICE_NAME, | ||
| OTEL_TRACES_SAMPLER, | ||
| OTEL_TRACES_SAMPLER_ARG | ||
| OTEL_TRACES_SAMPLER_ARG, | ||
|         
                  mabdinur marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| 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 = {} | ||
|  | @@ -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) | ||
|         
                  BridgeAR marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| 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', | ||
|  | @@ -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) { | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe 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` | ||
|         
                  BridgeAR marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
|  | ||
| this._setBoolean(calc, 'isGitUploadEnabled', | ||
| calc.isIntelligentTestRunnerEnabled && !isFalse(this._isCiVisibilityGitUploadEnabled())) | ||
| this._setBoolean(calc, 'spanComputePeerService', this._getSpanComputePeerService()) | ||
|  | ||
| 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 | ||
|         
                  mabdinur marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| * | ||
| * @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 | ||
| 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 | ||
| } | 
| 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) | ||||||||
| } | ||||||||
|         
                  mabdinur marked this conversation as resolved.
              Show resolved
            Hide resolved | ||||||||
|  | ||||||||
| // Note: timestamp is in nanoseconds (as defined by OpenTelemetry LogRecord API) | ||||||||
| if (!logRecord.timestamp) { | ||||||||
| logRecord.timestamp = Number(process.hrtime.bigint()) | ||||||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
        Suggested change
       
 Is it possible to rename the property to include that information? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type is defined by otel. We can't modify it here: https://github.com/open-telemetry/opentelemetry-js/blob/bfede5741ce2744b2653cdbff633a6dec03f22f7/experimental/packages/api-logs/src/types/LogRecord.ts#L60 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||
Uh oh!
There was an error while loading. Please reload this page.