dd-trace 5.106.0 → 5.108.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/index.d.ts +20 -1
- package/package.json +9 -11
- package/packages/datadog-core/src/storage.js +47 -48
- package/packages/datadog-esbuild/index.js +6 -1
- package/packages/datadog-instrumentations/src/ai.js +12 -3
- package/packages/datadog-instrumentations/src/body-parser.js +5 -2
- package/packages/datadog-instrumentations/src/connect.js +3 -2
- package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
- package/packages/datadog-instrumentations/src/cucumber.js +7 -0
- package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
- package/packages/datadog-instrumentations/src/express-session.js +12 -11
- package/packages/datadog-instrumentations/src/express.js +24 -20
- package/packages/datadog-instrumentations/src/fastify.js +18 -6
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
- package/packages/datadog-instrumentations/src/http/client.js +9 -12
- package/packages/datadog-instrumentations/src/http/server.js +30 -16
- package/packages/datadog-instrumentations/src/http2/client.js +15 -12
- package/packages/datadog-instrumentations/src/http2/server.js +15 -8
- package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
- package/packages/datadog-instrumentations/src/jest.js +143 -73
- package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
- package/packages/datadog-instrumentations/src/multer.js +3 -2
- package/packages/datadog-instrumentations/src/mysql2.js +34 -0
- package/packages/datadog-instrumentations/src/net.js +8 -6
- package/packages/datadog-instrumentations/src/openai.js +19 -7
- package/packages/datadog-instrumentations/src/pg.js +19 -0
- package/packages/datadog-instrumentations/src/router.js +12 -10
- package/packages/datadog-instrumentations/src/vitest.js +29 -4
- package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
- package/packages/datadog-plugin-cucumber/src/index.js +2 -0
- package/packages/datadog-plugin-cypress/src/support.js +31 -1
- package/packages/datadog-plugin-http/src/client.js +0 -3
- package/packages/datadog-plugin-http/src/server.js +11 -1
- package/packages/datadog-plugin-mocha/src/index.js +2 -0
- package/packages/datadog-plugin-pg/src/index.js +10 -0
- package/packages/dd-trace/src/aiguard/index.js +34 -15
- package/packages/dd-trace/src/aiguard/sdk.js +34 -3
- package/packages/dd-trace/src/aiguard/tags.js +6 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +3 -2
- package/packages/dd-trace/src/appsec/iast/index.js +3 -2
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +2 -1
- package/packages/dd-trace/src/appsec/reporter.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +14 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -1
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +5 -9
- package/packages/dd-trace/src/config/parsers.js +8 -0
- package/packages/dd-trace/src/config/supported-configurations.json +20 -6
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/datastreams/writer.js +1 -2
- package/packages/dd-trace/src/debugger/config.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
- package/packages/dd-trace/src/debugger/index.js +1 -2
- package/packages/dd-trace/src/dogstatsd.js +2 -3
- package/packages/dd-trace/src/encode/0.4.js +49 -41
- package/packages/dd-trace/src/encode/agentless-json.js +5 -1
- package/packages/dd-trace/src/encode/tags-processors.js +14 -0
- package/packages/dd-trace/src/exporters/agent/index.js +1 -2
- package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
- package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
- package/packages/dd-trace/src/exporters/common/request.js +26 -0
- package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
- package/packages/dd-trace/src/llmobs/sdk.js +4 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
- package/packages/dd-trace/src/llmobs/tagger.js +5 -3
- package/packages/dd-trace/src/llmobs/util.js +54 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
- package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
- package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
- package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
- package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +3 -2
- package/packages/dd-trace/src/opentracing/span.js +23 -18
- package/packages/dd-trace/src/opentracing/tracer.js +16 -12
- package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
- package/packages/dd-trace/src/priority_sampler.js +6 -5
- package/packages/dd-trace/src/profiling/config.js +3 -2
- package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
- package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
- package/packages/dd-trace/src/proxy.js +13 -10
- package/packages/dd-trace/src/remote_config/index.js +1 -2
- package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
- package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
- package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
- package/packages/dd-trace/src/span_format.js +33 -25
- package/packages/dd-trace/src/span_stats.js +1 -1
- package/packages/dd-trace/src/startup-log.js +1 -2
- package/packages/dd-trace/src/telemetry/send-data.js +1 -1
- package/packages/dd-trace/src/tracer.js +1 -1
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/dd-trace/src/agent/url.js +0 -28
- package/scripts/preinstall.js +0 -34
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const getConfig = require('../config')
|
|
4
4
|
const { MsgpackChunk } = require('../msgpack')
|
|
5
5
|
const log = require('../log')
|
|
6
|
-
const { normalizeSpan } = require('./tags-processors')
|
|
6
|
+
const { normalizeSpan, eventTimeNano } = require('./tags-processors')
|
|
7
7
|
|
|
8
8
|
const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
|
|
9
9
|
// Values longer than this byte threshold skip the `_stringMap` lookup and
|
|
@@ -114,8 +114,10 @@ const ATTR_PAYLOAD_BOOL_FALSE = Buffer.concat([ATTR_PREFIX_BOOL, Buffer.from([0x
|
|
|
114
114
|
function formatSpanWithLegacyEvents (span) {
|
|
115
115
|
span = normalizeSpan(span)
|
|
116
116
|
if (span.span_events) {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
117
|
+
// Reads the raw `_events` array directly (no formatter pre-reshape) and
|
|
118
|
+
// serializes to the legacy meta.events JSON string. The serialization is
|
|
119
|
+
// still the main cost on the legacy path; the native span_events slot
|
|
120
|
+
// (`#encodeSpanEvents`) avoids it entirely.
|
|
119
121
|
span.meta.events = stringifySpanEvents(span.span_events)
|
|
120
122
|
// `= undefined` over `delete` to keep the span's hidden class — `delete`
|
|
121
123
|
// would push every event-bearing span into V8 dictionary mode.
|
|
@@ -125,14 +127,15 @@ function formatSpanWithLegacyEvents (span) {
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
|
-
* Hand-written stringifier for `span.span_events`.
|
|
129
|
-
* `
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
130
|
+
* Hand-written stringifier for `span.span_events`. Events arrive in their raw
|
|
131
|
+
* `{ name, startTime, attributes? }` shape; `time_unix_nano` is derived per
|
|
132
|
+
* event via `eventTimeNano` and empty attribute objects are dropped, matching
|
|
133
|
+
* what the formatter used to precompute. Attribute values are pre-sanitized to
|
|
134
|
+
* primitives or arrays of primitives, so we skip everything `JSON.stringify`
|
|
135
|
+
* does for the generic case (toJSON probing, prototype-chain key iteration,
|
|
136
|
+
* replacer hooks).
|
|
134
137
|
*
|
|
135
|
-
* @param {Array<{ name:
|
|
138
|
+
* @param {Array<{ name: unknown, startTime: number, attributes?: object }>} spanEvents
|
|
136
139
|
* @returns {string}
|
|
137
140
|
*/
|
|
138
141
|
function stringifySpanEvents (spanEvents) {
|
|
@@ -140,17 +143,21 @@ function stringifySpanEvents (spanEvents) {
|
|
|
140
143
|
for (let index = 0; index < spanEvents.length; index++) {
|
|
141
144
|
if (index > 0) result += ','
|
|
142
145
|
const event = spanEvents[index]
|
|
146
|
+
// `_sanitizeEventAttributes` leaves `attributes` undefined when empty, so a
|
|
147
|
+
// present value always has entries — no emptiness probe here.
|
|
148
|
+
const attributes = event.attributes
|
|
143
149
|
// `addEvent` does not type-check `name`; defer the unusual cases to
|
|
144
|
-
// `JSON.stringify` so non-string names match the prior behaviour
|
|
145
|
-
//
|
|
150
|
+
// `JSON.stringify` so non-string names match the prior behaviour instead
|
|
151
|
+
// of throwing in `escapeJsonString`. Build the wire-shaped object so the
|
|
152
|
+
// emitted key stays `time_unix_nano`, not the raw `startTime`.
|
|
146
153
|
if (typeof event.name !== 'string') {
|
|
147
|
-
result += JSON.stringify(event)
|
|
154
|
+
result += JSON.stringify({ name: event.name, time_unix_nano: eventTimeNano(event), attributes })
|
|
148
155
|
continue
|
|
149
156
|
}
|
|
150
157
|
result += '{"name":' + escapeJsonString(event.name) +
|
|
151
|
-
',"time_unix_nano":' + jsonNumber(event
|
|
152
|
-
if (
|
|
153
|
-
result += ',"attributes":' + stringifyAttributes(
|
|
158
|
+
',"time_unix_nano":' + jsonNumber(eventTimeNano(event))
|
|
159
|
+
if (attributes) {
|
|
160
|
+
result += ',"attributes":' + stringifyAttributes(attributes)
|
|
154
161
|
}
|
|
155
162
|
result += '}'
|
|
156
163
|
}
|
|
@@ -241,8 +248,8 @@ class AgentEncoder {
|
|
|
241
248
|
this.#config = getConfig()
|
|
242
249
|
this.#debugEncoding = this.#config.DD_TRACE_ENCODING_DEBUG
|
|
243
250
|
// Pick the per-span formatter once so the hot loop pays no per-span
|
|
244
|
-
// config check. The native path
|
|
245
|
-
//
|
|
251
|
+
// config check. The native path keeps the raw `span_events` slot for
|
|
252
|
+
// `#encodeSpanEvents`; the legacy path serializes it into meta.events.
|
|
246
253
|
this.#formatSpan = this.#config.DD_TRACE_NATIVE_SPAN_EVENTS
|
|
247
254
|
? normalizeSpan
|
|
248
255
|
: formatSpanWithLegacyEvents
|
|
@@ -320,16 +327,18 @@ class AgentEncoder {
|
|
|
320
327
|
const resourceLen = resourceEntry.length
|
|
321
328
|
const serviceLen = serviceEntry.length
|
|
322
329
|
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// each integer
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
330
|
+
// `error` is `0` or `1` on nearly every span, and `start` is a
|
|
331
|
+
// nanosecond timestamp ≥ 2³² (always a msgpack u64). Decide the fused
|
|
332
|
+
// error key+value up front (`KEY_ERROR_0` / `KEY_ERROR_1`, or `undefined`
|
|
333
|
+
// for the rare non-binary flag) so the tail fuses without re-deciding the
|
|
334
|
+
// error shape twice. The fused tail also needs `start` as a u64; when
|
|
335
|
+
// either misses (synthetic small `start`, non-binary error) the tail
|
|
336
|
+
// routes each integer through `writeIntOrFloat` for the shortest
|
|
337
|
+
// encoding.
|
|
338
|
+
const errorEntry = span.error === 0
|
|
339
|
+
? KEY_ERROR_0
|
|
340
|
+
: span.error === 1 ? KEY_ERROR_1 : undefined
|
|
341
|
+
const fuseTail = errorEntry !== undefined && span.start >= 0x1_00_00_00_00
|
|
333
342
|
|
|
334
343
|
let blockSize = 1 +
|
|
335
344
|
KEY_TRACE_ID_PREFIX.length + 8 +
|
|
@@ -340,7 +349,7 @@ class AgentEncoder {
|
|
|
340
349
|
KEY_SERVICE.length + serviceLen
|
|
341
350
|
if (typeEntry) blockSize += KEY_TYPE.length + typeEntry.length
|
|
342
351
|
if (fuseTail) {
|
|
343
|
-
blockSize +=
|
|
352
|
+
blockSize += errorEntry.length + KEY_START_PREFIX.length + 8 + KEY_DURATION.length
|
|
344
353
|
}
|
|
345
354
|
|
|
346
355
|
const blockOffset = bytes.length
|
|
@@ -377,8 +386,8 @@ class AgentEncoder {
|
|
|
377
386
|
cursor += serviceLen
|
|
378
387
|
|
|
379
388
|
if (fuseTail) {
|
|
380
|
-
target.set(
|
|
381
|
-
cursor +=
|
|
389
|
+
target.set(errorEntry, cursor)
|
|
390
|
+
cursor += errorEntry.length
|
|
382
391
|
|
|
383
392
|
target.set(KEY_START_PREFIX, cursor)
|
|
384
393
|
cursor += KEY_START_PREFIX.length
|
|
@@ -389,15 +398,14 @@ class AgentEncoder {
|
|
|
389
398
|
cursor += 8
|
|
390
399
|
|
|
391
400
|
target.set(KEY_DURATION, cursor)
|
|
401
|
+
} else if (errorEntry) {
|
|
402
|
+
bytes.set(errorEntry)
|
|
403
|
+
bytes.set(KEY_START)
|
|
404
|
+
bytes.writeIntOrFloat(span.start)
|
|
405
|
+
bytes.set(KEY_DURATION)
|
|
392
406
|
} else {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
} else if (span.error === 1) {
|
|
396
|
-
bytes.set(KEY_ERROR_1)
|
|
397
|
-
} else {
|
|
398
|
-
bytes.set(KEY_ERROR)
|
|
399
|
-
bytes.writeIntOrFloat(span.error)
|
|
400
|
-
}
|
|
407
|
+
bytes.set(KEY_ERROR)
|
|
408
|
+
bytes.writeIntOrFloat(span.error)
|
|
401
409
|
bytes.set(KEY_START)
|
|
402
410
|
bytes.writeIntOrFloat(span.start)
|
|
403
411
|
bytes.set(KEY_DURATION)
|
|
@@ -763,7 +771,7 @@ class AgentEncoder {
|
|
|
763
771
|
* values — no `formatSpanEvents` pre-pass and no recursive generic walk.
|
|
764
772
|
*
|
|
765
773
|
* @param {MsgpackChunk} bytes
|
|
766
|
-
* @param {Array<{ name:
|
|
774
|
+
* @param {Array<{ name: unknown, startTime: number, attributes?: object }>} spanEvents
|
|
767
775
|
*/
|
|
768
776
|
#encodeSpanEvents (bytes, spanEvents) {
|
|
769
777
|
const offset = bytes.length
|
|
@@ -784,7 +792,7 @@ class AgentEncoder {
|
|
|
784
792
|
bytes.set(KEY_NAME)
|
|
785
793
|
this._encodeString(bytes, event.name)
|
|
786
794
|
bytes.set(KEY_EVENT_TIME)
|
|
787
|
-
bytes.writeFloat(event
|
|
795
|
+
bytes.writeFloat(eventTimeNano(event))
|
|
788
796
|
|
|
789
797
|
const attributes = event.attributes
|
|
790
798
|
if (attributes !== null && typeof attributes === 'object') {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const log = require('../log')
|
|
4
4
|
const { TOP_LEVEL_KEY } = require('../constants')
|
|
5
5
|
const { normalizeSpan } = require('./tags-processors')
|
|
6
|
+
const { stringifySpanEvents } = require('./0.4')
|
|
6
7
|
|
|
7
8
|
// Soft limit for estimated payload size. Triggers an early flush to stay under intake request size limits.
|
|
8
9
|
const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
|
|
@@ -20,7 +21,10 @@ function formatSpan (span, isFirstSpan) {
|
|
|
20
21
|
delete span.meta['_dd.p.tid']
|
|
21
22
|
|
|
22
23
|
if (span.span_events) {
|
|
23
|
-
|
|
24
|
+
// Events arrive raw (`{ name, startTime, attributes? }`); stringifySpanEvents
|
|
25
|
+
// derives `time_unix_nano` and drops empty attributes, matching the JSON the
|
|
26
|
+
// reshaped array used to produce.
|
|
27
|
+
span.meta.events = stringifySpanEvents(span.span_events)
|
|
24
28
|
delete span.span_events
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -46,6 +46,19 @@ function truncateSpanTestOpt (span) {
|
|
|
46
46
|
return span
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Convert a raw span event's `startTime` (milliseconds, sub-millisecond
|
|
51
|
+
* precision) to the wire `time_unix_nano`. Single source of truth for the
|
|
52
|
+
* formula so the four encoders that consume `span_events` stay in lockstep;
|
|
53
|
+
* the formatter no longer reshapes events, it hands the raw array through.
|
|
54
|
+
*
|
|
55
|
+
* @param {{ startTime: number }} event
|
|
56
|
+
* @returns {number}
|
|
57
|
+
*/
|
|
58
|
+
function eventTimeNano (event) {
|
|
59
|
+
return Math.round(event.startTime * 1e6)
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
function normalizeSpan (span) {
|
|
50
63
|
span.service = span.service || DEFAULT_SERVICE_NAME
|
|
51
64
|
if (span.service.length > MAX_SERVICE_LENGTH) {
|
|
@@ -69,6 +82,7 @@ module.exports = {
|
|
|
69
82
|
truncateSpan,
|
|
70
83
|
truncateSpanTestOpt,
|
|
71
84
|
normalizeSpan,
|
|
85
|
+
eventTimeNano,
|
|
72
86
|
MAX_META_KEY_LENGTH,
|
|
73
87
|
MAX_META_VALUE_LENGTH,
|
|
74
88
|
MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION,
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const { URL } = require('url')
|
|
4
4
|
const log = require('../../log')
|
|
5
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
6
5
|
const Writer = require('./writer')
|
|
7
6
|
|
|
8
7
|
class AgentExporter {
|
|
@@ -11,7 +10,7 @@ class AgentExporter {
|
|
|
11
10
|
constructor (config, prioritySampler) {
|
|
12
11
|
this._config = config
|
|
13
12
|
const { lookup, protocolVersion, stats = {}, apmTracingEnabled } = config
|
|
14
|
-
this._url =
|
|
13
|
+
this._url = config.url
|
|
15
14
|
|
|
16
15
|
const headers = {}
|
|
17
16
|
if (stats.enabled || apmTracingEnabled === false) {
|
|
@@ -18,25 +18,21 @@ class AgentlessExporter {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {object} config - Configuration object
|
|
21
|
-
* @param {string} [config.site] - The Datadog site. Defaults to 'datadoghq.com'.
|
|
22
|
-
* @param {string} [config.url] - Override intake URL
|
|
21
|
+
* @param {string} [config.site] - The Datadog site. Defaults to 'datadoghq.com'.
|
|
23
22
|
* @param {number} [config.flushInterval] - Batch flush interval in ms
|
|
24
23
|
* @param {string} [config.env] - Environment name
|
|
25
24
|
* @param {object} [config.tags] - Tags including runtime-id
|
|
26
25
|
*/
|
|
27
26
|
constructor (config) {
|
|
28
27
|
this._config = config
|
|
29
|
-
const
|
|
28
|
+
const site = config.site ?? 'datadoghq.com'
|
|
30
29
|
|
|
31
30
|
try {
|
|
32
|
-
|
|
31
|
+
// Agentless traffic carries the Datadog API key, so the intake is always the public https
|
|
32
|
+
// endpoint; never derive it from config.url (the agent's cleartext http) or the key leaks.
|
|
33
|
+
this._url = new URL(`https://public-trace-http-intake.logs.${site}`)
|
|
33
34
|
} catch (err) {
|
|
34
|
-
log.error(
|
|
35
|
-
'Invalid URL configuration for agentless exporter. url=%s, site=%s. Error: %s',
|
|
36
|
-
url || 'not set',
|
|
37
|
-
site,
|
|
38
|
-
err.message
|
|
39
|
-
)
|
|
35
|
+
log.error('Invalid site for agentless exporter. site=%s. Error: %s', site, err.message)
|
|
40
36
|
this._url = null
|
|
41
37
|
}
|
|
42
38
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { incrementCountMetric, TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION } = require('../../ci-visibility/telemetry')
|
|
4
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Base exporter that buffers traces until a writer is initialized.
|
|
@@ -14,7 +13,7 @@ class BufferingExporter {
|
|
|
14
13
|
|
|
15
14
|
constructor (tracerConfig) {
|
|
16
15
|
this._config = tracerConfig
|
|
17
|
-
this._url =
|
|
16
|
+
this._url = tracerConfig.url
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
export (trace) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const { Readable } = require('stream')
|
|
7
7
|
const http = require('http')
|
|
8
8
|
const https = require('https')
|
|
9
|
+
const net = require('net')
|
|
9
10
|
const zlib = require('zlib')
|
|
10
11
|
|
|
11
12
|
const { storage } = require('../../../../datadog-core')
|
|
@@ -45,6 +46,17 @@ function parseUrl (urlObjOrString) {
|
|
|
45
46
|
return url
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} hostname Host as resolved by {@link parseUrl}; IPv6 is unbracketed (`::1`).
|
|
51
|
+
*/
|
|
52
|
+
function isLoopbackHost (hostname) {
|
|
53
|
+
// The 127.0.0.0/8 block is loopback, but only when the host is an actual IPv4 literal: a
|
|
54
|
+
// hostname like `127.evil.com` shares the prefix yet resolves anywhere, so net.isIPv4 gates it.
|
|
55
|
+
return hostname === 'localhost' ||
|
|
56
|
+
hostname === '::1' ||
|
|
57
|
+
(hostname.startsWith('127.') && net.isIPv4(hostname))
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
/**
|
|
49
61
|
* @param {Buffer|string|Readable|Array<Buffer|string>} data
|
|
50
62
|
* @param {object} options
|
|
@@ -67,6 +79,20 @@ function request (data, options, callback) {
|
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
// Never put the Datadog API key on a cleartext connection to a non-loopback host; that would
|
|
83
|
+
// expose it on the wire. Loopback (local agent, dev proxy, tests) is exempt. Strip the key
|
|
84
|
+
// rather than drop the request: the agent proxies telemetry with its own key, while an https
|
|
85
|
+
// intake URL is required to authenticate agentless traffic.
|
|
86
|
+
const hasApiKey = options.headers['dd-api-key'] !== undefined || options.headers['DD-API-KEY'] !== undefined
|
|
87
|
+
if (hasApiKey && options.protocol === 'http:' && !isLoopbackHost(options.hostname)) {
|
|
88
|
+
log.error(
|
|
89
|
+
'Not sending the Datadog API key over a non-TLS connection to %s. Configure an https intake URL.',
|
|
90
|
+
options.hostname
|
|
91
|
+
)
|
|
92
|
+
delete options.headers['dd-api-key']
|
|
93
|
+
delete options.headers['DD-API-KEY']
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
if (data instanceof Readable) {
|
|
71
97
|
const chunks = []
|
|
72
98
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
4
3
|
const { Writer } = require('./writer')
|
|
5
4
|
|
|
6
5
|
class SpanStatsExporter {
|
|
7
6
|
constructor (config) {
|
|
8
|
-
this._url =
|
|
7
|
+
this._url = config.url
|
|
9
8
|
this._writer = new Writer({ url: this._url })
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
getOperation,
|
|
6
6
|
extractMetrics,
|
|
7
7
|
extractMetadata,
|
|
8
|
+
extractToolDefinitions,
|
|
8
9
|
aggregateStreamingChunks,
|
|
9
10
|
formatInputMessages,
|
|
10
11
|
formatEmbeddingInput,
|
|
@@ -79,6 +80,9 @@ class GenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
79
80
|
const metadata = extractMetadata(config)
|
|
80
81
|
this._tagger.tagMetadata(span, metadata)
|
|
81
82
|
|
|
83
|
+
const toolDefinitions = extractToolDefinitions(config)
|
|
84
|
+
if (toolDefinitions.length > 0) this._tagger.tagToolDefinitions(span, toolDefinitions)
|
|
85
|
+
|
|
82
86
|
if (error) {
|
|
83
87
|
this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
|
|
84
88
|
return
|
|
@@ -155,6 +155,50 @@ function extractMetadata (config) {
|
|
|
155
155
|
return metadata
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Extract tool definitions from config
|
|
160
|
+
* @param {object} config
|
|
161
|
+
* @returns {Array}
|
|
162
|
+
*/
|
|
163
|
+
function extractToolDefinitions (config) {
|
|
164
|
+
const toolDefinitions = []
|
|
165
|
+
|
|
166
|
+
if (!Array.isArray(config?.tools)) {
|
|
167
|
+
return toolDefinitions
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const tool of config.tools) {
|
|
171
|
+
// Only extract tools with valid function declarations
|
|
172
|
+
if (!Array.isArray(tool?.functionDeclarations)) {
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const currDeclaration of tool.functionDeclarations) {
|
|
177
|
+
// A valid declaration must have a name
|
|
178
|
+
if (!currDeclaration?.name) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const toolDef = { name: currDeclaration.name }
|
|
183
|
+
|
|
184
|
+
if (currDeclaration.description !== undefined) {
|
|
185
|
+
toolDef.description = currDeclaration.description
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Parameters can be in two different fields depending on user input
|
|
189
|
+
if (currDeclaration.parameters !== undefined) {
|
|
190
|
+
toolDef.schema = currDeclaration.parameters
|
|
191
|
+
} else if (currDeclaration.parametersJsonSchema !== undefined) {
|
|
192
|
+
toolDef.schema = currDeclaration.parametersJsonSchema
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
toolDefinitions.push(toolDef)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return toolDefinitions
|
|
200
|
+
}
|
|
201
|
+
|
|
158
202
|
/**
|
|
159
203
|
* Format function call message
|
|
160
204
|
* @param {Array} parts
|
|
@@ -498,6 +542,7 @@ module.exports = {
|
|
|
498
542
|
getOperation,
|
|
499
543
|
extractMetrics,
|
|
500
544
|
extractMetadata,
|
|
545
|
+
extractToolDefinitions,
|
|
501
546
|
aggregateStreamingChunks,
|
|
502
547
|
formatInputMessages,
|
|
503
548
|
formatEmbeddingInput,
|
|
@@ -259,7 +259,7 @@ class LLMObs extends NoopLLMObs {
|
|
|
259
259
|
throw new Error('LLMObs span must have a span kind specified')
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
const { inputData, outputData, metadata, metrics, tags, prompt, costTags } = options
|
|
262
|
+
const { inputData, outputData, metadata, metrics, tags, prompt, costTags, toolDefinitions } = options
|
|
263
263
|
|
|
264
264
|
if (inputData || outputData) {
|
|
265
265
|
if (spanKind === 'llm') {
|
|
@@ -289,6 +289,9 @@ class LLMObs extends NoopLLMObs {
|
|
|
289
289
|
if (prompt) {
|
|
290
290
|
this._tagger.tagPrompt(span, prompt)
|
|
291
291
|
}
|
|
292
|
+
if (toolDefinitions != null) {
|
|
293
|
+
this._tagger.tagToolDefinitions(span, toolDefinitions)
|
|
294
|
+
}
|
|
292
295
|
} catch (e) {
|
|
293
296
|
if (e.ddErrorTag) {
|
|
294
297
|
err = e.ddErrorTag
|
|
@@ -332,8 +332,24 @@ class LLMObsSpanProcessor {
|
|
|
332
332
|
return tags
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
/**
|
|
336
|
+
* @param {Record<string, unknown>} tags
|
|
337
|
+
*/
|
|
335
338
|
#objectTagsToStringArrayTags (tags) {
|
|
336
|
-
|
|
339
|
+
const out = []
|
|
340
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
341
|
+
// Comma is the intake-side tag delimiter, so a single `"key:v1,v2"`
|
|
342
|
+
// entry fans into two orphan tags. One-per-element keeps each value
|
|
343
|
+
// addressable; empty arrays fall through to the scalar branch and
|
|
344
|
+
// still emit `key:` so `_dd.cost_tags` references keep finding a
|
|
345
|
+
// wire entry.
|
|
346
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
347
|
+
for (const item of value) out.push(`${key}:${item ?? ''}`)
|
|
348
|
+
} else {
|
|
349
|
+
out.push(`${key}:${value ?? ''}`)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out
|
|
337
353
|
}
|
|
338
354
|
|
|
339
355
|
/**
|
|
@@ -43,7 +43,7 @@ const {
|
|
|
43
43
|
INSTRUMENTATION_METHOD_ANNOTATED,
|
|
44
44
|
} = require('./constants/tags')
|
|
45
45
|
const { storage } = require('./storage')
|
|
46
|
-
const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags } = require('./util')
|
|
46
|
+
const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags, validateToolDefinitions } = require('./util')
|
|
47
47
|
|
|
48
48
|
// global registry of LLMObs spans
|
|
49
49
|
// maps LLMObs spans to their annotations
|
|
@@ -176,8 +176,10 @@ class LLMObsTagger {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
tagToolDefinitions (span, toolDefinitions) {
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
const validatedToolDefinitions = validateToolDefinitions(toolDefinitions)
|
|
180
|
+
|
|
181
|
+
if (validatedToolDefinitions.length > 0) {
|
|
182
|
+
this._setTag(span, TOOL_DEFINITIONS, validatedToolDefinitions)
|
|
181
183
|
} else {
|
|
182
184
|
this.#handleFailure('Tool definitions must be a non-empty array.', 'invalid_tool_definitions')
|
|
183
185
|
}
|
|
@@ -89,6 +89,59 @@ function validateCostTags (span, costTags, source, spanTags) {
|
|
|
89
89
|
return [...validatedCostTags]
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Validates tool definition entires
|
|
93
|
+
function validateToolDefinitions (toolDefinitions) {
|
|
94
|
+
if (!Array.isArray(toolDefinitions)) {
|
|
95
|
+
log.warn('toolDefinitions must be an array.')
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
const validated = []
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < toolDefinitions.length; i++) {
|
|
101
|
+
const currToolDef = toolDefinitions[i]
|
|
102
|
+
if (!currToolDef || typeof currToolDef !== 'object') {
|
|
103
|
+
log.warn('Tool definition at index %d must be an object. Skipping.', i)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Name is not optional
|
|
108
|
+
if (!currToolDef.name || typeof currToolDef.name !== 'string' || currToolDef.name.length <= 0) {
|
|
109
|
+
log.warn('Tool definition at index %d must have a non empty string "name". Skipping.', i)
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
const validatedToolDef = { name: currToolDef.name }
|
|
113
|
+
|
|
114
|
+
// Description, Schema, and Version are optional types
|
|
115
|
+
if (currToolDef.description !== undefined) {
|
|
116
|
+
if (typeof currToolDef.description === 'string') {
|
|
117
|
+
validatedToolDef.description = currToolDef.description
|
|
118
|
+
} else {
|
|
119
|
+
log.warn('Tool definition "description" at index %d must be a string. Skipping field.', i)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (currToolDef.schema !== undefined) {
|
|
124
|
+
if (currToolDef.schema !== null && typeof currToolDef.schema === 'object' && !Array.isArray(currToolDef.schema)) {
|
|
125
|
+
validatedToolDef.schema = currToolDef.schema
|
|
126
|
+
} else {
|
|
127
|
+
log.warn('Tool definition "schema" at index %d must be a plain object. Skipping field.', i)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (currToolDef.version !== undefined) {
|
|
132
|
+
if (typeof currToolDef.version === 'string') {
|
|
133
|
+
validatedToolDef.version = currToolDef.version
|
|
134
|
+
} else {
|
|
135
|
+
log.warn('Tool definition "version" at index %d must be a string. Skipping field.', i)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
validated.push(validatedToolDef)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return validated
|
|
143
|
+
}
|
|
144
|
+
|
|
92
145
|
// extracts the argument names from a function string
|
|
93
146
|
function parseArgumentNames (str) {
|
|
94
147
|
const result = []
|
|
@@ -318,4 +371,5 @@ module.exports = {
|
|
|
318
371
|
safeJsonParse,
|
|
319
372
|
spanHasError,
|
|
320
373
|
writeBridgeTags,
|
|
374
|
+
validateToolDefinitions,
|
|
321
375
|
}
|
|
@@ -14,7 +14,6 @@ const {
|
|
|
14
14
|
EVP_SUBDOMAIN_HEADER_NAME,
|
|
15
15
|
EVP_PROXY_AGENT_BASE_PATH,
|
|
16
16
|
} = require('../constants/writers')
|
|
17
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
18
17
|
const { parseResponseAndLog } = require('./util')
|
|
19
18
|
|
|
20
19
|
class LLMObsBuffer {
|
|
@@ -210,7 +209,7 @@ class BaseLLMObsWriter {
|
|
|
210
209
|
|
|
211
210
|
const overrideOriginEnv = getEnvironmentVariable('_DD_LLMOBS_OVERRIDE_ORIGIN')
|
|
212
211
|
const overrideOriginUrl = overrideOriginEnv && new URL(overrideOriginEnv)
|
|
213
|
-
const base = overrideOriginUrl ??
|
|
212
|
+
const base = overrideOriginUrl ?? this._config.url
|
|
214
213
|
|
|
215
214
|
return {
|
|
216
215
|
url: base,
|
|
@@ -4,7 +4,6 @@ const logger = require('../../log')
|
|
|
4
4
|
const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/writers')
|
|
5
5
|
const telemetry = require('../telemetry')
|
|
6
6
|
const { fetchAgentInfo } = require('../../agent/info')
|
|
7
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* @param {import('../../config/config-base')} config
|
|
@@ -17,7 +16,7 @@ function setAgentStrategy (config, setWritersAgentlessValue) {
|
|
|
17
16
|
return
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
fetchAgentInfo(
|
|
19
|
+
fetchAgentInfo(config.url, (err, agentInfo) => {
|
|
21
20
|
if (err) {
|
|
22
21
|
setWritersAgentlessValue(true)
|
|
23
22
|
return
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const request = require('../../exporters/common/request')
|
|
4
4
|
const { safeJSONStringify } = require('../../exporters/common/util')
|
|
5
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
6
5
|
|
|
7
6
|
const log = require('../../log')
|
|
8
7
|
|
|
@@ -37,7 +36,7 @@ class BaseFFEWriter {
|
|
|
37
36
|
|
|
38
37
|
this._config = config
|
|
39
38
|
this._endpoint = endpoint
|
|
40
|
-
this._baseUrl = agentUrl ??
|
|
39
|
+
this._baseUrl = agentUrl ?? config.url
|
|
41
40
|
this._payloadSizeLimit = payloadSizeLimit
|
|
42
41
|
this._eventSizeLimit = eventSizeLimit
|
|
43
42
|
this._headers = headers || {}
|
|
@@ -154,14 +153,6 @@ class BaseFFEWriter {
|
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
|
|
157
|
-
/**
|
|
158
|
-
* @private
|
|
159
|
-
* @returns {URL} Constructs agent URL from config
|
|
160
|
-
*/
|
|
161
|
-
_getAgentUrl () {
|
|
162
|
-
return getAgentUrl(this._config)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
156
|
/**
|
|
166
157
|
* @private
|
|
167
158
|
* @param {Array<object>} payload - Payload to encode
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const logger = require('../../log')
|
|
4
4
|
const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/constants')
|
|
5
5
|
const { fetchAgentInfo } = require('../../agent/info')
|
|
6
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Determines if the agent supports EVP proxy and sets the writer enabled state accordingly
|
|
@@ -11,7 +10,7 @@ const { getAgentUrl } = require('../../agent/url')
|
|
|
11
10
|
* @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
|
|
12
11
|
*/
|
|
13
12
|
function setAgentStrategy (config, setWriterEnabledValue) {
|
|
14
|
-
fetchAgentInfo(
|
|
13
|
+
fetchAgentInfo(config.url, (err, agentInfo) => {
|
|
15
14
|
if (err) {
|
|
16
15
|
logger.debug('FFE Writer disabled - error getting agent info:', err.message)
|
|
17
16
|
setWriterEnabledValue(false)
|