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() 맵으로 데이터를 주고받을 수 있는데, 이 맵은 같은 리스너의 메서드 간은 물론 여러 리스너 사이에서도 공유돼요. 호출별 메타데이터를 리스너에 넘기려면 ChatRequestOptions 의 listenerAttributes 를 쓰면 돼요(테넌트·상관 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(...) 를 노출하면 감싸기 없이 그쪽을 쓰는 게 좋아요. EmbeddingStoreListener 는 embeddingStore.addListener(...), ContentRetrieverListener 는 contentRetriever.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 API 로 ChatModelListener 를 구현해 메트릭과 트레이스를 투명하게 생성해요.
- 트레이스: 채팅 상호작용마다 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-bridges 는 OpenTelemetry 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 빈을 자동으로 찾아 텔레메트리로 감싸요.
더 알아보기
- Spring Boot 통합 — Spring Boot 에서의 리스너·메트릭 설정
- AI Services — 관측 이벤트의 기반이 되는 선언적 서비스
- Non-blocking and Reactive — 리스너 콜백이 블로킹되지 않아야 하는 이유
- OpenTelemetry GenAI semantic conventions
- Micrometer 관측성 관찰 앱 구축: Observability with Spring Boot 3