| name | tracing |
| description | Rust tracing + tracing-subscriber 구조화 로깅 - 초기화, EnvFilter, 매크로, |
tracing 구조화 로깅 가이드
소스: https://docs.rs/tracing/latest/tracing/ | https://docs.rs/tracing-subscriber/latest/tracing_subscriber/
검증일: 2026-06-20
주의: tracing 0.1.x / tracing-subscriber 0.3.x 기준. tracing 0.2는 미출시 상태이며, 향후 API 변경 가능성이 있으므로 실제 프로젝트의 Cargo.lock 버전을 확인할 것.
Cargo.toml 의존성
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
주요 feature flag (tracing-subscriber):
| Feature | 설명 |
|---|
env-filter | EnvFilter (RUST_LOG 환경변수 파싱) |
json | JSON 형식 로그 출력 (fmt::format::Json) |
fmt | 포맷팅 레이어 (기본 활성화) |
registry | Registry subscriber (기본 활성화) |
초기화 패턴
간단한 초기화
use tracing_subscriber::EnvFilter;
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info")))
.init();
Registry 기반 레이어 조합 (권장)
tracing_subscriber::registry()는 여러 Layer를 .with()로 조합할 수 있는 Subscriber 구현체다.
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,my_crate=debug")))
.with(tracing_subscriber::fmt::layer())
.init();
SubscriberExt 트레이트: registry()에 .with() 메서드 제공
SubscriberInitExt 트레이트: .init() / .try_init() 메서드 제공
.init()은 글로벌 subscriber를 설정하며, 프로세스당 1회만 호출 가능
JSON 로그 출력
tracing_subscriber::registry()
.with(EnvFilter::new("info"))
.with(tracing_subscriber::fmt::layer().json())
.init();
EnvFilter (RUST_LOG 환경변수)
EnvFilter는 RUST_LOG 환경변수를 파싱하여 로그 레벨을 제어한다.
RUST_LOG=info,my_crate=debug cargo run
RUST_LOG=my_crate::api=trace cargo run
RUST_LOG="info,[my_span]=debug" cargo run
디렉티브 문법:
| 패턴 | 의미 |
|---|
info | 전역 INFO 이상 |
my_crate=debug | my_crate 크레이트만 DEBUG 이상 |
my_crate::module=trace | 특정 모듈만 TRACE 이상 |
tower_http=debug | tower-http 내부 로그 활성화 |
코드에서 직접 설정:
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,tower_http=debug"));
이벤트 매크로 (info!, warn!, error!, debug!, trace!)
기본 사용법
use tracing::{info, warn, error, debug, trace};
info!("서버 시작");
warn!("설정 파일 없음, 기본값 사용");
error!("데이터베이스 연결 실패");
구조화 필드
매크로의 첫 번째 인자들로 key = value 형태의 구조화 필드를 전달한다. 메시지 문자열은 마지막에 위치한다.
info!(port = 3000, host = "0.0.0.0", "서버 시작");
let port = 3000;
info!(port, "서버 시작");
info!(user = %user_id, details = ?request, "요청 수신");
info!(result = tracing::field::Empty, "처리 시작");
메시지 포맷팅
let user = "alice";
info!("사용자 {} 로그인", user);
info!(user, "로그인 처리 완료");
info!(user, action = "login", "완료");
#[instrument] 매크로
함수 진입/종료 시 자동으로 스팬을 생성한다. tracing 크레이트에 포함되어 있다.
기본 사용법
use tracing::instrument;
#[instrument]
async fn handle_request(user_id: u64, action: String) {
info!("요청 처리 중");
}
함수의 모든 인자가 기본적으로 스팬 필드로 기록된다.
skip 옵션
민감한 정보나 Display/Debug를 구현하지 않은 인자를 제외한다.
#[instrument(skip(db, password))]
async fn create_user(
db: &DatabasePool,
username: String,
password: String,
) -> Result<User, Error> {
todo!()
}
skip_all: 모든 인자를 제외한다.
#[instrument(skip_all)]
async fn internal_process(data: LargeStruct) {
todo!()
}
fields 옵션
추가 필드를 정의하거나 인자 필드를 재정의한다.
#[instrument(skip(req), fields(method = %req.method(), uri = %req.uri()))]
async fn handle(req: Request) -> Response {
todo!()
}
Empty 필드를 선언하고 함수 내에서 나중에 채울 수 있다:
#[instrument(fields(result))]
async fn compute(input: u64) -> u64 {
let output = input * 2;
tracing::Span::current().record("result", output);
output
}
기타 옵션
#[instrument(
name = "custom_span_name", // 스팬 이름 지정 (기본: 함수명)
level = "debug", // 스팬 레벨 (기본: INFO)
target = "my_crate::api", // 로그 타깃 지정
err, // Result::Err 시 자동으로 error 이벤트 기록
ret, // 반환값을 자동 기록
)]
async fn my_function() -> Result<String, Error> {
todo!()
}
err: 함수가 Result를 반환할 때, Err 케이스를 ERROR 레벨로 기록
err(level = "warn"): err 기록 레벨 변경
ret: 반환값을 DEBUG 레벨로 기록
ret(level = "info"): ret 기록 레벨 변경
Span (스팬) 개념과 사용법
스팬은 작업의 시작과 끝을 나타내는 구간이다. 이벤트(info! 등)는 특정 시점의 기록이고, 스팬은 기간(duration)을 나타낸다. 스팬은 중첩되어 트리 구조를 형성한다.
스팬 생성 매크로
use tracing::{info_span, debug_span, warn_span, span, Level};
let span = info_span!("request", method = "GET", path = "/api");
let span = debug_span!("db_query", table = "users");
let span = span!(Level::INFO, "custom_span", key = "value");
스팬 진입
let span = info_span!("sync_work");
let _guard = span.enter();
do_sync_work();
let result = info_span!("compute").in_scope(|| {
expensive_computation()
});
주의: .enter()는 async 함수 내에서 사용하면 안 된다. .enter()가 반환하는 드롭 가드는 스코프를 벗어날 때 드롭되어야 스팬이 종료되는데, async에서 .await를 만나면 스코프를 양보하면서도 드롭 가드가 드롭되지 않는다. 결과적으로 스팬이 종료되지 않은 상태로 다른 태스크가 실행된다. 멀티스레드 런타임에서는 재개 시 다른 스레드에서 실행되는 추가 문제도 있다. async 코드에서는 #[instrument] 또는 .instrument() 메서드를 사용한다.
async 코드에서 스팬
use tracing::Instrument;
async fn parent_task() {
let span = info_span!("child_task", task_id = 42);
some_async_work()
.instrument(span)
.await;
}
스팬 필드 나중에 기록
let span = info_span!("process", result = tracing::field::Empty);
let _guard = span.enter();
let value = compute();
span.record("result", value);
Axum TraceLayer 연동
tower-http의 TraceLayer를 사용하여 Axum 요청/응답을 자동 추적한다.
[dependencies]
axum = "0.8"
tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
기본 설정
use axum::{routing::get, Router};
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,tower_http=debug")))
.with(tracing_subscriber::fmt::layer())
.init();
let app = Router::new()
.route("/", get(root))
.layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
tracing::info!("서버 시작: 0.0.0.0:3000");
axum::serve(listener, app).await.unwrap();
}
async fn root() -> &'static str {
tracing::info!("루트 핸들러 호출");
"Hello, World!"
}
TraceLayer 커스터마이징
use tower_http::trace::{TraceLayer, DefaultMakeSpan, DefaultOnResponse};
use tracing::Level;
let trace_layer = TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::INFO))
.on_response(DefaultOnResponse::new().level(Level::INFO));
let app: Router = Router::new()
.route("/", get(root))
.layer(trace_layer);
핸들러에서 #[instrument] 사용
use axum::extract::Path;
use tracing::instrument;
#[instrument(skip_all, fields(user_id))]
async fn get_user(Path(user_id): Path<u64>) -> String {
tracing::Span::current().record("user_id", user_id);
tracing::info!("사용자 조회");
format!("User {}", user_id)
}
실전 초기화 템플릿
use tracing_subscriber::{
layer::SubscriberExt,
util::SubscriberInitExt,
EnvFilter,
};
pub fn init_tracing() {
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
EnvFilter::new(
"info,my_app=debug,tower_http=debug,sqlx=warn"
)
});
tracing_subscriber::registry()
.with(env_filter)
.with(tracing_subscriber::fmt::layer()
.with_target(true)
.with_thread_ids(false)
.with_file(true)
.with_line_number(true)
)
.init();
}