필사 모드: Langfuse SDK Instrumentation — What Is Captured Automatically and What You Add by Hand
English- Opening — Instrumentation Is Boundary Drawing
- Starting Point — Three Keys and a Singleton Client
- Approach One — The Decorator
- Approach Two — The Context Manager
- Approach Three — Manual Creation That Does Not Move the Context
- Pushing Attributes Down — propagate_attributes
- Framework Integrations — How Far Automatic Capture Reaches
- The OpenTelemetry Relationship — What v4 Changed
- Filtering Out What Should Not Be Exported
- Why Data Disappears in Short-Lived Processes
- Closing — The Order of Instrumentation
- Try It Yourself
- Series
- References
Opening — Instrumentation Is Boundary Drawing
The first post covered the data model: one trace holds a tree of observations, and trace-level attributes propagate downward. This post is about actually building that tree.
Instrumentation mistakes cluster in one place: over- or under-estimating what gets captured automatically. Overestimate and the intermediate step you actually needed is empty. Underestimate and you re-record by hand what was already captured, and the tree comes out doubled.
Component and configuration names here were verified against the official documentation on 2026-08-15. Langfuse's architecture differs by version, so re-check the docs for the version you are running. This post is written against server v4 and Python SDK v4. The versioning documentation states that Python SDK v4 and JS/TS SDK v5 are the generally available SDKs on server v4.
Starting Point — Three Keys and a Singleton Client
The get started documentation requires the same three environment variables across every integration.
pip install langfuse
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://cloud.langfuse.com"
The default for LANGFUSE_BASE_URL is the EU region. The documentation lists separate hosts for the US, Japan, and HIPAA regions. Self-hosting means putting your own instance address here.
The client is a singleton. The Python SDK overview says it can be accessed anywhere in your application through get_client(). Use the Langfuse() constructor only when you want to pass configuration in code.
Approach One — The Decorator
The lowest-code way to start. The instrumentation documentation describes observe() as automatically capturing a function's inputs, outputs, timings, and errors.
from langfuse import observe
@observe()
def my_data_processing_function(data, parameter):
return {"processed_data": data, "status": "ok"}
@observe(name="llm-call", as_type="generation")
async def my_async_llm_call(prompt_text):
return "LLM response"
The documentation names four arguments. name changes the observation name, as_type chooses span or generation, and capture_input and capture_output turn off input and output collection. Both default to true.
Turning capture_input off comes up more often than you would expect, on paths where raw prompts or personal data arrive as function arguments. That is only a partial measure, though; proper masking is the fifth post.
Approach Two — The Context Manager
For cutting by block rather than by function. The documentation describes start_as_current_observation() as the primary way to create observations while keeping the active OpenTelemetry context updated.
from langfuse import get_client, propagate_attributes
langfuse = get_client()
with langfuse.start_as_current_observation(
as_type="span",
name="user-request-pipeline",
input={"user_query": "Tell me a joke"},
) as root_span:
with propagate_attributes(user_id="user_123", session_id="session_abc"):
with langfuse.start_as_current_observation(
as_type="generation",
name="joke-generation",
model="gpt-4o",
) as generation:
generation.update(output="Why did the span cross the road?")
root_span.update(output={"final_joke": "..."})
The nesting becomes the tree directly. Indentation of the with blocks maps one to one onto the parent-child relationships drawn in the first post. The model="gpt-4o" argument matters here: you need a generation with a model name before a cost axis exists at all.
There is also a way to update the currently active observation without holding a reference. The documentation points to update_current_span() and update_current_generation() for that, which is useful when you want to attach a value from deep inside a call stack.
Approach Three — Manual Creation That Does Not Move the Context
The documentation directs you to start_observation() when you want manual control without changing the active context.
from langfuse import get_client
langfuse = get_client()
span = langfuse.start_observation(name="manual-span")
span.update(input="Data for side task")
child = span.start_observation(name="child-span", as_type="generation")
child.end()
span.end()
This is what you need for async work, background tasks, and callbacks — code where the start and the end do not share a scope. In exchange, calling end() becomes entirely your responsibility. Miss it on an exception path and that observation stays unfinished.
When you want the trace-level input and output to differ from the root observation, use set_trace_io() or set_current_trace_io(). The point is that even if the root handles internal pipeline objects, the trace list shows the sentence the user actually typed.
Pushing Attributes Down — propagate_attributes
The first post said trace-level attributes propagate downward. propagate_attributes() is where you switch that propagation on in code.
from langfuse import propagate_attributes
with propagate_attributes(
user_id="user_123",
session_id="session_abc",
metadata={"experiment": "variant_a"},
version="1.0",
environment="staging",
trace_name="user-workflow",
as_baggage=True,
):
run_pipeline()
Those are the arguments the documentation lists. as_baggage=True enables propagation across services through HTTP headers, which you need when a gateway and a worker have to be stitched into one flow.
One thing trips people up here regularly. These values apply to observations created inside the block. They do not attach to observations that already exist outside it. So propagate_attributes belongs at the outermost point where the request arrives.
Framework Integrations — How Far Automatic Capture Reaches
The get started documentation lists the OpenAI SDK, the Vercel AI SDK, LangChain, the native SDKs, OpenTelemetry, and frameworks such as LlamaIndex, CrewAI, LiteLLM, AutoGen, and Google ADK. Install commands differ per integration.
# Python: OpenAI SDK wrapper
pip install langfuse
# Python: LangChain
pip install langfuse langchain-openai
# JS/TS: OpenAI wrapper
npm install @langfuse/openai
# JS/TS: LangChain
npm install @langfuse/core @langfuse/langchain
# JS/TS: native SDK
npm install @langfuse/tracing @langfuse/otel @opentelemetry/sdk-node
The OpenAI integration is done by changing the import.
from langfuse.openai import openai
completion = openai.chat.completions.create(
name="test-chat",
model="gpt-4o",
messages=[{"role": "user", "content": "1 + 1 = "}],
)
import OpenAI from "openai";
import { observeOpenAI } from "@langfuse/openai";
const openai = observeOpenAI(new OpenAI());
What integrations like these capture automatically is the model call itself: model name, prompt, response, token usage, latency. What they cannot capture is your code between those calls. How many documents were retrieved, how reranking changed the order, which branch the run took — all of that you add by hand.
The rule for where to draw the boundary is simple. Every step that has to answer "so which stage was the problem?" in an incident review needs to be its own observation.
The OpenTelemetry Relationship — What v4 Changed
The Python SDK overview states plainly that the Langfuse SDKs are built on top of OpenTelemetry. And the versioning documentation says the move from v3 to v4 replaced the legacy batch ingestion with OpenTelemetry. That is not an internal implementation detail — the ingestion path itself changed, so material written for v3 diverges right here.
The OpenTelemetry integration documentation describes the ingestion path this way.
- The endpoint path is
/api/public/otel. Cloud regions have different hosts. - The protocol is OTLP over HTTP, supporting
HTTP/JSONandHTTP/protobuf. The documentation states that gRPC is not supported yet. - Two authentication headers are required.
# Illustrative: headers passed to an OTLP exporter
OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel"
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic ${AUTH_STRING},x-langfuse-ingestion-version=4"
AUTH_STRING is the base64 encoding of the public and secret keys. The second header, x-langfuse-ingestion-version, is described in the documentation as the value that enables real-time ingestion in v4.
The attribute mapping rules are documented as well. A span carrying a model attribute automatically becomes a generation-type observation. On the GenAI semantic convention side, gen_ai.system, gen_ai.request.model, gen_ai.response.model, gen_ai.prompt, gen_ai.completion, gen_ai.usage.*, and gen_ai.usage.cost are mapped. In the Langfuse namespace there are langfuse.observation.type, langfuse.observation.model.name, langfuse.observation.usage_details, and langfuse.trace.metadata.*.
Knowing this mapping opens an option. A service already instrumented with OpenTelemetry can send traces to Langfuse by adding an export target, without the Langfuse SDK at all. In that case you own the job of making sure the spans carry the right attributes.
Filtering Out What Should Not Be Exported
Not every span needs to go to Langfuse. An application already running OpenTelemetry will also produce HTTP server spans and database spans. The advanced usage documentation points to should_export_span for choosing what to export.
from langfuse import Langfuse
langfuse = Langfuse(should_export_span=lambda span: True)
The documentation introduces built-in predicates you can compose: is_default_export_span, is_langfuse_span, and is_genai_span. It also states that the older blocked_instrumentation_scopes parameter is deprecated and that deny rules should be expressed through should_export_span.
Why Data Disappears in Short-Lived Processes
As the first post noted, Langfuse batches data locally and sends it in the background. The upside is that the request path is never blocked; the downside is that if the process ends before the batch drains, that data is gone.
Batch jobs, serverless functions, and CLI scripts all live here. The documentation distinguishes flush(), which sends every buffered observation, score, and media metadata record now, from shutdown(), which flushes and then waits for the background threads to finish and terminate.
from langfuse import get_client
langfuse = get_client()
try:
run_batch_job()
finally:
langfuse.flush() # send immediately
langfuse.shutdown() # wait for background threads to terminate
Long-running servers never have to think about this. It is always the short-lived processes that bite.
Closing — The Order of Instrumentation
To put it together: start with a framework integration so model calls are captured automatically. Then open propagate_attributes at the outermost point to attach user, session, and environment. Then add manual spans only for the intermediate stages likely to be named in an incident review. Finally, add flush() on short-lived execution paths.
Follow that order and the tree does not double up, and the axes you will need later survive. The next post looks at where all this data actually lands and in what shape, and why Langfuse uses ClickHouse.
Try It Yourself
- HTTP Request Builder — building a request against the OTLP endpoint with its headers makes the auth shape concrete.
- LLM API Cost Calculator — get a feel for what the token usage recorded on a generation is worth in actual money.
Series
References
- Langfuse get started: https://langfuse.com/docs/observability/get-started
- Langfuse Python SDK overview: https://langfuse.com/docs/observability/sdk/python/overview
- Langfuse Python SDK instrumentation: https://langfuse.com/docs/observability/sdk/python/instrumentation
- Langfuse Python SDK advanced usage: https://langfuse.com/docs/observability/sdk/python/advanced-usage
- Langfuse OpenTelemetry integration: https://langfuse.com/integrations/native/opentelemetry
현재 단락 (1/113)
The first post covered the data model: one trace holds a tree of observations, and trace-level attri...