로그 남기기

서버를 가동하다가 문제가 발생했을 때
원인을 찾고 문제를 해결하기 위해서는
어디서 어떤 문제가 발생했는지 알아야 한다.

이를 위해 정상 작동과 그렇지 않은 상황에 대한
로그를 남겨 두는 것이 좋다.

우선 터미널에 로그를 찍는 것을 알아보자.

작업공간 생성 및 구조 확인

~/workspace$ mkdir log-tracing && cd log-tracing
~/workspace/log-tracing$ python3 -m venv venv
~/workspace/log-tracing$ source venv/bin/activate
~/workspace/log-tracing$ # 평소에 설치하던 것 외에 추가된 라이브러리를 놓치지 말자
~/workspace/log-tracing$ pip install maturin fastapi uvicorn orjson
~/workspace/log-tracing$ maturin init
~/workspace/log-tracing$ # 선택지 중 기본값인 PyO3 선택
~/workspace/log-tracing$ # Cargo.toml과 src/lib.rs가 자동 생성된다
~/workspace/log-tracing$ # Python 코드는 직접 생성해 주어야 한다
~/workspace/log-tracing$ mkdir app && touch app/main.py
~/workspace/log-tracing$ tree -I venv
.
├── app
│   └── main.py
├── Cargo.toml
├── pyproject.toml
└── src
    └── lib.rs

Cargo.toml 파일을 열어 라이브러리 이름을 수정해 주겠다.

Rust에서 로그를 남기기 위해서는 tracing 크레이트를 사용해야 한다.
그리고 로그를 출력하고 필터링하기 위해 tracing-subscriber를 사용한다.
features = ["env-filter"] 를 명시해 주면
다시 컴파일할 필요 없이 환경변수만으로 필터를 조정할 수 있다.

유의미한 로그가 나오는 환경을 시뮬레이션하기 위해
병렬 처리를 위한 rayon도 사용한다.

Cargo.toml

[package]
name = "log-tracing"
version = "0.1.0"
edition = "2024"

[lib]
name = "rust_engine"
crate-type = ["cdylib"]

[dependencies]
pyo3 = "0.28.0"
rayon = "1.11"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

코드 작성

Rust 코드

Python에서 rust_engine 모듈을 가져올 때 최초로 한 번
로그에 어떤 스레드가 작업하는지 표시하도록 하며
EnvFilter 를 통해 환경변수에서 가져온 로그 수준을 반영하는
초기화 과정을 거친다.

#[instrument] 속성을 사용하면 함수 호출 시
자동으로 맥락(Span)을 생성하며
함수의 인자로 전달된 task_id 가 로그에 자동으로 찍힌다.

span!() 매크로를 사용하면
로그 수준에 따라 로그 앞에 부가적인 정보를 붙일 수도 있다.

지연 및 조건부 경고/에러에 대한 시뮬레이션을 작성한다.

src/lib.rs

use pyo3::prelude::*;
use rayon::prelude::*;
use tracing::{info, debug, warn, error, instrument, span, Level};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use std::time::Duration;

fn init_tracing() {
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info");

    let _ = tracing_subscriber::registry()
        .with(fmt::layer().with_thread_ids(true))
        .with(filter)
        .try_init();
}

#[pyfunction]
#[instrument(skip(events))] // 인자로 전달된 `events` 자체에 대한 건 생략
fn process_events_parallel(events: Vec<String>) -> PyResult<String> {
    info!("총 {}개의 이벤트 병렬 처리 시작", events.len());

    let results: Vec<bool> = events
        .into_par_iter()
        .enumerate()
        .map(|(idx, event_name)| {
            let event_span = span!(Level::DEBUG, "event_worker", id = idx);
            let _enter = event_span.enter();

            debug!("데이터 검증 중: {}", event_name);

            if event_name.contains("error") {
                error!("심각한 페이로드 발견: {}", event_name);
                false
            } else if event_name.len() > 10 {
                warn!("비정상적으로 긴 이벤트명 감지");
                true
            } else {
                // 이벤트 처리에 걸리는 시간 시뮬레이션
                std::thread::sleep(Duration::from_millis(10));
                true
            }
        })
        .collect();

    let success_count = results.iter().filter(|&&r| r).count();

    if success_count < results.len() {
        warn!("일부 작업이 실패했습니다. (성공: {} / 전체: {})", success_count, results.len());
    } else {
        info!("{}개의 모든 이벤트가 성공적으로 처리되었습니다.", results.len());
    }

    Ok(format!("처리 완료: {}건 성공", success_count))
}

#[pymodule]
fn rust_engine(m: &Bound<'_, PyModule>) -> PyResult<()> {
    init_tracing();
    tracing::info!("Rust Engine 가동 및 Tracing 시스템 초기화 완료");

    m.add_function(wrap_pyfunction!(process_events_parallel, m)?)?;

    Ok(())
}

Python 코드

Rust로 작성한 모듈을 불러오기 전에 환경 변수를 설정한다.

여기서 설정해주지 않을 경우 서버를 실행할 때
다음과 같이 환경 변수를 설정해 주어도 된다.

~/workspace/log-tracing$ RUST_LOG=debug uvicorn app.main:app --reload

app/main.py

import os
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

