
로그를 터미널에 출력하고 넘어가면
실시간으로 관측하고 있지 않은 이상 문제를 파악하기 어렵다.
따라서 로그를 파일의 형태로 아카이빙할 필요가 있다.
여기서는 조금 더 다양한 상황을 연출하기 위해
병렬 처리뿐만 아니라 비동기 처리에 대한 시뮬레이션을 추가했다.
~/workspace$ mkdir log-archiving && cd log-archiving ~/workspace/log-archiving$ python3 -m venv venv ~/workspace/log-archiving$ source venv/bin/activate ~/workspace/log-archiving$ # 평소에 설치하던 것 외에 추가된 라이브러리를 놓치지 말자 ~/workspace/log-archiving$ pip install maturin fastapi uvicorn orjson ~/workspace/log-archiving$ maturin init ~/workspace/log-archiving$ # 선택지 중 기본값인 PyO3 선택 ~/workspace/log-archiving$ # Cargo.toml과 src/lib.rs가 자동 생성된다 ~/workspace/log-archiving$ # Python 코드는 직접 생성해 주어야 한다 ~/workspace/log-archiving$ mkdir app && touch app/main.py ~/workspace/log-archiving$ tree -I venv . ├── app │ └── main.py ├── Cargo.toml ├── pyproject.toml └── src └── lib.rs
Cargo.toml 파일을 열어 라이브러리 이름을 수정해 주겠다.
로그 추적을 위한 tracing 크레이트와
로그를 필터링하기 위한 tracing-subscriber 크레이트,
로그를 파일에 기록하기 위한 tracing-appender 크레이트를 사용한다.
features 에 "json" 을 추가해 주어야
데이터 분석 도구에서 사용하기 수월한 JSON 형태로 저장할 수 있다.
병렬 처리를 위한 rayon 크레이트와 더불어
비동기 처리를 위한 tokio 크레이트도 추가한다.
Cargo.toml[package] name = "log-archiving" version = "0.1.0" edition = "2024" [lib] name = "rust_engine" crate-type = ["cdylib"] [dependencies] pyo3 = "0.28.0" rayon = "1.11" tokio = { version = "1.50", features = ["full"] } tracing-appender = "0.2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing_appender::non_blocking 을 사용하여
별도의 스레드가 매 시간마다 새로운 로그 파일을 생성하고
logs 폴더에 저장하도록 한다.
guard 는 프로그램이 갑자기 종료될 때
버퍼에 남아있던 로그들을 파일에 마저 쓰고 닫는 역할을 하는데,
이를 전역 변수에 저장하여 프로그램 종료 시까지 살려두어야
함수가 끝나는 즉시 사라지지 않고
프로그램이 끝날 때까지 살아 있을 수 있다.
std::sync::OnceLock 을 사용하면
Python이 실행되는 동안 로그 시스템이 죽지 않고 살아있게 된다.
src/lib.rsuse pyo3::prelude::*; use rayon::prelude::*; use tracing::{info, debug, warn, error, instrument, span, Level}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use std::sync::OnceLock; use std::time::Duration; static LOG_GUARD: OnceLock<WorkerGuard> = OnceLock::new(); fn init_tracing() { let filter_appender = tracing_appender::rolling::hourly("./logs", "server.log"); let (non_blocking, guard) = tracing_appender::non_blocking(filter_appender); let filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("info")); let _ = LOG_GUARD.set(guard); tracing_subscriber::registry() .with(filter) .with(fmt::layer().with_thread_ids(true)) // 콘솔 출력 .with(fmt::layer().with_writer(non_blocking).json()) // 파일 저장 .init(); } /// 비동기 IO 시뮬레이션 함수 async fn mock_db_call(id: u32) { let _span = span!(Level::INFO, "db_query", id = id).entered(); debug!("DB 데이터 조회 중..."); tokio::time::sleep(Duration::from_millis(50)).await; info!("DB 조회 완료"); } #[pyfunction] #[instrument] fn complex_task_runner(events: Vec<String>) -> PyResult<String> { info!("통합 태스크 러너 가동"); // 비동기 처리 let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { mock_db_call(101).await; }); // 병렬 처리 events.into_par_iter() .enumerate() .for_each(|(idx, name)| { let _sapn = span!(Level::DEBUG, "worker", id = idx).entered(); if name.contains("error") { error!("심각한 페이로드 발견: {}", name); } else if name.len() > 20 { warn!("비정상적으로 긴 이벤트명 감지: {}", name); } else { // 이벤트 처리에 걸리는 시간 시뮬레이션 std::thread::sleep(Duration::from_millis(10)); debug!("일반 태스크 처리 중: {}", name); } }); Ok("모든 작업 수행 및 로깅 완료".to_string()) } #[pymodule] fn rust_engine(m: &Bound<'_, PyModule>) -> PyResult<()> { init_tracing(); tracing::info!("Rust Engine 가동 및 Tracing 시스템 초기화 완료"); m.add_function(wrap_pyfunction!(complex_task_runner, m)?)?; Ok(()) }
app/main.pyimport os import time from fastapi import FastAPI from fastapi.responses import ORJSONResponse os.environ["RUST_LOG"] = "debug" import rust_engine class UTF8ORJSONResponse(ORJSONResponse): media_type = "application/json; charset=utf-8" app = FastAPI(default_response_class=UTF8ORJSONResponse) @app.get("/") def read_root(): return { "status": "200", "info": "서버 가동 중입니다." } @app.get("/run-complex-task") async def run_task(): events = [ "normal_event_1", "critical_error_payload", "short", "very_long_event_name_for_warning", "normal_event_2" ] start = time.perf_counter() result = rust_engine.complex_task_runner(events) end = time.perf_counter() duration = end - start return { "status": "ok", "rust_result": result, "rust_pure_time": f"{duration:.4f} sec", "log_check": "프로젝트 루트의 /logs 디렉토리에서 확인" }
Maturin 라이브러리를 통해 Rust 코드를 Python에서 호출 가능한 형태로 컴파일한다.
병렬 처리가 포함된 코드는 성능 최적화를 위해 --release 를 붙여 컴파일한다.
컴파일 후 pip list 명령어를 사용해 보면 Cargo.toml 파일에 작성한 패키지 이름을 확인할 수 있다.
uvicorn 라이브러리를 통해 FastAPI를 실행한다.
~/workspace/log-archiving$ maturin develop --release ~/workspace/log-archiving$ uvicorn app.main:app --reload
curl 명령어 또는 브라우저를 통해 다음과 같은 테스트를 해볼 수 있다.
http://127.0.0.1:8000/run-complex-task~$ curl -i http://127.0.0.1:8000/run-complex-task HTTP/1.1 200 OK date: Sun, 05 Apr 2026 22:48:13 GMT server: uvicorn content-length: 169 content-type: application/json; charset=utf-8 {"status":"ok","rust_result":"모든 작업 수행 및 로깅 완료","rust_pure_time":"0.0635 sec","log_check":"프로젝트 루트의 /logs 디렉토리에서 확인"}%INFO: Started reloader process [75975] using StatReload 2026-04-05T22:48:07.792253Z INFO ThreadId(02) rust_engine: Rust Engine 가동 및 Tracing 시스템 초기화 완료 INFO: Started server process [75977] INFO: Waiting for application startup. INFO: Application startup complete. 2026-04-05T22:48:14.300101Z INFO ThreadId(02) complex_task_runner{events=["normal_event_1", "critical_error_payload", "short", "very_long_event_name_for_warning", "normal_event_2"]}: rust_engine: 통합 태스크 러너 가동 2026-04-05T22:48:14.300434Z DEBUG ThreadId(02) complex_task_runner{events=["normal_event_1", "critical_error_payload", "short", "very_long_event_name_for_warning", "normal_event_2"]}:db_query{id=101}: rust_engine: DB 데이터 조회 중... 2026-04-05T22:48:14.352516Z INFO ThreadId(02) complex_task_runner{events=["normal_event_1", "critical_error_payload", "short", "very_long_event_name_for_warning", "normal_event_2"]}:db_query{id=101}: rust_engine: DB 조회 완료 2026-04-05T22:48:14.352827Z WARN ThreadId(26) worker{id=3}: rust_engine: 비정상적으로 긴 이벤트명 감지: very_long_event_name_for_warning 2026-04-05T22:48:14.352837Z ERROR ThreadId(22) worker{id=1}: rust_engine: 심각한 페이로드 발견: critical_error_payload 2026-04-05T22:48:14.362937Z DEBUG ThreadId(29) worker{id=0}: rust_engine: 일반 태스크 처리 중: normal_event_1 2026-04-05T22:48:14.362966Z DEBUG ThreadId(27) worker{id=4}: rust_engine: 일반 태스크 처리 중: normal_event_2 2026-04-05T22:48:14.363032Z DEBUG ThreadId(18) worker{id=2}: rust_engine: 일반 태스크 처리 중: short INFO: 127.0.0.1:52324 - "GET /run-complex-task HTTP/1.1" 200 OK로그에 출력된
events인자가 길어 생략하고 싶다면
저번 실습에서처럼#[instrument(skip(events))]를 사용하면 되지만
어떤 인자가 전달되었는지도 로그 분석에 필요할 수 있으니 여기서는 남겨두는 방식으로 작성했다.
~/workspace/log-archiving$ tree -I venv -I target . ├── app │ └── main.py ├── Cargo.lock ├── Cargo.toml ├── logs │ └── server.log.2026-04-05-22 ├── pyproject.toml └── src └── lib.rs ~/workspace/log-archiving$ cat logs/server.log.2026-04-05-22 {"timestamp":"2026-04-05T22:48:07.792269Z","level":"INFO","fields":{"message":"Rust Engine 가동 및 Tracing 시스템 초기화 완료"},"target":"rust_engine"} {"timestamp":"2026-04-05T22:48:14.300122Z","level":"INFO","fields":{"message":"통합 태스크 러너 가동"},"target":"rust_engine","span":{"events":"[\"normal_event_1\", \"critical_error_payload\", \"short\", \"very_long_event_name_for_warning\", \"normal_event_2\"]","name":"complex_task_runner"},"spans":[{"events":"[\"normal_event_1\", \"critical_error_payload\", \"short\", \"very_long_event_name_for_warning\", \"normal_event_2\"]","name":"complex_task_runner"}]} {"timestamp":"2026-04-05T22:48:14.300449Z","level":"DEBUG","fields":{"message":"DB 데이터 조회 중..."},"target":"rust_engine","span":{"id":101,"name":"db_query"},"spans":[{"events":"[\"normal_event_1\", \"critical_error_payload\", \"short\", \"very_long_event_name_for_warning\", \"normal_event_2\"]","name":"complex_task_runner"},{"id":101,"name":"db_query"}]} {"timestamp":"2026-04-05T22:48:14.352541Z","level":"INFO","fields":{"message":"DB 조회 완료"},"target":"rust_engine","span":{"id":101,"name":"db_query"},"spans":[{"events":"[\"normal_event_1\", \"critical_error_payload\", \"short\", \"very_long_event_name_for_warning\", \"normal_event_2\"]","name":"complex_task_runner"},{"id":101,"name":"db_query"}]} {"timestamp":"2026-04-05T22:48:14.352843Z","level":"WARN","fields":{"message":"비정상적으로 긴 이벤트명 감지: very_long_event_name_for_warning"},"target":"rust_engine","span":{"id":3,"name":"worker"},"spans":[{"id":3,"name":"worker"}]} {"timestamp":"2026-04-05T22:48:14.352847Z","level":"ERROR","fields":{"message":"심각한 페이로드 발견: critical_error_payload"},"target":"rust_engine","span":{"id":1,"name":"worker"},"spans":[{"id":1,"name":"worker"}]} {"timestamp":"2026-04-05T22:48:14.362949Z","level":"DEBUG","fields":{"message":"일반 태스크 처리 중: normal_event_1"},"target":"rust_engine","span":{"id":0,"name":"worker"},"spans":[{"id":0,"name":"worker"}]} {"timestamp":"2026-04-05T22:48:14.362974Z","level":"DEBUG","fields":{"message":"일반 태스크 처리 중: normal_event_2"},"target":"rust_engine","span":{"id":4,"name":"worker"},"spans":[{"id":4,"name":"worker"}]} {"timestamp":"2026-04-05T22:48:14.363044Z","level":"DEBUG","fields":{"message":"일반 태스크 처리 중: short"},"target":"rust_engine","span":{"id":2,"name":"worker"},"spans":[{"id":2,"name":"worker"}]}