Quarkus LangChain4j tracing with Future AGI

Send the spans Quarkus LangChain4j already creates for @RegisterAiService calls and tools to Future AGI through Quarkus OpenTelemetry. Covers the endpoint and host-name pitfalls, error status, privacy defaults and version requirements.

📝
TL;DR
  • Quarkus LangChain4j already traces every @RegisterAiService call, model call and tool call once quarkus-opentelemetry is on the classpath. You configure the exporter. No additional traceAI package is required for this Quarkus recipe.
  • Set the protocol to http/protobuf, the traces endpoint to {base}/tracer (Quarkus appends /v1/traces itself), both X-Api-Key / X-Secret-Key headers and a project_name resource attribute.
  • Copy two small classes so failures show as ERROR and the host name is not sent.
  • With all the settings and both classes below, prompts, completions, tool arguments and results, error messages and stack traces are off by default. Each has a documented opt-in.

About

Quarkus LangChain4j (io.quarkiverse.langchain4j, Apache-2.0) generates the implementation of each @RegisterAiService interface at build time. With quarkus-opentelemetry present, it opens a span per AI service method, per model call and per tool call (upstream observability page). This recipe points Quarkus’ own OTLP exporter at Future AGI.

This is not the LangChain4j integration. traceai-langchain4j wraps a plain LangChain4j ChatLanguageModel that you build yourself. It does not see the AI services Quarkus generates, and adding it to a Quarkus app would only risk a second span for the same call. Use this page for Quarkus LangChain4j, and the LangChain4j page for LangChain4j without Quarkus.

This recipe uses Quarkus LangChain4j 1.14.2, Quarkus 3.33.3.1 and Java 21. It covers a JVM command-mode application with synchronous OpenAI-compatible calls and OTLP/HTTP export to self-hosted Future AGI. See the complete example.

1. Add the dependencies

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.quarkus.platform</groupId>
            <artifactId>quarkus-bom</artifactId>
            <version>3.33.3.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>io.quarkiverse.langchain4j</groupId>
            <artifactId>quarkus-langchain4j-bom</artifactId>
            <version>1.14.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>io.quarkiverse.langchain4j</groupId>
        <artifactId>quarkus-langchain4j-openai</artifactId>
    </dependency>
    <!-- Without this extension Quarkus LangChain4j creates no spans and nothing is exported. -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-opentelemetry</artifactId>
    </dependency>
</dependencies>

quarkus-opentelemetry enables span creation and export for this setup.

2. Set environment variables

export FI_API_KEY="YOUR_API_KEY"
export FI_SECRET_KEY="YOUR_SECRET_KEY"
export FI_PROJECT_NAME="my-quarkus-app"
# Your reachable self-hosted Future AGI base URL, no trailing slash.
export FI_BASE_URL="http://localhost:14338"

Keep the keys in the environment or a secret store, not in application.properties.

3. Configure application.properties

quarkus.otel.exporter.otlp.traces.protocol=http/protobuf
quarkus.otel.exporter.otlp.traces.endpoint=${FI_BASE_URL}/tracer
quarkus.otel.exporter.otlp.traces.headers=X-Api-Key=${FI_API_KEY},X-Secret-Key=${FI_SECRET_KEY}
quarkus.otel.exporter.otlp.traces.compression=none
quarkus.otel.resource.attributes=project_name=${FI_PROJECT_NAME},project_type=observe

# Content stays off (these are the upstream defaults, written out).
quarkus.langchain4j.tracing.include-prompt=false
quarkus.langchain4j.tracing.include-completion=false
quarkus.langchain4j.tracing.include-tool-arguments=false
quarkus.langchain4j.tracing.include-tool-result=false

# No span events: drops exception messages and stack traces. Needs Quarkus with OpenTelemetry SDK 1.51+.
quarkus.otel.span.event.count.limit=0
PropertyWhy
traces.protocol=http/protobufQuarkus defaults to gRPC on localhost:4317. Future AGI’s route is OTLP over HTTP.
traces.endpoint=.../tracerQuarkus appends /v1/traces to whatever you set, so this posts to /tracer/v1/traces. See the warning below.
traces.headersBoth keys, comma separated. One header is a failed setup.
traces.compression=noneThe Quarkus default. Optional: Future AGI also accepts gzip.
resource.attributesproject_name must be a resource attribute; a span attribute does not count and the batch is dropped without it.
tracing.include-*The four content switches. See Privacy.
span.event.count.limit=0Quarkus LangChain4j records each failure as an exception event with exception.message and the full exception.stacktrace. This drops them; the span still gets ERROR (step 4).

