Observability

Observability (관측성)

AI 기능이 돌아가는 애플리케이션을 운영하다 보면 "지금 LLM 호출이 어떻게 진행되고 있지? 어느 단계에서 실패했지?"를 알고 싶어져요. LangChain4j 는 이를 위해 AI Service 레벨 이벤트, ChatModel 레벨 리스너, Micrometer 메트릭·트레이스 등 계층별 관측 수단을 제공해요. 이 페이지에서 그 구조를 정리할게요.

출처: 공식문서

AI Service 관측성

:::note AI Service 관측성은 실험 기능이에요. API 와 동작이 향후 버전에서 바뀔 수 있어요. :::

AI Service 관측 메커니즘은 AiService 호출 중 무슨 일이 일어나는지 추적하게 해줘요. 한 번의 호출에 여러 번의 LLM 호출이 포함될 수 있고, 그중 어떤 것은 성공하고 어떤 것은 실패할 수 있어요. AI Service 관측성은 그 전체 호출 시퀀스와 결과를 추적할 수 있게 해줘요. 이 기능은 AI Services 를 쓸 때만 사용할 수 있어요.

이 구현은 원래 Quarkus LangChain4j 확장에 있었던 것을 백포팅한 것이에요.

이벤트 유형

각 이벤트 유형은 고유 식별자를 갖고 있어 여러 호출 간 이벤트를 상호 연관지을 수 있어요. 각 이벤트는 InvocationContext 안에 담긴 정보를 포함해요. 현재 제공되는 이벤트 유형은 다음과 같아요:

Event Name Description
AiServiceStartedEvent LLM 호출이 시작됐을 때
AiServiceRequestIssuedEvent LLM 요청 직전에 발생. 요청 세부 내용 포함. 도구·가드레일이 있으면 한 번의 AiService 호출에서 여러 번 호출될 수 있어요
AiServiceResponseReceivedEvent LLM 응답을 받았을 때. 응답과 그에 대응하는 요청 포함. 역시 여러 번 호출될 수 있어요
AiServiceErrorEvent LLM 호출이 실패했을 때. 네트워크 실패, AiService 사용 불가, 가드레일 차단 등 다양한 원인
AiServiceCompletedEvent LLM 호출이 성공적으로 완료됐을 때
ToolExecutedEvent 도구 호출이 완료됐을 때. 한 번의 LLM 호출 안에서 여러 번 호출될 수 있어요
ToolCompensatedEvent 이미 완료된 도구가 나중에 취소·실패로 보상(compensate)됐을 때. 실험적이며 논블로킹 모드가 발생시켜요
InputGuardrailExecutedEvent 입력 가드레일 검증이 실행됐을 때. 가드레일 호출마다 하나씩 발생
OutputGuardrailExecutedEvent 출력 가드레일 검증이 실행됐을 때

이벤트 수신(Listening)

각 이벤트 유형마다 자신의 리스너 인터페이스가 있어요 (AiServiceStartedListener, AiServiceRequestIssuedListener, AiServiceResponseReceivedListener, AiServiceErrorListener, AiServiceCompletedListener, ToolExecutedEventListener, ToolCompensatedEventListener, InputGuardrailExecutedListener, OutputGuardrailExecutedListener). 원하는 이벤트만 골라 리스너를 만들면 돼요.

리스너를 정의한 뒤에는 AI Service 를 만들 때 등록해요. AiServices 클래스에 registerListener 변형 메서드가 여럿 있어요. AiServiceCompletedEvent 리스너를 만들어 등록하는 예시예요:

import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import dev.langchain4j.observability.api.AiServiceListenerRegistrar;
import dev.langchain4j.observability.api.event.AiServiceCompletedEvent;
import dev.langchain4j.observability.api.listener.AiServiceCompletedListener;
import dev.langchain4j.invocation.InvocationContext;

public class MyAiServiceCompletedListener implements AiServiceCompletedListener {
    @Override
    public void onEvent(AiServiceCompletedEvent event) {
        InvocationContext invocationContext = event.invocationContext();
        Optional<Object> result = event.result();

        // The invocationId will be the same for all events related to the same LLM invocation
        UUID invocationId = invocationContext.invocationId();
        String aiServiceInterfaceName = invocationContext.interfaceName();
        String aiServiceMethodName = invocationContext.methodName();
        List<Object> aiServiceMethodArgs = invocationContext.methodArguments();
        Object chatMemoryId = invocationContext.chatMemoryId();
        Instant eventTimestamp = invocationContext.timestamp();

        // Do something with the data
    }
}

// When creating your AI Service
MyAiServiceCompletedListener myListener = new MyAiServiceCompletedListener();

var myService = AiServices.builder(MyAiService.class)
        .chatModel(chatModel)  // Could also be .streamingChatModel(...)
        .registerListener(myListener)
        .build();

직접 이벤트·리스너 만들기

