# error-path
**한국어** | [English](README.en.md)
`error-path`는 기존 Rust 에러 위에 **운영용 오류 주소(operational error address)** 를 얹는 가벼운 라이브러리입니다. `anyhow`, `thiserror`로 만든 자체 타입, `eyre`, `error-stack` 등을 대체하지 않습니다. 대신 오류가 시스템의 어느 기능과 경계에 속하는지 일관된 주소로 표현하도록 돕습니다.
```text
login.api.request_login
```
이 주소는 로그 검색, 고객 지원 응대, 운영 문서, UI 메시지, 오류 카탈로그에서 같은 식별자로 사용할 수 있습니다. 호출 순서를 복원하는 stack trace가 아니라, 사람이 정한 안정적인 운영 주소를 누적하는 것이 목적입니다.
## 설치
속성 매크로까지 함께 사용하려면 다음 한 줄이면 충분합니다.
```toml
[dependencies]
error-path = { version = "0.1", features = ["macros"] }
```
`ErrorPath`와 `WithErrorPath`만 사용한다면 feature 없이 설치합니다.
```toml
[dependencies]
error-path = "0.1"
```
최소 지원 Rust 버전(MSRV)은 1.77입니다.
## 빠른 시작
가장 안쪽 계층부터 안정적인 경로 세그먼트를 선택하고, 바깥 계층의 함수는 주소 조각을 붙입니다.
```rust
use error_path::{error_path, ErrorPath, ErrorPathExt, WithErrorPath};
#[error_path("login.api.request_login")]
fn request_login() -> Result<(), ServiceError> {
Err(ServiceError::new())
}
#[derive(Debug)]
struct ServiceError {
path: ErrorPath,
}
impl ServiceError {
fn new() -> Self {
Self { path: ErrorPath::new() }
}
}
impl WithErrorPath for ServiceError {
fn with_error_path(mut self, path: &'static str) -> Self {
self.path.prepend_path(path);
self
}
}
impl ErrorPathExt for ServiceError {
fn error_path(&self) -> Option<ErrorPath> {
Some(self.path.clone())
}
}
let error = request_login().unwrap_err();
assert_eq!(error.path.to_string(), "login.api.request_login");
```
라이브러리의 핵심 계약은 `WithErrorPath`입니다. 자체 오류 타입이 `ErrorPath`를 보관하고 이 trait을 구현하면 같은 매크로를 사용할 수 있습니다.
```rust
use error_path::{ErrorPath, WithErrorPath};
struct MyError { path: ErrorPath }
impl WithErrorPath for MyError {
fn with_error_path(mut self, path: &'static str) -> Self {
self.path.prepend_path(path);
self
}
}
```
## 오류 처리 지점에서 주소 조회
`WithErrorPath`는 주소를 누적하고, `ErrorPathExt`는 이미 생성된 오류에서 주소만 읽습니다. 로그·UI·CS 처리 지점에서는 trait을 import한 뒤 `error.error_path()`를 사용합니다.
```rust
use error_path::ErrorPathExt;
let error = request_login().unwrap_err();
let path = error.error_path().expect("ServiceError stores ErrorPath");
assert_eq!(path.to_string(), "login.api.request_login");
```
자체 오류 타입이 `ErrorPathExt`를 구현하지 않으면 기본값은 `None`입니다.
## 주소와 경로의 의미
주소는 바깥쪽에서 안쪽 순서로 구성됩니다. `#[error_path]`와 `#[error_path_impl]`이 안정적인 경로 세그먼트를 앞쪽에 추가합니다.
자체 오류 타입은 코드까지 포함한 구조화된 `ErrorPath`를 반환하는 API를 제공할 수 있습니다. `segments()`는 로그나 JSON 필드에 파싱 없이 사용할 수 있고, `to_string_with()`는 원하는 구분자로 표시합니다.
## 매크로 사용
`macros` feature는 다음 속성을 재노출합니다.
```rust
use error_path::{error_path_impl, error_path_skip};
struct LoginApi;
#[error_path_impl("login.api")]
impl LoginApi {
fn request_login(&self) -> Result<(), ServiceError> {
Err(ServiceError::new())
}
#[error_path_skip]
fn health_check(&self) -> Result<(), ServiceError> {
Ok(())
}
}
```
- `#[error_path]`는 인자가 없으면 함수 이름을, 문자열 인자가 있으면 그 문자열을 붙입니다.
- `#[error_path_impl]`는 각 `Result` 반환 메서드에 `{base}.{method_name}`을 붙입니다. 인자가 없으면 trait impl은 trait 이름, inherent impl은 타입 이름을 base로 사용합니다.
- `#[error_path_skip]`는 `#[error_path_impl]` 블록 안에서 해당 메서드를 제외합니다.
매크로는 반환된 `Err(E)`에 `WithErrorPath::with_error_path(e, path)`를 호출합니다. 따라서 대상 함수의 오류 타입 `E`가 trait을 구현해야 합니다.
주소는 attribute가 붙은 함수가 `Err`를 반환할 때만 앞에 추가됩니다. 중첩 호출 `A -> B -> C`에서 `C`가 실패하면 unwind 중 반환 경계를 거쳐 `A.B.C`가 됩니다. 함수 진입 시 부모 주소를 자동으로 받지 않으며, task-local·thread-local·함수 인자 전파는 범위 밖입니다.
`::` 구분자를 쓰려면 첫 `ErrorPath` 생성 또는 주소 누적 전에 설정하고, 매크로에도 같은 구분자를 사용합니다.
```rust
use error_path::{error_path_impl, set_path_delimiter};
set_path_delimiter("::")?;
#[error_path_impl("login::api")]
impl LoginApi for LoginApiImpl {
fn request_login(&self) -> Result<(), ServiceError> { todo!() }
}
// login::api::request_login
```
## 선택 기능과 어댑터
필요한 에러 시스템의 feature만 켭니다.
```toml
[dependencies]
error-path = { version = "0.1", features = ["macros", "anyhow"] }
anyhow = "1"
```
- `macros`: `#[error_path]`, `#[error_path_impl]`, `#[error_path_skip]` 재노출
- `anyhow`: 매크로 경로를 typed context로 추가하고 조회
- `eyre`: `eyre::Report`에 경로를 report context로 추가
- `error-stack`: `error_stack::Report<C>`에 `ErrorPathSegment` attachment 추가
- `stacked-errors`: `stacked_errors::Error`에 locationless kind 추가
- `full`: 위 feature를 모두 활성화
외부 에러 어댑터는 각 라이브러리의 context/attachment 기능을 사용합니다. `anyhow::Error`, `eyre::Report`, `error_stack::Report<C>`, `stacked_errors::Error` 모두 `ErrorPathExt`를 구현하므로 상위 매크로 경계를 지난 뒤 `error.error_path()`에서 매크로가 추가한 주소 세그먼트를 반환합니다. 원본 오류 메시지는 해석하거나 포함하지 않습니다.
```rust
use error_path::{ErrorPathExt, WithErrorPath};
let error = anyhow::anyhow!("HTTP communication failed");
let error = error.with_error_path("request_login");
let error = error.with_error_path("login.api");
assert_eq!(error.error_path().unwrap().to_string(), "login.api.request_login");
```
최종 주소는 자체 오류 타입이 `ErrorPath`를 보관하고 `ErrorPathExt`를 구현해 명시적으로 구성할 수 있습니다. `thiserror`는 오류 타입을 생성하는 derive macro이므로 별도 어댑터가 필요하지 않습니다. `#[derive(thiserror::Error)]`로 만든 타입에 `WithErrorPath`를 구현하면 됩니다. `anyhow::bail!` 같은 매크로도 반환하는 오류 타입이 `WithErrorPath`를 구현하거나 해당 adapter feature가 활성화되어 있으면 동작합니다.
## 구분자 설정
기본 구분자는 `.`입니다. 전역 구분자는 경로 문자열 해석과 출력에 함께 적용됩니다. 애플리케이션 시작 시 `set_path_delimiter`로 프로세스 전체 구분자를 한 번 설정할 수 있습니다.
```rust
use error_path::set_path_delimiter;
set_path_delimiter("::").expect("첫 ErrorPath 생성 전에 한 번만 설정합니다");
```
설정 뒤 `ErrorPath`의 `Display`, `to_string()`, 경로 문자열 해석은 `::`를 사용합니다. 따라서 매크로 인자와 직접 prefix도 `login::api::request_login`처럼 같은 구분자로 작성합니다. 첫 `ErrorPath` 생성·주소 누적·출력 전 설정하지 않으면 기본값 `.`이 고정됩니다. 빈 문자열과 두 번째 설정은 오류를 반환하며, 환경변수는 읽지 않습니다.
전역 설정과 관계없이 한 번만 다른 형식이 필요하면 `ErrorPath::to_string_with("/")`를 사용합니다.
## 범위와 제한
- `Result<T, E>` 경계를 다룹니다. panic, thread, spawn 내부 오류를 자동으로 수집하거나 변환하지 않습니다.
- 경로는 runtime stack trace나 파일 경로에서 추론하지 않습니다. 안정적인 운영 이름을 직접 정하거나 macro 기본 이름을 사용합니다.
- `#[error_path]`는 `Result`를 반환하는 함수에만 적용해야 합니다. `#[error_path_impl]`은 반환 타입 이름이 `Result`로 끝나는 메서드만 자동 감쌉니다.
- 매크로가 생성하는 경로는 `::error_path`를 참조합니다. 의존성 이름을 변경했다면 `error_path`라는 이름으로 다시 노출해야 합니다.
- 전역 구분자 설정은 `ErrorPath`의 기본 렌더링에 적용됩니다. 외부 adapter가 이미 보관한 context/attachment 표현을 바꾸지 않습니다.
## 더 알아보기
- [프로젝트 문서와 예제](https://github.com/yongaru/error-path-kit)
- [API 문서](https://docs.rs/error-path)
- [변경 기록](https://github.com/yongaru/error-path-kit/blob/main/CHANGELOG.md)
## 라이선스
Apache-2.0 또는 MIT 라이선스 중 하나를 선택해 사용할 수 있습니다.
## 만든이
용사장이 만들고 관리합니다. [akira76@gmail.com](mailto:akira76@gmail.com)