Skip to content

ChoMinGi/langfuse-otel-java

Repository files navigation

langfuse-otel-java

OpenTelemetry-based Langfuse tracing for Java, Spring AI, and LangChain4j.

CI Java Core 11%2B / Starter 17%2B Spring Boot 3.5.16 tested License: MIT OpenTelemetry

Quick Start · What gets traced · Features · Compatibility · Migration · Roadmap

The core module provides a small synchronous tracing API. The Spring Boot starter instruments supported Spring AI and LangChain4j calls and can either reuse the application's OpenTelemetry SDK or own a dedicated Langfuse exporter.


Quick Start

Release status: 0.2.x is the production-preview line for Spring Boot 3, Spring AI 1, and LangChain4j. The dependency examples below use 0.2.0.

Spring Boot (Dedicated Exporter Quick Start)

<dependency>
    <groupId>io.github.chomingi</groupId>
    <artifactId>langfuse-otel-spring-boot-starter</artifactId>
    <version>0.2.0</version>
</dependency>

Spring AI and LangChain4j are optional integrations. The starter does not choose an LLM framework or provider for the application, so keep the framework and provider dependency that supplies your model bean. The example projects use OpenAI, but no provider client is pulled in transitively.

# application.yml
langfuse:
  otel-mode: standalone              # create and own a dedicated Langfuse exporter
  public-key: ${LANGFUSE_PUBLIC_KEY}
  secret-key: ${LANGFUSE_SECRET_KEY}
  host: https://cloud.langfuse.com   # or your self-hosted URL
  content:
    capture-input: false             # safe default
    capture-output: false            # safe default

This quick start deliberately selects standalone, so the configured keys and host are used by a dedicated SDK/exporter. For production applications that already own OpenTelemetry, use the external setup below instead of creating a second SDK.

Standalone endpoints must use HTTPS. For a loopback development receiver only, plaintext HTTP can be enabled explicitly with langfuse.allow-insecure-http-for-development=true; never use that option with production credentials.

Standalone (No Spring)

<dependency>
    <groupId>io.github.chomingi</groupId>
    <artifactId>langfuse-otel-core</artifactId>
    <version>0.2.0</version>
</dependency>
try (LangfuseOtel langfuse = LangfuseOtel.builder()
        .publicKey("pk-lf-...").secretKey("sk-lf-...")
        .host("https://cloud.langfuse.com")
        .serviceName("my-app").build()) {

    langfuse.trace("my-flow", trace -> {
        trace.userId("user-123").sessionId("session-456");
        trace.generation("llm-call", gen -> {
            gen.model("gpt-4o").input(prompt);
            gen.output(callLLM(prompt)).inputTokens(52).outputTokens(85);
        });
    });
}

Existing OpenTelemetry SDK (Production Recommended)

With the default langfuse.otel-mode=auto, the starter reuses an application OpenTelemetry bean when Spring can select one unambiguously: either it is the only bean or one candidate is marked @Primary. With no application bean, auto creates the standalone pipeline. Multiple candidates without a primary bean fail at startup.

When an application bean is selected, the starter does not create an exporter and never flushes or shuts down the application-owned SDK. Configure that SDK or an OpenTelemetry Collector to export traces to Langfuse. If Langfuse keys are also configured, the starter warns that they are ignored for the selected external pipeline.

For an OTLP/HTTP exporter configured through standard environment variables:

export AUTH_STRING="$(printf '%s' "${LANGFUSE_PUBLIC_KEY}:${LANGFUSE_SECRET_KEY}" | base64 | tr -d '\n')"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic ${AUTH_STRING},x-langfuse-ingestion-version=4"

Configure equivalent endpoint and headers when building the SDK or Collector directly. The ingestion-version header enables real-time v4 ingestion; without it, directly ingested traces can be delayed. See Langfuse's OpenTelemetry guide for other regions, self-hosting, and signal-specific settings.