AiServiceEvent 인터페이스를 구현해 자신의 이벤트를 정의하고, AiServiceListener 를 구현해 리스너를 만들 수 있어요. 그다음 AiServiceListenerRegistrar 인스턴스를 얻어 fireEvent(event) 로 이벤트를 발생시키면 돼요. 확장 지점으로 AiServiceListenerRegistrarFactory 를 구현하고 Java SPI 로 등록해 나만의 AiServiceListenerRegistrar 를 만들 수도 있어요.

ChatModel 관측성

일부 ChatModel/StreamingChatModel 구현(관측성 컬럼 항목)은 ChatModelListener 를 설정해 LLM 요청·응답·오류 이벤트를 들을 수 있어요. 이벤트는 OpenTelemetry Generative AI Semantic Conventions 에 설명된 속성들을 포함해요 — 요청엔 Messages, Model, Temperature, Top P, Max Tokens, Tools, Response Format 등을, 응답엔 Assistant Message, ID, Model, Token Usage, Finish Reason 등을 담아요.

ChatModelListener 사용 예시의 핵심만 보면, onRequest/onResponse/onError 세 콜백을 구현하고 빌더의 .listeners(List.of(listener)) 로 모델에 붙여요. 리스너 내부에서 requestContext.attributes() 맵으로 데이터를 주고받을 수 있는데, 이 맵은 같은 리스너의 메서드 간은 물론 여러 리스너 사이에서도 공유돼요. 호출별 메타데이터를 리스너에 넘기려면 ChatRequestOptionslistenerAttributes 를 쓰면 돼요(테넌트·상관 ID 등). 이 옵션들은 LangChain4j 호출 체인 안에서만 쓰이고 LLM 프로바이더로는 전송되지 않아요.

ChatRequest chatRequest = ChatRequest.builder()
        .messages(UserMessage.from("Tell me a joke about Java"))
        .build();

ChatRequestOptions options = ChatRequestOptions.builder()
        .addListenerAttribute("tenantId", "tenant-123")
        .addListenerAttribute("correlationId", "corr-456")
        .build();

model.chat(chatRequest, options);

StreamingChatModel.chat(chatRequest, options, handler) 도 같아요.

리스너 동작 방식

  • 리스너는 List<ChatModelListener> 로 지정되며 반복 순서대로 호출돼요.
  • 리스너는 동기적으로, 같은 스레드에서 호출돼요. 두 번째 리스너는 첫 번째가 반환해야 호출돼요.
  • onRequest() 는 LLM 프로바이더 API 호출 직전에 호출되고, 요청당 한 번만 호출돼요. 재시도가 일어나도 재시도마다 다시 호출되지 않아요.
  • onResponse() 는 성공 응답 직후 한 번, onError() 도 한 번만 호출돼요.
  • 리스너 메서드에서 예외가 던져지면 WARN 레벨로 로깅되고 나머지 리스너 실행은 계속돼요.
  • 스트리밍의 경우 onResponse()/onError()onRequest() 와 다른 스레드에서 호출돼요. 스레드 컨텍스트는 자동 전파되지 않으므로 attributes 맵으로 데이터를 전달하세요.

Moderation 모델 관측성

리스너를 지원하는 ModerationModel 구현(OpenAiModerationModel, MistralAiModerationModel, WatsonxModerationModel)은 ModerationModelListener 를 설정할 수 있어요. 요청·응답·오류 이벤트를 듣고 attributes 로 시작 시각을 기록해 응답 은닉 시간을 재는 방식도 동일해요.

RAG 관측성 (EmbeddingModel, EmbeddingStore, ContentRetriever)

EmbeddingModel, EmbeddingStore, ContentRetriever 는 리스너로 계측할 수 있어요. 관측할 수 있는 것은:

  • 지연(latency) — attributes 로 시작 시각을 기록해 지속시간 측정
  • 페이로드 — 예: EmbeddingSearchRequest.queryEmbedding() 와 검색된 매치/콘텐츠
  • 오류

예를 들어 EmbeddingModelListener 를 구현하고 빌더의 listeners(...) 로 붙이거나(OpenAiEmbeddingModel.builder()...listeners(List.of(...))), 이미 만든 모델을 embeddingModel.addListener(new MyEmbeddingModelListener()) 로 감싸 붙일 수 있어요. 빌더가 listeners(...) 를 노출하면 감싸기 없이 그쪽을 쓰는 게 좋아요. EmbeddingStoreListenerembeddingStore.addListener(...), ContentRetrieverListenercontentRetriever.addListener(...) 로 붙여요.

리스너 동작 규칙은 ChatModel 과 동일해요: 리스트 순서대로 동기 호출, onRequest 는 작업 직전, onResponse 는 성공 후 한 번, onError 는 예외 시 한 번, 리스너 예외는 WARN 로깅 후 무시.

Micrometer 메트릭

langchain4j-micrometer-metrics 모듈은 Micrometer 기반 메트릭 구현을 제공해요. ChatModel/StreamingChatModel 상호작용에서 ChatModelListener 구현이 Micrometer 의 MeterRegistry 로 메트릭을 수집해요. 메트릭 이름은 OpenTelemetry Generative AI Metrics Semantic Conventions(v1.39.0) 를 따릅니다.

