Skip to content

Commit fe78b1b

Browse files
nirgaclaudeavivhalfon
authored
feat(langchain): implement callback-based instrumentation with auto-injection (#649)
Co-authored-by: Claude <[email protected]> Co-authored-by: avivhalfon <[email protected]>
1 parent fabc504 commit fe78b1b

File tree

19 files changed

+1170
-2450
lines changed

19 files changed

+1170
-2450
lines changed

packages/instrumentation-bedrock/src/instrumentation.ts

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -335,26 +335,45 @@ 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`] =
355+
message.role;
356+
promptAttributes[`${SpanAttributes.LLM_PROMPTS}.${index}.content`] =
357+
typeof message.content === "string"
358+
? message.content
359+
: JSON.stringify(message.content);
360+
});
361+
return { ...baseAttributes, ...promptAttributes };
362+
}
363+
364+
// Handle legacy prompt format
365+
if (requestBody["prompt"]) {
366+
return {
367+
...baseAttributes,
368+
[`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user",
369+
[`${SpanAttributes.LLM_PROMPTS}.0.content`]: requestBody["prompt"]
370+
// The format is removing when we are setting span attribute
371+
.replace("\n\nHuman:", "")
372+
.replace("\n\nAssistant:", ""),
373+
};
374+
}
375+
376+
return baseAttributes;
358377
}
359378
case "cohere": {
360379
return {
@@ -368,7 +387,7 @@ export class BedrockInstrumentation extends InstrumentationBase {
368387
? {
369388
[`${SpanAttributes.LLM_PROMPTS}.0.role`]: "user",
370389
[`${SpanAttributes.LLM_PROMPTS}.0.content`]:
371-
requestBody["prompt"],
390+
requestBody["message"] || requestBody["prompt"],
372391
}
373392
: {}),
374393
};
@@ -439,30 +458,67 @@ export class BedrockInstrumentation extends InstrumentationBase {
439458
};
440459
}
441460
case "anthropic": {
442-
return {
461+
const baseAttributes = {
443462
[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]:
444463
response["stop_reason"],
445464
[`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant",
446-
...(this._shouldSendPrompts()
447-
? {
448-
[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]:
449-
response["completion"],
450-
}
451-
: {}),
452465
};
466+
467+
if (!this._shouldSendPrompts()) {
468+
return baseAttributes;
469+
}
470+
471+
// Handle new messages API format response
472+
if (response["content"]) {
473+
const content = Array.isArray(response["content"])
474+
? response["content"].map((c: any) => c.text || c).join("")
475+
: response["content"];
476+
return {
477+
...baseAttributes,
478+
[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]: content,
479+
};
480+
}
481+
482+
// Handle legacy completion format
483+
if (response["completion"]) {
484+
return {
485+
...baseAttributes,
486+
[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]:
487+
response["completion"],
488+
};
489+
}
490+
491+
return baseAttributes;
453492
}
454493
case "cohere": {
455-
return {
494+
const baseAttributes = {
456495
[`${SpanAttributes.LLM_COMPLETIONS}.0.finish_reason`]:
457-
response["generations"][0]["finish_reason"],
496+
response["generations"]?.[0]?.["finish_reason"],
458497
[`${SpanAttributes.LLM_COMPLETIONS}.0.role`]: "assistant",
459498
...(this._shouldSendPrompts()
460499
? {
461500
[`${SpanAttributes.LLM_COMPLETIONS}.0.content`]:
462-
response["generations"][0]["text"],
501+
response["generations"]?.[0]?.["text"],
463502
}
464503
: {}),
465504
};
505+
506+
// Add token usage if available
507+
if (response["meta"] && response["meta"]["billed_units"]) {
508+
const billedUnits = response["meta"]["billed_units"];
509+
return {
510+
...baseAttributes,
511+
[SpanAttributes.LLM_USAGE_PROMPT_TOKENS]:
512+
billedUnits["input_tokens"],
513+
[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS]:
514+
billedUnits["output_tokens"],
515+
[SpanAttributes.LLM_USAGE_TOTAL_TOKENS]:
516+
(billedUnits["input_tokens"] || 0) +
517+
(billedUnits["output_tokens"] || 0),
518+
};
519+
}
520+
521+
return baseAttributes;
466522
}
467523
case "meta": {
468524
return {

packages/instrumentation-langchain/package.json

Lines changed: 4 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,15 +47,17 @@
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",
60+
"@types/node": "^24.0.15",
5961
"langchain": "^0.3.30",
6062
"mocha": "^11.7.1",
6163
"ts-mocha": "^11.1.0"
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)