# rust_engine을 로드하기 전에 로그 수준 설정
# debug: 각 스레드의 상세 log 확인 가능
# info: 요약 정보만 확인 가능
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("/process")
def process():
    test_events = [
        "login_event",
        "purchase_event_with_long_name",
        "system_error_critical",
        "logout_event",
        "something"
    ]

    result = rust_engine.process_events_parallel(test_events)
   
    return {
        "status": "ok",
        "message": result
    }

빌드 및 실행

Maturin 라이브러리를 통해 Rust 코드를 Python에서 호출 가능한 형태로 컴파일한다.
병렬 처리가 포함된 코드는 성능 최적화를 위해 --release 를 붙여 컴파일한다.
컴파일 후 pip list 명령어를 사용해 보면 Cargo.toml 파일에 작성한 패키지 이름을 확인할 수 있다.

uvicorn 라이브러리를 통해 FastAPI를 실행한다.

~/workspace/log-tracing$ maturin develop --release
~/workspace/log-tracing$ uvicorn app.main:app --reload

curl 명령어 또는 브라우저를 통해 다음과 같은 테스트를 해볼 수 있다.

  • http://127.0.0.1:8000/process

로그 수준이 DEBUG일 때

~$ curl -i http://127.0.0.1:8000/process
HTTP/1.1 200 OK
date: Thu, 02 Apr 2026 23:21:58 GMT
server: uvicorn
content-length: 54
content-type: application/json; charset=utf-8

{"status":"ok","message":"처리 완료: 4건 성공"}% 
INFO:     Started reloader process [65590] using StatReload
2026-04-02T23:21:55.889185Z  INFO ThreadId(01) rust_engine: Rust Engine 가동 및 Tracing 시스템 초기화 완료
INFO:     Started server process [65592]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
2026-04-02T23:21:59.932540Z  INFO ThreadId(02) process_events_parallel: rust_engine: 총 5개의 이벤트 병렬 처리 시작
2026-04-02T23:21:59.932686Z DEBUG ThreadId(16) event_worker{id=1}: rust_engine: 데이터 검증 중: purchase_event_with_long_name
2026-04-02T23:21:59.932693Z DEBUG ThreadId(13) event_worker{id=0}: rust_engine: 데이터 검증 중: login_event
2026-04-02T23:21:59.932686Z DEBUG ThreadId(15) event_worker{id=2}: rust_engine: 데이터 검증 중: system_error_critical
2026-04-02T23:21:59.932699Z DEBUG ThreadId(06) event_worker{id=3}: rust_engine: 데이터 검증 중: logout_event
2026-04-02T23:21:59.932721Z ERROR ThreadId(15) event_worker{id=2}: rust_engine: 심각한 페이로드 발견: system_error_critical
2026-04-02T23:21:59.932722Z  WARN ThreadId(06) event_worker{id=3}: rust_engine: 비정상적으로 긴 이벤트명 감지
2026-04-02T23:21:59.932733Z  WARN ThreadId(13) event_worker{id=0}: rust_engine: 비정상적으로 긴 이벤트명 감지
2026-04-02T23:21:59.932722Z  WARN ThreadId(16) event_worker{id=1}: rust_engine: 비정상적으로 긴 이벤트명 감지
2026-04-02T23:21:59.932737Z DEBUG ThreadId(05) event_worker{id=4}: rust_engine: 데이터 검증 중: something
2026-04-02T23:21:59.945342Z  WARN ThreadId(02) process_events_parallel: rust_engine: 일부 작업이 실패했습니다. (성공: 4 / 전체: 5)
INFO:     127.0.0.1:49647 - "GET /process HTTP/1.1" 200 OK

로그 수준이 INFO일 때 혹은 로그 수준을 명시하지 않았을 때

~$ curl -i http://127.0.0.1:8000/process
HTTP/1.1 200 OK
date: Thu, 02 Apr 2026 23:22:29 GMT
server: uvicorn
content-length: 54
content-type: application/json; charset=utf-8

{"status":"ok","message":"처리 완료: 4건 성공"}% 
INFO:     Started reloader process [65603] using StatReload
2026-04-02T23:22:27.262593Z  INFO ThreadId(01) rust_engine: Rust Engine 가동 및 Tracing 시스템 초기화 완료
INFO:     Started server process [65605]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
2026-04-02T23:22:30.232762Z  INFO ThreadId(02) process_events_parallel: rust_engine: 총 5개의 이벤트 병렬 처리 시작
2026-04-02T23:22:30.232913Z ERROR ThreadId(04) rust_engine: 심각한 페이로드 발견: system_error_critical
2026-04-02T23:22:30.232929Z  WARN ThreadId(15) rust_engine: 비정상적으로 긴 이벤트명 감지
2026-04-02T23:22:30.232932Z  WARN ThreadId(14) rust_engine: 비정상적으로 긴 이벤트명 감지
2026-04-02T23:22:30.232950Z  WARN ThreadId(16) rust_engine: 비정상적으로 긴 이벤트명 감지
2026-04-02T23:22:30.245526Z  WARN ThreadId(02) process_events_parallel: rust_engine: 일부 작업이 실패했습니다. (성공: 4 / 전체: 5)
INFO:     127.0.0.1:49648 - "GET /process HTTP/1.1" 200 OK
profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다

0개의 댓글