Skip to content

Commit d820eae

Browse files
authored
fix(llmobs): add per-integration opt-out (#9733)
Adds a documented and typed `llmobs: false` per-integration option for langchain and modelcontextprotocol-sdk.
1 parent 866f669 commit d820eae

9 files changed

Lines changed: 258 additions & 57 deletions

File tree

docs/API.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ tracer.use('pg', {
2121
})
2222
```
2323

24+
The `langchain` and `modelcontextprotocol-sdk` integrations accept an `llmobs` option. Setting it to `false` stops LLM Observability span capture for that integration only — APM spans and distributed trace context propagation are unaffected. This is useful when another enabled integration already captures the same operation and the input/output payloads would otherwise be stored twice:
25+
26+
```javascript
27+
// Keep APM tracing for MCP, but let LangChain own the LLM Observability spans.
28+
tracer.use('modelcontextprotocol-sdk', {
29+
llmobs: false
30+
})
31+
```
32+
2433
<h5 id="amqplib"></h5>
2534
<h5 id="amqplib-tags"></h5>
2635
<h5 id="amqplib-config"></h5>

docs/test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ tracer.use('kafkajs');
372372
tracer.use('koa');
373373
tracer.use('koa', httpServerOptions);
374374
tracer.use('langchain');
375+
tracer.use('langchain', { llmobs: false });
375376
tracer.use('mariadb', { service: () => `my-custom-mariadb` })
376377
tracer.use('langgraph');
377378
tracer.use('memcached');
@@ -381,6 +382,7 @@ tracer.use('mocha');
381382
tracer.use('mocha', { service: 'mocha-service' });
382383
tracer.use('moleculer', moleculerOptions);
383384
tracer.use('modelcontextprotocol-sdk');
385+
tracer.use('modelcontextprotocol-sdk', { llmobs: false });
384386
tracer.use('mongodb-core');
385387
tracer.use('mongoose');
386388
tracer.use('mysql');

index.d.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,6 +1940,18 @@ declare namespace tracer {
19401940
/** @hidden */
19411941
interface Instrumentation extends Integration, Analyzable {}
19421942

1943+
/** @hidden */
1944+
interface LLMObsIntegration extends Integration {
1945+
/**
1946+
* Whether to capture LLM Observability spans for this integration. When set to `false`,
1947+
* the integration keeps emitting APM spans and propagating trace context, but no LLM
1948+
* Observability spans are produced. Useful when another integration already captures the
1949+
* same operation and the payloads would otherwise be stored twice.
1950+
* @default true
1951+
*/
1952+
llmobs?: boolean;
1953+
}
1954+
19431955
/** @hidden */
19441956
interface DatabaseInstrumentation extends Instrumentation {
19451957
/**
@@ -2804,7 +2816,7 @@ declare namespace tracer {
28042816
* This plugin automatically instruments the
28052817
* [langchain](https://js.langchain.com/) module
28062818
*/
2807-
interface langchain extends Instrumentation {}
2819+
interface langchain extends Instrumentation, LLMObsIntegration {}
28082820

28092821
/**
28102822
* This plugin automatically instruments the
@@ -2846,7 +2858,7 @@ declare namespace tracer {
28462858
* This plugin automatically instruments the
28472859
* [modelcontextprotocol-sdk](https://github.com/npmjs/package/@modelcontextprotocol/sdk) library.
28482860
*/
2849-
interface modelcontextprotocol_sdk extends Instrumentation {}
2861+
interface modelcontextprotocol_sdk extends Instrumentation, LLMObsIntegration {}
28502862

28512863
/**
28522864
* This plugin automatically instruments the

index.d.v5.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2052,6 +2052,18 @@ declare namespace tracer {
20522052
/** @hidden */
20532053
interface Instrumentation extends Integration, Analyzable {}
20542054

2055+
/** @hidden */
2056+
interface LLMObsIntegration extends Integration {
2057+
/**
2058+
* Whether to capture LLM Observability spans for this integration. When set to `false`,
2059+
* the integration keeps emitting APM spans and propagating trace context, but no LLM
2060+
* Observability spans are produced. Useful when another integration already captures the
2061+
* same operation and the payloads would otherwise be stored twice.
2062+
* @default true
2063+
*/
2064+
llmobs?: boolean;
2065+
}
2066+
20552067
/** @hidden */
20562068
interface DatabaseInstrumentation extends Instrumentation {
20572069
/**
@@ -2974,7 +2986,7 @@ declare namespace tracer {
29742986
* This plugin automatically instruments the
29752987
* [langchain](https://js.langchain.com/) module
29762988
*/
2977-
interface langchain extends Instrumentation {}
2989+
interface langchain extends Instrumentation, LLMObsIntegration {}
29782990

29792991
/**
29802992
* This plugin automatically instruments the
@@ -3016,7 +3028,7 @@ declare namespace tracer {
30163028
* This plugin automatically instruments the
30173029
* [modelcontextprotocol-sdk](https://github.com/npmjs/package/@modelcontextprotocol/sdk) library.
30183030
*/
3019-
interface modelcontextprotocol_sdk extends Instrumentation {}
3031+
interface modelcontextprotocol_sdk extends Instrumentation, LLMObsIntegration {}
30203032

30213033
/**
30223034
* This plugin automatically instruments the

packages/datadog-plugin-modelcontextprotocol-sdk/test/index.spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const { expectSomeSpan } = require('../../dd-trace/test/plugins/helpers')
1010
const TestSetup = require('./test-setup')
1111

1212
const testSetup = new TestSetup()
13+
const llmobsDisabledTestSetup = new TestSetup()
1314
const legacyStorage = storage('legacy')
1415

1516
describe('plugin lifecycle', () => {
@@ -556,3 +557,70 @@ createIntegrationTestSuite('modelcontextprotocol-sdk', '@modelcontextprotocol/sd
556557
})
557558
})
558559
})
560+
561+
// `llmobs: false` opts the integration out of LLM Observability span capture only: APM tracing and
562+
// the client -> server trace context propagation must keep working.
563+
createIntegrationTestSuite('modelcontextprotocol-sdk', '@modelcontextprotocol/sdk', {
564+
subModule: '@modelcontextprotocol/sdk/client',
565+
pluginConfig: { llmobs: false },
566+
}, (meta) => {
567+
const { agent } = meta
568+
569+
before(async () => {
570+
await llmobsDisabledTestSetup.setup(meta.mod, meta.versionMod)
571+
})
572+
573+
after(async () => {
574+
await llmobsDisabledTestSetup.teardown()
575+
})
576+
577+
describe('with llmobs disabled', () => {
578+
it('should still generate the client tool call span', async () => {
579+
const traceAssertion = expectSomeSpan(agent, {
580+
name: 'mcp.request',
581+
type: 'mcp',
582+
resource: 'client_tool_call',
583+
meta: {
584+
component: 'modelcontextprotocol_client',
585+
'_dd.integration': 'modelcontextprotocol_client',
586+
'span.kind': 'client',
587+
},
588+
})
589+
590+
const result = await llmobsDisabledTestSetup.clientCallTool()
591+
assert.equal(result.content[0].text, 'Result from test-tool')
592+
593+
return traceAssertion
594+
})
595+
596+
it('should still generate the client list tools span', async () => {
597+
const traceAssertion = expectSomeSpan(agent, {
598+
name: 'mcp.request',
599+
type: 'mcp',
600+
resource: 'ClientSession.list_tools',
601+
meta: {
602+
component: 'modelcontextprotocol_list_tools',
603+
'_dd.integration': 'modelcontextprotocol_list_tools',
604+
'span.kind': 'client',
605+
},
606+
})
607+
608+
const result = await llmobsDisabledTestSetup.clientListTools()
609+
assert.equal(result.tools.length, 2)
610+
611+
return traceAssertion
612+
})
613+
614+
it('should still parent the server request span to the client tool call span', async () => {
615+
const spans = []
616+
const traceAssertion = agent.assertSomeTraces(traces => {
617+
spans.push(...traces.flatMap(trace => trace))
618+
assertClientServerParenting(spans, 'client_tool_call')
619+
})
620+
621+
await llmobsDisabledTestSetup.clientCallTool()
622+
623+
return traceAssertion
624+
})
625+
})
626+
})

packages/dd-trace/src/llmobs/plugins/base.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,11 @@ class LLMObsPlugin extends TracingPlugin {
8484
}
8585

8686
configure (config) {
87-
// we do not want to enable any LLMObs plugins if it is disabled on the tracer
87+
// we do not want to enable any LLMObs plugins if it is disabled on the tracer, or if the
88+
// integration opted out via `tracer.use(<name>, { llmobs: false })`. Opting out only disables
89+
// the LLMObs layer: the integration keeps emitting APM spans and propagating trace context.
8890
const llmobsEnabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
89-
if (llmobsEnabled === false) {
91+
if (llmobsEnabled === false || config?.llmobs === false) {
9092
config = typeof config === 'boolean' ? false : { ...config, enabled: false } // override to false
9193
}
9294
super.configure(config)

packages/dd-trace/test/llmobs/plugins/langchain/index.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,44 @@ describe('integrations', () => {
121121
})
122122
})
123123

124+
describe('langchain with llmobs disabled', () => {
125+
const { assertNoLlmObsSpans } = useLlmObs({
126+
plugin: 'langchain',
127+
pluginConfig: { llmobs: false },
128+
})
129+
130+
withVersions('langchain', ['@langchain/core'], (version) => {
131+
let disabledTool
132+
133+
beforeEach(() => {
134+
disabledTool = require(`../../../../../../versions/@langchain/core@${version}`)
135+
.get('@langchain/core/tools')
136+
.tool
137+
})
138+
139+
it('does not create an LLMObs span for a tool invocation', async function () {
140+
if (!disabledTool) this.skip()
141+
142+
const add = disabledTool(
143+
({ a, b }) => a + b,
144+
{
145+
name: 'add',
146+
description: 'A tool that adds two numbers',
147+
schema: {
148+
a: { type: 'number' },
149+
b: { type: 'number' },
150+
},
151+
}
152+
)
153+
154+
const result = await add.invoke({ a: 1, b: 2 })
155+
assert.equal(result, 3)
156+
157+
await assertNoLlmObsSpans()
158+
})
159+
})
160+
})
161+
124162
describe('langchain', () => {
125163
const { getEvents } = useLlmObs({ plugin: 'langchain' })
126164

0 commit comments

Comments
 (0)