⚠️ Experimental: 이 모듈은 @Experimental 로 표시되어 있어 향후 버전에서 파괴적 변경이 있을 수 있어요. ⚠️ Warning: OpenTelemetry Generative AI Semantic Conventions 는 현재 실험적이며 안정적이지 않아요. 컨벤션이 업데이트되면 대시보드·알림·자동화에 파괴적 변경이 필요할 수 있어요.

수집 메트릭

Metric Name Type Description
gen_ai.client.token.usage Histogram (DistributionSummary) 채팅 모델 요청당 사용된 input·output 토큰 수

gen_ai.client.token.usage 의 태그

Tag Description Example Values
gen_ai.operation.name 수행 중인 연산 chat
gen_ai.provider.name AI 프로바이더 이름 openai, azure.ai.inference, anthropic
gen_ai.request.model 요청의 모델 이름 gpt-4, gpt-35-turbo
gen_ai.response.model 응답의 모델 이름 gpt-4-0613
gen_ai.token.type 카운트된 토큰 타입 input, output

MicrometerMetricsChatModelListener 만들기

import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.micrometer.metrics.listeners.MicrometerMetricsChatModelListener;
import dev.langchain4j.model.azure.AzureOpenAiChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import io.micrometer.core.instrument.MeterRegistry;

import java.util.List;

// Get the MeterRegistry
MeterRegistry meterRegistry = new SimpleMeterRegistry();

// 1. Create the listener with the MeterRegistry and AI system name
MicrometerMetricsChatModelListener listener = 
    new MicrometerMetricsChatModelListener(meterRegistry);

// 2. Add the listener to your ChatModel
AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder()
        .endpoint(System.getenv("AZURE_OPENAI_ENDPOINT"))
        .apiKey(System.getenv("AZURE_OPENAI_KEY"))
        .deploymentName(System.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"))
        .listeners(List.of(listener))
        .build();

// 3. Use the chat model as usual - metrics are collected automatically
ChatResponse response = chatModel.chat(ChatRequest.builder()
        .messages(UserMessage.from("Hello!"))
        .build());

Micrometer Observation API

langchain4j-observation 모듈은 Micrometer Observation APIChatModelListener 를 구현해 메트릭과 트레이스를 투명하게 생성해요.

  • 트레이스: 채팅 상호작용마다 span 을 생성해요.
  • 메트릭: gen_ai_client_token_usage, gen_ai_client_operation_duration 히스토그램.

리스너 콜백은 블로킹하면 안 돼요. ChatModelListener·EmbeddingModelListener 콜백은 모델 자신의 스레드에서 동기적으로 호출되고 절대 offload 되지 않아요. 비동기·리액티브 API 에서 onResponse/onError 는 응답을 읽는 transport I/O 워커에서 실행돼요. 그 스레드에서 블로킹 I/O(동기 DB 쓰기, 관측 백엔드로의 동기 HTTP)를 하면 그 워커가 멈춰 동시성 상황에서 모든 in-flight 호출의 처리량이 떨어져요. 메트릭 기록이나 span 시작/종료처럼 논블로킹으로 설계된 것을 쓰고, 정말 블로킹해야 한다면 콜백 안에서 자신의 executor 로 offload 하세요. (참고: 논블로킹·리액티브)

Spring Boot 애플리케이션에서의 관측성

Spring Boot 에서의 상세는 spring-boot-integration#observability 를, Micrometer 메트릭 수집은 spring-boot-integration#micrometer-metrics 를 참고하세요.

서드파티 통합

  • Arize Phoenix: Arize AI 의 오픈소스·셀프호스트 로컬 트레이스 검사·실험 옵션.
  • Arize AX: 프로덕션 LangChain4j 앱용 관리형 클라우드·엔터프라이즈 셀프호스트 관측성 지원.

OpenTelemetry GenAI instrumentation

커뮤니티 유지 프로젝트 otel-genai-bridgesOpenTelemetry Generative AI semantic conventions 로 LangChain4j 채팅 앱을 자동 계측하는 Spring Boot starter 를 제공해요.

  • 어떤 ChatModel 빈이든 감싸 span·이벤트·메트릭을 방출해요.
  • 프롬프트, 완성, 도구 호출, 지연, 토큰 사용량, 비용, RAG 검색 지연을 즉시 캡처해요.
  • Docker Compose 샘플(Collector → Tempo/Prometheus → Grafana)과 미리 만든 Grafana 대시보드를 제공해요.

pom.xml 에 starter 를 추가하고 (com.dineshkumarkummara.otel:langchain4j-otel:0.1.0-SNAPSHOT), application.yaml 에서 활성화해요:

otel:
  langchain4j:
    enabled: true
    system: openai
    default-model: gpt-4o
    capture-prompts: true
    capture-completions: true
    cost:
      enabled: true
      input-per-thousand: 0.0005
      output-per-thousand: 0.0015

중첩된 cost 스탠자는 선택이에요. 토큰당 비용 메트릭을 원할 때 넣으면 돼요. 의존성이 클래스패스에 있으면 starter 가 ChatModel 빈을 자동으로 찾아 텔레메트리로 감싸요.

더 알아보기