Quarkus exports traces only: metrics and logs export are off unless you turn them on (quarkus.otel.metrics.enabled, quarkus.otel.logs.enabled). Leave them off; Future AGI has no metrics or logs route.

Warning

Set the endpoint to .../tracer, not the full traces URL. Quarkus 3.33.3.1 appends /v1/traces; including it in the setting sends requests to the wrong path.

4. Copy two classes

Quarkus LangChain4j calls recordException when a model call, a tool or an AI service method fails, but never sets the span status, so a failure arrives as UNSET. And Quarkus always adds host.name to the resource, after quarkus.otel.resource.attributes and after the quarkus.otel.experimental.resource.disabled-keys filter, so configuration cannot remove it. These two classes fix both with documented extension points (ChatModelSpanContributor, the AiServiceErrorEvent CDI event, ToolSpanContributor) and a Quarkus SDK customizer. They create no spans.

// FutureAgiTracingDefaults.java
@Singleton
public class FutureAgiTracingDefaults implements ChatModelSpanContributor {

    private final boolean exportErrorDetails;

    public FutureAgiTracingDefaults(
            @ConfigProperty(name = "fi.export-error-details", defaultValue = "false") boolean exportErrorDetails) {
        this.exportErrorDetails = exportErrorDetails;
    }

    /** The model span of a failed model call. */
    @Override
    public void onError(ChatModelErrorContext errorContext, Span currentSpan) {
        markError(currentSpan, errorContext.error(), exportErrorDetails);
    }

    /** Any failed AI service call; it fires while the langchain4j.aiservices.* span is current. */
    void onAiServiceError(@Observes AiServiceErrorEvent event) {
        Span current = Span.current();
        if (current instanceof ReadableSpan readable && readable.getName().startsWith("langchain4j.aiservices.")) {
            markError(current, event.error(), exportErrorDetails);
        }
    }

    static void markError(Span span, Throwable error, boolean withDetails) {
        String description = withDetails && error != null && error.getMessage() != null ? error.getMessage() : "";
        span.setStatus(StatusCode.ERROR, description);
    }

    /** Removes host.name. Lowest priority, so it runs after Quarkus' own resource customizer. */
    @Singleton
    @Unremovable
    @Priority(Integer.MIN_VALUE)
    public static class DropHostName implements AutoConfiguredOpenTelemetrySdkBuilderCustomizer {
        @Override
        public void customize(AutoConfiguredOpenTelemetrySdkBuilder builder) {
            builder.addResourceCustomizer((resource, config) ->
                    resource.toBuilder().removeIf(AttributeKey.stringKey("host.name")::equals).build());
        }
    }
}
// ToolErrorStatus.java (Quarkus LangChain4j 1.12.0 or newer)
@Singleton
public class ToolErrorStatus implements ToolSpanContributor {

    private final boolean exportErrorDetails;

    public ToolErrorStatus(@ConfigProperty(name = "fi.export-error-details", defaultValue = "false") boolean exportErrorDetails) {
        this.exportErrorDetails = exportErrorDetails;
    }

    @Override
    public void onError(ToolExecutionErrorContext context, Span span) {
        FutureAgiTracingDefaults.markError(span, context.error(), exportErrorDetails);
    }
}

The imports are in the example source. AutoConfiguredOpenTelemetrySdkBuilderCustomizer lives in Quarkus’ runtime package.

5. Run it

Call your AI service as usual. The example is a command-mode app:

mvn package
java -jar target/quarkus-app/quarkus-run.jar "What is the weather in Paris?"

Quarkus exports spans in batches and flushes them on normal shutdown. Stop a service with SIGTERM or Ctrl-C; a forced kill can lose the last batch. For short-lived processes the example also sets quarkus.otel.simple=true (a build-time property) so each span is sent as it ends.

What is captured

One AI service call is one trace. In a command-mode app the AI service span is the root:

langchain4j.aiservices.Assistant.chat         type unknown
├── completion gpt-6-luna                     type llm, model, input/output tokens
│   └── POST /chat/completions                type unknown (Quarkus REST client)
├── langchain4j.tools.currentWeather          type tool
└── completion gpt-6-luna                     type llm, model, input/output tokens
    └── POST /chat/completions
