컨텍스트와 메타데이터
masterror는 문자열로 이어 붙인 컨텍스트(format!("failed to X: {e}"))를 세 가지 구조화된 메커니즘으로 대체합니다: Context 빌더, 타입 기반 Metadata 필드, 그리고 전송 경계에서 적용되는 리덕션 정책입니다.
ResultExt: 외부 오류 승격하기
ResultExt는 E: 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(...)는 더 낮은 수준의 대응 메서드입니다.
다운캐스팅
첨부된 소스는 is와 downcast_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 매크로 · 관측성 · 모범 사례