How does Langfuse's @observe decorator trace a Python function?
answer
- one decorator, one observation
- args in, return value out
- the call stack supplies the tree
- name, as_type, capture_input
- flush before a script exits
basics
~20 s@observe wraps a Python function so each call becomes one Langfuse observation, named after the function, timed, with the arguments captured as input and the return value as output, nested automatically under whatever observation is already active.
solid answer
~50 sYou import it with `from langfuse import observe` and put `@observe()` on any function, sync or async. Each call then opens an observation on entry and closes it on return: the name defaults to the function name, the arguments become the observation's input, the return value becomes its output, and the duration is measured for you. Nesting is implicit — a decorated function called from inside another decorated function becomes its child, so decorating three functions gives you a three-level tree with no ids passed around. `@observe(as_type="generation")` makes the observation a model call rather than a generic span, and `capture_input=False` / `capture_output=False` suppress recording the payloads when they hold data you must not store. Credentials come from `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` and `LANGFUSE_HOST`. Export is batched in the background, so a short-lived process should flush before exiting.
code
python · 21 linesimport os
from langfuse import observe, get_client
os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-..."
os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-..."
os.environ["LANGFUSE_HOST"] = "https://cloud.langfuse.com"
@observe(name="retrieve-docs")
def retrieve(query: str) -> list[str]:
return ["doc-1", "doc-2"]
@observe()
def answer(query: str) -> str:
docs = retrieve(query)
return f"{query}: {len(docs)} docs"
answer("what is a trace?")
get_client().flush()go deeper
Know that adding @observe() to a function records it as one timed observation with its arguments and return value, and that decorating several functions gives you a nested tree.
Explain that nesting comes from the currently-active observation in the execution context, and name the arguments you would actually use: name, as_type, capture_input and capture_output.
Discuss what breaks in production — context lost across thread pools and detached tasks, payload capture on sensitive functions, and lost traces in short-lived or serverless processes without a flush.
Own where the instrumentation boundary sits: which layers get decorated at all, naming conventions that survive refactors, and a default capture policy so no team has to decide per function whether payloads may be stored.
## What the decorator does `@observe()` is Langfuse's least-invasive instrumentation: you add one line above a function and every call to it is recorded as an observation. On entry the SDK opens an observation; on return it closes it. Along the way it fills in: - **name** — the function's name, unless you override it with `@observe(name="...")`. - **input** — the call's arguments, serialised. - **output** — the return value. - **timing** — start and end, hence the duration you see in the waterfall. - **level and status** — if the function raises, the observation is marked accordingly and the exception is re-raised to your caller. Instrumentation never swallows your errors, and equally the SDK will not raise a tracing error into your request path. It works on both `def` and `async def` functions, and on generators, so you do not need a separate decorator for async code. ## Nesting is implicit The SDK keeps a notion of the *currently active* observation. When a decorated function runs, its observation becomes current for the duration of the body; anything created inside — another decorated function, a context-managed observation, an instrumented model call — attaches to it as a child. That is the whole reason the decorator is pleasant to use: to get a tree you decorate the functions you already have, and the call stack supplies the structure. If nothing is active when a decorated function runs, its observation becomes the root of a new trace. The corollary is the classic failure: work handed to a thread pool or scheduled as a detached task may run outside the active context, and its observations then land in their own trace instead of the one you expected. When you see orphaned traces that should have been children, that is the first thing to check. ## Arguments worth knowing - `as_type="generation"` records the call as a model call rather than a generic span, which is what you want if you are hand-rolling a provider call inside the function and want its tokens and cost to count. - `name="..."` gives a stable, readable label. Function names leak refactors into your dashboards; explicit names do not. - `capture_input=False` and `capture_output=False` stop the payloads being recorded at all. This is the blunt, code-local control for a function that handles data you are not allowed to store — a raw document, a payment payload, a full user profile. It is coarser than a masking function, but it is decided at the call site, which sometimes matters more. ## Configuration and lifecycle Credentials and destination come from the environment: `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, and `LANGFUSE_HOST` (which you must set for self-hosted deployments and for the non-default cloud region). The decorator uses the same client you can reach explicitly with `get_client()`. Export is asynchronous and batched. In a long-running server that is invisible and desirable. In a script, a CLI, a notebook cell or a serverless handler it matters: if the process exits or freezes before the batch is sent, the trace never arrives. Call `get_client().flush()` before the process ends. ## When the decorator is not the right tool The decorator's unit is the function. When the thing you want to time is a block inside a function, or when you need to create an observation, keep working, and end it later on some other code path, reach for the context-manager and manual APIs instead. They are not alternatives so much as different granularities of the same model, and they compose: a decorated function can open context-managed children freely. Equally, do not hand-decorate what an integration already covers. If you are calling OpenAI through Langfuse's drop-in client, the generation is captured for you; the decorator's job is to give that generation a meaningful parent. ## Interview framing Say what it captures (name, input, output, timing), say that nesting comes from the active-observation context rather than from anything you pass, name one real control you would use in production (`capture_input=False`, or an explicit `name`), and mention flushing in short-lived processes. That last point is what distinguishes someone who has actually shipped it from someone who has read the quickstart.
- A decorated function runs inside a thread pool and its observation shows up as its own trace. Why?Because nesting depends on the currently-active observation, which lives in the execution context. Work handed to a pool or a detached task may run without that context, so the decorator finds nothing active and starts a fresh trace. The fix is to propagate the context into the worker, or to open the observation on the calling side and pass the object to the worker rather than relying on implicit nesting.
- How do you keep a function's arguments out of Langfuse entirely?Decorate it with `@observe(capture_input=False)`, and add `capture_output=False` if the return value is sensitive too. The observation is still created and timed, so you keep the structure and the latency, but the payloads are never sent. That is a code-local control; project-wide redaction is configured separately by whoever owns the deployment.
- Why would you pass an explicit name to @observe rather than take the function name?Because the default couples your dashboards to your code structure: rename or move the function and every chart, filter and saved view keyed on the old name silently stops matching. An explicit name is a stable contract, and it also lets you give the observation a domain-meaningful label like `rerank-candidates` rather than an internal helper's name.
saying these in an interview costs you the question
- Thinks you must pass parent ids to nest observations
- Believes traces are sent synchronously on each call
- Says @observe only works on sync functions
- Expects a decorated model call to report tokens without a generation type
- Forgets to flush in a script or serverless handler