SpanAttributes (content off)
langchain4j.aiservices.<Interface>.<method>none
completion <model>gen_ai.operation.name=chat, gen_ai.provider.name (open_ai for the OpenAI extension), gen_ai.request.model, gen_ai.response.model, gen_ai.response.id, gen_ai.response.finish_reasons (TOOL_EXECUTION, STOP), gen_ai.request.temperature, gen_ai.request.top_p, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens (only when the provider returns usage)
langchain4j.tools.<tool>gen_ai.operation.name=execute_tool, gen_ai.tool.name, gen_ai.tool.call.id, gen_ai.tool.type
POST /chat/completionshttp.request.method, url.full (the provider URL; avoid putting secrets in URLs), http.response.status_code
Resourceproject_name, project_type, service.name, service.version, telemetry.sdk.*, webengine.name, webengine.version

Future AGI reads the model and token columns from the completion spans. The AI service span carries no input, output or token totals; the trace list shows the AI service name, status and latency.

Privacy

WhatDefaultHow to turn it onWhere it lands in Future AGI
Prompt and completionoffquarkus.langchain4j.tracing.include-prompt=true, include-completion=true (or QUARKUS_LANGCHAIN4J_TRACING_INCLUDE_PROMPT=true …)span attributes gen_ai.prompt (every message of the request joined: system, user, earlier answers, tool results) and gen_ai.completion. The Input and Output columns stay empty.
Tool arguments and resultoffinclude-tool-arguments=true, include-tool-result=truespan attributes gen_ai.tool.call.arguments, gen_ai.tool.call.result
Error messageoff (ERROR with an empty status message)FI_EXPORT_ERROR_DETAILS=truestatus message of the failed AI service, model or tool span
Exception events (exception.message, exception.stacktrace)off (span.event.count.limit=0)raise the limitFuture AGI keeps only the event name and time, not the message or stack trace
Host nameremoved by DropHostNamenone

Your application’s own logs are a separate path: LangChain4j logs the provider’s error body at WARN when it retries, and Quarkus LangChain4j logs a failed tool’s exception at ERROR. They are not exported (OpenTelemetry logs are off).

Errors

  • A failed model call: the model span and the AI service span are ERROR. The REST client span is ERROR for a 5xx response; a provider 4xx can leave it UNSET.
  • A tool that throws: its tool span is ERROR. Quarkus LangChain4j sends the exception message back to the model as the tool result, so the AI service call itself usually succeeds and stays UNSET. With prompt capture on, that message is part of the next model span’s gen_ai.prompt.
  • Without the two classes, all of these arrive as UNSET.

Versions

Use the pinned Quarkus LangChain4j 1.14.2 and Quarkus 3.33.3.1 pair for this recipe. quarkus.otel.span.event.count.limit=0 requires OpenTelemetry SDK 1.51 or newer; older SDK versions reject that setting. ToolSpanContributor requires Quarkus LangChain4j 1.12.0 or newer.

Limitations

  • JVM command mode and synchronous OpenAI-compatible calls only. Native image, streaming and other provider extensions are outside the supported scope.
  • The AI service span is type unknown and has no input, output or token totals.
  • Opt-in content is stored as span attributes, not in Future AGI’s Input and Output fields.
  • Micrometer metrics (langchain4j.aiservices.timed, gen_ai.client.token.usage) and LangChain4j CDI observability events are not exported. Future AGI ingests traces only.
  • One project per process: project_name is fixed at startup.

Troubleshooting

  • Nothing arrives, no error. quarkus-opentelemetry is missing, or quarkus.otel.sdk.disabled=true / quarkus.otel.traces.enabled=false is set.
  • Requests go to localhost:4317. traces.protocol or traces.endpoint is not set: that is the gRPC default.
  • 404s from the collector (seen on the self-hosted collector). The endpoint ends in /v1/traces; Quarkus appends it again. Stop at /tracer.
  • Rejected requests or no project. One of the two headers is missing, or project_name is not under quarkus.otel.resource.attributes.
  • The app does not start: maxNumberOfEvents must be greater than 0. Your Quarkus ships an OpenTelemetry SDK older than 1.51. See Versions.
  • Failures show as UNSET. The two classes are not in the application, or ToolErrorStatus is missing for tools.
  • host.name still on the resource. DropHostName is missing its @Priority(Integer.MIN_VALUE) and ran before Quarkus’ own customizer.
  • Two spans per model call. traceai-langchain4j is also wrapping a model. Remove it; Quarkus LangChain4j already traces the call.
  • Last spans missing. The process was killed. Let Quarkus shut down, or set quarkus.otel.simple=true for short-lived runs.
Was this page helpful?

Questions & Discussion