Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

컨텍스트와 메타데이터

masterror는 문자열로 이어 붙인 컨텍스트(format!("failed to X: {e}"))를 세 가지 구조화된 메커니즘으로 대체합니다: Context 빌더, 타입 기반 Metadata 필드, 그리고 전송 경계에서 적용되는 리덕션 정책입니다.

ResultExt: 외부 오류 승격하기

ResultExtE: Error + Send + Sync + 'static인 모든 Result<T, E>에 구현되어 있으며 두 가지 메서드를 제공합니다:

.context(msg) — anyhow 스타일

오류를 메시지로 감싸고, 원본 오류는 소스가 됩니다:

use masterror::ResultExt;

fn read_config() -> Result<String, std::io::Error> {
    Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}

let err = read_config().context("Failed to read config file").unwrap_err();
assert!(err.source_ref().is_some());

기저 오류가 이미 masterror::Error인 경우 .context()는 그 분류를 보존합니다. 종류, 코드, 메타데이터, 편집 정책, 재시도 힌트, 세부 정보가 이어지고, 메시지만 교체되며 원본 오류는 소스로 유지됩니다.

.ctx(|| Context) — 완전한 제어

use masterror::{AppErrorKind, Context, ResultExt, field};

fn validate() -> Result<(), std::io::Error> {
    Err(std::io::Error::other("boom"))
}

let err = validate()
    .ctx(|| {
        Context::new(AppErrorKind::Validation)
            .with(field::str("phase", "validate"))
            .redact(true)
            .track_caller()
    })
    .unwrap_err();

assert_eq!(err.kind, AppErrorKind::Validation);
assert!(err.metadata().get("phase").is_some());

클로저는 오류 경로에서만 평가됩니다.

Context 빌더

메서드효과
Context::new(kind)대상 범주. AppCode는 해당 종류의 표준 매핑으로 기본 설정됩니다
.code(AppCode)공개 코드 재정의
.category(kind)범주 변경. 코드가 재정의되지 않은 한 코드를 동기화 상태로 유지합니다
.with(field)메타데이터 Field 첨부
.redact(bool)메시지 리덕션 토글 (MessageEditPolicy::Redact / Preserve)
.redact_field(name, FieldRedaction)이름 있는 필드의 리덕션 정책 재정의
.track_caller()호출 지점을 caller.file, caller.line, caller.column 메타데이터로 기록

메타데이터 필드

Metadata는 정렬된 인라인 할당 방식의 타입 기반 필드 맵입니다(필드 0–4개는 스택에 유지됨). masterror::field 모듈로 필드를 만듭니다:

