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.
- Quarkus LangChain4j already traces every
@RegisterAiServicecall, model call and tool call oncequarkus-opentelemetryis 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/tracesitself), bothX-Api-Key/X-Secret-Keyheaders and aproject_nameresource attribute. - Copy two small classes so failures show as
ERRORand 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
| Property | Why |
|---|---|
traces.protocol=http/protobuf | Quarkus defaults to gRPC on localhost:4317. Future AGI’s route is OTLP over HTTP. |
traces.endpoint=.../tracer | Quarkus appends /v1/traces to whatever you set, so this posts to /tracer/v1/traces. See the warning below. |
traces.headers | Both keys, comma separated. One header is a failed setup. |
traces.compression=none | The Quarkus default. Optional: Future AGI also accepts gzip. |
resource.attributes | project_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=0 | Quarkus 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
| Span | Attributes (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/completions | http.request.method, url.full (the provider URL; avoid putting secrets in URLs), http.response.status_code |
| Resource | project_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
| What | Default | How to turn it on | Where it lands in Future AGI |
|---|---|---|---|
| Prompt and completion | off | quarkus.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 result | off | include-tool-arguments=true, include-tool-result=true | span attributes gen_ai.tool.call.arguments, gen_ai.tool.call.result |
| Error message | off (ERROR with an empty status message) | FI_EXPORT_ERROR_DETAILS=true | status message of the failed AI service, model or tool span |
Exception events (exception.message, exception.stacktrace) | off (span.event.count.limit=0) | raise the limit | Future AGI keeps only the event name and time, not the message or stack trace |
| Host name | removed by DropHostName | none |
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 isERRORfor a 5xx response; a provider 4xx can leave itUNSET. - 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 staysUNSET. With prompt capture on, that message is part of the next model span’sgen_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
unknownand 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_nameis fixed at startup.
Troubleshooting
- Nothing arrives, no error.
quarkus-opentelemetryis missing, orquarkus.otel.sdk.disabled=true/quarkus.otel.traces.enabled=falseis set. - Requests go to
localhost:4317.traces.protocolortraces.endpointis 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_nameis not underquarkus.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, orToolErrorStatusis missing for tools. host.namestill on the resource.DropHostNameis missing its@Priority(Integer.MIN_VALUE)and ran before Quarkus’ own customizer.- Two spans per model call.
traceai-langchain4jis 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=truefor short-lived runs.
Questions & Discussion