Trace Multi-Agent LLM Pipelines with Langfuse and OpenTelemetry
Instrument a two-agent Claude pipeline so every LLM call, tool hop, and token cost lands in one trace.
What you'll build
A two-agent LLM pipeline (a research agent that calls a tool, then a writer agent) fully instrumented with Langfuse over OpenTelemetry, so every LLM call, tool invocation, token count, and dollar cost shows up as a nested span in one queryable trace timeline.
Prerequisites
- Python 3.10+ (both
langfuseand the instrumentor require ≥3.10). Verified with Python 3.12. - Package versions verified for this tutorial:
langfuse4.14.4 (the v4 SDK, rewritten on OpenTelemetry — note the env var is nowLANGFUSE_BASE_URL, not v3'sLANGFUSE_HOST),opentelemetry-instrumentation-anthropic0.62.3 (from OpenLLMetry), and a recentanthropicSDK (0.116+). - A Langfuse account — the Cloud free tier works; note whether you signed up in the EU or US region. Self-hosted works identically, just point
LANGFUSE_BASE_URLat your instance. - An Anthropic API key from the Claude Console. Any OTel-instrumented provider works the same way; this tutorial uses Claude.
- OS: anything with a shell. Commands below are macOS/Linux; on Windows use
setinstead ofexport.
1. Create a Langfuse project and grab keys
Sign in at cloud.langfuse.com (or us.cloud.langfuse.com for the US region), create an organization and a project, then go to Project Settings → API Keys → Create new API keys. You get a public key (pk-lf-...) and a secret key (sk-lf-...). The secret is shown once — copy both now.
2. Install dependencies and set environment variables
python -m venv .venv && source .venv/bin/activate
pip install "langfuse>=4.14" "opentelemetry-instrumentation-anthropic>=0.62" anthropic
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # or https://us.cloud.langfuse.com
export ANTHROPIC_API_KEY="sk-ant-..."
LANGFUSE_BASE_URL must match the region you signed up in — this is the single most common source of silent trace loss.
3. Understand the two instrumentation layers
You're wiring together two things, and it helps to know who does what:
AnthropicInstrumentor(OpenLLMetry) monkey-patches the Anthropic SDK and emits a standard OTel span for every API call, carryinggen_ai.*semantic-convention attributes: model, prompt, completion, and token usage.- The Langfuse v4 SDK is itself an OTel tracer provider. Any span emitted by any OTel instrumentation library while Langfuse is initialized lands inside your Langfuse trace tree automatically — no exporter config, no OTLP endpoint wrangling. Langfuse maps
gen_ai.usage.*to token counts and multiplies them against its built-in model price list (updated daily against provider docs) to compute cost per generation.
Your own agent and tool functions become spans via Langfuse's @observe decorator, and the auto-instrumented LLM spans nest under whichever @observe function made the call.
4. Build the instrumented pipeline
Save this as pipeline.py. It's the complete, runnable file:
import os
from anthropic import Anthropic
from langfuse import get_client, observe, propagate_attributes
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
# 1. Patch the Anthropic SDK BEFORE making any calls.
AnthropicInstrumentor().instrument()
# 2. Initialize Langfuse (reads LANGFUSE_* env vars) and fail fast on bad creds.
langfuse = get_client()
if not langfuse.auth_check():
raise SystemExit("Langfuse rejected credentials — check keys and LANGFUSE_BASE_URL region")
client = Anthropic()
MODEL = "claude-opus-5"
def ask(system: str, prompt: str) -> str:
# Opus 5 thinks by default and max_tokens caps thinking + answer together,
# so leave generous headroom.
response = client.messages.create(
model=MODEL,
max_tokens=16000,
system=system,
messages=[{"role": "user", "content": prompt}],
)
return "".join(b.text for b in response.content if b.type == "text")
@observe() # tool invocation -> its own span, input/output captured
def fetch_release_notes(project: str) -> str:
# Stub tool: swap in a real HTTP call or DB query.
return (
f"{project} changelog: v4 SDK is OTel-native; ingestion adds "
"x-langfuse-ingestion-version=4; cost table now audited daily."
)
@observe(name="research-agent")
def research(topic: str) -> str:
notes = fetch_release_notes(topic)
return ask(
"You are a research agent. Extract the three most important facts as bullets.",
f"Source material:\n{notes}",
)
@observe(name="writer-agent")
def write_summary(facts: str) -> str:
return ask(
"You are a writing agent. Turn these facts into a two-sentence executive summary.",
facts,
)
@observe(name="research-pipeline")
def run_pipeline(topic: str) -> str:
return write_summary(research(topic))
if __name__ == "__main__":
# Attributes set here propagate to every span in the trace,
# so you can filter/group traces by session or user in the UI.
with propagate_attributes(session_id="demo-session-1", user_id="tutorial-reader"):
print(run_pipeline("Langfuse"))
langfuse.flush() # spans are buffered; short scripts must flush before exit
Two details matter more than they look. AnthropicInstrumentor().instrument() runs before anything else so every SDK call is patched. And langfuse.flush() runs last — the SDK exports spans on a background thread, and a script that exits without flushing loses the trace.
Privacy note: the instrumentor records prompts and completions into span attributes by default. Set TRACELOOP_TRACE_CONTENT=false to keep payloads out of your traces.
5. Run it
python pipeline.py
The script prints a two-sentence summary, e.g.:
Langfuse's v4 SDK is now built natively on OpenTelemetry, with ingestion
tagged via x-langfuse-ingestion-version=4. Its model cost table is audited
daily, keeping per-generation pricing accurate.
Verify it works
Open your project in the Langfuse UI and click Tracing → Traces. You should see a trace named research-pipeline within a few seconds. Click it and check:
- The timeline nests correctly:
research-pipeline→research-agent→ (fetch_release_notesspan + oneanthropic.chatgeneration), thenwriter-agent→ a second generation. Two generations total. - Each generation shows
claude-opus-5as the model, the full prompt and completion, input/output token counts, latency, and a USD cost computed from Langfuse's price table. - Trace metadata shows
user_id: tutorial-readerandsession_id: demo-session-1; under Tracing → Sessions,demo-session-1groups this run (re-run the script and both traces appear in the session).
If all three hold, every hop of the pipeline is now queryable — you can filter traces by session, sort by cost, and drill into any slow or expensive generation.
Troubleshooting
auth_check()fails (or the script exits with "Langfuse rejected credentials") — nine times out of ten the keys are fine andLANGFUSE_BASE_URLpoints at the wrong region: US-region keys againsthttps://cloud.langfuse.com(the EU default) return 401. Match the URL to where you created the project. SetLANGFUSE_DEBUG=Trueto see the failing request.- Script succeeds but no trace appears in the UI — the process exited before the background exporter drained its buffer. Ensure
langfuse.flush()(orlangfuse.shutdown()) runs at the end of the script, including on exception paths; in serverless handlers, flush inside the handler before returning. anthropic.AuthenticationError: Error code: 401 ... 'invalid x-api-key'— the Anthropic key is missing, mistyped, or was revoked. Re-exportANTHROPIC_API_KEY; if you use a key manager, confirm the venv shell actually inherits it (echo $ANTHROPIC_API_KEY).- Generations show token counts but the cost column is blank — the model string on the span didn't match any Langfuse model definition (common with brand-new or fine-tuned models). Add one under Project Settings → Models with a regex match pattern and per-token prices; custom definitions override built-ins and apply to new traces immediately.
Next steps
- Add scores to traces (user feedback, LLM-as-a-judge) so you can correlate quality with cost per agent.
- Instrument other layers of the stack — any OTel library (HTTP clients, databases, other LLM SDKs) drops its spans into the same trace; see Langfuse's OpenTelemetry docs for the attribute mapping and the OTLP endpoint if you'd rather bring your own collector.
- Use
langfuse.start_as_current_observation(as_type="generation", ...)for manual control when auto-instrumentation doesn't fit — for example, custom retry loops or providers without an instrumentor. - Wire the same env vars into CI and production; traces are cheap enough to leave on everywhere, and the session/user attributes you propagated make per-tenant cost accounting a saved table view.
Sources & further reading
- Observability for Anthropic with Langfuse Integration — langfuse.com
- Langfuse Python SDK Overview (v4) — langfuse.com
- Langfuse Python SDK Instrumentation — langfuse.com
- Token and Cost Tracking — langfuse.com
- OpenTelemetry (OTLP) Integration — langfuse.com
- opentelemetry-instrumentation-anthropic — pypi.org
Priya covers AI frameworks, developer productivity tooling, and the startup ecosystem across South and Southeast Asia, bringing a researcher's rigour and a practitioner's empathy to every story. She is deeply sceptical of benchmarks and asks hard questions so her readers don't have to.
Discussion 0
No comments yet
Be the first to weigh in.