CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-core

Core runtime module for Quarkus LangChain4j integration with declarative AI services, guardrails, and observability

Overview
Eval results
Files

observability.mddocs/

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.

Core Imports

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;

@AiServiceSelector Annotation

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();
}

Usage

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");
    }
}

AiServiceSelectorLiteral

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));
    }
}

AiServiceEvents Enum

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();
}

AiServiceListenerAdapter

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:

  • Created automatically by AiServiceEvents.createListener(Class<?> aiServiceClass)
  • Receives events from LangChain4j's observability framework
  • Fires CDI events qualified with @AiServiceSelector for the specific AI service
  • Integrates seamlessly with Quarkus CDI event bus

Usage 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());
    }
}

Event Types

  • STARTED: Fired when AI service method invocation begins
  • RESPONSE_RECEIVED: Fired when response is received from the model
  • TOOL_EXECUTED: Fired after each tool execution
  • INPUT_GUARDRAIL_EXECUTED: Fired after each input guardrail execution
  • OUTPUT_GUARDRAIL_EXECUTED: Fired after each output guardrail execution
  • COMPLETED: Fired when AI service method completes successfully
  • ERROR: Fired when AI service method fails with an error

Event Classes

All event classes come from LangChain4j's observability API: dev.langchain4j.observability.api.event.*

AiServiceStartedEvent

package dev.langchain4j.observability.api.event;

public class AiServiceStartedEvent extends AiServiceEvent {
    // Event data for service start
}

AiServiceResponseReceivedEvent

package dev.langchain4j.observability.api.event;

public class AiServiceResponseReceivedEvent extends AiServiceEvent {
    // Event data for model response
}

ToolExecutedEvent

package dev.langchain4j.observability.api.event;

public class ToolExecutedEvent extends AiServiceEvent {
    // Event data for tool execution
}

InputGuardrailExecutedEvent

package dev.langchain4j.observability.api.event;

public class InputGuardrailExecutedEvent extends AiServiceEvent {
    // Event data for input guardrail execution
}

OutputGuardrailExecutedEvent

package dev.langchain4j.observability.api.event;

public class OutputGuardrailExecutedEvent extends AiServiceEvent {
    // Event data for output guardrail execution
}

AiServiceCompletedEvent

package dev.langchain4j.observability.api.event;

public class AiServiceCompletedEvent extends AiServiceEvent {
    // Event data for successful completion
}

AiServiceErrorEvent

package dev.langchain4j.observability.api.event;

public class AiServiceErrorEvent extends AiServiceEvent {
    // Event data for error
}

Usage Examples

Basic Event Observation

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);
    }
}

Service-Specific Observation

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");
    }
}

Tool Execution Monitoring

@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);
    }
}

Guardrail Execution Monitoring

@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);
    }
}

Metrics Collection

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();
    }
}

Audit Logging

@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());
        }
    }
}

Cost Tracking

@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);
        }
    }
}

Error Alerting

@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
        }
    }
}

Response Logging

@ApplicationScoped
public class ResponseLogger {
    void logResponse(@Observes AiServiceResponseReceivedEvent event) {
        LOG.infof("AI Response received: service=%s, response=%s",
            getServiceName(event),
            getResponseSummary(event));
    }
}

Comprehensive Monitoring

@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;
    }
}

Event Flow

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)

Notes

  • Event Source: All events come from LangChain4j's observability API
  • CDI Integration: Events are fired through CDI's event bus
  • Selector Qualifier: Use @AiServiceSelector to filter events by service
  • Async Processing: Event observers can be asynchronous using @ObservesAsync
  • Event Data: Each event contains relevant context and data about the operation
  • Thread Safety: Event observers should be thread-safe if processing asynchronously

Install with Tessl CLI

npx tessl i tessl/maven-io-quarkiverse-langchain4j--quarkus-langchain4j-core@1.7.0

docs

ai-services.md

authentication.md

cost-estimation.md

guardrails.md

index.md

media-content.md

memory.md

observability.md

response-augmentation.md

tools.md

tile.json