Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 8 additions & 15 deletions docs/integrations/llms/langchain.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,21 @@ integration: built-in

# LangChain

[LangChain](https://www.langchain.com/) (and thus [LangGraph](https://www.langchain.com/langgraph)) has [built-in OpenTelemetry tracing via Langsmith](https://docs.smith.langchain.com/observability/how_to_guides/trace_langchain_with_otel) which you can use with **Logfire**. It's enabled by these two environment variables:
Logfire provides instrumentation for [LangChain](https://www.langchain.com/) and [LangGraph](https://www.langchain.com/langgraph) via `logfire.instrument_langchain()`.

```
LANGSMITH_OTEL_ENABLED=true
LANGSMITH_TRACING=true
```
## Installation

Here's a complete example using LangGraph:

```python
import os
Install Logfire with the `langchain` extra:

import logfire
{{ install_logfire(extras=['langchain']) }}

# These environment variables need to be set before importing langchain or langgraph
os.environ['LANGSMITH_OTEL_ENABLED'] = 'true'
os.environ['LANGSMITH_TRACING'] = 'true'
## Usage

from langchain.agents import create_agent
```python
import logfire

logfire.configure()
logfire.instrument_langchain()


def add(a: float, b: float) -> float:
Expand All @@ -33,7 +27,6 @@ def add(a: float, b: float) -> float:


math_agent = create_agent('openai:gpt-4o', tools=[add], name='math_agent')

result = math_agent.invoke({'messages': [{'role': 'user', 'content': "what's 123 + 456?"}]})
print(result['messages'][-1].content)
```
Expand Down
2 changes: 2 additions & 0 deletions logfire/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
instrument_openai = DEFAULT_LOGFIRE_INSTANCE.instrument_openai
instrument_openai_agents = DEFAULT_LOGFIRE_INSTANCE.instrument_openai_agents
instrument_anthropic = DEFAULT_LOGFIRE_INSTANCE.instrument_anthropic
instrument_langchain = DEFAULT_LOGFIRE_INSTANCE.instrument_langchain
instrument_google_genai = DEFAULT_LOGFIRE_INSTANCE.instrument_google_genai
instrument_litellm = DEFAULT_LOGFIRE_INSTANCE.instrument_litellm
instrument_print = DEFAULT_LOGFIRE_INSTANCE.instrument_print
Expand Down Expand Up @@ -129,6 +130,7 @@ def loguru_handler() -> Any:
'instrument_openai',
'instrument_openai_agents',
'instrument_anthropic',
'instrument_langchain',
'instrument_google_genai',
'instrument_litellm',
'instrument_print',
Expand Down
96 changes: 96 additions & 0 deletions logfire/_internal/exporters/processor_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,64 @@ def _tweak_fastapi_span(span: ReadableSpanDict):
span['events'] = new_events[::-1]


def _normalize_content_block(block: dict[str, Any]) -> dict[str, Any]:
"""Normalize a content block to OTel GenAI schema.

Handles:
- Text: converts 'text' field to 'content' (OTel uses 'content')
- tool_use: converts to 'tool_call' (OTel standard)
- tool_result: converts to 'tool_call_response' (OTel standard)
"""
block_type = block.get('type', 'text')

if block_type == 'text':
return {
'type': 'text',
'content': block.get('content', block.get('text', '')),
}

if block_type == 'tool_use':
return {
'type': 'tool_call',
'id': block.get('id'),
'name': block.get('name'),
'arguments': block.get('input', block.get('arguments')),
}

if block_type == 'tool_result':
return {
'type': 'tool_call_response',
'id': block.get('tool_use_id', block.get('id')),
'response': block.get('content', block.get('response')),
}

return block


def _convert_to_otel_message(msg: dict[str, Any]) -> dict[str, Any]:
"""Convert a message dict to OTel GenAI message schema with role and parts."""
otel_msg: dict[str, Any] = {'role': msg.get('role', 'user'), 'parts': []}
content = msg.get('content')
if content:
if isinstance(content, str):
otel_msg['parts'].append({'type': 'text', 'content': content})
elif isinstance(content, list):
for block in content:
if isinstance(block, dict):
otel_msg['parts'].append(_normalize_content_block(block))
if tool_calls := msg.get('tool_calls'):
for tc in tool_calls:
otel_msg['parts'].append(
{
'type': 'tool_call',
'id': tc.get('id'),
'name': tc.get('function', {}).get('name') or tc.get('name'),
'arguments': tc.get('function', {}).get('arguments') or tc.get('args'),
}
)
return otel_msg


def _transform_langchain_span(span: ReadableSpanDict):
"""Transform spans generated by LangSmith to work better in the Logfire UI.

Expand Down Expand Up @@ -387,6 +445,18 @@ def _transform_langchain_span(span: ReadableSpanDict):
# Remove gen_ai.system=langchain as this also interferes with costs in the UI.
attributes = {k: v for k, v in attributes.items() if k != 'gen_ai.system'}

with suppress(Exception):
completion = parsed_attributes.get('gen_ai.completion', {})
stop_reason = (
completion.get('generations', [[{}]])[0][0]
.get('message', {})
.get('kwargs', {})
.get('response_metadata', {})
.get('stop_reason')
)
if stop_reason:
new_attributes['gen_ai.response.finish_reasons'] = json.dumps([stop_reason])

# Add `all_messages_events`
with suppress(Exception):
input_messages = parsed_attributes.get('input.value', parsed_attributes.get('gen_ai.prompt', {}))['messages']
Expand Down Expand Up @@ -422,6 +492,32 @@ def _transform_langchain_span(span: ReadableSpanDict):
new_attributes['all_messages_events'] = json.dumps(message_events)
properties['all_messages_events'] = {'type': 'array'}

input_msgs = []
output_msgs = []
system_instructions = []
for msg in message_events:
role = msg.get('role')
if role == 'system':
content = msg.get('content', '')
if isinstance(content, str):
system_instructions.append({'type': 'text', 'content': content})
elif isinstance(content, list):
system_instructions.extend(content)
elif role == 'assistant':
output_msgs.append(_convert_to_otel_message(msg))
else:
input_msgs.append(_convert_to_otel_message(msg))

if input_msgs:
new_attributes['gen_ai.input.messages'] = json.dumps(input_msgs)
properties['gen_ai.input.messages'] = {'type': 'array'}
if output_msgs:
new_attributes['gen_ai.output.messages'] = json.dumps(output_msgs)
properties['gen_ai.output.messages'] = {'type': 'array'}
if system_instructions:
new_attributes['gen_ai.system_instructions'] = json.dumps(system_instructions)
properties['gen_ai.system_instructions'] = {'type': 'array'}

span['attributes'] = {
**attributes,
ATTRIBUTES_JSON_SCHEMA_KEY: attributes_json_schema(properties),
Expand Down
Loading
Loading