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.
- package/LICENSE-3rdparty.csv +2 -0
- package/index.d.ts +21 -0
- package/package.json +8 -6
- package/packages/datadog-esbuild/index.js +8 -0
- package/packages/datadog-instrumentations/src/azure-service-bus.js +49 -22
- package/packages/datadog-instrumentations/src/cookie-parser.js +2 -0
- package/packages/datadog-instrumentations/src/jest.js +59 -14
- package/packages/datadog-instrumentations/src/mocha/utils.js +3 -4
- package/packages/datadog-plugin-amqplib/src/consumer.js +1 -1
- package/packages/datadog-plugin-azure-functions/src/index.js +24 -14
- package/packages/datadog-plugin-azure-service-bus/src/index.js +1 -1
- package/packages/datadog-plugin-azure-service-bus/src/producer.js +60 -12
- package/packages/datadog-plugin-jest/src/index.js +53 -18
- package/packages/datadog-plugin-ws/src/close.js +1 -1
- package/packages/datadog-plugin-ws/src/producer.js +1 -1
- package/packages/datadog-plugin-ws/src/receiver.js +1 -1
- package/packages/dd-trace/src/appsec/index.js +9 -1
- package/packages/dd-trace/src/appsec/reporter.js +2 -3
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +8 -3
- package/packages/dd-trace/src/llmobs/plugins/base.js +11 -12
- package/packages/dd-trace/src/llmobs/sdk.js +20 -4
- package/packages/dd-trace/src/llmobs/tagger.js +12 -0
- package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +7 -127
- package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +19 -134
- package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +720 -0
- package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +78 -0
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +177 -0
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +163 -0
- package/packages/dd-trace/src/opentelemetry/{protos → otlp}/protobuf_loader.js +24 -6
- package/packages/dd-trace/src/supported-configurations.json +1 -0
- /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/common.proto +0 -0
- /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/logs.proto +0 -0
- /package/packages/dd-trace/src/opentelemetry/{protos → otlp}/logs_service.proto +0 -0
- /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.
|
|
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
|
-
|
|
291
|
+
testSuiteSpan.setTag('error', error)
|
|
292
|
+
testSuiteSpan.setTag(TEST_STATUS, 'fail')
|
|
279
293
|
} else if (errorMessage) {
|
|
280
|
-
|
|
294
|
+
testSuiteSpan.setTag('error', new Error(errorMessage))
|
|
295
|
+
testSuiteSpan.setTag(TEST_STATUS, 'fail')
|
|
281
296
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
45
|
-
|
|
46
|
-
this._tagger.registerLLMObsSpan(span, {
|
|
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
|
|
60
|
-
llmobsStorage.enterWith(
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
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
|
|