dd-trace 6.20.1 → 6.22.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/ci/diagnose.js +9 -14
- package/ci/init.js +1 -2
- package/ci/test-optimization-validation/ci-discovery.js +9 -4
- package/ci/test-optimization-validation/executable.js +1 -5
- package/ci/test-optimization-validation/framework-adapters/cypress.js +1 -4
- package/ci/test-optimization-validation/framework-adapters/vitest.js +1 -1
- package/ci/test-optimization-validation/generated-files.js +6 -4
- package/ci/test-optimization-validation/generated-test-contract.js +7 -3
- package/ci/test-optimization-validation/manifest-scaffold.js +7 -2
- package/ci/test-optimization-validation/manifest-schema.js +6 -7
- package/ci/test-optimization-validation/report-writer.js +6 -2
- package/ci/test-optimization-validation/runner-command.js +2 -4
- package/ci/test-optimization-validation/runner-contract.js +1 -2
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +1 -15
- package/ci/vitest-no-worker-init-setup.mjs +10 -7
- package/index.d.ts +116 -17
- package/initialize.mjs +1 -3
- package/openfeature/rules-based.d.ts +11 -0
- package/openfeature/rules-based.js +7 -0
- package/package.json +13 -11
- package/packages/datadog-core/src/storage.js +16 -0
- package/packages/datadog-esbuild/index.js +18 -12
- package/packages/datadog-instrumentations/src/aerospike.js +3 -3
- package/packages/datadog-instrumentations/src/ai.js +3 -3
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
- package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
- package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
- package/packages/datadog-instrumentations/src/cucumber.js +48 -33
- package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
- package/packages/datadog-instrumentations/src/electron.js +6 -6
- package/packages/datadog-instrumentations/src/fastify.js +13 -4
- package/packages/datadog-instrumentations/src/fs.js +2 -2
- package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
- package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
- package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/register.js +36 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +87 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +64 -11
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
- package/packages/datadog-instrumentations/src/ioredis.js +7 -5
- package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
- package/packages/datadog-instrumentations/src/jest.js +122 -11
- package/packages/datadog-instrumentations/src/knex.js +9 -5
- package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
- package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
- package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
- package/packages/datadog-instrumentations/src/mocha/utils.js +127 -38
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +61 -34
- package/packages/datadog-instrumentations/src/mocha/worker.js +7 -5
- package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
- package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
- package/packages/datadog-instrumentations/src/nova-sonic/session.js +483 -0
- package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
- package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
- package/packages/datadog-instrumentations/src/openai.js +7 -0
- package/packages/datadog-instrumentations/src/pino.js +5 -6
- package/packages/datadog-instrumentations/src/playwright.js +3 -0
- package/packages/datadog-instrumentations/src/redis.js +27 -13
- package/packages/datadog-instrumentations/src/router.js +30 -0
- package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -5
- package/packages/datadog-instrumentations/src/vitest-main.js +9 -17
- package/packages/datadog-instrumentations/src/vitest-util.js +2 -2
- package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
- package/packages/datadog-instrumentations/src/vitest.js +2 -2
- package/packages/datadog-instrumentations/src/webdriverio.js +118 -21
- package/packages/datadog-instrumentations/src/ws.js +1 -4
- package/packages/datadog-plugin-amqplib/src/client.js +1 -2
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
- package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
- package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
- package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
- package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +18 -5
- package/packages/datadog-plugin-cypress/src/index.js +6 -6
- package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
- package/packages/datadog-plugin-cypress/src/support.js +1 -4
- package/packages/datadog-plugin-express/src/code_origin.js +1 -2
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
- package/packages/datadog-plugin-graphql/src/execute.js +5 -5
- package/packages/datadog-plugin-grpc/src/util.js +1 -2
- package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
- package/packages/datadog-plugin-mocha/src/index.js +223 -39
- package/packages/datadog-plugin-mocha/src/webdriverio-video.js +453 -0
- package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
- package/packages/datadog-plugin-next/src/index.js +1 -2
- package/packages/datadog-plugin-openai/src/tracing.js +5 -2
- package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
- package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
- package/packages/datadog-plugin-postgres/src/index.js +24 -0
- package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
- package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
- package/packages/datadog-plugin-vitest/src/index.js +5 -2
- package/packages/datadog-shimmer/src/shimmer.js +5 -4
- package/packages/datadog-webpack/index.js +55 -43
- package/packages/datadog-webpack/src/loader.js +56 -8
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
- package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
- package/packages/dd-trace/src/aiguard/redaction.js +8 -2
- package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
- package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
- package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
- package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
- package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
- package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
- package/packages/dd-trace/src/appsec/lambda.js +1 -3
- package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
- package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
- package/packages/dd-trace/src/appsec/reporter.js +21 -1
- package/packages/dd-trace/src/appsec/stack_trace.js +1 -2
- package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
- package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
- package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
- package/packages/dd-trace/src/config/config-types.d.ts +2 -2
- package/packages/dd-trace/src/config/defaults.js +5 -5
- package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -0
- package/packages/dd-trace/src/config/helper.js +34 -3
- package/packages/dd-trace/src/config/index.js +10 -9
- package/packages/dd-trace/src/config/normalize-service.js +1 -1
- package/packages/dd-trace/src/config/parsers.js +2 -2
- package/packages/dd-trace/src/config/supported-configurations.json +35 -0
- package/packages/dd-trace/src/datastreams/processor.js +3 -2
- package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
- package/packages/dd-trace/src/debugger/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +5 -3
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +17 -12
- package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -2
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/worker-error.js +21 -0
- package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
- package/packages/dd-trace/src/debugger/index.js +24 -2
- package/packages/dd-trace/src/debugger/probe_sampler.js +108 -11
- package/packages/dd-trace/src/dogstatsd.js +1 -1
- package/packages/dd-trace/src/encode/tags-processors.js +2 -2
- package/packages/dd-trace/src/exporters/common/request.js +2 -5
- package/packages/dd-trace/src/exporters/common/url.js +1 -1
- package/packages/dd-trace/src/id.js +15 -5
- package/packages/dd-trace/src/lambda/handler.js +179 -19
- package/packages/dd-trace/src/lambda/index.js +15 -1
- package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
- package/packages/dd-trace/src/llmobs/agent-manifest.js +296 -0
- package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
- package/packages/dd-trace/src/llmobs/constants/tags.js +13 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +56 -2
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
- package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +23 -6
- package/packages/dd-trace/src/llmobs/span_processor.js +18 -8
- package/packages/dd-trace/src/llmobs/tagger.js +56 -3
- package/packages/dd-trace/src/llmobs/util.js +1 -2
- package/packages/dd-trace/src/llmobs/writers/base.js +7 -6
- package/packages/dd-trace/src/llmobs/writers/util.js +3 -1
- package/packages/dd-trace/src/log/log.js +1 -5
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +30 -1
- package/packages/dd-trace/src/openfeature/configuration_source.js +17 -2
- package/packages/dd-trace/src/openfeature/debug-logging-hook.js +25 -0
- package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +59 -3
- package/packages/dd-trace/src/openfeature/remote_config.js +9 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +43 -4
- package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
- package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/resource-attributes.js +1 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -23
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
- package/packages/dd-trace/src/otel-sampling.js +4 -5
- package/packages/dd-trace/src/otel-thread-ctx.js +81 -15
- package/packages/dd-trace/src/plugins/database.js +7 -16
- package/packages/dd-trace/src/plugins/util/git.js +9 -25
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
- package/packages/dd-trace/src/plugins/util/test.js +48 -4
- package/packages/dd-trace/src/priority_sampler.js +1 -2
- package/packages/dd-trace/src/profiling/profiler.js +4 -6
- package/packages/dd-trace/src/profiling/profilers/wall.js +4 -10
- package/packages/dd-trace/src/proxy.js +3 -2
- package/packages/dd-trace/src/ritm.js +4 -2
- package/packages/dd-trace/src/serverless.js +10 -0
- package/packages/dd-trace/src/span_processor.js +13 -17
- package/packages/dd-trace/src/storage-channels.js +112 -46
- package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
- package/packages/dd-trace/src/util.js +29 -0
- package/packages/dd-trace/src/web-tags-cache.js +6 -2
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
- package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
- package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
- 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
|
@@ -239,11 +239,19 @@ function mergeChunkUsage (usage, chunk) {
|
|
|
239
239
|
const mergedUsage = usage ?? { input_tokens: 0, output_tokens: 0 }
|
|
240
240
|
mergedUsage.output_tokens = chunk.usage.output_tokens
|
|
241
241
|
|
|
242
|
+
// `message_delta` counts are cumulative; input grows past the `message_start` count when server
|
|
243
|
+
// tools (e.g. web search) feed results back into the model
|
|
244
|
+
const inputTokens = chunk.usage.input_tokens
|
|
245
|
+
if (inputTokens != null) mergedUsage.input_tokens = inputTokens
|
|
246
|
+
|
|
242
247
|
const cacheCreationTokens = chunk.usage.cache_creation_input_tokens
|
|
243
248
|
const cacheReadTokens = chunk.usage.cache_read_input_tokens
|
|
244
249
|
if (cacheCreationTokens) mergedUsage.cache_creation_input_tokens = cacheCreationTokens
|
|
245
250
|
if (cacheReadTokens) mergedUsage.cache_read_input_tokens = cacheReadTokens
|
|
246
251
|
|
|
252
|
+
const serverToolUse = chunk.usage.server_tool_use
|
|
253
|
+
if (serverToolUse) mergedUsage.server_tool_use = serverToolUse
|
|
254
|
+
|
|
247
255
|
return mergedUsage
|
|
248
256
|
}
|
|
249
257
|
|
|
@@ -280,6 +288,13 @@ function extractUsage (result) {
|
|
|
280
288
|
metrics.cacheWrite1hTokens = 0
|
|
281
289
|
}
|
|
282
290
|
|
|
291
|
+
// Read the usage counter, not server_tool_use blocks: dynamic filtering nests searches under code
|
|
292
|
+
// execution, and `response_inclusion: "excluded"` drops the blocks while the searches are still billed.
|
|
293
|
+
// Failed searches aren't billed and are already excluded. web_fetch_requests is skipped because web
|
|
294
|
+
// fetch has no per-call price.
|
|
295
|
+
const webSearchRequests = usage.server_tool_use?.web_search_requests
|
|
296
|
+
if (webSearchRequests > 0) metrics.webSearchCount = webSearchRequests
|
|
297
|
+
|
|
283
298
|
return metrics
|
|
284
299
|
}
|
|
285
300
|
|
|
@@ -84,8 +84,7 @@ function determineRole (candidate, parts = []) {
|
|
|
84
84
|
* @param {string} role
|
|
85
85
|
*/
|
|
86
86
|
function normalizeRole (role) {
|
|
87
|
-
if (role === ROLES.MODEL) return ROLES.ASSISTANT
|
|
88
|
-
if (role === ROLES.ASSISTANT) return ROLES.ASSISTANT
|
|
87
|
+
if (role === ROLES.MODEL || role === ROLES.ASSISTANT) return ROLES.ASSISTANT
|
|
89
88
|
if (role === ROLES.USER) return ROLES.USER
|
|
90
89
|
if (role === ROLES.REASONING) return ROLES.REASONING
|
|
91
90
|
return ROLES.USER // default
|
|
@@ -62,11 +62,9 @@ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
|
|
|
62
62
|
*/
|
|
63
63
|
getTokenUsage (results) {
|
|
64
64
|
const tokens = this.checkTokenUsageChatOrLLMResult(results)
|
|
65
|
-
if (tokens.totalTokens > 0) return tokens
|
|
66
|
-
|
|
67
65
|
// providers that report usage on each generated message instead of on `llmOutput`; counts are
|
|
68
66
|
// summed per run so a run split across generations is totalled once
|
|
69
|
-
if (!results.generations) return tokens
|
|
67
|
+
if (tokens.totalTokens > 0 || !results.generations) return tokens
|
|
70
68
|
|
|
71
69
|
const tokensPerRunId = {}
|
|
72
70
|
for (const messageSet of results.generations) {
|
|
@@ -76,7 +76,7 @@ class NextStreamLLMObsPlugin extends LLMObsPlugin {
|
|
|
76
76
|
const { streamInputs: inputs, chunks } = streamData
|
|
77
77
|
const input = inputs == null ? undefined : formatIO(inputs)
|
|
78
78
|
const lastChunk = chunks.length > 0 ? chunks.at(-1) : undefined
|
|
79
|
-
const output =
|
|
79
|
+
const output = hasError || lastChunk == null ? undefined : formatIO(lastChunk)
|
|
80
80
|
|
|
81
81
|
this._tagger.tagTextIO(span, input, output)
|
|
82
82
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { pcm16ToWav } = require('../audio-codec')
|
|
4
|
+
const { fitsInlineAudioBudget, formatAudioPartWithGuard } = require('../audio-utils')
|
|
5
|
+
const { AUDIO_FALLBACK, LLMOBS_AUDIO_INLINE_MAX_BYTES } = require('../constants/audio')
|
|
6
|
+
const { storage } = require('../storage')
|
|
7
|
+
const LLMObsPlugin = require('./base')
|
|
8
|
+
|
|
9
|
+
class SonicLLMObsPlugin extends LLMObsPlugin {
|
|
10
|
+
static id = 'bedrockruntime_sonic_llmobs'
|
|
11
|
+
static integration = 'bedrock'
|
|
12
|
+
static system = 'aws.bedrock'
|
|
13
|
+
static prefix = 'tracing:apm:aws:bedrockruntime:sonic:span'
|
|
14
|
+
static emitsGenAiApmTags = false
|
|
15
|
+
|
|
16
|
+
constructor (...args) {
|
|
17
|
+
super(...args)
|
|
18
|
+
this.addSub('dd-trace:aws:bedrockruntime:sonic:capture-context', context => {
|
|
19
|
+
if (!this._llmobsEnabled) return
|
|
20
|
+
context.enabled = true
|
|
21
|
+
// Capture once at invocation, even when absent or already finished. Scoped replay restores
|
|
22
|
+
// the consumer's context on every exit, including exceptions, without repairing IDs later.
|
|
23
|
+
const store = storage.getStore()
|
|
24
|
+
const previous = context.runInContext ?? (fn => fn())
|
|
25
|
+
context.runInContext = fn => previous(() => storage.run(store, fn))
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @param {object} ctx */
|
|
30
|
+
getLLMObsSpanRegisterOptions (ctx) {
|
|
31
|
+
const options = {
|
|
32
|
+
kind: ctx.kind,
|
|
33
|
+
name: ctx.name,
|
|
34
|
+
sessionId: ctx.descriptor.sessionId,
|
|
35
|
+
}
|
|
36
|
+
if (ctx.kind === 'llm') {
|
|
37
|
+
options.modelName = ctx.model
|
|
38
|
+
options.modelProvider = 'amazon'
|
|
39
|
+
}
|
|
40
|
+
return options
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {object} ctx */
|
|
44
|
+
start (ctx) {
|
|
45
|
+
super.start(ctx)
|
|
46
|
+
// Replay already has the complete descriptor. Annotate before any end subscriber can finish
|
|
47
|
+
// the span, including when disabling/re-enabling LLMObs changes subscriber registration order.
|
|
48
|
+
if (this._llmobsEnabledFor(ctx)) this.setLLMObsTags(ctx)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param {object} ctx */
|
|
52
|
+
setLLMObsTags (ctx) {
|
|
53
|
+
const span = ctx.currentStore?.span
|
|
54
|
+
if (!span) return
|
|
55
|
+
const { turn, history, metadata } = ctx.descriptor
|
|
56
|
+
const text = turn.finalText || turn.speculativeText
|
|
57
|
+
if (ctx.kind !== 'llm') {
|
|
58
|
+
if (ctx.name === 'nova sonic audio turn') this._tagger.tagTextIO(span, turn.userText, text)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const input = { role: 'user', content: turn.userText || (turn.inputAudioObserved ? AUDIO_FALLBACK : '') }
|
|
63
|
+
const output = { role: 'assistant', content: text || (turn.outputAudioObserved ? AUDIO_FALLBACK : '') }
|
|
64
|
+
if (turn.tools.length) output.toolCalls = turn.tools
|
|
65
|
+
const inputs = [...history, input, ...turn.toolResults]
|
|
66
|
+
const outputs = [output]
|
|
67
|
+
// Reserve the serialized text/tools and metadata before spending one budget across both WAVs.
|
|
68
|
+
let budget = Math.max(0, LLMOBS_AUDIO_INLINE_MAX_BYTES -
|
|
69
|
+
Buffer.byteLength(JSON.stringify([inputs, outputs, metadata])) - 256)
|
|
70
|
+
for (const [message, pcm, rate] of [
|
|
71
|
+
[input, turn.inputPcm, turn.inputRate],
|
|
72
|
+
[output, turn.output.pcm(), turn.output.rate],
|
|
73
|
+
]) {
|
|
74
|
+
if (!pcm?.length || !rate) continue
|
|
75
|
+
const part = fitsInlineAudioBudget(pcm.length + 44, budget)
|
|
76
|
+
? formatAudioPartWithGuard(pcm16ToWav(pcm, rate), 'audio/wav', budget)
|
|
77
|
+
: undefined
|
|
78
|
+
if (part) {
|
|
79
|
+
message.audioParts = [part]
|
|
80
|
+
budget -= part.content.length
|
|
81
|
+
} else if (message === output) {
|
|
82
|
+
metadata.output_audio_omitted_reason = 'payload_limit'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (turn.output.omittedReason) metadata.output_audio_omitted_reason = turn.output.omittedReason
|
|
86
|
+
this._tagger.tagLLMIO(span, inputs, outputs)
|
|
87
|
+
this._tagger.tagMetadata(span, metadata)
|
|
88
|
+
this._tagger.tagMetrics(span, turn.metrics)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = SonicLLMObsPlugin
|
|
@@ -8,7 +8,7 @@ const {
|
|
|
8
8
|
} = require('../../constants/tags')
|
|
9
9
|
const { audioMimeTypeFromFormat, formatAudioPart } = require('../../audio-utils')
|
|
10
10
|
const { safeJsonParse } = require('../../util')
|
|
11
|
-
const { AUDIO_MIME_TYPES } = require('./constants')
|
|
11
|
+
const { AUDIO_MIME_TYPES, IMAGE_FALLBACK, INPUT_TYPE_IMAGE } = require('./constants')
|
|
12
12
|
const {
|
|
13
13
|
extractChatTemplateFromInstructions,
|
|
14
14
|
normalizePromptVariables,
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
extractContentParts,
|
|
17
17
|
hasMultimodalInputs,
|
|
18
18
|
getModelProviderAndClient,
|
|
19
|
+
getServerToolUsageMetrics,
|
|
19
20
|
} = require('./utils')
|
|
20
21
|
|
|
21
22
|
const allowedParamKeys = new Set([
|
|
@@ -107,6 +108,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
107
108
|
this._tagEmbedding(span, inputs, response, error)
|
|
108
109
|
} else if (operation === 'response') {
|
|
109
110
|
this.#tagResponse(span, inputs, response, error)
|
|
111
|
+
} else if (operation === 'decision') {
|
|
112
|
+
this.#tagDecision(span, inputs, response, error)
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
if (!error) {
|
|
@@ -148,12 +151,20 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
148
151
|
// Responses API - always include
|
|
149
152
|
const cacheReadTokens = tokenUsage.input_tokens_details.cached_tokens
|
|
150
153
|
if (cacheReadTokens !== undefined) metrics.cacheReadTokens = cacheReadTokens
|
|
154
|
+
const cacheWriteTokens = tokenUsage.input_tokens_details.cache_write_tokens
|
|
155
|
+
if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
|
|
156
|
+
// Cache writes are billed at a different rate per TTL
|
|
157
|
+
const cacheWriteDetails = tokenUsage.input_tokens_details.cache_write_tokens_details
|
|
158
|
+
if (cacheWriteDetails?.ttl_30m_tokens != null) metrics.cacheWrite30mTokens = cacheWriteDetails.ttl_30m_tokens
|
|
159
|
+
if (cacheWriteDetails?.ttl_12h_tokens != null) metrics.cacheWrite12hTokens = cacheWriteDetails.ttl_12h_tokens
|
|
151
160
|
} else if (tokenUsage.prompt_tokens_details) {
|
|
152
161
|
// Chat/Completions API - only include if > 0
|
|
153
162
|
const cacheReadTokens = tokenUsage.prompt_tokens_details.cached_tokens
|
|
154
163
|
if (cacheReadTokens != null) {
|
|
155
164
|
metrics.cacheReadTokens = cacheReadTokens
|
|
156
165
|
}
|
|
166
|
+
const cacheWriteTokens = tokenUsage.prompt_tokens_details.cache_write_tokens
|
|
167
|
+
if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
|
|
157
168
|
}
|
|
158
169
|
// Reasoning tokens - Responses API returns `output_tokens_details`, `completion_tokens_details`
|
|
159
170
|
const reasoningOutputObject = tokenUsage.output_tokens_details ?? tokenUsage.completion_tokens_details
|
|
@@ -161,6 +172,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
161
172
|
if (reasoningOutputTokens !== undefined) metrics.reasoningOutputTokens = reasoningOutputTokens
|
|
162
173
|
}
|
|
163
174
|
|
|
175
|
+
Object.assign(metrics, getServerToolUsageMetrics(response))
|
|
176
|
+
|
|
164
177
|
return metrics
|
|
165
178
|
}
|
|
166
179
|
|
|
@@ -464,6 +477,30 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
464
477
|
|
|
465
478
|
this._tagger.tagMetadata(span, outputMetadata) // update the metadata with the output metadata
|
|
466
479
|
}
|
|
480
|
+
|
|
481
|
+
#tagDecision (span, inputs, response, error) {
|
|
482
|
+
const { input, questions } = inputs
|
|
483
|
+
|
|
484
|
+
const inputMessages = typeof input === 'string'
|
|
485
|
+
? [{ role: 'user', content: input }]
|
|
486
|
+
: (Array.isArray(input) ? input : []).map(message => ({
|
|
487
|
+
role: message.role ?? 'user',
|
|
488
|
+
content: decisionMessageContent(message.content),
|
|
489
|
+
}))
|
|
490
|
+
|
|
491
|
+
if (Array.isArray(questions)) {
|
|
492
|
+
this._tagger.tagMetadata(span, { questions })
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (error) {
|
|
496
|
+
this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
this._tagger.tagLLMIO(span, inputMessages, [
|
|
501
|
+
{ role: 'assistant', content: JSON.stringify(response.answers ?? []) },
|
|
502
|
+
])
|
|
503
|
+
}
|
|
467
504
|
}
|
|
468
505
|
|
|
469
506
|
// TODO: this will be moved to the APM integration
|
|
@@ -485,13 +522,28 @@ function normalizeOpenAIResourceName (resource) {
|
|
|
485
522
|
case 'responses.create':
|
|
486
523
|
return 'createResponse'
|
|
487
524
|
|
|
525
|
+
// decisions
|
|
526
|
+
case 'decisions.create':
|
|
527
|
+
return 'createDecision'
|
|
528
|
+
|
|
488
529
|
default:
|
|
489
530
|
return resource
|
|
490
531
|
}
|
|
491
532
|
}
|
|
492
533
|
|
|
534
|
+
// Decision images must be inline data URLs, so they are replaced with a marker instead of copied onto the span
|
|
535
|
+
function decisionMessageContent (content) {
|
|
536
|
+
if (!Array.isArray(content)) return content ?? ''
|
|
537
|
+
|
|
538
|
+
return content
|
|
539
|
+
.map(part => (part?.type === INPUT_TYPE_IMAGE ? IMAGE_FALLBACK : part?.text ?? ''))
|
|
540
|
+
.filter(Boolean)
|
|
541
|
+
.join('\n')
|
|
542
|
+
}
|
|
543
|
+
|
|
493
544
|
function gateResource (resource) {
|
|
494
|
-
return ['createCompletion', 'createChatCompletion', 'createEmbedding', 'createResponse']
|
|
545
|
+
return ['createCompletion', 'createChatCompletion', 'createEmbedding', 'createResponse', 'createDecision']
|
|
546
|
+
.includes(resource)
|
|
495
547
|
? resource
|
|
496
548
|
: undefined
|
|
497
549
|
}
|
|
@@ -506,6 +558,8 @@ function getOperation (resource) {
|
|
|
506
558
|
return 'embedding'
|
|
507
559
|
case 'createResponse':
|
|
508
560
|
return 'response'
|
|
561
|
+
case 'createDecision':
|
|
562
|
+
return 'decision'
|
|
509
563
|
default:
|
|
510
564
|
// should never happen
|
|
511
565
|
return 'unknown'
|
|
@@ -198,6 +198,43 @@ function getModelProviderAndClient (baseUrl = '') {
|
|
|
198
198
|
return { modelProvider, client: 'OpenAI' }
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Count the billable server-side tool calls in an OpenAI Responses response.
|
|
203
|
+
*
|
|
204
|
+
* Web searches are read from the undocumented `tool_usage.web_search.num_requests` when present, otherwise
|
|
205
|
+
* counted from completed `web_search_call` items whose action is `search` (`open_page` / `find_in_page` steps are
|
|
206
|
+
* not billed). File searches have no usage counter, so each completed `file_search_call` item counts once.
|
|
207
|
+
* Zero counts are omitted.
|
|
208
|
+
*
|
|
209
|
+
* @param {Record<string, unknown>} [response] - Any OpenAI response; only Responses API fields are read
|
|
210
|
+
* @returns {{ webSearchCount?: number, storageSearchCount?: number }}
|
|
211
|
+
*/
|
|
212
|
+
function getServerToolUsageMetrics (response) {
|
|
213
|
+
const metrics = {}
|
|
214
|
+
const reportedWebSearches = response?.tool_usage?.web_search?.num_requests
|
|
215
|
+
const hasReportedWebSearches = typeof reportedWebSearches === 'number'
|
|
216
|
+
if (hasReportedWebSearches && reportedWebSearches > 0) metrics.webSearchCount = reportedWebSearches
|
|
217
|
+
|
|
218
|
+
const output = response?.output
|
|
219
|
+
if (!Array.isArray(output)) return metrics
|
|
220
|
+
|
|
221
|
+
for (const item of output) {
|
|
222
|
+
if (item?.status !== 'completed') continue
|
|
223
|
+
|
|
224
|
+
if (item.type === 'web_search_call') {
|
|
225
|
+
if (hasReportedWebSearches) continue
|
|
226
|
+
// Items without an action (older payloads) are assumed to be searches.
|
|
227
|
+
const actionType = item.action?.type
|
|
228
|
+
if (actionType !== undefined && actionType !== 'search') continue
|
|
229
|
+
metrics.webSearchCount = (metrics.webSearchCount ?? 0) + 1
|
|
230
|
+
} else if (item.type === 'file_search_call') {
|
|
231
|
+
metrics.storageSearchCount = (metrics.storageSearchCount ?? 0) + 1
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return metrics
|
|
236
|
+
}
|
|
237
|
+
|
|
201
238
|
module.exports = {
|
|
202
239
|
extractChatTemplateFromInstructions,
|
|
203
240
|
normalizePromptVariables,
|
|
@@ -206,4 +243,5 @@ module.exports = {
|
|
|
206
243
|
hasMultimodalInputs,
|
|
207
244
|
getModelProviderAndClient,
|
|
208
245
|
getOpenAIModelProvider,
|
|
246
|
+
getServerToolUsageMetrics,
|
|
209
247
|
}
|
|
@@ -84,13 +84,11 @@ function promptFromData (data, source) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Copy a prompt with a different source.
|
|
88
87
|
* @param {ManagedPrompt} prompt
|
|
89
|
-
* @param {'registry'|'cache'|'fallback'|'ff'|'resolve'} source
|
|
90
88
|
*/
|
|
91
|
-
function
|
|
92
|
-
if (
|
|
93
|
-
return new ManagedPrompt({ ...prompt, source })
|
|
89
|
+
function withCacheSource (prompt) {
|
|
90
|
+
if (SOURCE_CACHE === prompt.source) return prompt
|
|
91
|
+
return new ManagedPrompt({ ...prompt, source: /** @type {ManagedPrompt['source']} */ (SOURCE_CACHE) })
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
function detailFromBody (body) {
|
|
@@ -310,7 +308,7 @@ class PromptManager {
|
|
|
310
308
|
const cacheable = generationAtStart === this.cacheGeneration && latest
|
|
311
309
|
if (result.prompt) {
|
|
312
310
|
if (cacheable) {
|
|
313
|
-
const cached =
|
|
311
|
+
const cached = withCacheSource(result.prompt)
|
|
314
312
|
if (hot) this.hotCache.set(request.key, cached)
|
|
315
313
|
if (!request.resolve) this.warmCache.set(request.key, cached)
|
|
316
314
|
}
|
|
@@ -335,7 +333,7 @@ class PromptManager {
|
|
|
335
333
|
async #backgroundFetch (request, signal) {
|
|
336
334
|
const result = await this.#fetchAndCache(request, { hot: false, signal })
|
|
337
335
|
if (!result.cacheable) return
|
|
338
|
-
if (result.prompt) return
|
|
336
|
+
if (result.prompt) return withCacheSource(result.prompt)
|
|
339
337
|
if (result.notFound) return
|
|
340
338
|
throw new Error(result.reason)
|
|
341
339
|
}
|
|
@@ -93,7 +93,7 @@ class ManagedPrompt {
|
|
|
93
93
|
format (variables = {}) {
|
|
94
94
|
if (typeof this.template === 'string') return render(this.template, variables)
|
|
95
95
|
return this.template.flatMap(item => {
|
|
96
|
-
if (!isPlaceholder(item)) return
|
|
96
|
+
if (!isPlaceholder(item)) return { ...item, content: render(item.content, variables) }
|
|
97
97
|
if (!Object.hasOwn(variables, item.name)) {
|
|
98
98
|
throw new TypeError(`Missing message placeholder variable '${item.name}'`)
|
|
99
99
|
}
|
|
@@ -29,6 +29,8 @@ const {
|
|
|
29
29
|
getFunctionArguments,
|
|
30
30
|
validateKind,
|
|
31
31
|
} = require('./util')
|
|
32
|
+
const { buildAgentDeclaration } = require('./agent-manifest')
|
|
33
|
+
/** @typedef {import('./agent-manifest').AgentDeclaration} AgentDeclaration */
|
|
32
34
|
const { storage } = require('./storage')
|
|
33
35
|
const telemetry = require('./telemetry')
|
|
34
36
|
const LLMObsTagger = require('./tagger')
|
|
@@ -300,7 +302,7 @@ class LLMObs extends NoopLLMObs {
|
|
|
300
302
|
throw new Error('LLMObs span must have a span kind specified')
|
|
301
303
|
}
|
|
302
304
|
|
|
303
|
-
const { inputData, outputData, metadata, metrics, tags, prompt, costTags, toolDefinitions } = options
|
|
305
|
+
const { inputData, outputData, metadata, metrics, tags, prompt, costTags, toolDefinitions, agent } = options
|
|
304
306
|
|
|
305
307
|
const hasInputOrOutput = spanKind === 'experiment'
|
|
306
308
|
? inputData !== undefined || outputData !== undefined
|
|
@@ -338,6 +340,15 @@ class LLMObs extends NoopLLMObs {
|
|
|
338
340
|
if (toolDefinitions != null) {
|
|
339
341
|
this._tagger.tagToolDefinitions(span, toolDefinitions)
|
|
340
342
|
}
|
|
343
|
+
if (agent != null) {
|
|
344
|
+
if (spanKind === 'agent') {
|
|
345
|
+
this._tagger.tagAgent(span, agent)
|
|
346
|
+
} else {
|
|
347
|
+
logger.warn(
|
|
348
|
+
'Dropping agent annotation on non-agent span kind, annotating agents is only supported for agent spans.'
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
341
352
|
} catch (e) {
|
|
342
353
|
if (e.ddErrorTag) {
|
|
343
354
|
err = e.ddErrorTag
|
|
@@ -602,14 +613,20 @@ class LLMObs extends NoopLLMObs {
|
|
|
602
613
|
annotationContext (options, fn) {
|
|
603
614
|
if (!this.enabled) return fn()
|
|
604
615
|
|
|
616
|
+
/** @type {{ annotationContext?: object, agentDeclarations?: AgentDeclaration[] } | undefined} */
|
|
605
617
|
const currentStore = storage.getStore()
|
|
606
|
-
|
|
618
|
+
const { agent, ...contextOptions } = options ?? {}
|
|
619
|
+
/** @type {{ annotationContext: object, agentDeclarations?: AgentDeclaration[] }} */
|
|
607
620
|
const store = {
|
|
608
621
|
...currentStore,
|
|
609
|
-
annotationContext: {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
622
|
+
annotationContext: { ...currentStore?.annotationContext, ...contextOptions },
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (agent != null) {
|
|
626
|
+
const declaration = buildAgentDeclaration(agent)
|
|
627
|
+
// Outside `annotationContext`, whose keys come from the caller. Outermost first, so nested declarations fold
|
|
628
|
+
// onto the outer ones.
|
|
629
|
+
if (declaration) store.agentDeclarations = [...(currentStore?.agentDeclarations ?? []), declaration]
|
|
613
630
|
}
|
|
614
631
|
|
|
615
632
|
return storage.run(store, fn)
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
MODEL_PROVIDER,
|
|
16
16
|
METADATA,
|
|
17
17
|
COST_TAGS,
|
|
18
|
+
AGENT_MANIFEST,
|
|
18
19
|
TOOL_DEFINITIONS,
|
|
19
20
|
EXPERIMENT_INPUT,
|
|
20
21
|
EXPERIMENT_OUTPUT,
|
|
@@ -43,6 +44,7 @@ const {
|
|
|
43
44
|
DEFAULT_MODEL,
|
|
44
45
|
} = require('./constants/tags')
|
|
45
46
|
const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
|
|
47
|
+
const { MANUAL_FRAMEWORK_NAME } = require('./agent-manifest')
|
|
46
48
|
const { setGenAiApmTags } = require('./gen-ai-tags')
|
|
47
49
|
const telemetry = require('./telemetry')
|
|
48
50
|
const LLMObsTagger = require('./tagger')
|
|
@@ -90,9 +92,8 @@ class LLMObsSpanProcessor {
|
|
|
90
92
|
|
|
91
93
|
// TODO: instead of relying on the tagger's weakmap registry, can we use some namespaced storage correlation?
|
|
92
94
|
process (span) {
|
|
93
|
-
if (!this.#config.llmobs.DD_LLMOBS_ENABLED) return
|
|
94
95
|
// if the span is not in our private tagger map, it is not an llmobs span
|
|
95
|
-
if (!LLMObsTagger.tagMap.has(span)) return
|
|
96
|
+
if (!this.#config.llmobs.DD_LLMOBS_ENABLED || !LLMObsTagger.tagMap.has(span)) return
|
|
96
97
|
|
|
97
98
|
try {
|
|
98
99
|
this.#setGenAiApmTags(span)
|
|
@@ -152,15 +153,26 @@ class LLMObsSpanProcessor {
|
|
|
152
153
|
meta.model_provider = (mlObsTags[MODEL_PROVIDER] || DEFAULT_MODEL).toLowerCase()
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
const name = mlObsTags[NAME] || span._name
|
|
157
|
+
const agentManifest = spanKind === 'agent' ? mlObsTags[AGENT_MANIFEST] : undefined
|
|
158
|
+
|
|
159
|
+
if (mlObsTags[METADATA] || mlObsTags[COST_TAGS] || agentManifest) {
|
|
160
|
+
/** @type {Record<string, unknown>} */
|
|
156
161
|
const metadata = {}
|
|
157
162
|
if (mlObsTags[METADATA]) this.#addObject(mlObsTags[METADATA], metadata)
|
|
158
|
-
// Only seed `metadata._dd` when there's something to put in it
|
|
159
|
-
//
|
|
160
|
-
//
|
|
163
|
+
// Only seed `metadata._dd` when there's something to put in it. Mirrors dd-trace-py and the
|
|
164
|
+
// cross-language wire format enforced by system-tests, so metadata-only spans must not carry
|
|
165
|
+
// an empty `_dd: {}` block.
|
|
161
166
|
if (mlObsTags[COST_TAGS]) {
|
|
162
167
|
this.#getDdMetadata(metadata).cost_tags = mlObsTags[COST_TAGS]
|
|
163
168
|
}
|
|
169
|
+
if (agentManifest) {
|
|
170
|
+
this.#getDdMetadata(metadata).agent_manifest = {
|
|
171
|
+
framework: MANUAL_FRAMEWORK_NAME,
|
|
172
|
+
...agentManifest,
|
|
173
|
+
name: agentManifest.name ?? name,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
164
176
|
meta.metadata = metadata
|
|
165
177
|
}
|
|
166
178
|
|
|
@@ -227,8 +239,6 @@ class LLMObsSpanProcessor {
|
|
|
227
239
|
const sessionId = mlObsTags[SESSION_ID]
|
|
228
240
|
const parentId = mlObsTags[PARENT_ID_KEY]
|
|
229
241
|
|
|
230
|
-
const name = mlObsTags[NAME] || span._name
|
|
231
|
-
|
|
232
242
|
const tags = this.#getTags(span, mlApp, sessionId, error)
|
|
233
243
|
llmObsSpan._tags = tags
|
|
234
244
|
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
OUTPUT_VALUE,
|
|
19
19
|
METADATA,
|
|
20
20
|
COST_TAGS,
|
|
21
|
+
AGENT_MANIFEST,
|
|
21
22
|
METRICS,
|
|
22
23
|
TOOL_DEFINITIONS,
|
|
23
24
|
PARENT_ID_KEY,
|
|
@@ -53,6 +54,7 @@ const {
|
|
|
53
54
|
TRACE_ID,
|
|
54
55
|
PROPAGATED_TRACE_ID_KEY,
|
|
55
56
|
} = require('./constants/tags')
|
|
57
|
+
const { buildAgentDeclaration } = require('./agent-manifest')
|
|
56
58
|
const { storage } = require('./storage')
|
|
57
59
|
const {
|
|
58
60
|
findGenAIAncestorSpanId,
|
|
@@ -116,8 +118,8 @@ class LLMObsTagger {
|
|
|
116
118
|
integration,
|
|
117
119
|
_decorator,
|
|
118
120
|
} = {}) {
|
|
119
|
-
if
|
|
120
|
-
if (!kind) return
|
|
121
|
+
// Do not register it in the map if it does not have an LLMObs span kind.
|
|
122
|
+
if (!this.#config.llmobs.DD_LLMOBS_ENABLED || !kind) return
|
|
121
123
|
|
|
122
124
|
const spanMlApp =
|
|
123
125
|
mlApp ||
|
|
@@ -187,6 +189,11 @@ class LLMObsTagger {
|
|
|
187
189
|
this.tagCostTags(span, annotationContext.costTags, 'annotation_context')
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
const agentDeclarations = /** @type {import('./agent-manifest').AgentDeclaration[] | undefined} */ (
|
|
193
|
+
storage.getStore()?.agentDeclarations
|
|
194
|
+
)
|
|
195
|
+
if (agentDeclarations) this.#applyAgentDeclarations(span, agentDeclarations)
|
|
196
|
+
|
|
190
197
|
// apply annotation context name
|
|
191
198
|
const annotationContextName = annotationContext?.name
|
|
192
199
|
if (annotationContextName) this._setTag(span, NAME, annotationContextName)
|
|
@@ -204,6 +211,43 @@ class LLMObsTagger {
|
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Applies the agents declared by the enclosing annotation contexts, outermost first, to every span in the block.
|
|
216
|
+
* The manifest is emitted only if the span is an agent when it finishes, since some integrations promote a span
|
|
217
|
+
* to an agent after registration.
|
|
218
|
+
*
|
|
219
|
+
* @param {import('../opentracing/span')} span
|
|
220
|
+
* @param {import('./agent-manifest').AgentDeclaration[]} declarations
|
|
221
|
+
*/
|
|
222
|
+
#applyAgentDeclarations (span, declarations) {
|
|
223
|
+
for (const declaration of declarations) {
|
|
224
|
+
this.#tagAgentManifestFields(span, declaration.manifest)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Tags the agent a caller declared on an agent span, merged onto what earlier annotations declared.
|
|
230
|
+
*
|
|
231
|
+
* @param {import('../opentracing/span')} span
|
|
232
|
+
* @param {unknown} agent
|
|
233
|
+
*/
|
|
234
|
+
tagAgent (span, agent) {
|
|
235
|
+
const declaration = buildAgentDeclaration(agent)
|
|
236
|
+
if (declaration) this.#tagAgentManifestFields(span, declaration.manifest)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {import('../opentracing/span')} span
|
|
241
|
+
* @param {import('./agent-manifest').AgentManifestFields} manifest
|
|
242
|
+
*/
|
|
243
|
+
#tagAgentManifestFields (span, manifest) {
|
|
244
|
+
// A shallow update, matching dd-trace-py: each declaration is already validated, so a later field replaces an
|
|
245
|
+
// earlier one whole (`model_settings` included) and an unset or invalid one leaves it in place. Declarations are
|
|
246
|
+
// never mutated, so the first one is stored without a copy.
|
|
247
|
+
const existing = registry.get(span)?.[AGENT_MANIFEST]
|
|
248
|
+
this._setTag(span, AGENT_MANIFEST, existing ? { ...existing, ...manifest } : manifest)
|
|
249
|
+
}
|
|
250
|
+
|
|
207
251
|
#tagSamplingDecision (span, parent) {
|
|
208
252
|
const traceTags = span.context()._trace.tags
|
|
209
253
|
const parentTags = registry.get(parent)
|
|
@@ -337,7 +381,16 @@ class LLMObsTagger {
|
|
|
337
381
|
if (currentTags) {
|
|
338
382
|
Object.assign(currentTags, tags)
|
|
339
383
|
} else {
|
|
340
|
-
|
|
384
|
+
// Copied so a later annotation cannot write into a caller's object shared by an annotation context. The copy
|
|
385
|
+
// reads caller getters and Proxy traps during span registration, so it must not throw into the application.
|
|
386
|
+
let copy
|
|
387
|
+
try {
|
|
388
|
+
copy = { ...tags }
|
|
389
|
+
} catch {
|
|
390
|
+
log.warn('Dropping span tags that could not be read.')
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
this._setTag(span, TAGS, copy)
|
|
341
394
|
}
|
|
342
395
|
}
|
|
343
396
|
|
|
@@ -260,8 +260,7 @@ function findArgumentsBounds (str) {
|
|
|
260
260
|
|
|
261
261
|
const memo = new WeakMap()
|
|
262
262
|
function getFunctionArguments (fn, args = []) {
|
|
263
|
-
if (!fn) return
|
|
264
|
-
if (!args.length) return
|
|
263
|
+
if (!fn || !args.length) return
|
|
265
264
|
if (args.length === 1) return args[0]
|
|
266
265
|
|
|
267
266
|
try {
|
|
@@ -4,7 +4,7 @@ const { URL, format } = require('node:url')
|
|
|
4
4
|
const path = require('node:path')
|
|
5
5
|
const request = require('../../exporters/common/request')
|
|
6
6
|
const { getEnvironmentVariable } = require('../../config/helper')
|
|
7
|
-
const { createServerlessDeliveryTracker } = require('../../serverless')
|
|
7
|
+
const { createServerlessDeliveryTracker, IS_STANDARD_AWS_LAMBDA } = require('../../serverless')
|
|
8
8
|
|
|
9
9
|
const logger = require('../../log')
|
|
10
10
|
|
|
@@ -23,13 +23,12 @@ class LLMObsBuffer {
|
|
|
23
23
|
/**
|
|
24
24
|
* @param {{
|
|
25
25
|
* events: Record<string, unknown>[],
|
|
26
|
-
* size: number,
|
|
27
26
|
* routing?: { apiKey?: string, site?: string }
|
|
28
27
|
* }} options
|
|
29
28
|
*/
|
|
30
|
-
constructor ({ events,
|
|
29
|
+
constructor ({ events, routing = {} }) {
|
|
31
30
|
this.events = events
|
|
32
|
-
this.size =
|
|
31
|
+
this.size = 0
|
|
33
32
|
this.routing = routing
|
|
34
33
|
}
|
|
35
34
|
|
|
@@ -53,7 +52,7 @@ class BaseLLMObsWriter {
|
|
|
53
52
|
this._eventType = eventType
|
|
54
53
|
|
|
55
54
|
/** @type {LLMObsBuffer} */
|
|
56
|
-
this._buffer = new LLMObsBuffer({ events: []
|
|
55
|
+
this._buffer = new LLMObsBuffer({ events: [] })
|
|
57
56
|
|
|
58
57
|
/** @type {import('../../config/config-base')} */
|
|
59
58
|
this._config = config
|
|
@@ -92,7 +91,7 @@ class BaseLLMObsWriter {
|
|
|
92
91
|
const apiKey = routing.apiKey
|
|
93
92
|
let buffer = this.#multiTenantBuffers.get(apiKey)
|
|
94
93
|
if (!buffer) {
|
|
95
|
-
buffer = new LLMObsBuffer({ events: [],
|
|
94
|
+
buffer = new LLMObsBuffer({ events: [], routing })
|
|
96
95
|
this.#multiTenantBuffers.set(apiKey, buffer)
|
|
97
96
|
}
|
|
98
97
|
return buffer
|
|
@@ -118,6 +117,8 @@ class BaseLLMObsWriter {
|
|
|
118
117
|
|
|
119
118
|
buffer.size += eventSize
|
|
120
119
|
buffer.events.push(event)
|
|
120
|
+
// The standard Lambda sandbox can freeze before the periodic flush runs.
|
|
121
|
+
if (IS_STANDARD_AWS_LAMBDA) this.flush()
|
|
121
122
|
return true
|
|
122
123
|
}
|
|
123
124
|
|