Lesson 8 / 31
Langfuse: The @observe Decorator
Trace an app with the @observe decorator and the drop-in OpenAI integration with minimal code changes.
By Rahul Agarwal · 15 years in web development
Decorate functions, get a tree
The quickest way to instrument Python code with Langfuse is the @observe() decorator. Put it on any function and each call becomes an observation: the function's arguments are captured as input, its return value as output, and its start and end times as duration. Because the SDK tracks the current observation with context variables, nested decorated calls automatically become children: if answer() calls retrieve() and both are decorated, the trace shows answer with retrieve inside it. The outermost decorated call creates the trace. It works for sync and async functions. For model calls, the drop-in OpenAI integration (from langfuse.openai import openai) records each completion as a generation with model, messages, usage and cost, and it nests under whatever decorated function is running. You can turn off input or output capture for a function (for example when it handles large files or secrets) with decorator options such as capture_input=False.
A traced RAG answer in a few lines
Requires the langfuse and openai packages, Langfuse keys and an OpenAI key. Not run here because it calls external services.
from langfuse import observe, get_client
from langfuse.openai import openai # drop-in: each call becomes a generation
@observe()
def retrieve(question: str) -> list[str]:
# a real app would query a vector store here
return ["Refunds are allowed within 7 days of purchase."]
@observe()
def answer(question: str) -> str:
docs = retrieve(question) # child observation
resp = openai.chat.completions.create( # child generation
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "Answer only from: " + " ".join(docs)},
{"role": "user", "content": question},
],
)
return resp.choices[0].message.content
print(answer("Can I get a refund after 10 days?"))
get_client().flush() # short scripts: send buffered events before exitCCTV at each counter
Decorating functions is like fitting a camera above every counter in a bank. You do not ask staff to write diaries; the cameras record who came in, what was handed over and how long it took, and the footage lines up by time automatically.
Decorate the right layer
Decorate functions that represent meaningful steps (retrieve, rerank, generate, call tool), not every tiny helper. Too many spans make traces noisy and add overhead; too few hide where time goes.
Quick check: Function `answer()` and function `retrieve()` both have `@observe()`, and `answer()` calls `retrieve()`. What does the trace look like?
- One trace with `answer` as the parent and `retrieve` nested inside it
- Two separate traces, one per function
- One flat list with no parent-child relationship
- Only `answer` is recorded because nested decorators are ignored
Answer
One trace with `answer` as the parent and `retrieve` nested inside it — The SDK tracks the current observation in context, so the inner decorated call becomes a child of the outer one, and the outermost call defines the trace.