Core runtime module for Quarkus LangChain4j integration with declarative AI services, guardrails, and observability
The observability framework provides CDI event-based monitoring for AI service lifecycle events. Events are fired at key points during AI service execution, enabling logging, metrics, tracing, and custom business logic.
import io.quarkiverse.langchain4j.observability.AiServiceSelector;
import io.quarkiverse.langchain4j.observability.AiServiceEvents;
import io.quarkiverse.langchain4j.observability.AiServiceSelectorLiteral;
import io.quarkiverse.langchain4j.observability.listener.AiServiceListenerAdapter;
import dev.langchain4j.observability.api.event.*;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.context.ApplicationScoped;package io.quarkiverse.langchain4j.observability;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.inject.Qualifier;
@Qualifier
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface AiServiceSelector {
Class<?> value();
}Qualifies CDI event observers to receive events only from a specific AI service interface. The interface specified in value() must be annotated with @RegisterAiService.
@ApplicationScoped
public class MyServiceMonitor {
void onStart(@Observes @AiServiceSelector(MyAiService.class) AiServiceStartedEvent event) {
log.info("MyAiService started");
}
}package io.quarkiverse.langchain4j.observability;
import jakarta.enterprise.util.AnnotationLiteral;
public class AiServiceSelectorLiteral extends AnnotationLiteral<AiServiceSelector>
implements AiServiceSelector {
protected AiServiceSelectorLiteral(Builder builder);
@Override
public Class<?> value();
public Class<?> aiServiceClass();
public Builder toBuilder();
public static Builder builder();
public static class Builder {
public Builder aiServiceClass(Class<?> aiServiceClass);
public AiServiceSelector build();
}
}Programmatically creates @AiServiceSelector qualifier annotations at runtime. This is useful when you need to dynamically select event observers or inject CDI beans qualified by AI service class.
Usage - Programmatic Event Firing:
import io.quarkiverse.langchain4j.observability.AiServiceSelectorLiteral;
import io.quarkus.arc.Arc;
import dev.langchain4j.observability.api.event.AiServiceStartedEvent;
@ApplicationScoped
public class CustomEventPublisher {
public void publishEventForService(Class<?> aiServiceClass, AiServiceStartedEvent event) {
AiServiceSelector selector = AiServiceSelectorLiteral.builder()
.aiServiceClass(aiServiceClass)
.build();
Arc.container()
.beanManager()
.getEvent()
.select(AiServiceStartedEvent.class, selector)
.fire(event);
}
}Usage - Dynamic Bean Selection:
@ApplicationScoped
public class ServiceAnalyzer {
public void analyzeService(Class<?> aiServiceClass) {
AiServiceSelector selector = AiServiceSelectorLiteral.builder()
.aiServiceClass(aiServiceClass)
.build();
// Use the selector to retrieve service-specific beans
Instance<ServiceMonitor> monitors = Arc.container()
.select(ServiceMonitor.class, selector);
monitors.stream().forEach(monitor ->
monitor.analyze(aiServiceClass));
}
}package io.quarkiverse.langchain4j.observability;
import dev.langchain4j.observability.api.event.*;
public enum AiServiceEvents {
COMPLETED(AiServiceCompletedEvent.class),
ERROR(AiServiceErrorEvent.class),
INPUT_GUARDRAIL_EXECUTED(InputGuardrailExecutedEvent.class),
OUTPUT_GUARDRAIL_EXECUTED(OutputGuardrailExecutedEvent.class),
RESPONSE_RECEIVED(AiServiceResponseReceivedEvent.class),
STARTED(AiServiceStartedEvent.class),
TOOL_EXECUTED(ToolExecutedEvent.class);
public <T extends AiServiceEvent> AiServiceListenerAdapter<T> createListener(
Class<?> aiServiceClass);
public <T extends AiServiceEvent> Class<T> getEventClass();
}package io.quarkiverse.langchain4j.observability.listener;
import dev.langchain4j.observability.api.event.AiServiceEvent;
import dev.langchain4j.observability.api.listener.AiServiceListener;
public record AiServiceListenerAdapter<T extends AiServiceEvent>(
Class<T> eventClass,
Class<?> aiServiceClass) implements AiServiceListener<T> {
@Override
public Class<T> getEventClass();
@Override
public void onEvent(T event);
}Adapter class that implements the LangChain4j AiServiceListener interface to fire CDI events. This bridges LangChain4j's observability system with Quarkus CDI events, allowing AI service events to be observed via standard CDI event observers.
How It Works:
AiServiceEvents.createListener(Class<?> aiServiceClass)@AiServiceSelector for the specific AI serviceUsage via AiServiceEvents:
import io.quarkiverse.langchain4j.observability.AiServiceEvents;
import io.quarkiverse.langchain4j.observability.AiServiceSelectorLiteral;
import io.quarkiverse.langchain4j.observability.listener.AiServiceListenerAdapter;
import io.quarkiverse.langchain4j.observability.listener.AiServiceListenerAdapter;
import dev.langchain4j.observability.api.listener.AiServiceListener;
@ApplicationScoped
public class ObservabilitySetup {
@Inject
AiServices aiServices;
void setupCustomListener(@Observes StartupEvent event) {
// Create a listener adapter for a specific AI service
AiServiceListenerAdapter<?> adapter =
AiServiceEvents.STARTED.createListener(MyAiService.class);
// Register with LangChain4j (if manual registration needed)
// Typically this is handled automatically by Quarkus
}
}Direct CDI Event Observing (Recommended):
Instead of using the adapter directly, observe CDI events:
@ApplicationScoped
public class AiServiceMonitor {
void onServiceStarted(
@Observes @AiServiceSelector(MyAiService.class) AiServiceStartedEvent event) {
log.info("Service started: " + event);
}
void onServiceCompleted(
@Observes @AiServiceSelector(MyAiService.class) AiServiceCompletedEvent event) {
log.info("Service completed: " + event);
}
void onToolExecuted(
@Observes @AiServiceSelector(MyAiService.class) ToolExecutedEvent event) {
log.info("Tool executed: " + event.getToolName());
}
}All event classes come from LangChain4j's observability API: dev.langchain4j.observability.api.event.*
package dev.langchain4j.observability.api.event;
public class AiServiceStartedEvent extends AiServiceEvent {
// Event data for service start
}package dev.langchain4j.observability.api.event;
public class AiServiceResponseReceivedEvent extends AiServiceEvent {
// Event data for model response
}package dev.langchain4j.observability.api.event;
public class ToolExecutedEvent extends AiServiceEvent {
// Event data for tool execution
}package dev.langchain4j.observability.api.event;
public class InputGuardrailExecutedEvent extends AiServiceEvent {
// Event data for input guardrail execution
}package dev.langchain4j.observability.api.event;
public class OutputGuardrailExecutedEvent extends AiServiceEvent {
// Event data for output guardrail execution
}package dev.langchain4j.observability.api.event;
public class AiServiceCompletedEvent extends AiServiceEvent {
// Event data for successful completion
}package dev.langchain4j.observability.api.event;
public class AiServiceErrorEvent extends AiServiceEvent {
// Event data for error
}import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import io.quarkiverse.langchain4j.observability.AiServiceSelector;
import dev.langchain4j.observability.api.event.*;
import org.jboss.logging.Logger;
@ApplicationScoped
public class AiServiceMonitor {
private static final Logger LOG = Logger.getLogger(AiServiceMonitor.class);
void onServiceStarted(@Observes AiServiceStartedEvent event) {
LOG.infof("AI service started: %s", event);
}
void onServiceCompleted(@Observes AiServiceCompletedEvent event) {
LOG.infof("AI service completed: %s", event);
}
void onServiceError(@Observes AiServiceErrorEvent event) {
LOG.errorf("AI service error: %s", event);
}
}import io.quarkiverse.langchain4j.RegisterAiService;
@RegisterAiService
public interface CustomerSupportService {
String help(String request);
}
@RegisterAiService
public interface SalesAssistant {
String processSalesQuery(String query);
}
@ApplicationScoped
public class ServiceSpecificMonitor {
private static final Logger LOG = Logger.getLogger(ServiceSpecificMonitor.class);
// Only observes CustomerSupportService events
void onCustomerSupportStarted(
@Observes @AiServiceSelector(CustomerSupportService.class)
AiServiceStartedEvent event) {
LOG.info("Customer support query started");
}
// Only observes SalesAssistant events
void onSalesStarted(
@Observes @AiServiceSelector(SalesAssistant.class)
AiServiceStartedEvent event) {
LOG.info("Sales query started");
}
}@ApplicationScoped
public class ToolMonitor {
private static final Logger LOG = Logger.getLogger(ToolMonitor.class);
void onToolExecuted(@Observes ToolExecutedEvent event) {
LOG.infof("Tool executed: %s", event);
// Access tool execution details from event
}
void onToolExecutedForSpecificService(
@Observes @AiServiceSelector(MyAiService.class)
ToolExecutedEvent event) {
LOG.infof("Tool executed in MyAiService: %s", event);
}
}@ApplicationScoped
public class GuardrailMonitor {
private static final Logger LOG = Logger.getLogger(GuardrailMonitor.class);
void onInputGuardrailExecuted(@Observes InputGuardrailExecutedEvent event) {
LOG.infof("Input guardrail executed: %s", event);
}
void onOutputGuardrailExecuted(@Observes OutputGuardrailExecutedEvent event) {
LOG.infof("Output guardrail executed: %s", event);
}
}import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Timer;
import jakarta.inject.Inject;
@ApplicationScoped
public class AiServiceMetrics {
@Inject
MeterRegistry registry;
private final Map<String, Timer.Sample> activeSamples = new ConcurrentHashMap<>();
void onServiceStarted(@Observes AiServiceStartedEvent event) {
String requestId = getRequestId(event);
Timer.Sample sample = Timer.start(registry);
activeSamples.put(requestId, sample);
}
void onServiceCompleted(@Observes AiServiceCompletedEvent event) {
String requestId = getRequestId(event);
Timer.Sample sample = activeSamples.remove(requestId);
if (sample != null) {
sample.stop(Timer.builder("ai.service.duration")
.tag("service", getServiceName(event))
.register(registry));
}
Counter.builder("ai.service.completed")
.tag("service", getServiceName(event))
.register(registry)
.increment();
}
void onServiceError(@Observes AiServiceErrorEvent event) {
activeSamples.remove(getRequestId(event));
Counter.builder("ai.service.errors")
.tag("service", getServiceName(event))
.register(registry)
.increment();
}
void onToolExecuted(@Observes ToolExecutedEvent event) {
Counter.builder("ai.service.tool.executions")
.tag("tool", getToolName(event))
.register(registry)
.increment();
}
}@ApplicationScoped
public class AuditLogger {
@Inject
AuditService auditService;
void logServiceInvocation(@Observes AiServiceStartedEvent event) {
auditService.log(AuditEvent.builder()
.type("AI_SERVICE_INVOCATION")
.service(getServiceName(event))
.user(getUserId(event))
.timestamp(Instant.now())
.build());
}
void logToolExecution(@Observes ToolExecutedEvent event) {
auditService.log(AuditEvent.builder()
.type("TOOL_EXECUTION")
.tool(getToolName(event))
.user(getUserId(event))
.result(getToolResult(event))
.timestamp(Instant.now())
.build());
}
void logGuardrailViolation(@Observes InputGuardrailExecutedEvent event) {
if (isFailure(event)) {
auditService.log(AuditEvent.builder()
.type("GUARDRAIL_VIOLATION")
.guardrail(getGuardrailName(event))
.reason(getFailureReason(event))
.user(getUserId(event))
.timestamp(Instant.now())
.build());
}
}
}@ApplicationScoped
public class CostTracker {
@Inject
CostEstimatorService costEstimator;
@Inject
BillingService billing;
void trackCost(@Observes AiServiceResponseReceivedEvent event) {
Cost cost = costEstimator.estimate(event.getResponseContext());
if (cost != null) {
String userId = getUserId(event);
billing.recordUsage(userId, cost);
LOG.infof("Cost tracked for user %s: %s", userId, cost);
}
}
}@ApplicationScoped
public class ErrorAlerting {
@Inject
AlertService alertService;
private final Map<String, Integer> errorCounts = new ConcurrentHashMap<>();
void onError(@Observes AiServiceErrorEvent event) {
String service = getServiceName(event);
int count = errorCounts.merge(service, 1, Integer::sum);
if (count >= 5) {
alertService.sendAlert(Alert.builder()
.severity(Severity.HIGH)
.title("High AI Service Error Rate")
.message(String.format(
"Service %s has experienced %d errors",
service, count))
.build());
errorCounts.put(service, 0); // Reset counter
}
}
}@ApplicationScoped
public class ResponseLogger {
void logResponse(@Observes AiServiceResponseReceivedEvent event) {
LOG.infof("AI Response received: service=%s, response=%s",
getServiceName(event),
getResponseSummary(event));
}
}@ApplicationScoped
public class ComprehensiveMonitor {
private static final Logger LOG = Logger.getLogger(ComprehensiveMonitor.class);
@Inject
MeterRegistry metrics;
@Inject
AuditService audit;
private final Map<String, MonitoringContext> contexts = new ConcurrentHashMap<>();
void onStarted(@Observes @AiServiceSelector(PaymentService.class)
AiServiceStartedEvent event) {
String requestId = getRequestId(event);
MonitoringContext context = new MonitoringContext();
context.startTime = System.currentTimeMillis();
context.userId = getUserId(event);
contexts.put(requestId, context);
LOG.infof("Payment service started for user: %s", context.userId);
audit.log("PAYMENT_SERVICE_STARTED", context.userId);
}
void onToolExecuted(@Observes @AiServiceSelector(PaymentService.class)
ToolExecutedEvent event) {
String requestId = getRequestId(event);
MonitoringContext context = contexts.get(requestId);
if (context != null) {
context.toolExecutions++;
LOG.infof("Tool executed: %s (count: %d)",
getToolName(event), context.toolExecutions);
}
}
void onGuardrailExecuted(@Observes @AiServiceSelector(PaymentService.class)
InputGuardrailExecutedEvent event) {
if (isFailure(event)) {
String requestId = getRequestId(event);
MonitoringContext context = contexts.get(requestId);
if (context != null) {
context.guardrailFailures++;
LOG.warnf("Guardrail failure for user %s: %s",
context.userId, getFailureReason(event));
audit.log("GUARDRAIL_FAILURE", context.userId,
Map.of("reason", getFailureReason(event)));
}
}
}
void onCompleted(@Observes @AiServiceSelector(PaymentService.class)
AiServiceCompletedEvent event) {
String requestId = getRequestId(event);
MonitoringContext context = contexts.remove(requestId);
if (context != null) {
long duration = System.currentTimeMillis() - context.startTime;
Timer.builder("payment.service.duration")
.tag("user", context.userId)
.register(metrics)
.record(duration, TimeUnit.MILLISECONDS);
Counter.builder("payment.service.tool.executions")
.tag("user", context.userId)
.register(metrics)
.increment(context.toolExecutions);
LOG.infof("Payment service completed: user=%s, duration=%dms, tools=%d, failures=%d",
context.userId, duration, context.toolExecutions, context.guardrailFailures);
audit.log("PAYMENT_SERVICE_COMPLETED", context.userId, Map.of(
"duration", duration,
"toolExecutions", context.toolExecutions,
"guardrailFailures", context.guardrailFailures
));
}
}
void onError(@Observes @AiServiceSelector(PaymentService.class)
AiServiceErrorEvent event) {
String requestId = getRequestId(event);
MonitoringContext context = contexts.remove(requestId);
if (context != null) {
LOG.errorf("Payment service error for user %s: %s",
context.userId, getErrorMessage(event));
Counter.builder("payment.service.errors")
.tag("user", context.userId)
.register(metrics)
.increment();
audit.log("PAYMENT_SERVICE_ERROR", context.userId, Map.of(
"error", getErrorMessage(event)
));
}
}
private static class MonitoringContext {
long startTime;
String userId;
int toolExecutions;
int guardrailFailures;
}
}AI Service Method Invocation
├─→ STARTED event
├─→ INPUT_GUARDRAIL_EXECUTED events (per guardrail)
├─→ TOOL_EXECUTED events (per tool call)
├─→ OUTPUT_GUARDRAIL_EXECUTED events (per guardrail)
├─→ RESPONSE_RECEIVED event
└─→ COMPLETED event (success) OR ERROR event (failure)@ObservesAsyncInstall with Tessl CLI
npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-core