Rust에서 Span, Event 및 Tower-HTTP를 이용한 관찰 가능성 살펴보기
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
현대 소프트웨어 개발의 복잡한 세계에서, 특히 분산 시스템의 애플리케이션 동작을 이해하는 것은 매우 중요합니다. 문제가 발생하거나, 심지어 제대로 작동하고 있을 때에도 실행 흐름, 작업에 소요된 시간, 이벤트 주변의 컨텍스트를 파악하는 것은 디버깅, 성능 최적화 및 일반적인 시스템 상태 모니터링에 중요합니다. 이것이 바로 강력한 관찰 가능성 도구가 등장하는 이유입니다. Rust 생태계는 구조화된 로깅 및 분산 추적을 위한 강력하고 유연한 프레임워크인 tracing을 제공합니다. 이는 정교한 진단 도구를 구축할 수 있는 기반을 제공합니다. 이 글은 tracing을 깊이 파고들어 핵심 구성 요소인 Span과 Event를 살펴보고, 인기 있는 HTTP 미들웨어 모음인 tower-http와 원활하게 통합하여 웹 서비스에 실행 가능한 관찰 가능성을 제공하는 방법을 시연할 것입니다.
추적의 핵심 이해
실제 예제를 살펴보기 전에, tracing 내의 두 가지 핵심 개념인 Span과 Event에 대한 확고한 이해를 확립해 봅시다.
Span: 실행의 포괄
Span은 애플리케이션 내의 실행 기간을 나타냅니다. 특정 작업을 캡슐화하는 논리적 작업 단위, 즉 바운드된 컨텍스트로 생각하십시오. Span은 시작과 끝이 있으며 계층 구조를 형성합니다. 예를 들어, 단일 HTTP 요청 핸들러가 Span이 될 수 있습니다. 해당 핸들러 내에서 데이터베이스 쿼리 또는 외부 API 호출이 중첩된 Span일 수 있습니다. 이 계층적 관계는 작업의 인과 관계를 이해하고 시스템 전체의 요청 흐름을 재구성하는 데 중요합니다. Span은 해당 특정 작업과 관련된 컨텍스트 정보(예: 요청 ID, 사용자 ID 또는 입력 매개변수)를 제공하는 필드라는 관련 데이터를 전달할 수 있습니다.
Event: 시간상의 지점
Span과 달리 Event는 애플리케이션 실행 내 특정 시점의 개별적이고 순간적인 발생을 나타냅니다. 이는 지속 시간을 포함하지 않는 중요한 발생(예: 오류 감지, 사용자 로그인 또는 특정 데이터 변환 단계 완료)을 보고하는 데 자주 사용되는 원자적 로그 메시지입니다. Span과 마찬가지로 Event도 관련 컨텍스트를 제공하기 위해 필드를 전달합니다.
tracing의 강력함은 Span과 Event가 함께 작동하는 방식에 있습니다. Event는 종종 활성 Span의 컨텍스트 내에서 발생하며, 해당 컨텍스트 필드를 상속하고 해당 특정 작업 단위 내에서 무슨 일이 일어나고 있는지에 대한 내러티브를 강화합니다.
레이어 및 구독자
tracing은 출력을 콘솔에 직접 출력하거나 추적 백엔드로 데이터를 보내지 않습니다. 대신 구독자(subscriber) 와 레이어(layer) 의 시스템을 통해 작동합니다. 구독자는 tracing Subscriber 트레이트를 구현하는 유형으로, 생성되는 추적 데이터(Span 및 Event)를 처리할 책임이 있습니다. 레이어는 추적 데이터 소스와 구독자 사이에 위치하는 컴포넌트 가능한 단위로, 최종 구독자에 도달하기 전에 데이터를 필터링, 형식화 및 풍부하게 할 수 있습니다. 이 아키텍처는 다양한 대상으로 로깅하거나 특정 유형의 이벤트를 필터링하는 것처럼 특정 요구 사항에 맞게 관찰 가능성 솔루션을 조정할 수 있는 엄청난 유연성을 제공합니다.
Span 및 Event를 사용한 실용적인 추적
몇 가지 Rust 코드를 통해 이 개념을 설명해 보겠습니다. 먼저 Cargo.toml에 tracing 및 tracing-subscriber를 추가해야 합니다.
[dependencies] tracing = "0.1" tracing-subscriber = "0.3"
이제 몇 가지 작업을 시뮬레이션하는 간단한 함수를 고려해 보겠습니다.
use tracing::{info, span, Level}; #[tracing::instrument] // 이 매크로는 함수에 대한 Span을 생성합니다. async fn perform_complex_operation(input_value: u32) -> String { // Span 내의 Event info!("Starting complex operation with input_value={}", input_value); // 일부 작업 시뮬레이션 tokio::time::sleep(std::time::Duration::from_millis(50)).await; let intermediate_result = input_value * 2; // 다른 Event info!("Calculated intermediate_result={}", intermediate_result); // 새 중첩 Span 진입 let nested_span = span!(Level::INFO, "nested_processing", step = 1); let _guard = nested_span.enter(); // Span 컨텍스트 진입 tokio::time::sleep(std::time::Duration::from_millis(30)).await; let final_result = format!("Processed: {}", intermediate_result + 10); info!("Finished nested processing"); drop(_guard); // 중첩 Span 명시적으로 종료 info!("Complex operation completed, returning: {}", final_result); final_result } #[tokio::main] async fn main() { // 콘솔 출력을 위한 간단한 구독자 초기화 tracing_subscriber::fmt::init(); let result = perform_complex_operation(42).await; info!("Application finished with result: {}", result); }
이 예제에서는 다음과 같습니다.
perform_complex_operation의#[tracing::instrument]는 함수의 전체 실행을 포함하는 Span을 자동으로 생성합니다. 또한 함수 인수들을 Span의 필드로 자동으로 포함합니다.info!매크로 호출은 Event를 생성합니다.info!("Starting complex operation with input_value={}", input_value);가#[tracing::instrument]로 인해 현재 Span의 컨텍스트에서input_value를 필드로 자동 가져오는 것을 주목하십시오.span!매크로를 사용하여 중첩된nested_processingSpan을 수동으로 생성하고nested_span.enter()로 해당 컨텍스트에 진입합니다._guard는 Span이 범위를 벗어날 때(또는drop으로 명시적으로) Span이 종료되도록 보장합니다. 이는 세밀한 제어를 위한 수동 Span 생성을 보여줍니다.tracing_subscriber::fmt::init()은 계층 구조를 볼 수 있도록 콘솔에 형식화된 추적 데이터를 인쇄하는 기본 구독자를 설정합니다.
이를 실행하면 (명확성을 위해 단순화된) 다음과 유사한 출력을 관찰할 수 있습니다.
INFO tokio_app: Starting complex operation with input_value=42 span=perform_complex_operation
INFO tokio_app: Calculated intermediate_result=84 span=perform_complex_operation
INFO tokio_app: Finished nested processing span=perform_complex_operation::nested_processing step=1
INFO tokio_app: Complex operation completed, returning: Processed: 94 span=perform_complex_operation
INFO tokio_app: Application finished with result: Processed: 94
각 Event의 활성 Span을 나타내는 span=...와 중첩 Span을 보여주는 span=perform_complex_operation::nested_processing을 주목하십시오.
Tower-HTTP와 통합
이제 tracing과 tower-http ( tower 서비스 모음 HTTP 미들웨어)를 통합하여 관찰 가능성을 향상시켜 보겠습니다.
먼저 Cargo.toml에 필요한 종속성을 추가합니다.
[dependencies] tracing = "0.1" tracing-subscriber = "0.3" tokio = { version = "1", features = ["full"] } axum = "0.6" # 간단한 웹 서버를 위해 axum 사용 tower = "0.4" tower-http = { version = "0.4", features = ["trace"] }
이제 간단한 axum 애플리케이션을 만들고 (내부적으로 tower 사용), Trace 미들웨어를 적용해 보겠습니다.
use axum::{routing::get, Router}; use tower_http::trace::{TraceLayer, DefaultOnRequest, DefaultOnResponse}; use tracing::{info, Level}; use std::time::Duration; // 추적된 함수를 사용하는 간단한 핸들러 async fn hello_handler() -> String { info!("Handler received request"); let result = perform_complex_operation(100).await; format!("Hello, from handler! {}", result) } // 이전의 추적된 함수 #[tracing::instrument] async fn perform_complex_operation(input_value: u32) -> String { info!("Starting complex operation with input_value={}", input_value); tokio::time::sleep(Duration::from_millis(50)).await; let intermediate_result = input_value * 2; info!("Calculated intermediate_result={}", intermediate_result); // ... (이전과 동일한 함수 나머지) let nested_span = tracing::span!(Level::INFO, "nested_processing", step = 1); let _guard = nested_span.enter(); tokio::time::sleep(Duration::from_millis(30)).await; let final_result = format!("Processed: {}", intermediate_result + 10); info!("Finished nested processing"); drop(_guard); info!("Complex operation completed, returning: {}", final_result); final_result } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let app = Router::new() .route("/hello", get(hello_handler)) .layer( TraceLayer::new_for_http() // HTTP 서비스에 대한 새 TraceLayer 생성 .on_request(DefaultOnRequest::new().level(Level::INFO)) // 요청 세부 정보 로깅 .on_response(DefaultOnResponse::new().level(Level::INFO)), // 응답 세부 정보 로깅 ); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); info!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
이 향상된 예제에서는 다음과 같습니다.
/hello엔드포인트를 가진axumRouter를 만들었습니다.TraceLayer::new_for_http()미들웨어가 애플리케이션에 적용되었습니다. 이는 수신된 각 HTTP 요청에 대해 자동으로 루트 Span을 생성합니다..on_request(DefaultOnRequest::new().level(Level::INFO))는 미들웨어가 요청이 시작될 때 메서드 및 경로와 같은 세부 정보를 포함하여INFO레벨 Event를 발생시키도록 구성합니다..on_response(DefaultOnResponse::new().level(Level::INFO))는 상태 코드 및 응답 시간을 포함하여 응답이 전송될 때INFO레벨 Event를 발생시키도록 구성합니다.
이 애플리케이션을 실행하고 http://127.0.0.1:3000/hello에 요청을 보내면 (예: curl 사용), 포괄적인 추적 출력을 볼 수 있습니다.
INFO tower_http::trace::make_span: started processing request request.method=GET request.uri=/hello request.version=HTTP/1.1 remote_addr=127.0.0.1:49877 request_id=... span=http_request
INFO axum_app: Handler received request span=http_request
INFO axum_app: Starting complex operation with input_value=100 span=http_request::perform_complex_operation
INFO axum_app: Calculated intermediate_result=200 span=http_request::perform_complex_operation
INFO axum_app: Finished nested processing span=http_request::perform_complex_operation::nested_processing step=1
INFO axum_app: Complex operation completed, returning: Processed: 210 span=http_request::perform_complex_operation
INFO tower_http::trace::make_span: finished processing request status=200 response.time=100ms span=http_request
tower-http가 최상위 http_request Span을 생성하고, 핸들러 내의 모든 후속 info! Event 및 수동 Span이 이 요청 Span 아래에 자동으로 중첩되는 것을 관찰하십시오. 이는 HTTP 요청의 전체 수명 주기, 즉 도착부터 최종 응답까지, 모든 내부 작업이 상세한 컨텍스트를 제공하는 것을 명확하게 보여줍니다. 이 구조화된 계층적 보기는 병목 현상을 식별하고, 요청 흐름을 이해하고, 웹 서비스 내의 문제를 디버깅하는 데 매우 중요합니다.
결론
tracing 크레이트는 Rust 관찰 가능성 도구 키트에서 필수적인 도구로, 애플리케이션의 동작을 이해하기 위한 강력하고 유연한 프레임워크를 제공합니다. 포괄적인 작업 단위로서의 Span과 순간적인 발생으로서의 Event 개념을 숙달함으로써 코드 실행에 대한 상세한 그림을 그릴 수 있는 힘을 얻게 됩니다. tracing을 tower-http와 통합하면 이 비교할 수 없는 가시성이 웹 서비스에 직접 제공되어 불투명한 HTTP 요청을 명확하고 추적 가능한 내러티브로 바꿉니다. tracing을 채택함으로써 더 탄력적이고 성능이 뛰어나며 이해하기 쉬운 Rust 애플리케이션을 구축할 수 있습니다.

