The Ticket You Cannot Reconstruct Is Why You Need Langfuse

The ticket you cannot reconstruct is not a logging problem; it is a missing record of the prompt, the model, and the reply, and Langfuse is the open-source layer that captures that record with one import change.

Rick Hightower

Cover image for “The Ticket You Cannot Reconstruct Is Why You Need Langfuse” by Rick Hightower

Print logs vanish when the process dies. Langfuse LLM observability keeps the prompt, the reply, the tokens, and the cost.

A customer is furious about a refund, last night's logs have a timestamp and nothing else, and you cannot tell whether the model invented the policy or you shipped a bad one. Print logs vanish when the process dies.

In this article: You will learn why print logs, a general APM, and a closed LLM-ops vendor all fail when you need to reconstruct a bad model reply. You will install the Langfuse Python SDK v4 or JS/TS SDK v5, point three environment variables at a Cloud project, and send one Harbor Desk support ticket through the official OpenAI client. By the end you will have a generation in the UI: the system prompt, the ticket, the one-word label, tokens, and cost.

A customer writes in about a wet life jacket. Your assistant drafts a refund reply. The customer is furious. You open last night's logs and find a timestamp, a status code, and nothing else. The prompt is gone. The model is a guess. The ticket text lives in a Slack screenshot. You cannot tell whether the model invented the policy or you shipped a bad one.

That is the hole Langfuse fills. It is an open-source AI engineering platform: observability, prompt management, evaluation, and metrics on one data model, Cloud or self-hosted. This article gets a single trace into a project. Nested trees, agent frameworks, judges, and experiments come later. Today you change one import, send one Harbor Desk ticket, and see the call in the UI.

Harbor Desk is a support-ticket assistant for a tiny shop. A ticket comes in. Eventually it will classify the intent, retrieve a policy snippet, and draft a reply. Today it does none of that. Today it is one chat completion, because you cannot judge a tree until a single generation shows up.

A support ticket that ends in empty print logs versus the same call captured as a Langfuse generation with prompt, reply, tokens, and cost.

Three things you already have, and why none of them is this

You already debug LLM apps. You just do it with tools that were not built for them.

print() and log lines. Cheap, local, gone when the process exits. They do not group a request into a tree. They do not know what a token costs. They do not let a product person score last week's replies.

A general APM. Great at latency and HTTP. Blind to prompts, completions, token usage, and evaluation scores. Langfuse is purpose-built for LLM applications: it natively understands token usage, model parameters, prompt and completion pairs, and scores. It also does the things a Datadog-shaped tool will not: LLM-as-a-judge, prompt management, experiments and datasets, and custom dashboards.

A closed LLM-ops vendor. The features exist. The data and the server are not yours. Langfuse is open source and self-hostable. Cloud is the fastest start. Self-hosting is the same architecture when you need it.

Langfuse is not an agent framework. It does not replace the Claude Agent SDK or DeepAgents. It is the layer you wrap around the loop you already have. LiteLLM is a different product too: a multi-provider SDK and proxy that can emit to Langfuse. It is not the default path here. The default path is Langfuse's drop-in of the official OpenAI SDK.

A mindmap of what Langfuse is not (print logs, a general APM, a closed vendor) and what it actually is: observability, prompt management, evaluation, and metrics.

Install it, then give it keys

This series uses Python SDK v4 (langfuse on PyPI) and JS/TS SDK v5. Both sit on OpenTelemetry. Pick your language.

pip install langfuse openai
npm install @langfuse/openai openai @langfuse/otel @opentelemetry/sdk-node

The Python drop-in is compatible with OpenAI SDK versions >=0.27.8. Async and streaming need >=1.0.0. The JS wrapper needs OpenAI SDK >=4.0.0.

Create a project at Langfuse Cloud or self-host and put the keys in the environment. LANGFUSE_BASE_URL is the region, not a vanity URL.

LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com

EU is https://cloud.langfuse.com. US is https://us.cloud.langfuse.com. Japan is https://jp.cloud.langfuse.com. HIPAA is https://hipaa.cloud.langfuse.com. Point LANGFUSE_BASE_URL at your self-hosted origin if that is where the project lives.

Hello, Harbor Desk

The smallest useful program is not "hello world." It is the ticket you could not reconstruct: one completion, captured.

In Python, Langfuse is a drop-in import for the official OpenAI client. You keep chat.completions.create. You change the import. Azure OpenAI works the same way via AzureOpenAI from langfuse.openai.

from langfuse import get_client
from langfuse.openai import openai  # ①

TICKET = (
    "Order 4412. The life jacket arrived soaked and smells like bilge. "
    "I want a refund, not a replacement."
)

completion = openai.chat.completions.create(  # ②
    name="classify-intent",  # ③
    model="gpt-4o",
    messages=[
        {
            "role": "system",
            "content": (
                "You classify harbor-shop support tickets. "
                "Reply with exactly one word: refund, shipping, or other."
            ),
        },
        {"role": "user", "content": TICKET},
    ],
)

print(completion.choices[0].message.content)

# Short-lived scripts must flush or the process exits before the batch ships.
get_client().flush()  # ④

① The drop-in import keeps the official OpenAI client API; only the import path changes. ② chat.completions.create is the same call you already write; Langfuse records it in the background. ③ name is Langfuse's observation name, not an OpenAI argument. ④ flush() ships the queued events before a short-lived process exits.

