OpenTelemetry, Aspire, 그리고 Application Insights를 활용한 에이전트 관측성 (Observability)
요약
에이전트 시스템의 복잡한 동작을 추적하기 위해 OpenTelemetry, Aspire, Application Insights를 활용한 관측성 구축 방법을 다룹니다. 단순 로그를 넘어 모델 호출, 도구 실행, 승인 프로세스 등을 체인 형태로 시각화하여 디버깅과 비용 관리를 최적화하는 전략을 제시합니다.
핵심 포인트
- 에이전트 실행을 단순 로그가 아닌 모델·도구 호출의 체인으로 파악해야 함
- OpenTelemetry를 통해 표준화된 텔레메트리 형식 구축 가능
- 비용, 지연 시간, 실패 원인 분석을 위한 상세 트레이스 설계 필요
- 운영 환경에서 프롬프트 및 도구 데이터의 보안 및 개인정보 보호 주의
이 글은 Microsoft Agent Framework 시리즈의 17번째 파트입니다. 원문 게시글은 lukaswalter.dev에서 확인하실 수 있습니다.
이전 기사에서 우리는 AG-UI와 에이전트를 위한 프론트엔드 경계(frontend boundary)를 살펴보았습니다.
핵심 요점은 에이전트의 동작이 단순한 채팅 텍스트로 평탄화(flattened)되어서는 안 된다는 것이었습니다.
도구 호출 (Tool calls), 승인 (approvals), 상태 업데이트 (state updates), 그리고 취소 (cancellations)는 별개의 이벤트입니다.
이 규칙은 운영 환경 (production)에서도 동일하게 적용됩니다.
에이전트 실행을 하나의 불투명한 로그 라인으로 취급하지 마십시오.
에이전트 실행은 모델 호출 (model calls), 도구 호출 (tool calls), 재시도 (retries), 승인 (approvals), 상태 변경 (state changes), 그리고 외부 의존성 (external dependencies)의 체인입니다.
만약 이 체인을 볼 수 없다면, 비용 (cost), 지연 시간 (latency), 실패 (failure), 또는 안전하지 않은 동작 (unsafe behavior)을 디버깅할 수 없습니다.
나는 트레이스 (trace)가 다음과 같은 형태를 띠기를 원합니다:
HTTP request
-> agent run
-> model call
...
해당 트레이스를 통해, 나는 일반적인 운영 환경의 질문들에 답하고 싶습니다:
- 어떤 모델이 호출되었는가?
- 입력 및 출력 토큰 (input and output tokens)이 얼마나 사용되었는가?
- 어떤 도구 (tool)가 실행되었는가?
- 각 단계에 시간이 얼마나 걸렸는가?
- 도구가 실패했는가, 아니면 재시도했는가?
- 사람이 부작용 (side effect)을 승인했는가?
- 민감한 프롬프트 (prompt)나 도구 데이터가 캡처되었는가?
- 클라우드로 보내기 전에 로컬에서 동일한 흐름을 검사할 수 있는가?
OpenTelemetry는 공유된 텔레메트리 (telemetry) 형식입니다.
Aspire는 나에게 로컬 루프 (local loop)를 제공합니다.
Application Insights는 Azure Monitor의 에이전트 중심 뷰를 포함하여, 동일한 시그널 (signals)들을 Azure에서 검색 가능하게 만드는 곳입니다.
질문부터 시작하기
패키지를 추가하기 전에, 텔레메트리가 무엇을 설명해야 하는지 결정하십시오.
대부분의 에이전트 시스템의 경우, 나는 다음 질문들부터 시작할 것입니다:
| 질문 | 시그널 (Signal) |
|---|---|
| 에이전트가 느린가? | 실행 (run), 모델 호출 (model call), 도구 호출 (tool call), 의존성 호출 (dependency call)별 트레이스 지속 시간 |
| ... |
표에 없는 내용에 주목하십시오:
나중에 도움이 되기를 바라며 전체 프롬프트를 로그에 남기는 것.
이는 개발 환경에서는 도움이 될 수 있습니다.
하지만 운영 환경(Production)에서는 종종 잘못된 기본 설정이 될 수 있습니다.
프롬프트(Prompts), 도구 인자(tool arguments), 검색된 문서(retrieved documents), 그리고 도구 결과(tool results)에는 고객 데이터, 비밀 정보(secrets), 내부 티켓, 파일 내용 또는 개인 식별 정보(PII)가 포함될 수 있기 때문입니다.
관측성 (Observability)이 모든 것을 저장해야 한다는 의미는 아닙니다.
관측성 데이터를 다른 곳에 적용하는 것과 동일한 접근 제어 및 보관 규정(retention discipline)을 적용하여, 이를 운영 데이터(production data)처럼 취급하십시오.
기본적인 OpenTelemetry 설정
에이전트 프레임워크(Agent Framework)는 Microsoft.Extensions.AI를 기반으로 구축되었습니다.
이것이 중요한 이유는 Microsoft.Extensions.AI가 채팅 클라이언트(chat clients)를 위한 OpenTelemetry 미들웨어(middleware)를 제공하기 때문입니다.
확장 메서드는 ChatClientBuilder에 있는 UseOpenTelemetry입니다.
채팅 클라이언트부터 시작하십시오:
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
...
여기서 두 가지 세부 사항이 중요합니다.
첫째, sourceName은 의도된 설정입니다.
OpenTelemetry 트레이싱 (tracing)은 트레이서 프로바이더 (tracer provider)가 경청하는 활동 소스(activity sources)만 수집합니다.
Aspire 서비스 기본값은 이미 애플리케이션 이름을 트레이스 소스 (trace source)로 추가합니다.
builder.Environment.ApplicationName을 사용하면 채팅 클라이언트 활동 소스를 애플리케이션 텔레메트리 (telemetry) 소스와 일치하게 유지할 수 있습니다.
둘째, 개발 설정에서 명시적으로 활성화하지 않는 한 민감한 데이터는 비활성화됩니다.
기본적으로 OpenTelemetry 채팅 클라이언트는 모델 정보 및 토큰 수와 같은 메타데이터 (metadata)를 기록하지만, 원시 프롬프트 (raw prompts), 원시 출력 (raw outputs), 도구 인자 (tool arguments) 또는 도구 결과 (tool results)는 기록하지 않습니다.
이러한 기본 설정은 가장 좋은 의미에서 매우 무난합니다.
참고: 정확한 텔레메트리 속성 (telemetry attributes)은 OpenTelemetry Generative AI 시맨틱 컨벤션 (semantic conventions)을 따릅니다. 해당 컨벤션은 여전히 변경 중이므로, 대시보드나 KQL 쿼리에 고정하기 전에 패키지 버전을 기준으로 속성 이름을 확인하십시오.
서비스 기본값(Service Defaults)에 AI 소스 추가
Aspire를 사용 중이라면, 앱에 ConfigureOpenTelemetry 메서드가 포함된 ServiceDefaults 프로젝트가 있을 것입니다.
스타터 템플릿(starter template)은 이미 ASP.NET Core, HttpClient, 런타임 메트릭 (runtime metrics), OTLP 내보내기 (export), 그리고 트레이스 (trace) 수집을 구성해 두었습니다.
에이전트 텔레메트리 (agent telemetry)를 위해서는 UseOpenTelemetry에서 사용되는 것과 동일한 소스 이름이 트레이스 (traces)와 메트릭 (metrics) 모두에 대해 수집되도록 해야 합니다.
서비스 기본값 (service defaults) 프로젝트에서 OpenTelemetry 설정을 확장하십시오:
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
...
다음 두 줄이 중요합니다:
.AddMeter(builder.Environment.ApplicationName)
.AddSource(builder.Environment.ApplicationName)
채팅 클라이언트 (chat client)는 ActivitySource와 Meter를 생성합니다.
만약 OpenTelemetry 설정이 이를 수신(listen)하지 않는다면, 코드가 계측 (instrumented)되었더라도 대시보드에는 아무것도 나타나지 않을 수 있습니다.
채팅 클라이언트가 제공하는 정보
OpenTelemetry 채팅 클라이언트는 설정 과정에서 가장 쉬운 부분입니다.
이는 IChatClient 파이프라인 (pipeline)에 위치하며, 해당 클라이언트를 통해 이루어지는 호출을 관측 (observes)합니다.
일반적인 에이전트 실행 시, 다음과 같은 모델 메타데이터 (model metadata)를 기록할 수 있습니다:
- 작업 이름 (operation name)
- 요청된 모델 (requested model)
- 응답 모델 (response model)
- 제공자 이름 (provider name)
- 서버 주소 (server address)
- 스트리밍 플래그 (streaming flag)
- 종료 사유 (finish reason)
- 응답 ID (response id)
- 입력 토큰 수 (input token count)
- 출력 토큰 수 (output token count)
- 사용 가능한 경우, 캐시된 입력 토큰 수 (cached input token count)
- 사용 가능한 경우, 추론 출력 토큰 수 (reasoning output token count)
- 작업 지속 시간 (operation duration)
- 첫 번째 스트리밍 청크까지의 시간 (time to first streamed chunk)
- 스트리밍된 청크 간의 시간 (time between streamed chunks)
이 정보만으로도 운영 환경에서 발생하는 첫 번째 질문 세트들에 답할 수 있습니다.
예를 들어:
왜 이 요청이 34초나 걸렸나요?
-> 첫 번째 모델 호출에 1.4초가 소요되었습니다.
-> 검색 도구 (search tool) 사용에 29초가 소요되었습니다.
...
트레이스 (trace)는 "에이전트가 느렸다"라는 모호한 상황을 실제로 수정할 수 있는 정보로 바꿔줍니다.
토큰 사용량은 운영 신호입니다
토큰 사용량은 과금 데이터이기도 하지만, 행동 데이터 (behavior data)이기도 합니다.
높은 입력 토큰 수는 다음과 같은 의미일 수 있습니다:
- 채팅 리듀서 (chat reducer)가 실행되지 않음
- 검색 (retrieval) 결과로 너무 많은 컨텍스트가 반환됨
- 에이전트가 오래된 상태 (stale state)를 유지하고 있음
- 프론트엔드가 너무 많은 히스토리를 재전송함
- 도구 (tool) 결과가 너무 장황함
- 내부 에이전트 (inner agent)가 전체 대화 내용을 외부 에이전트 (outer agent)로 쏟아냄
높은 출력 토큰 (High output tokens) 수는 다음과 같은 의미일 수 있습니다:
- 에이전트가 과하게 설명함
- 지침 (instructions)이 너무 광범위함
- 구조화된 출력 (structured output)에 제약이 없음
- 모델이 수정 루프 (repair loop)에 빠짐
- 사용자가 대규모 결과물 (artifact)을 요청함
OpenTelemetry 미들웨어는 제공자 (provider)가 사용량 정보를 반환할 때 토큰 사용량을 기록합니다.
로컬 가드레일 (local guardrail)이 필요한 경우에는 응답에서 직접 사용량을 검사할 수도 있습니다:
AgentResponse response = await agent.RunAsync(
"Check release-2026-07-08 and tell me whether it is ready.",
session,
...
이를 주요 프로덕션 텔레메트리 (production telemetry) 경로로 전환하지는 마십시오.
지속적인 트레이스 (traces)와 메트릭 (metrics)에는 OpenTelemetry를 사용하십시오.
로컬 단언 (assertions), 테스트 또는 즉각적인 가드레일에는 직접적인 응답 사용량 검사를 사용하십시오.
예를 들어, 작아야 할 경로가 거대한 프롬프트 (prompt)를 소비하기 시작할 때 통합 테스트 (integration test)가 실패할 수 있습니다:
response.Usage?.InputTokenCount.Should().BeLessThan(8_000);
이를 통해 프롬프트 및 검색 회귀 (regressions)가 클라우드 비용 청구서로 이어지기 전에 잡아낼 수 있습니다.
도구 호출 (Tool calls)에는 자체 스팬 (spans)이 필요합니다
모델 텔레메트리 (Model telemetry)만으로는 충분하지 않습니다.
많은 에이전트 실패는 모델 외부에서 발생합니다:
- 검색 인덱스 (search index)가 느림
- MCP 서버가 다운됨
- 배포 API가 변경 티켓 (change ticket)을 거부함
- 데이터베이스 쿼리가 타임아웃됨
- 도구 결과가 너무 큼
- 승인은 되었으나 실행에 실패함
도구가 일반적인 C#이라면, 일반적인 C#처럼 계측 (instrument)하십시오.
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
...
그런 다음 도구 소스를 수집하십시오:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
...
대시보드용으로는 저카디널리티 (low-cardinality) 태그를 사용하세요.
예를 들어, deployment.environment=production은 괜찮습니다.
가공되지 않은 releaseId, 사용자 메시지, 고객 이름, 이메일 주소, 그리고 티켓 설명 등은 더 세심한 주의가 필요합니다.
원시 값(raw values) 없이 상관관계 (correlation)를 유지해야 할 때는, 지원 프로세스에서 접근 제어 하에 확인할 수 있는 안정적인 해시 (hash) 또는 내부 ID를 로그로 남기세요.
승인 텔레메트리 (Approval telemetry)는 도구 텔레메트리와 별개입니다
Human-in-the-loop 관련 기사에서 규칙은 다음과 같았습니다:
모델이 동작을 요청합니다.
애플리케이션이 해당 동작을 계속할지 결정합니다.
텔레메트리는 이러한 분리 상태를 유지해야 합니다.
승인 요청, 인간의 결정, 그리고 최종적인 도구 실행은 서로 연관되어 있지만, 동일한 이벤트는 아닙니다.
저는 보통 다음과 같은 승인 필드들을 원합니다:
approvalIdconversationIdagentRunIdtraceIdtoolNametoolArgumentsHashrequestedByapprovedBydecisiondecisionReasoncreatedAtdecidedAtexecutedAtexecutionResult
승인 로그는 다음과 같은 형태일 수 있습니다:
logger.LogInformation(
"Agent tool approval decided. ApprovalId={ApprovalId} ToolName={ToolName} Decision={Decision} TraceId={TraceId}",
approval.Id,
...
기본적으로 전체 도구 인자 (tool arguments)를 로그로 남기지 마세요.
운영 환경 (production)의 경우, 저는 보통 다음과 같은 방식을 선호합니다:
logger.LogInformation(
"Approval request created. ApprovalId={ApprovalId} ToolName={ToolName} ArgumentsHash={ArgumentsHash}",
approval.Id,
...
사용자 인터페이스 (UI)는 승인 시점에 정확한 인자들을 보여줄 수 있습니다.
그렇다고 해서 텔레메트리 백엔드가 해당 인자들을 영구적으로 보관해야 한다는 의미는 아닙니다.
민감한 데이터는 기능 플래그 (feature flag)이지, 가벼운 토글이 아닙니다
OpenTelemetry 채팅 클라이언트에는 EnableSensitiveData 설정이 있습니다.
또한 다음과 같은 환경 변수도 존재합니다:
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
이 설정이 활성화되면, 텔레메트리에 다음과 같은 원시 입력 및 출력 값이 포함될 수 있습니다:
- 시스템 지침 (system instructions)
- 사용자 메시지 (user messages)
- 어시스턴트 메시지 (assistant messages)
- 도구 정의 (tool definitions)
- 도구 호출 인자 (tool call arguments)
- 도구 호출 결과 (tool call results)
- 추가적인 요청 및 응답 속성 (additional request and response properties)
이는 로컬 디버깅 (local debugging) 중에 도움이 됩니다.
하지만 프로덕션 (production) 환경에서는 데이터 보존 (data-retention) 문제가 될 수 있습니다.
저는 이를 명시적인 환경 정책 (environment policy)으로 다룰 것입니다:
.UseOpenTelemetry(
sourceName: builder.Environment.ApplicationName,
configure: telemetry =>
...
설정을 명시적으로 만드세요:
{
"AI": {
"Telemetry": {
...
프로덕션의 경우, 보안 (security), 개인정보 보호 (privacy), 그리고 컴플라이언스 (compliance) 부서의 의견을 반영하여 결정하십시오.
만약 프롬프트 캡처 (prompt capture)를 활성화한다면, 다음 사항들도 함께 결정해야 합니다:
- 어떤 환경에서 캡처를 허용할 것인가
- 누가 이를 쿼리 (query)할 수 있는가
- 얼마나 오래 보존할 것인가
- 샘플링 (sampling)을 적용할 것인가
- 내보내기 (export) 전에 어떤 필드를 비식별화 (redact)할 것인가
- 고객 데이터가 리전 (region) 또는 테넌트 (tenant) 경계를 넘을 수 있는가
- 도구 결과에 비밀 정보 (secrets)나 기밀 문서가 포함되는가
기본값은 메타데이터 (metadata)만 포함해야 합니다.
디버깅의 가치가 데이터 리스크를 정당화할 때만 콘텐츠 캡처를 켜십시오.
가능한 경우 내보내기 전에 비식별화 (Redact) 하세요
OpenTelemetry 프로세서 (processors)는 텔레메트리가 프로세스를 떠나기 전에 이를 수정할 수 있습니다.
에이전트 시스템 (agent systems)의 경우, 민감한 콘텐츠가 속성 (attributes), 로그 (logs), 또는 예외 메시지 (exception messages)에 자주 나타나기 때문에 이 점이 중요합니다.
최소한의 커스텀 프로세서를 사용하여 알려진 고위험 속성을 제거할 수 있습니다:
using System.Diagnostics;
using OpenTelemetry;
...
트레이싱 (tracing)에 이를 등록합니다:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
...
이것은 단지 스케치일 뿐입니다.
실제 비식별화는 보통 애플리케이션 경계 (application boundary)에 더 가까운, 더 이른 단계에서 이루어져야 합니다.
저는 다음과 같은 순서를 선호합니다:
원시 데이터 수집 방지 (avoid collecting raw data)
-> 알려진 민감한 필드 비식별화 (redact known sensitive fields)
-> 대량의 텔레메트리 샘플링 (sample high-volume telemetry)
...
단 하나의 프로세서에 전체 개인정보 보호 전략을 의존하지 마십시오.
Aspire를 활용한 로컬 디버깅
Aspire는 에이전트 관측성 (agent observability)을 위한 훌륭한 로컬 루프 (local loop)입니다.
AppHost와 서비스 기본값 (service defaults)을 사용하면, 에이전트 백엔드를 로컬에서 실행하고 다음 항목들을 검사할 수 있습니다:
- 리소스 (resources)
- 환경 변수 (environment variables)
- 로그 (logs)
- 트레이스 (traces)
- 메트릭 (metrics)
- 외부 HTTP 호출 (outbound HTTP calls)
- 실패한 종속성 (failed dependencies)
- 모델 호출 스팬 (model-call spans)
- (추가한 경우) 도구 호출 스팬 (tool-call spans)
이 설정에서 제가 좋아하는 점은 로컬 흐름이 OTLP를 사용한다는 것입니다.
애플리케이션은 OpenTelemetry 데이터를 방출합니다.
Aspire 대시보드가 이를 수신합니다.
Azure Monitor로 보내기 전에 동일한 텔레메트리 (telemetry)를 검사할 수 있습니다.
최소한의 AppHost는 다음과 같이 보일 수 있습니다:
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.DeploymentAgent_Api>("deployment-agent-api")
...
그런 다음 AppHost를 실행합니다:
dotnet run --project src/DeploymentAgent.AppHost
대시보드에서 하나의 요청을 엔드 투 엔드 (end to end)로 검사합니다:
POST /chat
-> deployment-agent model call
-> CheckStagingStatusAsync tool span
...
이것이 제가 로컬에서 보고 싶은 모습입니다.
만약 ASP.NET Core 요청 스팬 (request spans)만 보인다면, 해당 앱은 웹 API로서는 관측 가능 (observable)하지만 아직 에이전트로서는 관측 가능하지 않은 상태입니다.
스팬이 누락되었다면, 먼저 지루한 부분들부터 확인하십시오:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기