빌더FieldValue 변형
field::str("key", value)Str(Cow<'static, str>)
field::i64("key", -1)I64
field::u64("key", 42)U64
field::f64("key", 0.5)F64
field::bool("key", true)Bool
field::uuid("key", uuid)Uuid
field::duration("key", dur)Duration
field::ip("key", addr)Ip (v4 또는 v6)
field::json("key", json!({...}))Json (serde_json 기능 필요)

오류를 생성할 때나 Context를 통해 필드를 첨부하세요:

use core::time::Duration;
use masterror::{AppError, FieldValue, field};

let err = AppError::service("downstream degraded")
    .with_field(field::str("request_id", "abc123"))
    .with_field(field::duration("elapsed", Duration::from_millis(1500)))
    .with_field(field::u64("attempt", 2));

assert_eq!(err.metadata().len(), 3);
assert_eq!(err.metadata().get("attempt"), Some(&FieldValue::U64(2)));

for (name, value) in err.metadata().iter() {
    println!("{name}={value}");
}

with_fields(iter)는 이터레이터로부터 확장하고, with_metadata(meta)는 컨테이너를 교체하며, Metadata::insert는 키를 덮어쓸 때 이전 값을 반환합니다.

리덕션 정책

메시지 정책: MessageEditPolicy

Preserve(기본값)는 공개 메시지를 유지하고, Redact는 전송에서 메시지를 제거하도록 지시합니다. 오류에 .redactable(), Context.redact(true), 또는 #[masterror(...)]redact(message)로 설정합니다:

use masterror::{AppError, MessageEditPolicy, ProblemJson};

let err = AppError::internal("db-3 credentials rejected").redactable();
assert_eq!(err.edit_policy, MessageEditPolicy::Redact);

let problem = ProblemJson::from_app_error(err);
assert!(problem.detail.is_none());

필드 정책: FieldRedaction

각 필드는 메타데이터가 ProblemJson으로 직렬화될 때 적용되는 자체 정책을 지닙니다:

정책공개 페이로드에 미치는 효과
None값이 그대로 유지됨
Redact필드가 완전히 제거됨
Hash값이 SHA-256 다이제스트로 대체됨
Last4마지막 네 글자를 제외한 전부가 마스킹됨
use masterror::{AppError, FieldRedaction, field};

let err = AppError::internal("payment failed")
    .with_field(field::str("card_number", "4111111111111111"))
    .redact_field("card_number", FieldRedaction::Last4);

비밀 정보로 보이는 흔한 이름에는 필드 생성 시 안전한 기본값이 자동으로 적용됩니다. password, secret, authorization, cookie, session, jwt, bearer, otp, pin을 포함하는 이름은 기본적으로 Redact가 되고, 토큰/키 계열 이름(api_token, refresh_token, key, apikey)은 기본적으로 Hash가 되며, 카드/계좌 세그먼트와 숫자 계열 세그먼트가 결합된 이름(card_number, iban_no, account_id)은 기본적으로 Last4가 됩니다. 감지는 대소문자를 구분하지 않습니다. 명시적인 redact_field/with_redaction이 항상 우선합니다.

오류 체인

오류는 전체 인과 체인을 유지합니다. chain()은 오류 자신부터 근본 원인까지 반복하고, root_cause()는 가장 깊은 오류로 바로 건너뜁니다:

use masterror::AppError;

let io_err = std::io::Error::other("disk offline");
let app_err = AppError::internal("db down").with_context(io_err);

let chain: Vec<_> = app_err.chain().collect();
assert_eq!(chain.len(), 2);
assert_eq!(app_err.root_cause().to_string(), "disk offline");

업스트림 오류를 첨부할 때는 with_context(...)를 권장합니다. 소유된 오류나 공유 Arc<dyn Error + Send + Sync> 값을 받아 기존 할당을 재사용합니다. with_source(...) / with_source_arc(...)는 더 낮은 수준의 대응 메서드입니다.

다운캐스팅

첨부된 소스는 isdowncast_ref/downcast_mut로 검사합니다:

use masterror::AppError;

let io_err = std::io::Error::other("disk offline");
let err = AppError::internal("boom").with_context(io_err);

assert!(err.is::<std::io::Error>());

if let Some(io) = err.downcast_ref::<std::io::Error>() {
    assert_eq!(io.to_string(), "disk offline");
}
  • is::<E>() — 직접 소스가 타입 E일 때 true (전체 체인을 순회하지 않음).
  • downcast_ref::<E>() — 소스를 E로 대여.
  • downcast::<E>() / downcast_mut::<E>() — 현재는 스텁입니다 (downcast는 항상 Err(self)를, downcast_mut는 항상 None을 반환). downcast_ref를 권장합니다.

더 깊은 매칭이 필요하면 chain()을 순회하며 각 요소에 source.is::<E>() / source.downcast_ref::<E>()를 사용하세요.

백트레이스

backtrace 기능을 사용하면 err.backtrace()RUST_BACKTRACE를 존중하는 지연 캡처된 std::backtrace::Backtrace를 반환하고, with_backtrace(bt)는 명시적 캡처를 첨부합니다. 오류가 다시 감싸질 때 백트레이스는 Arc로 공유되므로 .context() 체인은 다시 캡처하지 않습니다.


함께 보기: 시작하기 · 오류 종류와 코드 · Derive 매크로 · 관측성 · 모범 사례