Version 0.2 emits observation-first attributes for Langfuse v4. Trace roots and generic steps are span observations, chat/image calls and @ObserveGeneration are generation observations, and embedding calls are embedding observations. Root .input(...) and .output(...) write the v4 observation fields as well as the legacy trace fields for compatibility.

Set langfuse.otel-mode=external to require one unambiguously selectable application bean, or standalone to force a dedicated Langfuse SDK/exporter. external fails when no bean can be selected; both auto and external reject ambiguous candidates instead of silently choosing a telemetry pipeline.

Without Spring, select the same non-owning mode explicitly:

try (LangfuseOtel langfuse = LangfuseOtel.externalBuilder(openTelemetry).build()) {
    langfuse.trace("my-flow", trace -> { /* ... */ });
}
// openTelemetry remains owned by the application

The original builder() remains the standalone mode: it creates and owns a dedicated SDK and Langfuse OTLP exporter.

The library applies Langfuse context directly to spans it creates. It cannot add a processor to an already-built external SDK, so raw application or third-party spans need their own baggage/span processor or explicit attributes if they must carry the same trace-wide fields.


What Gets Traced

The Spring Boot starter instruments calls made through eligible Spring-managed proxies that implement the supported interfaces below. Objects constructed with new, direct provider-SDK calls, and calls that bypass the Spring proxy are outside this boundary.

Spring AI

Model Methods Operation
ChatModel call(Prompt) chat
ChatModel stream(Prompt) chat (with TTFT)
EmbeddingModel call(EmbeddingRequest), document and bulk embed(...) embeddings
ImageModel call(ImagePrompt) image_generation

Reactor scheduler transitions and downstream signals are bridged automatically. Provider adapters that own a raw, plain-thread source can place ReactorContextPropagation.wrap(rawPublisher) at that source boundary. See the async lifecycle design for the exact boundary.

LangChain4j

Model Methods Operation
ChatModel chat(ChatRequest) chat
StreamingChatModel chat(ChatRequest, Handler) chat (with TTFT)
EmbeddingModel embedAll(...), embed(...) embeddings
ImageModel generate(...), edit(...) image_generation

Streaming callbacks and model listeners restore the wrapper observation and immutable request metadata. Provider adapters that control task submission can use LangChain4jStreamingContext.wrap(...), taskWrapping(...), or a per-request Snapshot. Opaque provider executors remain outside the generic SPI; the async lifecycle design describes that limit.

Auto-captured attributes

Attribute Description
Model name Request & response model
Input Disabled by default; messages, embedding text, or image prompt when opted in
Output Disabled by default; response or accumulated stream when opted in
Token usage Input, output, and total tokens
Temperature, top_p, max_tokens Model parameters
TTFT Time to first token (streaming only)
Errors Exception type by default; policy-processed message/stack trace only after explicit opt-in

Features

@ObserveGeneration Annotation

Trace a proxy-interceptable Spring method as an LLM generation:

@Service
public class LLMService {
    @ObserveGeneration(name = "summarize", model = "gpt-4o", system = "openai")
    public String summarize(String text) {
        return callLLM(text);
    }
}

@ObserveGeneration covers synchronous methods, CompletionStage, and Reactor Mono/Flux. Stages retain their identity, and Reactor creates one observation per subscription. On a model bean, explicit annotations take precedence over automatic instrumentation so the same call is not traced twice. The method must be reached through its Spring proxy: self-invocation and private or final methods are not advised. Ordering relative to other around advice, such as @Transactional, is not guaranteed.

Request Context Propagation

LangfuseTraceContext metadata = LangfuseTraceContext.builder()
        .userId("user-123")
        .sessionId("session-456")
        .tags("prod", "v2")
        .build();

try (Scope ignored = LangfuseContext.makeCurrent(metadata)) {
    langfuse.trace("flow", trace -> { /* ... */ });
}

Spring MVC and WebFlux filters can also extract Principal and HTTP session metadata after explicit opt-in. The legacy LangfuseContext.set*() methods remain available for synchronous code.

