텔레메트리
ADK-Rust는 tracing 생태계와 OpenTelemetry을 사용하여 구조화된 로깅과 분산 트레이싱을 통합하는 adk-telemetry 크레이트를 통해 프로덕션 수준의 관측 가능성을 제공합니다.
개요
텔레메트리 시스템은 다음을 가능하게 합니다:
- 구조화된 로깅: 컨텍스트 정보가 포함된 풍부하고 쿼리 가능한 로그
- 분산 트레이싱: 에이전트 계층 및 서비스 경계를 넘어 요청 추적
- OpenTelemetry 통합: 관측 가능성 백엔드(Jaeger, Datadog, Honeycomb 등)로 트레이스 내보내기
- 자동 컨텍스트 전파: 세션, 사용자 및 호출 ID가 모든 작업을 통해 흐름
- 사전 구성된 스팬: 일반적인 ADK 작업에 대한 헬퍼 함수
빠른 시작
기본 콘솔 로깅
개발 및 간단한 배포를 위해 콘솔 로깅을 초기화합니다:
use adk_telemetry::init_telemetry;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize telemetry with your service name
init_telemetry("my-agent-service")?;
// Your agent code here
Ok(())
}
이는 합리적인 기본값으로 stdout에 구조화된 로깅을 구성합니다.
OpenTelemetry 내보내기
분산 트레이싱을 사용하는 프로덕션 배포의 경우:
use adk_telemetry::init_with_otlp;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize with OTLP exporter
init_with_otlp("my-agent-service", "http://localhost:4317")?;
// Your agent code here
// Flush traces before exit
adk_telemetry::shutdown_telemetry();
Ok(())
}
이는 트레이스와 메트릭을 OpenTelemetry collector 엔드포인트로 내보냅니다.
구성 가능한 레이어 (고급)
이미 tracing subscriber가 구성되어 있다면, build_otlp_layer를 사용하여 전역 subscriber를 초기화하는 대신 구성 가능한 레이어를 얻으세요:
use adk_telemetry::build_otlp_layer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
let otlp_layer = build_otlp_layer("my-agent", "http://localhost:4317")?;
tracing_subscriber::registry()
.with(otlp_layer)
.with(tracing_subscriber::fmt::layer())
.init();
로그 레벨
RUST_LOG 환경 변수를 사용하여 로깅 상세도를 제어합니다:
| 수준 | 설명 | 사용 사례 |
|---|---|---|
error | 오류만 | 프로덕션 (최소) |
warn | 경고 및 오류 | 프로덕션 (기본값) |
info | 정보성 메시지 | 개발, 스테이징 |
debug | 자세한 디버깅 정보 | 로컬 개발 |
trace | 매우 상세한 트레이싱 | 심층 디버깅 |
로그 레벨 설정
# Set global log level
export RUST_LOG=info
# Set per-module log levels
export RUST_LOG=adk_agent=debug,adk_model=info
# Combine global and module-specific levels
export RUST_LOG=warn,adk_agent=debug
텔레메트리 시스템은 RUST_LOG이(가) 설정되지 않은 경우 기본적으로 info 레벨로 설정됩니다.
로깅 매크로
로깅을 위해 표준 tracing 매크로를 사용하세요:
use adk_telemetry::{trace, debug, info, warn, error};
// Informational logging
info!("Agent started successfully");
// Structured logging with fields
info!(
agent.name = "my_agent",
session.id = "sess-123",
"Processing user request"
);
// Debug logging
debug!(user_input = ?input, "Received input");
// Warning and error logging
warn!("Rate limit approaching");
error!(error = ?err, "Failed to call model");
구조화된 필드
더 나은 필터링 및 분석을 위해 로그 메시지에 컨텍스트 필드를 추가하세요:
use adk_telemetry::info;
info!(
agent.name = "customer_support",
user.id = "user-456",
session.id = "sess-789",
invocation.id = "inv-abc",
"Agent execution started"
);
이 필드들은 관측성 백엔드에서 쿼리할 수 있게 됩니다.
계측
자동 계측
함수에 대한 스팬을 자동으로 생성하려면 #[instrument] 속성을 사용하세요:
use adk_telemetry::{instrument, info};
#[instrument]
async fn process_request(user_id: &str, message: &str) {
info!("Processing request");
// Function logic here
}
// Creates a span named "process_request" with user_id and message as fields
민감한 매개변수 건너뛰기
트레이스에서 민감한 데이터를 제외하세요:
use adk_telemetry::instrument;
#[instrument(skip(api_key))]
async fn call_external_api(api_key: &str, query: &str) {
// api_key won't appear in traces
}
사용자 지정 스팬 이름
use adk_telemetry::instrument;
#[instrument(name = "external_api_call")]
async fn fetch_data(url: &str) {
// Span will be named "external_api_call" instead of "fetch_data"
}
사전 구성된 스팬
ADK-Telemetry는 일반적인 작업을 위한 헬퍼 함수를 제공합니다:
Agent 실행 스팬
use adk_telemetry::agent_run_span;
let span = agent_run_span("my_agent", "inv-123");
let _enter = span.enter();
// Agent execution code here
// All logs within this scope inherit the span context
모델 호출 스팬
use adk_telemetry::model_call_span;
let span = model_call_span("gemini-2.5-flash");
let _enter = span.enter();
// Model API call here
Tool 실행 스팬
use adk_telemetry::tool_execute_span;
let span = tool_execute_span("weather_tool");
let _enter = span.enter();
// Tool execution code here
콜백 스팬
use adk_telemetry::callback_span;
let span = callback_span("before_model");
let _enter = span.enter();
// Callback logic here
컨텍스트 속성 추가
현재 스팬에 사용자 및 세션 컨텍스트 추가:
use adk_telemetry::add_context_attributes;
add_context_attributes("user-456", "sess-789");
LLM 토큰 사용량 추적
모든 LLM 공급자에서 OpenTelemetry GenAI 시맨틱 컨벤션을 사용하여 토큰 소비를 추적합니다. llm_generate_span는 사전 선언된 gen_ai.usage.* 필드를 사용하여 스팬을 생성하고, record_llm_usage는 응답이 도착한 후 해당 필드를 채웁니다:
use adk_telemetry::{llm_generate_span, record_llm_usage, LlmUsage};
let span = llm_generate_span("openai", "gpt-5-mini", true);
let _enter = span.enter();
// After receiving the LLM response with usage metadata:
record_llm_usage(&LlmUsage {
input_tokens: 100,
output_tokens: 50,
total_tokens: 150,
cache_read_tokens: Some(80),
..Default::default()
});
모든 ADK 모델 공급자(Gemini, OpenAI, Anthropic, Ollama, Bedrock, DeepSeek, Groq, Azure AI, 및 모든 OpenAI 호환 공급자)는 모든 generate_content 호출 시 토큰 사용량을 자동으로 기록합니다. 수동 계측은 필요하지 않습니다 — 추적 기능은 공급자 계층에 내장되어 있습니다.
기록된 스팬 필드는 OpenTelemetry GenAI 컨벤션을 따릅니다:
| 필드 | 설명 |
|---|---|
gen_ai.usage.input_tokens | 프롬프트 / 입력 토큰 수 |
gen_ai.usage.output_tokens | 완료/출력 토큰 수 |
gen_ai.usage.total_tokens | 총 토큰 수 |
gen_ai.usage.cache_read_tokens | 프롬프트 캐시에서 읽은 토큰 |
gen_ai.usage.cache_creation_tokens | 캐시를 생성하는 데 사용되는 토큰 |
gen_ai.usage.thinking_tokens | 연쇄적 사고 추론 토큰 |
gen_ai.usage.audio_input_tokens | 오디오 입력 토큰 수 |
gen_ai.usage.audio_output_tokens | 오디오 출력 토큰 수 |
선택적 필드는 공급자가 보고할 때만 기록됩니다 (non-None).
수동 스팬 생성
사용자 지정 계측을 위해 스팬을 수동으로 생성합니다:
use adk_telemetry::{info, Span};
let span = tracing::info_span!(
"custom_operation",
operation.type = "data_processing",
operation.id = "op-123"
);
let _enter = span.enter();
info!("Performing custom operation");
// Operation code here
스팬 속성
속성을 동적으로 추가합니다:
use adk_telemetry::Span;
let span = Span::current();
span.record("result.count", 42);
span.record("result.status", "success");
OpenTelemetry 구성
OTLP 엔드포인트
OTLP 익스포터는 트레이스를 컬렉터 엔드포인트로 보냅니다:
use adk_telemetry::init_with_otlp;
// Local Jaeger (default OTLP port)
init_with_otlp("my-service", "http://localhost:4317")?;
// Cloud provider endpoint
init_with_otlp("my-service", "https://otlp.example.com:4317")?;
로컬 컬렉터 실행
개발을 위해 OTLP를 지원하는 Jaeger를 실행합니다:
docker run -d --name jaeger \
-p 4317:4317 \
-p 16686:16686 \
jaegertracing/all-in-one:latest
# View traces at http://localhost:16686
트레이스 시각화
구성되면 트레이스가 관측성 백엔드에 표시되며 다음을 보여줍니다:
- Agent 실행 계층
- Model 호출 지연 시간
- Tool 실행 타이밍
- 오류 전파
- 컨텍스트 흐름 (사용자 ID, 세션 ID 등)
ADK 통합
텔레메트리 시스템이 초기화되면 ADK-Rust 구성 요소가 자동으로 텔레메트리를 내보냅니다:
use adk_rust::prelude::*;
use adk_telemetry::init_telemetry;
use std::sync::Arc;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
// Initialize telemetry first
init_telemetry("my-agent-app")?;
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
let agent = LlmAgentBuilder::new("support_agent")
.model(model)
.instruction("You are a helpful support agent.")
.build()?;
// Use Launcher for simple execution
Launcher::new(Arc::new(agent)).run().await?;
Ok(())
}
Agent, Model, Tool 작업은 자동으로 구조화된 로그와 트레이스를 내보냅니다.
텔레메트리 데모 예시
로컬에서 텔레메트리 기능 선택을 검증하세요:
cargo check -p adk-telemetry --no-default-features
cargo check -p adk-telemetry --no-default-features --features otlp
실제 모델 호출을 사용하는 전체 텔레메트리 예제는 이 사이트에 포함된 ADK-Rust Playground에서 확인하세요.
Tools의 커스텀 텔레메트리
커스텀 Tool에 텔레메트리를 추가하세요:
use adk_rust::prelude::*;
use adk_telemetry::{info, instrument, tool_execute_span};
use serde_json::{json, Value};
#[instrument(skip(ctx))]
async fn weather_tool_impl(
ctx: Arc<dyn ToolContext>,
args: Value,
) -> Result<Value> {
let span = tool_execute_span("weather_tool");
let _enter = span.enter();
let location = args["location"].as_str().unwrap_or("unknown");
info!(location = location, "Fetching weather data");
// Tool logic here
let result = json!({
"temperature": 72,
"condition": "sunny"
});
info!(location = location, "Weather data retrieved");
Ok(result)
}
let weather_tool = FunctionTool::new(
"get_weather",
"Get current weather for a location",
json!({
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}),
weather_tool_impl,
);
Callbacks의 커스텀 텔레메트리
Callbacks에 관측 가능성을 추가하세요:
use adk_rust::prelude::*;
use adk_telemetry::{info, callback_span};
use std::sync::Arc;
let agent = LlmAgentBuilder::new("observed_agent")
.model(model)
.before_callback(Box::new(|ctx| {
Box::pin(async move {
let span = callback_span("before_agent");
let _enter = span.enter();
info!(
agent.name = ctx.agent_name(),
user.id = ctx.user_id(),
session.id = ctx.session_id(),
"Agent execution starting"
);
Ok(None)
})
}))
.after_callback(Box::new(|ctx| {
Box::pin(async move {
let span = callback_span("after_agent");
let _enter = span.enter();
info!(
agent.name = ctx.agent_name(),
"Agent execution completed"
);
Ok(None)
})
}))
.build()?;
성능 고려 사항
샘플링
높은 처리량 시스템의 경우, 트레이스 샘플링을 고려하세요:
// Note: Sampling configuration depends on your OpenTelemetry setup
// Configure sampling in your OTLP collector or backend
Async Spans
적절한 span 컨텍스트를 보장하기 위해 async 함수에서 항상 #[instrument]을(를) 사용하세요:
use adk_telemetry::instrument;
// ✅ Correct - span context preserved across await points
#[instrument]
async fn async_operation() {
tokio::time::sleep(Duration::from_secs(1)).await;
}
// ❌ Incorrect - manual span may lose context
async fn manual_span_operation() {
let span = tracing::info_span!("operation");
let _enter = span.enter();
tokio::time::sleep(Duration::from_secs(1)).await;
// Context may be lost after await
}
프로덕션 환경의 로그 레벨
오버헤드를 줄이기 위해 프로덕션 환경에서 info 또는 warn 레벨을 사용하세요:
export RUST_LOG=warn,my_app=info
문제 해결
로그가 나타나지 않음
RUST_LOG환경 변수가 설정되었는지 확인init_telemetry()이 로깅 전에 호출되는지 확인- 텔레메트리가 한 번만 초기화되는지 확인 (내부적으로
Once사용)
내보내지지 않은 트레이스
- OTLP 엔드포인트에 도달 가능한지 확인
- 컬렉터가 실행 중이며 연결을 수락하는지 확인
- 애플리케이션 종료 전에
shutdown_telemetry()을 호출하여 보류 중인 스팬을 플러시 - 네트워크/방화벽 문제 확인
스팬에 컨텍스트 누락
- async 함수에서
#[instrument]사용 - 스팬이
let _enter = span.enter()으로 진입되는지 확인 - 작업 기간 동안
_enter가드를 스코프 내에 유지
모범 사례
- 일찍 초기화:
main()시작 시init_telemetry()를 호출합니다. - 구조화된 필드 사용: 문자열 보간 대신 키-값 쌍으로 컨텍스트를 추가합니다.
- 비동기 함수 계측: 비동기 함수에는 항상
#[instrument]를 사용합니다. - 종료 시 플러시: 애플리케이션 종료 전에
shutdown_telemetry()를 호출합니다. - 적절한 로그 레벨: 중요한 이벤트에는
info을, 세부 정보에는debug을 사용합니다. - 민감한 데이터 피하기:
#[instrument(skip(...))]로 민감한 매개변수를 건너뜁니다. - 일관된 이름 지정: 일관된 필드 이름(예:
user.id,session.id)을 사용합니다.