Note: The full extracted listing at code/langfuse/part-1-what-langfuse-is/listings/01-classify-intent.py is the complete runnable program.

The name="classify-intent" argument is Langfuse's, not OpenAI's. The drop-in accepts it and uses it as the observation name. You will treat that name as an API later, when evaluators and dashboards start targeting it. For now, run the script and open the project. You should see a generation: the system prompt, the ticket, the one-word label, tokens, and cost.

TypeScript is the same idea with one extra step. The JS SDK traces through OpenTelemetry, so you start NodeSDK with a LangfuseSpanProcessor before you wrap the client. Then you wrap the official OpenAI instance with observeOpenAI. That is not LiteLLM. It is the official OpenAI client, observed.

// instrumentation.ts: import this first
import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";

export const langfuseSpanProcessor = new LangfuseSpanProcessor();

export const sdk = new NodeSDK({
  spanProcessors: [langfuseSpanProcessor],
});

sdk.start();

The classify script imports that instrumentation first, wraps the official client, and flushes before the process exits.

import "./instrumentation"; // must be first  ①
import OpenAI from "openai";
import { observeOpenAI } from "@langfuse/openai";

const openai = observeOpenAI(new OpenAI(), {  // ②
  generationName: "classify-intent",  // ③
});

const ticket =
  "Order 4412. The life jacket arrived soaked and smells like bilge. " +
  "I want a refund, not a replacement.";

const completion = await openai.chat.completions.create({  // ④
  model: "gpt-4o",
  messages: [
    {
      role: "system",
      content:
        "You classify harbor-shop support tickets. " +
        "Reply with exactly one word: refund, shipping, or other.",
    },
    { role: "user", content: ticket },
  ],
});

console.log(completion.choices[0].message.content);

// Short-lived processes: flush before exit.
await langfuseSpanProcessor.forceFlush();  // ⑤
await sdk.shutdown();  // ⑥

① Import the OpenTelemetry bootstrap first so the span processor is running before any OpenAI call. ② observeOpenAI wraps the official client; it is not a proxy and not LiteLLM. ③ generationName is the observation name Langfuse will show in the UI. ④ The create call is the same official OpenAI request; tracing happens around it. ⑤ forceFlush() ships the queued spans before the process exits. ⑥ sdk.shutdown() tears down the OpenTelemetry SDK after the flush.

Note: The full extracted listing at code/langfuse/part-1-what-langfuse-is/listings/02-classify-intent.ts is the complete classify script.

Read what just happened. You did not wrap a proxy. You did not stand up an agent. You called OpenAI the way you already call OpenAI, and Langfuse recorded the generation in the background. Tracing is asynchronous: the SDKs queue events and flush in batches so that the request path is not waiting on Langfuse. That is also why a script that exits immediately looks broken.

Sequence of a Harbor Desk classify call through the Langfuse OpenAI drop-in: the official create returns immediately, events queue, and flush ships the generation to the project.

Gotcha: If the UI is empty after a one-shot script, you skipped flush() in Python or forceFlush() / sdk.shutdown() in JS. The exporter never got a turn. Long-running servers do not need this on every request. Short-lived jobs do, every time.

In production: Keep keys in the environment, never in the repo. LANGFUSE_BASE_URL is how you point staging at a self-hosted origin and production at Cloud, or the other way around, without touching code.

State machine of a short-lived script: events sit in the batch queue until flush, or the process exits and the UI stays empty.

Harbor Desk, one layer at a time

Here is the arc, so the order is not a surprise. Next comes the data model: observations, traces, sessions, and the observation types generation, retriever, and tool that make cost and evals work. Harbor Desk grows from one classify call into a nested tree. Those observation names become an API. Users, sessions, tags, and environments hang on the tree. Then you wrap the Claude Agent SDK and DeepAgents loops you may already ship, and you name LiteLLM so you do not confuse it with the drop-in you just used.

Then the engineering loop: prompts out of code, scores, and experiments. Then shipping: dashboards and cost, the public API, masking, and self-hosting. Every step adds one layer that the previous step left you reaching for.

A timeline of Harbor Desk growing from one classify call through a nested tree, scores, and experiments to a production-hardened assistant.

Do this today

Before you do anything else, get one generation into a project. Five minutes, four steps.

  • Create a Cloud project at cloud.langfuse.com, or point at a self-hosted origin if you already have one.
  • Export the three env vars. Set LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, and LANGFUSE_BASE_URL (EU, US, Japan, HIPAA, or your origin).
  • Install the SDK for the language you actually write: pip install langfuse openai or npm install @langfuse/openai openai @langfuse/otel @opentelemetry/sdk-node.
  • Run the classify script, call flush() or forceFlush() before exit, and open the generation. You should see the soaked-life-jacket ticket sitting next to the word refund.

The ticket is a missing record

The ticket you cannot reconstruct is not a logging problem. It is a missing record of the prompt, the model, and the reply. Langfuse is that record, plus the prompt store, the scores, and the experiments you will hang on it later. It is not the agent. It is not LiteLLM. It is the layer around whichever of those you already run.

Do the five-minute version first. Create a Cloud project, put the three env vars in place, run the classify script, and open the generation. Once you have seen the soaked-life-jacket ticket sitting next to the word refund, the empty-log story stops being abstract. Then you give that generation a tree to live in.