Trace name, user/session IDs, tags, metadata, version, release, and environment are copied when a library-created descendant starts. Set fluent trace-wide values before creating children when all observations must agree; already-started spans are not backfilled. While a LangfuseTrace is active, its trace-local carrier is authoritative, so nested storeIn(...) or makeCurrent(...) metadata does not replace that trace. Use the trace fluent setters for intentional updates.

Content Capture and Redaction

Automatic instrumentation is metadata-only by default. Enable input and output independently, and keep a finite post-redaction length limit:

langfuse:
  content:
    capture-input: true
    capture-output: true
    max-length: 8192

To redact content before export, provide one thread-safe bean:

@Bean
ContentRedactor contentRedactor() {
    return (type, content) -> content.replaceAll("(?i)api[-_ ]?key\\s*[:=]\\s*\\S+", "[REDACTED]");
}

No redactor is installed automatically. If capture is enabled without one, values are exported unchanged except for the length limit. Use one reviewed, thread-safe redactor in production; ambiguous or failing redactors drop the affected automatic content.

Exception details use a separate safe-by-default policy. Only the exception type is recorded unless message or stack capture is enabled:

langfuse:
  exception:
    capture-message: false
    capture-stack-trace: false
    max-length: 8192

Exception detail capture follows the same rule with ExceptionRedactor. See SECURITY.md for the full data boundary and fail-closed behavior.

Prompt Management

Integrates with langfuse-java for prompt versioning:

<dependency>
    <groupId>com.langfuse</groupId>
    <artifactId>langfuse-java</artifactId>
    <version>0.2.0</version>
</dependency>

The prompt client is optional and is not pulled in transitively. Create it with the same Langfuse credentials and host before using the helper:

LangfuseClient langfuseClient = LangfuseClient.builder()
        .credentials("pk-lf-...", "sk-lf-...")
        .url("https://cloud.langfuse.com")
        .build();

trace.generation("llm", gen -> {
    String compiled = gen.prompt(langfuseClient, "my-prompt")
            .variable("domain", "HR")
            .variable("question", "What is MBO?")
            .compile();
    // promptName & promptVersion auto-linked to the span
    gen.output(callLLM(compiled));
});

Core tracing API

The core API supports callbacks, try-with-resources, and explicit end(). All three are synchronous scope APIs: close a handle on the thread that created it and do not pass it into an asynchronous callback.

Operational Signals

With the default fail-safe setting, missing API keys or an invalid standalone configuration fall back to no-op mode without crashing the application. LangfuseOtel.getStatus() returns an immutable snapshot of that fallback, ownership, export, queue-drop, and flush state. Reading it does not flush or make a network request.

When the application includes spring-boot-starter-actuator, the starter registers a langfuse health component and Micrometer meters. The Actuator dependency remains optional and is not added transitively by this library.

  • UP: the standalone pipeline is owned and has no current failure. This can include the initial state before the first export.
  • DOWN: the latest export failed, a queue drop has not yet been followed by a successful export, or the latest flush failed or timed out.
  • OUT_OF_SERVICE: fail-safe construction produced the library's no-op fallback.
  • UNKNOWN: OpenTelemetry is application-owned, so its exporter, queue, and flush state are not observed here.

Owned pipelines publish fallback, export-failure, queue-drop, and flush meters. External mode omits transport meters because this library does not own that pipeline. A successful flush confirms a local SDK drain, not remote ingestion.

Keep this component out of liveness. Add it to readiness only when losing Langfuse delivery should intentionally stop the application from receiving traffic.


Modules

Module Java Description
langfuse-otel-core 11+ Core tracing library — no framework dependency
langfuse-otel-spring-boot-starter 17+ Auto-config for Spring AI & LangChain4j

Configuration (Spring Boot)

