dd-trace 5.72.0 → 5.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/index.d.ts +21 -0
  3. package/package.json +8 -6
  4. package/packages/datadog-esbuild/index.js +8 -0
  5. package/packages/datadog-instrumentations/src/azure-service-bus.js +49 -22
  6. package/packages/datadog-instrumentations/src/cookie-parser.js +2 -0
  7. package/packages/datadog-instrumentations/src/jest.js +59 -14
  8. package/packages/datadog-instrumentations/src/mocha/utils.js +3 -4
  9. package/packages/datadog-plugin-amqplib/src/consumer.js +1 -1
  10. package/packages/datadog-plugin-azure-functions/src/index.js +24 -14
  11. package/packages/datadog-plugin-azure-service-bus/src/index.js +1 -1
  12. package/packages/datadog-plugin-azure-service-bus/src/producer.js +60 -12
  13. package/packages/datadog-plugin-jest/src/index.js +53 -18
  14. package/packages/datadog-plugin-ws/src/close.js +1 -1
  15. package/packages/datadog-plugin-ws/src/producer.js +1 -1
  16. package/packages/datadog-plugin-ws/src/receiver.js +1 -1
  17. package/packages/dd-trace/src/appsec/index.js +9 -1
  18. package/packages/dd-trace/src/appsec/reporter.js +2 -3
  19. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -0
  20. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +8 -3
  21. package/packages/dd-trace/src/llmobs/plugins/base.js +11 -12
  22. package/packages/dd-trace/src/llmobs/sdk.js +20 -4
  23. package/packages/dd-trace/src/llmobs/tagger.js +12 -0
  24. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +7 -127
  25. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +19 -134
  26. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +720 -0
  27. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +78 -0
  28. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +177 -0
  29. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +163 -0
  30. package/packages/dd-trace/src/opentelemetry/{protos → otlp}/protobuf_loader.js +24 -6
  31. package/packages/dd-trace/src/supported-configurations.json +1 -0
  32. /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/common.proto +0 -0
  33. /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/logs.proto +0 -0
  34. /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/logs_service.proto +0 -0
  35. /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/resource.proto +0 -0
@@ -50,6 +50,7 @@ const {
50
50
  TELEMETRY_CODE_COVERAGE_NUM_FILES,
51
51
  TELEMETRY_TEST_SESSION
52
52
  } = require('../../dd-trace/src/ci-visibility/telemetry')
53
+ const log = require('../../dd-trace/src/log')
53
54
 
54
55
  const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
55
56
 
@@ -102,6 +103,7 @@ class JestPlugin extends CiPlugin {
102
103
  }
103
104
  process.on('message', handler)
104
105
  }
106
+ this.testSuiteSpanPerTestSuiteAbsolutePath = new Map()
105
107
 
106
108
  this.addSub('ci:jest:session:finish', ({
107
109
  status,
@@ -196,7 +198,8 @@ class JestPlugin extends CiPlugin {
196
198
  testSourceFile,
197
199
  testEnvironmentOptions,
198
200
  frameworkVersion,
199
- displayName
201
+ displayName,
202
+ testSuiteAbsolutePath
200
203
  }) => {
201
204
  const {
202
205
  _ddTestSessionId: testSessionId,
@@ -259,6 +262,7 @@ class JestPlugin extends CiPlugin {
259
262
  if (_ddTestCodeCoverageEnabled) {
260
263
  this.telemetry.ciVisEvent(TELEMETRY_CODE_COVERAGE_STARTED, 'suite', { library: 'istanbul' })
261
264
  }
265
+ this.testSuiteSpanPerTestSuiteAbsolutePath.set(testSuiteAbsolutePath, this.testSuiteSpan)
262
266
  })
263
267
 
264
268
  this.addSub('ci:jest:worker-report:coverage', data => {
@@ -272,25 +276,54 @@ class JestPlugin extends CiPlugin {
272
276
  })
273
277
  })
274
278
 
275
- this.addSub('ci:jest:test-suite:finish', ({ status, errorMessage, error }) => {
276
- this.testSuiteSpan.setTag(TEST_STATUS, status)
279
+ this.addSub('ci:jest:test-suite:finish', ({ status, errorMessage, error, testSuiteAbsolutePath }) => {
280
+ const testSuiteSpan = this.testSuiteSpanPerTestSuiteAbsolutePath.get(testSuiteAbsolutePath)
281
+ if (!testSuiteSpan) {
282
+ log.warn('"ci:jest:test-suite:finish": no span found for test suite absolute path %s', testSuiteAbsolutePath)
283
+ return
284
+ }
285
+ const hasStatus = testSuiteSpan.context()._tags[TEST_STATUS]
286
+ if (!hasStatus) {
287
+ // The status may have been set in 'ci:jest:test-suite:error'
288
+ testSuiteSpan.setTag(TEST_STATUS, status)
289
+ }
277
290
  if (error) {
278
- this.testSuiteSpan.setTag('error', error)
291
+ testSuiteSpan.setTag('error', error)
292
+ testSuiteSpan.setTag(TEST_STATUS, 'fail')
279
293
  } else if (errorMessage) {
280
- this.testSuiteSpan.setTag('error', new Error(errorMessage))
294
+ testSuiteSpan.setTag('error', new Error(errorMessage))
295
+ testSuiteSpan.setTag(TEST_STATUS, 'fail')
281
296
  }
282
- this.testSuiteSpan.finish()
283
- this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
284
- // Suites potentially run in a different process than the session,
285
- // so calling finishAllTraceSpans on the session span is not enough
286
- finishAllTraceSpans(this.testSuiteSpan)
287
- // Flushing within jest workers is cheap, as it's just interprocess communication
288
- // We do not want to flush after every suite if jest is running tests serially,
289
- // as every flush is an HTTP request.
290
- if (isJestWorker) {
291
- this.tracer._exporter.flush()
297
+ // We need to give the potential error in 'ci:jest:test-suite:error' time to be published
298
+ process.nextTick(() => {
299
+ testSuiteSpan.finish()
300
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
301
+ // Suites potentially run in a different process than the session,
302
+ // so calling finishAllTraceSpans on the session span is not enough
303
+ finishAllTraceSpans(testSuiteSpan)
304
+ // Flushing within jest workers is cheap, as it's just interprocess communication
305
+ // We do not want to flush after every suite if jest is running tests serially,
306
+ // as every flush is an HTTP request.
307
+ if (isJestWorker) {
308
+ this.tracer._exporter.flush()
309
+ }
310
+ this.removeAllDiProbes()
311
+ this.testSuiteSpanPerTestSuiteAbsolutePath.delete(testSuiteAbsolutePath)
312
+ })
313
+ })
314
+
315
+ this.addSub('ci:jest:test-suite:error', ({ error, errorMessage, testSuiteAbsolutePath }) => {
316
+ const runningTestSuiteSpan = this.testSuiteSpanPerTestSuiteAbsolutePath.get(testSuiteAbsolutePath)
317
+ if (!runningTestSuiteSpan) {
318
+ log.warn('"ci:jest:test-suite:error": no span found for test suite absolute path %s', testSuiteAbsolutePath)
319
+ return
320
+ }
321
+ if (error) {
322
+ runningTestSuiteSpan.setTag('error', error)
323
+ } else if (errorMessage) {
324
+ runningTestSuiteSpan.setTag('error', new Error(errorMessage))
292
325
  }
293
- this.removeAllDiProbes()
326
+ runningTestSuiteSpan.setTag(TEST_STATUS, 'fail')
294
327
  })
295
328
 
296
329
  /**
@@ -420,7 +453,8 @@ class JestPlugin extends CiPlugin {
420
453
  isJestRetry,
421
454
  isDisabled,
422
455
  isQuarantined,
423
- isModified
456
+ isModified,
457
+ testSuiteAbsolutePath
424
458
  } = test
425
459
 
426
460
  const extraTags = {
@@ -468,8 +502,9 @@ class JestPlugin extends CiPlugin {
468
502
  if (isNew) {
469
503
  extraTags[TEST_IS_NEW] = 'true'
470
504
  }
505
+ const testSuiteSpan = this.testSuiteSpanPerTestSuiteAbsolutePath.get(testSuiteAbsolutePath) || this.testSuiteSpan
471
506
 
472
- return super.startTestSpan(name, suite, this.testSuiteSpan, extraTags)
507
+ return super.startTestSpan(name, suite, testSuiteSpan, extraTags)
473
508
  }
474
509
  }
475
510
 
@@ -58,7 +58,7 @@ class WSClosePlugin extends TracingPlugin {
58
58
  }
59
59
 
60
60
  end (ctx) {
61
- if (!Object.hasOwn(ctx, 'result')) return
61
+ if (!Object.hasOwn(ctx, 'result') || !ctx.span) return
62
62
 
63
63
  if (ctx.socket.spanContext) ctx.span.addLink({ context: ctx.socket.spanContext })
64
64
 
@@ -48,7 +48,7 @@ class WSProducerPlugin extends TracingPlugin {
48
48
  }
49
49
 
50
50
  end (ctx) {
51
- if (!Object.hasOwn(ctx, 'result')) return
51
+ if (!Object.hasOwn(ctx, 'result') || !ctx.span) return
52
52
 
53
53
  if (ctx.socket.spanContext) {
54
54
  ctx.span.addLink({
@@ -58,7 +58,7 @@ class WSReceiverPlugin extends TracingPlugin {
58
58
  }
59
59
 
60
60
  end (ctx) {
61
- if (!Object.hasOwn(ctx, 'result')) return
61
+ if (!Object.hasOwn(ctx, 'result') || !ctx.span) return
62
62
 
63
63
  if (ctx.socket.spanContext) {
64
64
  ctx.span.addLink({
@@ -43,6 +43,7 @@ const { isInServerlessEnvironment } = require('../serverless')
43
43
 
44
44
  const responseAnalyzedSet = new WeakSet()
45
45
  const storedResponseHeaders = new WeakMap()
46
+ const storedBodies = new WeakMap()
46
47
 
47
48
  let isEnabled = false
48
49
  let config
@@ -114,6 +115,11 @@ function onRequestBodyParsed ({ req, res, body, abortController }) {
114
115
  const rootSpan = web.root(req)
115
116
  if (!rootSpan) return
116
117
 
118
+ if (!req.body) {
119
+ // do not store body if it is in req.body
120
+ storedBodies.set(req, body)
121
+ }
122
+
117
123
  const results = waf.run({
118
124
  persistent: {
119
125
  [addresses.HTTP_INCOMING_BODY]: body
@@ -200,11 +206,13 @@ function incomingHttpEndTranslator ({ req, res }) {
200
206
 
201
207
  const storedHeaders = storedResponseHeaders.get(req) || {}
202
208
 
203
- Reporter.finishRequest(req, res, storedHeaders)
209
+ const body = req.body || storedBodies.get(req)
210
+ Reporter.finishRequest(req, res, storedHeaders, body)
204
211
 
205
212
  if (storedHeaders) {
206
213
  storedResponseHeaders.delete(req)
207
214
  }
215
+ storedBodies.delete(req)
208
216
  }
209
217
 
210
218
  function onPassportVerify ({ framework, login, user, success, abortController }) {
@@ -483,7 +483,7 @@ function reportAttributes (attributes) {
483
483
  rootSpan.addTags(tags)
484
484
  }
485
485
 
486
- function finishRequest (req, res, storedResponseHeaders) {
486
+ function finishRequest (req, res, storedResponseHeaders, requestBody) {
487
487
  const rootSpan = web.root(req)
488
488
  if (!rootSpan) return
489
489
 
@@ -543,8 +543,7 @@ function finishRequest (req, res, storedResponseHeaders) {
543
543
  )
544
544
 
545
545
  if (extendedDataCollection) {
546
- // TODO add support for fastify, req.body is not available in fastify
547
- reportRequestBody(rootSpan, req.body)
546
+ reportRequestBody(rootSpan, requestBody)
548
547
  }
549
548
 
550
549
  if (tags['appsec.event'] === 'true' && typeof req.route?.path === 'string') {
@@ -3,6 +3,7 @@
3
3
  const request = require('../../exporters/common/request')
4
4
  const id = require('../../id')
5
5
  const { getEnvironmentVariable } = require('../../config-helper')
6
+ const log = require('../../log')
6
7
 
7
8
  function getTestManagementTests ({
8
9
  url,
@@ -53,6 +54,8 @@ function getTestManagementTests ({
53
54
  }
54
55
  })
55
56
 
57
+ log.debug('Requesting test management tests: %s', data)
58
+
56
59
  request(data, options, (err, res) => {
57
60
  if (err) {
58
61
  done(err)
@@ -60,6 +63,8 @@ function getTestManagementTests ({
60
63
  try {
61
64
  const { data: { attributes: { modules: testManagementTests } } } = JSON.parse(res)
62
65
 
66
+ log.debug('Test management tests received: %j', testManagementTests)
67
+
63
68
  done(null, testManagementTests)
64
69
  } catch (err) {
65
70
  done(err)
@@ -45,19 +45,24 @@ function getOperation (span) {
45
45
 
46
46
  /**
47
47
  * Get the LLM token usage from the span tags
48
+ * Supports both AI SDK v4 (promptTokens/completionTokens) and v5 (inputTokens/outputTokens)
48
49
  * @template T extends {inputTokens: number, outputTokens: number, totalTokens: number}
49
50
  * @param {T} tags
50
51
  * @returns {Pick<T, 'inputTokens' | 'outputTokens' | 'totalTokens'>}
51
52
  */
52
53
  function getUsage (tags) {
53
54
  const usage = {}
54
- const inputTokens = tags['ai.usage.promptTokens']
55
- const outputTokens = tags['ai.usage.completionTokens']
55
+
56
+ // AI SDK v5 uses inputTokens/outputTokens, v4 uses promptTokens/completionTokens
57
+ // Check v5 properties first, fall back to v4
58
+ const inputTokens = tags['ai.usage.inputTokens'] ?? tags['ai.usage.promptTokens']
59
+ const outputTokens = tags['ai.usage.outputTokens'] ?? tags['ai.usage.completionTokens']
56
60
 
57
61
  if (inputTokens != null) usage.inputTokens = inputTokens
58
62
  if (outputTokens != null) usage.outputTokens = outputTokens
59
63
 
60
- const totalTokens = inputTokens + outputTokens
64
+ // v5 provides totalTokens directly, v4 requires computation
65
+ const totalTokens = tags['ai.usage.totalTokens'] ?? (inputTokens + outputTokens)
61
66
  if (!Number.isNaN(totalTokens)) usage.totalTokens = totalTokens
62
67
 
63
68
  return usage
@@ -28,7 +28,7 @@ class LLMObsPlugin extends TracingPlugin {
28
28
  const enabled = this._tracerConfig.llmobs.enabled
29
29
  if (!enabled) return
30
30
 
31
- const parent = this.getLLMObsParent(ctx)
31
+ const parentStore = llmobsStorage.getStore()
32
32
  const apmStore = ctx.currentStore
33
33
  const span = apmStore?.span
34
34
 
@@ -40,10 +40,14 @@ class LLMObsPlugin extends TracingPlugin {
40
40
  telemetry.incrementLLMObsSpanStartCount({ autoinstrumented: true, integration: this.constructor.integration })
41
41
 
42
42
  ctx.llmobs = {} // initialize context-based namespace
43
- llmobsStorage.enterWith({ span })
44
- ctx.llmobs.parent = parent
45
-
46
- this._tagger.registerLLMObsSpan(span, { parent, integration: this.constructor.integration, ...registerOptions })
43
+ llmobsStorage.enterWith({ ...parentStore, span })
44
+ ctx.llmobs.parent = parentStore
45
+
46
+ this._tagger.registerLLMObsSpan(span, {
47
+ parent: parentStore?.span,
48
+ integration: this.constructor.integration,
49
+ ...registerOptions
50
+ })
47
51
  }
48
52
  }
49
53
 
@@ -56,8 +60,8 @@ class LLMObsPlugin extends TracingPlugin {
56
60
  const span = apmStore?.span
57
61
  if (!LLMObsTagger.tagMap.has(span)) return
58
62
 
59
- const parent = ctx.llmobs.parent
60
- llmobsStorage.enterWith({ span: parent })
63
+ const parentStore = ctx.llmobs.parent
64
+ llmobsStorage.enterWith(parentStore)
61
65
  }
62
66
 
63
67
  asyncEnd (ctx) {
@@ -87,11 +91,6 @@ class LLMObsPlugin extends TracingPlugin {
87
91
  }
88
92
  super.configure(config)
89
93
  }
90
-
91
- getLLMObsParent () {
92
- const store = llmobsStorage.getStore()
93
- return store?.span
94
- }
95
94
  }
96
95
 
97
96
  module.exports = LLMObsPlugin
@@ -422,6 +422,22 @@ class LLMObs extends NoopLLMObs {
422
422
  }
423
423
  }
424
424
 
425
+ annotationContext (options, fn) {
426
+ if (!this.enabled) return fn()
427
+
428
+ const currentStore = storage.getStore()
429
+
430
+ const store = {
431
+ ...currentStore,
432
+ annotationContext: {
433
+ ...currentStore?.annotationContext,
434
+ ...options
435
+ }
436
+ }
437
+
438
+ return storage.run(store, fn)
439
+ }
440
+
425
441
  flush () {
426
442
  if (!this.enabled) return
427
443
 
@@ -447,20 +463,20 @@ class LLMObs extends NoopLLMObs {
447
463
  }
448
464
 
449
465
  _activate (span, options, fn) {
450
- const parent = this._active()
451
- if (this.enabled) storage.enterWith({ span })
466
+ const parentStore = storage.getStore()
467
+ if (this.enabled) storage.enterWith({ ...parentStore, span })
452
468
 
453
469
  if (options) {
454
470
  this._tagger.registerLLMObsSpan(span, {
455
471
  ...options,
456
- parent
472
+ parent: parentStore?.span
457
473
  })
458
474
  }
459
475
 
460
476
  try {
461
477
  return fn()
462
478
  } finally {
463
- if (this.enabled) storage.enterWith({ span: parent })
479
+ if (this.enabled) storage.enterWith(parentStore)
464
480
  }
465
481
  }
466
482
 
@@ -29,6 +29,7 @@ const {
29
29
  DECORATOR,
30
30
  PROPAGATED_ML_APP_KEY
31
31
  } = require('./constants/tags')
32
+ const { storage } = require('./storage')
32
33
 
33
34
  // global registry of LLMObs spans
34
35
  // maps LLMObs spans to their annotations
@@ -97,6 +98,17 @@ class LLMObsTagger {
97
98
  span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY] ??
98
99
  ROOT_PARENT_ID
99
100
  this._setTag(span, PARENT_ID_KEY, parentId)
101
+
102
+ // apply annotation context
103
+ const annotationContext = storage.getStore()?.annotationContext
104
+
105
+ // apply annotation context tags
106
+ const tags = annotationContext?.tags
107
+ if (tags) this.tagSpanTags(span, tags)
108
+
109
+ // apply annotation context name
110
+ const annotationContextName = annotationContext?.name
111
+ if (annotationContextName) this._setTag(span, NAME, annotationContextName)
100
112
  }
101
113
 
102
114
  // TODO: similarly for the following `tag` methods,
@@ -1,18 +1,12 @@
1
1
  'use strict'
2
2
 
3
- const http = require('http')
4
- const { URL } = require('url')
5
- const log = require('../../log')
3
+ const OtlpHttpExporterBase = require('../otlp/otlp_http_exporter_base')
6
4
  const OtlpTransformer = require('./otlp_transformer')
7
- const telemetryMetrics = require('../../telemetry/metrics')
8
5
 
9
6
  /**
10
7
  * @typedef {import('@opentelemetry/resources').Resource} Resource
11
8
  * @typedef {import('@opentelemetry/api-logs').LogRecord} LogRecord
12
- *
13
- */
14
-
15
- const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
9
+ */
16
10
 
17
11
  /**
18
12
  * OtlpHttpLogExporter exports log records via OTLP over HTTP.
@@ -21,12 +15,9 @@ const tracerMetrics = telemetryMetrics.manager.namespace('tracers')
21
15
  * https://opentelemetry.io/docs/specs/otlp/#otlphttp
22
16
  *
23
17
  * @class OtlpHttpLogExporter
18
+ * @extends OtlpHttpExporterBase
24
19
  */
25
- class OtlpHttpLogExporter {
26
- #telemetryTags
27
-
28
- DEFAULT_LOGS_PATH = '/v1/logs'
29
-
20
+ class OtlpHttpLogExporter extends OtlpHttpExporterBase {
30
21
  /**
31
22
  * Creates a new OtlpHttpLogExporter instance.
32
23
  *
@@ -37,28 +28,8 @@ class OtlpHttpLogExporter {
37
28
  * @param {Resource} resource - Resource attributes
38
29
  */
39
30
  constructor (url, headers, timeout, protocol, resource) {
40
- const parsedUrl = new URL(url)
41
-
42
- this.protocol = protocol
31
+ super(url, headers, timeout, protocol, '/v1/logs', 'logs')
43
32
  this.transformer = new OtlpTransformer(resource, protocol)
44
- // If no path is provided, use default path
45
- const path = parsedUrl.pathname === '/' ? this.DEFAULT_LOGS_PATH : parsedUrl.pathname
46
- const isJson = protocol === 'http/json'
47
- this.options = {
48
- hostname: parsedUrl.hostname,
49
- port: parsedUrl.port,
50
- path: path + parsedUrl.search,
51
- method: 'POST',
52
- timeout,
53
- headers: {
54
- 'Content-Type': isJson ? 'application/json' : 'application/x-protobuf',
55
- ...this.#parseAdditionalHeaders(headers)
56
- }
57
- }
58
- this.#telemetryTags = [
59
- 'protocol:http',
60
- `encoding:${isJson ? 'json' : 'protobuf'}`
61
- ]
62
33
  }
63
34
 
64
35
  /**
@@ -74,99 +45,8 @@ class OtlpHttpLogExporter {
74
45
  }
75
46
 
76
47
  const payload = this.transformer.transformLogRecords(logRecords)
77
- this.#sendPayload(payload, resultCallback)
78
- tracerMetrics.count('otel.log_records', this.#telemetryTags).inc(logRecords.length)
79
- }
80
-
81
- /**
82
- * Sends the payload via HTTP request.
83
- * @param {Buffer|string} payload - The payload to send
84
- * @param {Function} resultCallback - Callback for the result
85
- * @private
86
- */
87
- #sendPayload (payload, resultCallback) {
88
- const options = {
89
- ...this.options,
90
- headers: {
91
- ...this.options.headers,
92
- 'Content-Length': payload.length
93
- }
94
- }
95
-
96
- const req = http.request(options, (res) => {
97
- let data = ''
98
-
99
- res.on('data', (chunk) => {
100
- data += chunk
101
- })
102
-
103
- res.on('end', () => {
104
- if (res.statusCode >= 200 && res.statusCode < 300) {
105
- resultCallback({ code: 0 })
106
- } else {
107
- const error = new Error(`HTTP ${res.statusCode}: ${data}`)
108
- resultCallback({ code: 1, error })
109
- }
110
- })
111
- })
112
-
113
- req.on('error', (error) => {
114
- log.error('Error sending OTLP logs:', error)
115
- resultCallback({ code: 1, error })
116
- })
117
-
118
- req.on('timeout', () => {
119
- req.destroy()
120
- const error = new Error('Request timeout')
121
- resultCallback({ code: 1, error })
122
- })
123
-
124
- req.write(payload)
125
- req.end()
126
- }
127
-
128
- /**
129
- * Parses additional HTTP headers from a comma-separated string.
130
- * @param {string} headersString - Comma-separated key=value pairs
131
- * @returns {Record<string, string>} Parsed headers object
132
- * @private
133
- */
134
- #parseAdditionalHeaders (headersString) {
135
- const headers = {}
136
- let key = ''
137
- let value = ''
138
- let readingKey = true
139
-
140
- for (const char of headersString) {
141
- if (readingKey) {
142
- if (char === '=') {
143
- readingKey = false
144
- key = key.trim()
145
- } else {
146
- key += char
147
- }
148
- } else if (char === ',') {
149
- value = value.trim()
150
- if (key && value) {
151
- headers[key] = value
152
- }
153
- key = ''
154
- value = ''
155
- readingKey = true
156
- } else {
157
- value += char
158
- }
159
- }
160
-
161
- // Add the last pair if present
162
- if (!readingKey) {
163
- value = value.trim()
164
- if (value) {
165
- headers[key] = value
166
- }
167
- }
168
-
169
- return headers
48
+ this._sendPayload(payload, resultCallback)
49
+ this._recordTelemetry('otel.log_records', logRecords.length)
170
50
  }
171
51
  }
172
52