Skip to content

Commit ed79220

Browse files
nirgaclaude
andcommitted
feat(langchain): implement callback-based instrumentation with auto-injection
- Switch from method patching to LangChain callback handler approach - Implement automatic callback injection via CallbackManager patching - Add proper token usage tracking for Bedrock models - Improve model name extraction and vendor detection - Update Bedrock instrumentation to handle messages API format - All tests passing with correct span naming (bedrock.chat) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f0e98b5 commit ed79220

File tree

19 files changed

+1097
-2446
lines changed

19 files changed

+1097
-2446
lines changed

packages/instrumentation-bedrock/src/instrumentation.ts

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -335,26 +335,44 @@ export class BedrockInstrumentation extends InstrumentationBase {
335335
};
336336
}
337337
case "anthropic": {
338-
return {
338+
const baseAttributes = {
339339
[SpanAttributes.LLM_REQUEST_TOP_P]: requestBody["top_p"],
340340
[SpanAttributes.LLM_TOP_K]: requestBody["top_k"],
341341
[SpanAttributes.LLM_REQUEST_TEMPERATURE]: requestBody["temperature"],
342342
[SpanAttributes.LLM_REQUEST_MAX_TOKENS]:
343-
requestBody["max_tokens_to_sample"],
344-
345-
// Prompt & Role
346-
...(this._shouldSendPrompts()
347-
? {
348-
[`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user",
349-
[`${SpanAttributes.LLM_PROMPTS}.0.content`]: requestBody[
350-
"prompt"
351-
]
352-
// The format is removing when we are setting span attribute
353-
.replace("\n\nHuman:", "")
354-
.replace("\n\nAssistant:", ""),
355-
}
356-
: {}),
343+
requestBody["max_tokens_to_sample"] || requestBody["max_tokens"],
357344
};
345+
346+
if (!this._shouldSendPrompts()) {
347+
return baseAttributes;
348+
}
349+
350+
// Handle new messages API format (used by langchain)
351+
if (requestBody["messages"]) {
352+
const promptAttributes: Record<string, any> = {};
353+
requestBody["messages"].forEach((message: any, index: number) => {
354+
promptAttributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = message.role;
355+
promptAttributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] =
356+
typeof message.content === "string"
357+
? message.content
358+
: JSON.stringify(message.content);
359+
});
360+
return { ...baseAttributes, ...promptAttributes };
361+
}
362+
363+
// Handle legacy prompt format
364+
if (requestBody["prompt"]) {
365+
return {
366+
...baseAttributes,
367+
[`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user",
368+
[`${SpanAttributes.LLM_PROMPTS}.0.content`]: requestBody["prompt"]
369+
// The format is removing when we are setting span attribute
370+
.replace("\n\nHuman:", "")
371+
.replace("\n\nAssistant:", ""),
372+
};
373+
}
374+
375+
return baseAttributes;
358376
}
359377
case "cohere": {
360378
return {
@@ -368,7 +386,7 @@ export class BedrockInstrumentation extends InstrumentationBase {
368386
? {
369387
[`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user",
370388
[`${SpanAttributes.LLM_PROMPTS}.0.content`]:
371-
requestBody["prompt"],
389+
requestBody["message"] || requestBody["prompt"],
372390
}
373391
: {}),
374392
};
@@ -439,30 +457,63 @@ export class BedrockInstrumentation extends InstrumentationBase {
439457
};
440458
}
441459
case "anthropic": {
442-
return {
460+
const baseAttributes = {
443461
[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]:
444462
response["stop_reason"],
445463
[`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant",
446-
...(this._shouldSendPrompts()
447-
? {
448-
[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]:
449-
response["completion"],
450-
}
451-
: {}),
452464
};
465+
466+
if (!this._shouldSendPrompts()) {
467+
return baseAttributes;
468+
}
469+
470+
// Handle new messages API format response
471+
if (response["content"]) {
472+
const content = Array.isArray(response["content"])
473+
? response["content"].map((c: any) => c.text || c).join("")
474+
: response["content"];
475+
return {
476+
...baseAttributes,
477+
[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: content,
478+
};
479+
}
480+
481+
// Handle legacy completion format
482+
if (response["completion"]) {
483+
return {
484+
...baseAttributes,
485+
[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: response["completion"],
486+
};
487+
}
488+
489+
return baseAttributes;
453490
}
454491
case "cohere": {
455-
return {
492+
const baseAttributes = {
456493
[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]:
457-
response["generations"][0]["finish_reason"],
494+
response["finish_reason"],
458495
[`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant",
459496
...(this._shouldSendPrompts()
460497
? {
461498
[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]:
462-
response["generations"][0]["text"],
499+
response["text"],
463500
}
464501
: {}),
465502
};
503+
504+
// Add token usage if available
505+
if (response["meta"] && response["meta"]["billed_units"]) {
506+
const billedUnits = response["meta"]["billed_units"];
507+
return {
508+
...baseAttributes,
509+
[SpanAttributes.LLM_USAGE_PROMPT_TOKENS]: billedUnits["input_tokens"],
510+
[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]: billedUnits["output_tokens"],
511+
[SpanAttributes.LLM_USAGE_TOTAL_TOKENS]:
512+
(billedUnits["input_tokens"] || 0) + (billedUnits["output_tokens"] || 0),
513+
};
514+
}
515+
516+
return baseAttributes;
466517
}
467518
case "meta": {
468519
return {

packages/instrumentation-langchain/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"access": "public"
3939
},
4040
"dependencies": {
41-
"@langchain/core": "^0.3.58",
41+
"@langchain/core": "^0.3.70",
4242
"@opentelemetry/api": "^1.9.0",
4343
"@opentelemetry/core": "^2.0.1",
4444
"@opentelemetry/instrumentation": "^0.203.0",
@@ -47,14 +47,15 @@
4747
"tslib": "^2.8.1"
4848
},
4949
"devDependencies": {
50-
"@langchain/community": "^0.3.49",
50+
"@langchain/community": "^0.3.50",
5151
"@langchain/openai": "^0.6.2",
5252
"@opentelemetry/context-async-hooks": "^2.0.1",
5353
"@opentelemetry/sdk-trace-node": "^2.0.1",
5454
"@pollyjs/adapter-fetch": "^6.0.6",
5555
"@pollyjs/adapter-node-http": "^6.0.6",
5656
"@pollyjs/core": "^6.0.6",
5757
"@pollyjs/persister-fs": "^6.0.6",
58+
"@traceloop/instrumentation-bedrock": "workspace:*",
5859
"@types/mocha": "^10.0.10",
5960
"langchain": "^0.3.30",
6061
"mocha": "^11.7.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{
2+
"log": {
3+
"_recordingName": "Test Langchain instrumentation/should set attributes in span for BedrockChat with tools",
4+
"creator": {
5+
"comment": "persister:fs",
6+
"name": "Polly.JS",
7+
"version": "6.0.6"
8+
},
9+
"entries": [
10+
{
11+
"_id": "33d0981109e372ba00d53a7539abbf6c",
12+
"_order": 0,
13+
"cache": {},
14+
"request": {
15+
"bodySize": 181,
16+
"cookies": [],
17+
"headers": [
18+
{
19+
"name": "accept",
20+
"value": "application/json"
21+
},
22+
{
23+
"name": "content-type",
24+
"value": "application/json"
25+
},
26+
{
27+
"name": "host",
28+
"value": "bedrock-runtime.us-east-1.amazonaws.com"
29+
},
30+
{
31+
"name": "x-amz-content-sha256",
32+
"value": "310e77b0f186ec71e53132330f18b74d81cec1ef6123bdfb6e7bb1d60acc149a"
33+
},
34+
{
35+
"name": "x-amz-date",
36+
"value": "20250817T135829Z"
37+
}
38+
],
39+
"headersSize": 599,
40+
"httpVersion": "HTTP/1.1",
41+
"method": "POST",
42+
"postData": {
43+
"mimeType": "application/json",
44+
"params": [],
45+
"text": "{\"anthropic_version\":\"bedrock-2023-05-31\",\"messages\":[{\"role\":\"user\",\"content\":\"What is a popular landmark in the most populous city in the US?\"}],\"max_tokens\":1024,\"temperature\":0}"
46+
},
47+
"queryString": [],
48+
"url": "https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-7-sonnet-20250219-v1:0/invoke"
49+
},
50+
"response": {
51+
"bodySize": 636,
52+
"content": {
53+
"mimeType": "application/json",
54+
"size": 636,
55+
"text": "{\"id\":\"msg_bdrk_012QekNLTDnyWFKgKZZvt5bU\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-3-7-sonnet-20250219\",\"content\":[{\"type\":\"text\",\"text\":\"One of the most popular landmarks in New York City (the most populous city in the US) is the Statue of Liberty. Other famous landmarks include the Empire State Building, Times Square, Central Park, and the Brooklyn Bridge. These iconic sites attract millions of visitors each year and are symbols of the city recognized worldwide.\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":21,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":67}}"
56+
},
57+
"cookies": [],
58+
"headers": [
59+
{
60+
"name": "connection",
61+
"value": "keep-alive"
62+
},
63+
{
64+
"name": "content-length",
65+
"value": "636"
66+
},
67+
{
68+
"name": "content-type",
69+
"value": "application/json"
70+
},
71+
{
72+
"name": "date",
73+
"value": "Sun, 17 Aug 2025 13:58:31 GMT"
74+
},
75+
{
76+
"name": "x-amzn-bedrock-input-token-count",
77+
"value": "21"
78+
},
79+
{
80+
"name": "x-amzn-bedrock-invocation-latency",
81+
"value": "1556"
82+
},
83+
{
84+
"name": "x-amzn-bedrock-output-token-count",
85+
"value": "67"
86+
},
87+
{
88+
"name": "x-amzn-requestid",
89+
"value": "05d04cc7-d04a-4303-ae05-764aae5b533a"
90+
}
91+
],
92+
"headersSize": 290,
93+
"httpVersion": "HTTP/1.1",
94+
"redirectURL": "",
95+
"status": 200,
96+
"statusText": "OK"
97+
},
98+
"startedDateTime": "2025-08-17T13:58:29.310Z",
99+
"time": 2038,
100+
"timings": {
101+
"blocked": -1,
102+
"connect": -1,
103+
"dns": -1,
104+
"receive": 0,
105+
"send": 0,
106+
"ssl": -1,
107+
"wait": 2038
108+
}
109+
}
110+
],
111+
"pages": [],
112+
"version": "1.2"
113+
}
114+
}

0 commit comments

Comments
 (0)