Property Default Description
langfuse.public-key Standalone mode public key; not used with an external OTel bean
langfuse.secret-key Standalone mode secret key; not used with an external OTel bean
langfuse.host https://cloud.langfuse.com Standalone mode Langfuse host URL
langfuse.allow-insecure-http-for-development false Allow a plaintext standalone HTTP endpoint on localhost or a literal loopback address only
langfuse.service-name langfuse-app Standalone mode service name
langfuse.environment Standalone resource environment (e.g., production)
langfuse.release Standalone resource release version
langfuse.enabled true Enable/disable all tracing
langfuse.otel-mode auto auto, external, or standalone OpenTelemetry ownership selection
langfuse.content.capture-input false Capture automatic model input
langfuse.content.capture-output false Capture automatic model output
langfuse.content.max-length 8192 Maximum UTF-16 units retained after redaction
langfuse.exception.capture-message false Capture policy-processed exception messages from automatic instrumentation
langfuse.exception.capture-stack-trace false Capture policy-processed exception stack traces from automatic instrumentation
langfuse.exception.max-length 8192 Maximum UTF-16 units retained per exception detail after redaction
langfuse.context.capture-user-id false Export the authenticated Principal name as user.id
langfuse.context.capture-session-id false Export the HTTP session identifier as session.id; avoid this for bearer-style session IDs

Production-preview limitations

  • Generic instrumentation cannot enter opaque provider-owned executors. Integrations that own a raw source or scheduling boundary must use the supplied context adapters.
  • During active instrumented Reactor subscriptions, a keyed Schedulers.onScheduleHook is process-global. Every Reactor scheduled task in the JVM performs a small decorator lookup during that window; unrelated tasks receive no Langfuse context, and the hook is removed after the last lease ends.
  • Async observations are terminal-driven and have no library timeout. A publisher, stage, or LangChain4j provider that never completes, errors, or is cancelled can leave its observation open; configure provider/application timeouts and cancellation.
  • Final or otherwise non-proxyable model types are left unchanged and logged as uninstrumented. Annotation-based tracing also cannot advise self-invocation or private/final methods.
  • 0.2.x is JVM-only; Spring AOT and GraalVM native-image support are not claimed.
  • Custom concrete publisher subtypes are returned unchanged when a compatible wrapper type cannot be preserved.

0.2.x remains a production-preview line. Work for later releases is tracked in ROADMAP.md.

Compatibility

Dependency Tested Version Notes
Java 11+ Core module
Java 17+ Spring Boot starter
OpenTelemetry SDK 1.62.0 Via BOM
Spring Boot 3.5.16 Non-web external and standalone consumer startup
Spring AI 1.0.9 / 1.1.8 Chat consumer smoke; adapter tests also cover streaming, embeddings, and images
LangChain4j 1.0.0 / 1.18.0 Chat consumer smoke; adapter tests also cover streaming, embeddings, and images
langfuse-java 0.2.0 Prompt management (optional)
Langfuse Cloud Managed service Current OTLP v4 ingestion and Observations API v2
Langfuse Self-hosted v3.22.0+ OTLP endpoint minimum; v4 is required for observation-first API v2 read-back

The blocking consumer smoke tests start a non-web Spring Boot 3.5.16 application on Java 17 and invoke a framework ChatModel for both listed Spring AI and LangChain4j versions. External mode verifies the resulting span with an application-owned SDK; standalone mode verifies the OTLP path, authentication and ingestion headers, service resource, and model attributes against a loopback receiver. The broader adapter behavior is covered by the Java 17/21 starter test matrix. Provider clients and other Spring Boot 3 minor releases are not part of that runtime smoke.

0.2.x is the Spring Boot 3 and Spring AI 1 line. 0.3.x will move the same starter coordinates to Spring Boot 4 and Spring AI 2; the core module remains framework-neutral. See SECURITY.md for the maintenance window.

Examples

See the examples directory:

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT

About

Zero-config LLM observability for Java — trace Spring AI & LangChain4j to Langfuse via OpenTelemetry

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages