로그 아카이빙

로그를 터미널에 출력하고 넘어가면
실시간으로 관측하고 있지 않은 이상 문제를 파악하기 어렵다.
따라서 로그를 파일의 형태로 아카이빙할 필요가 있다.

여기서는 조금 더 다양한 상황을 연출하기 위해
병렬 처리뿐만 아니라 비동기 처리에 대한 시뮬레이션을 추가했다.

작업공간 생성 및 구조 확인

~/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"] }

코드 작성

Rust 코드

tracing_appender::non_blocking 을 사용하여
별도의 스레드가 매 시간마다 새로운 로그 파일을 생성하고
logs 폴더에 저장하도록 한다.

guard 는 프로그램이 갑자기 종료될 때
버퍼에 남아있던 로그들을 파일에 마저 쓰고 닫는 역할을 하는데,
이를 전역 변수에 저장하여 프로그램 종료 시까지 살려두어야
함수가 끝나는 즉시 사라지지 않고
프로그램이 끝날 때까지 살아 있을 수 있다.

std::sync::OnceLock 을 사용하면
Python이 실행되는 동안 로그 시스템이 죽지 않고 살아있게 된다.

src/lib.rs

use 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(())
}

Python 코드

app/main.py

import 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"}]}
profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다

0개의 댓글