dd-trace 6.18.0 → 6.19.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 +1 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +3 -2
- package/package.json +6 -6
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +40 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +3 -10
- package/packages/datadog-instrumentations/src/playwright.js +26 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +19 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -4
- package/packages/dd-trace/src/config/supported-configurations.json +5 -5
- package/packages/dd-trace/src/constants.js +2 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
- package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
- package/packages/dd-trace/src/llmobs/tagger.js +2 -36
- package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/ritm.js +12 -9
- package/packages/dd-trace/src/span_processor.js +6 -1
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
|
@@ -38,19 +38,6 @@ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const outputMessages = []
|
|
41
|
-
let inputTokens = 0
|
|
42
|
-
let outputTokens = 0
|
|
43
|
-
let totalTokens = 0
|
|
44
|
-
let tokensSetTopLevel = false
|
|
45
|
-
const tokensPerRunId = {}
|
|
46
|
-
|
|
47
|
-
if (!isWorkflow) {
|
|
48
|
-
const tokens = this.checkTokenUsageChatOrLLMResult(results)
|
|
49
|
-
inputTokens = tokens.inputTokens
|
|
50
|
-
outputTokens = tokens.outputTokens
|
|
51
|
-
totalTokens = tokens.totalTokens
|
|
52
|
-
tokensSetTopLevel = totalTokens > 0
|
|
53
|
-
}
|
|
54
41
|
|
|
55
42
|
for (const messageSet of results.generations) {
|
|
56
43
|
for (const chatCompletion of messageSet) {
|
|
@@ -59,35 +46,47 @@ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
|
|
|
59
46
|
const content = chatCompletionMessage.text || ''
|
|
60
47
|
const toolCalls = this.extractToolCalls(chatCompletionMessage)
|
|
61
48
|
outputMessages.push({ content, role, toolCalls })
|
|
62
|
-
|
|
63
|
-
if (!isWorkflow && !tokensSetTopLevel) {
|
|
64
|
-
const { tokens, runId } = this.checkTokenUsageFromAIMessage(chatCompletionMessage)
|
|
65
|
-
if (tokensPerRunId[runId]) {
|
|
66
|
-
tokensPerRunId[runId].inputTokens += tokens.inputTokens
|
|
67
|
-
tokensPerRunId[runId].outputTokens += tokens.outputTokens
|
|
68
|
-
tokensPerRunId[runId].totalTokens += tokens.totalTokens
|
|
69
|
-
} else {
|
|
70
|
-
tokensPerRunId[runId] = tokens
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
49
|
}
|
|
74
50
|
}
|
|
75
51
|
|
|
76
|
-
if (!isWorkflow && !tokensSetTopLevel) {
|
|
77
|
-
inputTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.inputTokens, 0)
|
|
78
|
-
outputTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.outputTokens, 0)
|
|
79
|
-
totalTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.totalTokens, 0)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
52
|
if (isWorkflow) {
|
|
83
53
|
this._tagger.tagTextIO(span, inputMessages, outputMessages)
|
|
84
54
|
} else {
|
|
85
55
|
this._tagger.tagLLMIO(span, inputMessages, outputMessages)
|
|
86
|
-
this._tagger.tagMetrics(span,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
56
|
+
this._tagger.tagMetrics(span, this.getTokenUsage(results))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @override
|
|
62
|
+
*/
|
|
63
|
+
getTokenUsage (results) {
|
|
64
|
+
const tokens = this.checkTokenUsageChatOrLLMResult(results)
|
|
65
|
+
if (tokens.totalTokens > 0) return tokens
|
|
66
|
+
|
|
67
|
+
// providers that report usage on each generated message instead of on `llmOutput`; counts are
|
|
68
|
+
// summed per run so a run split across generations is totalled once
|
|
69
|
+
if (!results.generations) return tokens
|
|
70
|
+
|
|
71
|
+
const tokensPerRunId = {}
|
|
72
|
+
for (const messageSet of results.generations) {
|
|
73
|
+
for (const chatCompletion of messageSet) {
|
|
74
|
+
const { tokens: messageTokens, runId } = this.checkTokenUsageFromAIMessage(chatCompletion.message)
|
|
75
|
+
if (tokensPerRunId[runId]) {
|
|
76
|
+
tokensPerRunId[runId].inputTokens += messageTokens.inputTokens
|
|
77
|
+
tokensPerRunId[runId].outputTokens += messageTokens.outputTokens
|
|
78
|
+
tokensPerRunId[runId].totalTokens += messageTokens.totalTokens
|
|
79
|
+
} else {
|
|
80
|
+
tokensPerRunId[runId] = messageTokens
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const perRun = Object.values(tokensPerRunId)
|
|
86
|
+
return {
|
|
87
|
+
inputTokens: perRun.reduce((acc, val) => acc + val.inputTokens, 0),
|
|
88
|
+
outputTokens: perRun.reduce((acc, val) => acc + val.outputTokens, 0),
|
|
89
|
+
totalTokens: perRun.reduce((acc, val) => acc + val.totalTokens, 0),
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
|
|
@@ -12,6 +12,16 @@ class LangChainLLMObsHandler {
|
|
|
12
12
|
|
|
13
13
|
setMetaTags () {}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Token usage for a chat or LLM result. Subclasses widen this where the provider reports usage
|
|
17
|
+
* somewhere other than `llmOutput`.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} results
|
|
20
|
+
*/
|
|
21
|
+
getTokenUsage (results) {
|
|
22
|
+
return this.checkTokenUsageChatOrLLMResult(results)
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
checkTokenUsageChatOrLLMResult (results) {
|
|
16
26
|
const llmOutput = results.llmOutput
|
|
17
27
|
const tokens = {
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
const log = require('../../../log')
|
|
4
4
|
const LLMObsPlugin = require('../base')
|
|
5
5
|
|
|
6
|
-
const pluginManager = require('../../../../../..')._pluginManager
|
|
7
|
-
|
|
8
6
|
const ANTHROPIC_PROVIDER_NAME = 'anthropic'
|
|
9
7
|
const BEDROCK_PROVIDER_NAME = 'amazon_bedrock'
|
|
10
8
|
const OPENAI_PROVIDER_NAME = 'openai'
|
|
@@ -76,6 +74,28 @@ class BaseLangChainLLMObsPlugin extends LLMObsPlugin {
|
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @override
|
|
79
|
+
*/
|
|
80
|
+
getGenAiApmEndTags (ctx, spanKind) {
|
|
81
|
+
// the helper reports zeros when the result carries no usage, and zeroed token metrics would
|
|
82
|
+
// read as a real measurement
|
|
83
|
+
const tokens = this._handlers[ctx.type]?.getTokenUsage(ctx.result ?? {})
|
|
84
|
+
const metrics = tokens?.totalTokens ? tokens : undefined
|
|
85
|
+
|
|
86
|
+
// langchain-openai calls an untraced beta client when `response_format` is set, so this span is
|
|
87
|
+
// the only model span for the call. `handlers/chat_model.js` applies the same correction through
|
|
88
|
+
// `changeKind` while building the LLMObs payload.
|
|
89
|
+
if (spanKind !== WORKFLOW || ctx.type !== 'chat_model' || !ctx.arguments?.[1]?.response_format) {
|
|
90
|
+
return { metrics }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const provider = ctx.currentStore?.span?.context().getTags()['langchain.request.provider']
|
|
94
|
+
const isOpenAI = this.getIntegrationName(ctx.type, provider) === OPENAI_PROVIDER_NAME
|
|
95
|
+
|
|
96
|
+
return isOpenAI ? { spanKind: LLM, metrics } : { metrics }
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
setLLMObsTags (ctx) {
|
|
80
100
|
ctx.args = ctx.arguments
|
|
81
101
|
ctx.instance = ctx.self
|
|
@@ -158,6 +178,9 @@ class BaseLangChainLLMObsPlugin extends LLMObsPlugin {
|
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
isLLMIntegrationEnabled (integration) {
|
|
181
|
+
// read off the owning manager rather than a module-scope capture: the tracer, and with it the
|
|
182
|
+
// plugin manager, can be rebuilt after this module is first loaded
|
|
183
|
+
const pluginManager = this._tracer?._pluginManager
|
|
161
184
|
return SUPPORTED_INTEGRATIONS.has(integration) && pluginManager?._pluginsByName[integration]?.llmobs?._enabled
|
|
162
185
|
}
|
|
163
186
|
}
|
|
@@ -14,15 +14,16 @@ class PregelStreamLLMObsPlugin extends LLMObsPlugin {
|
|
|
14
14
|
getLLMObsSpanRegisterOptions (ctx) {
|
|
15
15
|
const name = ctx.self.name || 'LangGraph'
|
|
16
16
|
|
|
17
|
-
const enabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
|
|
18
|
-
if (!enabled) return
|
|
19
|
-
|
|
20
17
|
const span = ctx.currentStore?.span
|
|
21
18
|
if (!span) return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
|
|
20
|
+
// only the LLMObs payload consumes the accumulated stream
|
|
21
|
+
if (this._llmobsEnabledFor(ctx)) {
|
|
22
|
+
streamDataMap.set(span, {
|
|
23
|
+
streamInputs: ctx.arguments?.[0],
|
|
24
|
+
chunks: [],
|
|
25
|
+
})
|
|
26
|
+
}
|
|
26
27
|
|
|
27
28
|
return {
|
|
28
29
|
kind: 'workflow',
|
|
@@ -74,6 +74,19 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @override
|
|
79
|
+
*/
|
|
80
|
+
getGenAiApmEndTags (ctx) {
|
|
81
|
+
const response = ctx.result?.data
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
// the response model is the resolved one, e.g. the dated version behind an alias
|
|
85
|
+
modelName: response?.model,
|
|
86
|
+
metrics: response && this._extractMetrics(response),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
setLLMObsTags (ctx) {
|
|
78
91
|
const span = ctx.currentStore?.span
|
|
79
92
|
const resource = ctx.methodName
|
|
@@ -155,6 +155,11 @@ class RealtimeLLMObsPlugin extends LLMObsPlugin {
|
|
|
155
155
|
static integration = 'openai'
|
|
156
156
|
static system = 'openai'
|
|
157
157
|
|
|
158
|
+
// The instrumentation retains a turn's audio only while these plugins are subscribed, and only
|
|
159
|
+
// the LLM Observability payload reads those bytes. Staying subscribed for the `gen_ai.*` tags
|
|
160
|
+
// alone would buffer megabytes per turn for no consumer, so realtime opts out.
|
|
161
|
+
static emitsGenAiApmTags = false
|
|
162
|
+
|
|
158
163
|
/**
|
|
159
164
|
* The instrumentation replays the turn with `traceSync`, which never publishes `asyncEnd`, so tag
|
|
160
165
|
* on `end` instead — before the sibling tracing plugin's `end` finishes the span, since the LLM
|
|
@@ -23,6 +23,13 @@ class VertexAILLMObsPlugin extends LLMObsPlugin {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @override
|
|
28
|
+
*/
|
|
29
|
+
getGenAiApmEndTags (ctx) {
|
|
30
|
+
return { metrics: extractMetrics(ctx.result) }
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
setLLMObsTags (ctx) {
|
|
27
34
|
const span = ctx.currentStore?.span
|
|
28
35
|
if (!span) return
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('../../../../../index').llmobs.FormattedPromptMessage} FormattedPromptMessage */
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Match double braces first; preserve surrounding braces such as the closing object in {"age": {age}}.
|
|
6
|
+
const VARIABLE_PATTERN = /\{\{\s*(\w+)\s*\}\}|\{\s*(\w+)\s*\}/g
|
|
6
7
|
|
|
7
8
|
function isMessage (value) {
|
|
8
9
|
return typeof value?.role === 'string' &&
|
|
@@ -38,41 +38,13 @@ const {
|
|
|
38
38
|
SAMPLE_RATE,
|
|
39
39
|
SAMPLING_DECISION,
|
|
40
40
|
TRACE_ID,
|
|
41
|
-
|
|
42
|
-
OUTPUT_TOKENS_METRIC_KEY,
|
|
43
|
-
TOTAL_TOKENS_METRIC_KEY,
|
|
44
|
-
CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
45
|
-
CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
46
|
-
REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
47
|
-
GEN_AI_OPERATION_NAME,
|
|
48
|
-
GEN_AI_REQUEST_MODEL,
|
|
49
|
-
GEN_AI_PROVIDER_NAME,
|
|
50
|
-
GEN_AI_APPLICATION_NAME,
|
|
51
|
-
GEN_AI_CONVERSATION_ID,
|
|
52
|
-
GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY,
|
|
53
|
-
GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY,
|
|
54
|
-
GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY,
|
|
55
|
-
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
56
|
-
GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
57
|
-
GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
58
|
-
ARTIFICIAL_GEN_AI_TAGS,
|
|
41
|
+
DEFAULT_MODEL,
|
|
59
42
|
} = require('./constants/tags')
|
|
60
43
|
const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
|
|
44
|
+
const { setGenAiApmTags } = require('./gen-ai-tags')
|
|
61
45
|
const telemetry = require('./telemetry')
|
|
62
46
|
const LLMObsTagger = require('./tagger')
|
|
63
47
|
|
|
64
|
-
const DEFAULT_MODEL = 'custom'
|
|
65
|
-
const MODEL_BACKED_SPAN_KINDS = new Set(['llm', 'embedding'])
|
|
66
|
-
|
|
67
|
-
const GEN_AI_TOKEN_METRIC_KEYS = [
|
|
68
|
-
[INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY],
|
|
69
|
-
[OUTPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY],
|
|
70
|
-
[TOTAL_TOKENS_METRIC_KEY, GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY],
|
|
71
|
-
[CACHE_READ_INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY],
|
|
72
|
-
[CACHE_WRITE_INPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY],
|
|
73
|
-
[REASONING_OUTPUT_TOKENS_METRIC_KEY, GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY],
|
|
74
|
-
]
|
|
75
|
-
|
|
76
48
|
class LLMObservabilitySpan {
|
|
77
49
|
/**
|
|
78
50
|
* @param {string} kind span kind
|
|
@@ -320,46 +292,19 @@ class LLMObsSpanProcessor {
|
|
|
320
292
|
}
|
|
321
293
|
|
|
322
294
|
/**
|
|
323
|
-
* Writes the scalar `gen_ai.*` attributes onto the APM span, so model, provider, application,
|
|
324
|
-
* conversation and token usage are searchable in APM. Message bodies stay off the APM span.
|
|
325
|
-
*
|
|
326
295
|
* @param {import('../opentracing/span')} span
|
|
327
296
|
*/
|
|
328
297
|
#setGenAiApmTags (span) {
|
|
329
298
|
const mlObsTags = LLMObsTagger.tagMap.get(span)
|
|
330
|
-
const spanContext = span.context()
|
|
331
|
-
const spanKind = mlObsTags[SPAN_KIND]
|
|
332
|
-
|
|
333
|
-
if (spanKind) spanContext.setTag(GEN_AI_OPERATION_NAME, spanKind)
|
|
334
|
-
|
|
335
|
-
const modelName = mlObsTags[MODEL_NAME]
|
|
336
|
-
const modelProvider = mlObsTags[MODEL_PROVIDER]
|
|
337
|
-
const modelBacked = MODEL_BACKED_SPAN_KINDS.has(spanKind)
|
|
338
|
-
if (modelBacked) {
|
|
339
|
-
spanContext.setTag(GEN_AI_REQUEST_MODEL, modelName || DEFAULT_MODEL)
|
|
340
|
-
spanContext.setTag(GEN_AI_PROVIDER_NAME, (modelProvider || DEFAULT_MODEL).toLowerCase())
|
|
341
|
-
} else {
|
|
342
|
-
if (modelName) spanContext.setTag(GEN_AI_REQUEST_MODEL, modelName)
|
|
343
|
-
if (modelProvider) spanContext.setTag(GEN_AI_PROVIDER_NAME, modelProvider.toLowerCase())
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const mlApp = mlObsTags[ML_APP]
|
|
347
|
-
if (mlApp) spanContext.setTag(GEN_AI_APPLICATION_NAME, mlApp)
|
|
348
|
-
|
|
349
|
-
const sessionId = mlObsTags[SESSION_ID]
|
|
350
|
-
if (sessionId) spanContext.setTag(GEN_AI_CONVERSATION_ID, sessionId)
|
|
351
|
-
|
|
352
|
-
const metrics = mlObsTags[METRICS]
|
|
353
|
-
// Other kinds carry unrelated metrics that would be misleading under a `gen_ai.usage.*` key.
|
|
354
|
-
if (modelBacked && metrics) {
|
|
355
|
-
for (const [metricKey, genAiKey] of GEN_AI_TOKEN_METRIC_KEYS) {
|
|
356
|
-
const value = metrics[metricKey]
|
|
357
|
-
if (value != null) spanContext.setTag(genAiKey, value)
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
299
|
|
|
361
|
-
|
|
362
|
-
|
|
300
|
+
setGenAiApmTags(span, {
|
|
301
|
+
spanKind: mlObsTags[SPAN_KIND],
|
|
302
|
+
modelName: mlObsTags[MODEL_NAME],
|
|
303
|
+
modelProvider: mlObsTags[MODEL_PROVIDER],
|
|
304
|
+
mlApp: mlObsTags[ML_APP],
|
|
305
|
+
sessionId: mlObsTags[SESSION_ID],
|
|
306
|
+
metrics: mlObsTags[METRICS],
|
|
307
|
+
})
|
|
363
308
|
}
|
|
364
309
|
|
|
365
310
|
// For now, this only applies to metadata, as we let users annotate this field with any object
|
|
@@ -29,14 +29,7 @@ const {
|
|
|
29
29
|
PROPAGATED_PARENT_AGENT_ID_KEY,
|
|
30
30
|
PROPAGATED_PARENT_AGENT_NAME_KEY,
|
|
31
31
|
ROOT_PARENT_ID,
|
|
32
|
-
|
|
33
|
-
CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
34
|
-
CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY,
|
|
35
|
-
CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY,
|
|
36
|
-
INPUT_TOKENS_METRIC_KEY,
|
|
37
|
-
OUTPUT_TOKENS_METRIC_KEY,
|
|
38
|
-
TOTAL_TOKENS_METRIC_KEY,
|
|
39
|
-
REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
32
|
+
METRIC_KEY_ALIASES,
|
|
40
33
|
INTEGRATION,
|
|
41
34
|
DECORATOR,
|
|
42
35
|
PROPAGATED_ML_APP_KEY,
|
|
@@ -307,35 +300,8 @@ class LLMObsTagger {
|
|
|
307
300
|
tagMetrics (span, metrics) {
|
|
308
301
|
const filterdMetrics = {}
|
|
309
302
|
for (const [key, value] of Object.entries(metrics)) {
|
|
310
|
-
let processedKey = key
|
|
311
|
-
|
|
312
303
|
// processing these specifically for our metrics ingestion
|
|
313
|
-
|
|
314
|
-
case 'inputTokens':
|
|
315
|
-
processedKey = INPUT_TOKENS_METRIC_KEY
|
|
316
|
-
break
|
|
317
|
-
case 'outputTokens':
|
|
318
|
-
processedKey = OUTPUT_TOKENS_METRIC_KEY
|
|
319
|
-
break
|
|
320
|
-
case 'totalTokens':
|
|
321
|
-
processedKey = TOTAL_TOKENS_METRIC_KEY
|
|
322
|
-
break
|
|
323
|
-
case 'cacheReadTokens':
|
|
324
|
-
processedKey = CACHE_READ_INPUT_TOKENS_METRIC_KEY
|
|
325
|
-
break
|
|
326
|
-
case 'cacheWriteTokens':
|
|
327
|
-
processedKey = CACHE_WRITE_INPUT_TOKENS_METRIC_KEY
|
|
328
|
-
break
|
|
329
|
-
case 'cacheWrite5mTokens':
|
|
330
|
-
processedKey = CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY
|
|
331
|
-
break
|
|
332
|
-
case 'cacheWrite1hTokens':
|
|
333
|
-
processedKey = CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY
|
|
334
|
-
break
|
|
335
|
-
case 'reasoningOutputTokens':
|
|
336
|
-
processedKey = REASONING_OUTPUT_TOKENS_METRIC_KEY
|
|
337
|
-
break
|
|
338
|
-
}
|
|
304
|
+
const processedKey = METRIC_KEY_ALIASES[key] ?? key
|
|
339
305
|
|
|
340
306
|
if (typeof value === 'number') {
|
|
341
307
|
filterdMetrics[processedKey] = value
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { VERSION } = require('../../../../../version')
|
|
4
|
+
const { SDK_OTLP_EXPORT_KEY, SDK_SEMANTICS_KEY } = require('../../constants')
|
|
4
5
|
const { registerResourceAttributeRefresh } = require('../resource-attributes')
|
|
5
6
|
const OtlpHttpTraceExporter = require('./otlp_http_trace_exporter')
|
|
6
7
|
|
|
@@ -46,6 +47,10 @@ function buildResourceAttributes (config) {
|
|
|
46
47
|
const { service, version, env, ...filteredTags } = config.tags
|
|
47
48
|
Object.assign(resourceAttributes, filteredTags)
|
|
48
49
|
|
|
50
|
+
// Tracer-owned adoption markers; set after the global tags so a user tag can't contradict them.
|
|
51
|
+
resourceAttributes[SDK_OTLP_EXPORT_KEY] = 'true'
|
|
52
|
+
resourceAttributes[SDK_SEMANTICS_KEY] = config.DD_TRACE_OTEL_SEMANTICS_ENABLED ? 'otel' : 'datadog'
|
|
53
|
+
|
|
49
54
|
if (config.OTEL_TRACES_SPAN_METRICS_ENABLED) {
|
|
50
55
|
resourceAttributes['_dd.stats_computed'] = 'true'
|
|
51
56
|
}
|
|
@@ -2025,6 +2025,10 @@ function getModifiedFilesFromDiff (diff) {
|
|
|
2025
2025
|
if (lineMatch && currentFile) {
|
|
2026
2026
|
const start = Number(lineMatch.groups.start)
|
|
2027
2027
|
const count = lineMatch.groups.count ? Number(lineMatch.groups.count) : 1
|
|
2028
|
+
if (count === 0) {
|
|
2029
|
+
// Keep the surviving line before a deletion so line-based consumers can match its scope.
|
|
2030
|
+
result[currentFile].push(start)
|
|
2031
|
+
}
|
|
2028
2032
|
for (let j = 0; j < count; j++) {
|
|
2029
2033
|
result[currentFile].push(start + j)
|
|
2030
2034
|
}
|
|
@@ -12,8 +12,21 @@ let activeSSIHeuristics
|
|
|
12
12
|
/** @type {import('./profiling') | undefined} */
|
|
13
13
|
let profilingModule
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Configuration published while the profiler was finishing a shutdown export, waiting for that
|
|
17
|
+
* export to settle before it is acted on.
|
|
18
|
+
*
|
|
19
|
+
* @type {import('./config/config-base') | undefined}
|
|
20
|
+
*/
|
|
21
|
+
let deferredConfig
|
|
22
|
+
|
|
15
23
|
function getProfilingModule () {
|
|
16
|
-
profilingModule
|
|
24
|
+
if (profilingModule === undefined) {
|
|
25
|
+
profilingModule = require('./profiling')
|
|
26
|
+
// The profiling layer never restarts itself; it only reports that a shutdown export has
|
|
27
|
+
// settled, which is when a configuration deferred during that window gets its decision.
|
|
28
|
+
profilingModule.profiler.on('stopped', applyDeferredConfig)
|
|
29
|
+
}
|
|
17
30
|
return profilingModule
|
|
18
31
|
}
|
|
19
32
|
|
|
@@ -25,6 +38,12 @@ function isStarted () {
|
|
|
25
38
|
return profilingModule !== undefined && profilingModule.profiler.enabled
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
// A stopped profiler still exports one final profile, and until that settles it reports neither
|
|
42
|
+
// the state a decision would be based on nor any willingness to start again.
|
|
43
|
+
function isStopping () {
|
|
44
|
+
return profilingModule !== undefined && profilingModule.profiler.isStopping()
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
/** @type {typeof import('./profiling/ssi-heuristics') | undefined} */
|
|
29
48
|
let ssiHeuristicsModule
|
|
30
49
|
|
|
@@ -57,8 +76,11 @@ function start (config) {
|
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
function stop () {
|
|
60
|
-
//
|
|
61
|
-
//
|
|
79
|
+
// An explicit stop is the latest desired state, so it retracts a configuration still waiting
|
|
80
|
+
// for an in-flight shutdown to settle.
|
|
81
|
+
deferredConfig = undefined
|
|
82
|
+
|
|
83
|
+
// A stop command for a profiler that has never been loaded is already satisfied.
|
|
62
84
|
if (profilingModule === undefined) return
|
|
63
85
|
|
|
64
86
|
try {
|
|
@@ -93,7 +115,17 @@ function runWithLabels (labels, fn) {
|
|
|
93
115
|
return getProfilingModule().profiler.runWithLabels(labels, fn)
|
|
94
116
|
}
|
|
95
117
|
|
|
96
|
-
|
|
118
|
+
function applyDeferredConfig () {
|
|
119
|
+
const config = deferredConfig
|
|
120
|
+
if (config === undefined) return
|
|
121
|
+
deferredConfig = undefined
|
|
122
|
+
applyConfig(config)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {import('./config/config-base')} config - Tracer configuration
|
|
127
|
+
*/
|
|
128
|
+
function applyConfig (config) {
|
|
97
129
|
const enabled = config.profiling.DD_PROFILING_ENABLED
|
|
98
130
|
if (enabled === 'true') {
|
|
99
131
|
// A non-auto value means the SSI heuristics no longer get a say; disable them so a trigger that
|
|
@@ -118,7 +150,9 @@ configUpdateChannel.subscribe((config) => {
|
|
|
118
150
|
heuristics.start()
|
|
119
151
|
heuristics.onTriggered(() => {
|
|
120
152
|
// Explicit true/false publishes disable the heuristics, so reaching this callback guarantees
|
|
121
|
-
// the latest valid published value is still 'auto'.
|
|
153
|
+
// the latest valid published value is still 'auto'. A heuristic is only ever created for a
|
|
154
|
+
// profiler that is neither running nor stopping, and nothing can start that profiler behind
|
|
155
|
+
// an active heuristic's back, so this start can not collide with a shutdown in flight.
|
|
122
156
|
if (!isStarted()) start(config)
|
|
123
157
|
// The heuristic has made its decision, so release all of its listeners and timer before
|
|
124
158
|
// dropping the module-level reference. This does not stop the profiler that was just
|
|
@@ -132,6 +166,18 @@ configUpdateChannel.subscribe((config) => {
|
|
|
132
166
|
// like 'auto' or crash the customer application.
|
|
133
167
|
log.warn('Unexpected DD_PROFILING_ENABLED value: %o', enabled)
|
|
134
168
|
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
configUpdateChannel.subscribe((config) => {
|
|
172
|
+
// A shutdown export in flight makes every decision here unsafe: the profiler can not be
|
|
173
|
+
// started, and it does not yet report the state the auto branch would branch on. Defer instead,
|
|
174
|
+
// keeping only the latest configuration, exactly as a sequence of updates outside such a window
|
|
175
|
+
// would have left it.
|
|
176
|
+
if (isStopping()) {
|
|
177
|
+
deferredConfig = config
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
applyConfig(config)
|
|
135
181
|
})
|
|
136
182
|
|
|
137
183
|
globalThis[Symbol.for('dd-trace')].beforeExitHandlers.add(stop)
|
|
@@ -61,7 +61,6 @@ class Profiler extends EventEmitter {
|
|
|
61
61
|
#exporters
|
|
62
62
|
#flushInterval
|
|
63
63
|
#lastStart
|
|
64
|
-
#pendingStart
|
|
65
64
|
#profileSeq = 0
|
|
66
65
|
#profilers
|
|
67
66
|
#spanFinishListener
|
|
@@ -87,6 +86,14 @@ class Profiler extends EventEmitter {
|
|
|
87
86
|
return this.#enabled
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
/**
|
|
90
|
+
* True while a stop()'s final collection is still encoding and exporting. The profiler no
|
|
91
|
+
* longer samples, but it also can not be started again until that settles and 'stopped' fires.
|
|
92
|
+
*/
|
|
93
|
+
isStopping () {
|
|
94
|
+
return this.#stopping !== undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
/**
|
|
91
98
|
* Declares the set of custom label keys that will be used with
|
|
92
99
|
* {@link runWithLabels}. This is used for profile upload metadata and
|
|
@@ -184,12 +191,12 @@ class Profiler extends EventEmitter {
|
|
|
184
191
|
if (this.#startFailed) return false
|
|
185
192
|
|
|
186
193
|
// A prior stop()'s shutdown collection may still be encoding/exporting via #tags,
|
|
187
|
-
// #exporters and #endpointCounts
|
|
188
|
-
//
|
|
189
|
-
//
|
|
194
|
+
// #exporters and #endpointCounts, which this start() would overwrite out from under it.
|
|
195
|
+
// Refuse rather than queue: the caller owns the decision of what to do once the shutdown
|
|
196
|
+
// settles, and by then its configuration may no longer be the one requested here. This is
|
|
197
|
+
// thrown outside the setup try/catch below so it does not count as a permanent start failure.
|
|
190
198
|
if (this.#stopping) {
|
|
191
|
-
|
|
192
|
-
return true
|
|
199
|
+
throw new Error('Cannot start the profiler while a shutdown collection is still in flight')
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
try {
|
|
@@ -294,11 +301,12 @@ class Profiler extends EventEmitter {
|
|
|
294
301
|
this._timeoutInterval = this.#flushInterval
|
|
295
302
|
}
|
|
296
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Stops the profiler after collecting and exporting one final profile. That export outlives
|
|
306
|
+
* this call: {@link isStopping} returns true and start() throws until it settles, at which
|
|
307
|
+
* point a 'stopped' event is emitted.
|
|
308
|
+
*/
|
|
297
309
|
stop () {
|
|
298
|
-
// A stop() always reflects the latest desired state, so it cancels any restart queued by a
|
|
299
|
-
// start() that arrived while a prior shutdown collection was still in flight.
|
|
300
|
-
this.#pendingStart = undefined
|
|
301
|
-
|
|
302
310
|
if (!this.enabled) return
|
|
303
311
|
|
|
304
312
|
// collect and export current profiles
|
|
@@ -311,16 +319,12 @@ class Profiler extends EventEmitter {
|
|
|
311
319
|
})
|
|
312
320
|
.finally(() => {
|
|
313
321
|
this.#stopping = undefined
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.start(config)
|
|
321
|
-
} catch (error) {
|
|
322
|
-
log.error(error)
|
|
323
|
-
}
|
|
322
|
+
// Nothing awaits this promise, so a listener throwing here would become an unhandled
|
|
323
|
+
// rejection instead of reaching whoever requested the stop.
|
|
324
|
+
try {
|
|
325
|
+
this.emit('stopped')
|
|
326
|
+
} catch (error) {
|
|
327
|
+
log.error(error)
|
|
324
328
|
}
|
|
325
329
|
})
|
|
326
330
|
this.#stop()
|
|
@@ -118,17 +118,20 @@ function Hook (modules, options, onrequire) {
|
|
|
118
118
|
if (moduleLoadStartChannel.hasSubscribers) {
|
|
119
119
|
moduleLoadStartChannel.publish(payload)
|
|
120
120
|
}
|
|
121
|
-
let exports
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
let exports
|
|
122
|
+
try {
|
|
123
|
+
exports = origRequire.apply(this, arguments)
|
|
124
|
+
payload.module = exports
|
|
125
|
+
} finally {
|
|
126
|
+
// Failed loads must close the subscriber's load stack too. Leave module
|
|
127
|
+
// unset on failure and preserve the original exception for the caller.
|
|
128
|
+
if (moduleLoadEndChannel.hasSubscribers) {
|
|
129
|
+
moduleLoadEndChannel.publish(payload)
|
|
130
|
+
exports = payload.module
|
|
131
|
+
}
|
|
132
|
+
delete patching[moduleId]
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
// The module has already been loaded,
|
|
129
|
-
// so the patching mark can be cleaned up.
|
|
130
|
-
delete patching[moduleId]
|
|
131
|
-
|
|
132
135
|
if (builtin) {
|
|
133
136
|
hooks = moduleHooks[moduleId]
|
|
134
137
|
if (!hooks) return exports // abort if module name isn't on whitelist
|
|
@@ -7,7 +7,7 @@ const SpanSampler = require('./span_sampler')
|
|
|
7
7
|
const GitMetadataTagger = require('./git_metadata_tagger')
|
|
8
8
|
const processTags = require('./process-tags')
|
|
9
9
|
const { applyHttpOtelSemantics } = require('./plugins/util/http-otel-semantics')
|
|
10
|
-
const { APM_TRACING_ENABLED_KEY } = require('./constants')
|
|
10
|
+
const { APM_TRACING_ENABLED_KEY, SDK_OTLP_EXPORT_KEY } = require('./constants')
|
|
11
11
|
|
|
12
12
|
const startedSpans = new WeakSet()
|
|
13
13
|
const finishedSpans = new WeakSet()
|
|
@@ -31,6 +31,7 @@ class SpanProcessor {
|
|
|
31
31
|
this._processTags = config.DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED
|
|
32
32
|
? processTags.serialized
|
|
33
33
|
: false
|
|
34
|
+
this._nativeExport = config.OTEL_TRACES_EXPORTER !== 'otlp'
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
sample (span) {
|
|
@@ -80,6 +81,10 @@ class SpanProcessor {
|
|
|
80
81
|
if (stampApmDisabled) {
|
|
81
82
|
formattedSpan.metrics[APM_TRACING_ENABLED_KEY] = 0
|
|
82
83
|
}
|
|
84
|
+
// Stamped after formatting so a span tag with the same key can't override it.
|
|
85
|
+
if (isFirstSpanInChunk && this._nativeExport) {
|
|
86
|
+
formattedSpan.meta[SDK_OTLP_EXPORT_KEY] = 'false'
|
|
87
|
+
}
|
|
83
88
|
isFirstSpanInChunk = false
|
|
84
89
|
// Span stats read Datadog HTTP tag names from the formatted span, so
|
|
85
90
|
// record them before the OTel rename — an export-only transform.
|