Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [0.14.0] - 2025-09-24

### Added
- Introduced optional `tracing`, `metrics` and `backtrace` features. When
enabled they emit structured `tracing` events, increment the
`error_total{code,category}` counter and capture lazy [`Backtrace`] snapshots
from a new `AppError::emit_telemetry` hook.

### Changed
- Reworked the `AppError` core to emit telemetry exactly once, track dirty
mutations and expose a crate-private `new_raw` constructor for contexts that
enrich errors before flushing instrumentation.
- Updated Axum and Actix integrations to rely on the telemetry hook instead of
manually logging errors while preserving backward-compatible APIs.

### Tests
- Added tracing dispatcher coverage to assert a single telemetry event with MDC
propagated `trace_id` values.
- Installed a deterministic metrics recorder in unit tests to confirm
`error_total` increments once per error.

## [0.13.1] - 2025-09-23

### Fixed
Expand Down
99 changes: 98 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "masterror"
version = "0.13.1"
version = "0.14.0"
rust-version = "1.90"
edition = "2024"
license = "MIT OR Apache-2.0"
Expand Down Expand Up @@ -50,6 +50,9 @@ readme = "README.md"

[features]
default = []
tracing = ["dep:tracing", "dep:log", "dep:log-mdc"]
metrics = ["dep:metrics"]
backtrace = []
axum = ["dep:axum", "dep:serde_json"]
actix = ["dep:actix-web", "dep:serde_json"]

Expand Down Expand Up @@ -77,7 +80,10 @@ masterror-template = { version = "0.3.6" }
[dependencies]
masterror-derive = { version = "0.7" }
masterror-template = { workspace = true }
tracing = "0.1"
tracing = { version = "0.1", optional = true }
log = { version = "0.4", optional = true }
log-mdc = { version = "0.1", optional = true }
metrics = { version = "0.24", optional = true }

serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", optional = true }
Expand Down Expand Up @@ -125,6 +131,7 @@ tokio = { version = "1", features = [
trybuild = "1"
toml = "0.9"
tempfile = "3"
tracing-subscriber = { version = "0.3", features = ["registry"] }

[build-dependencies]
serde = { version = "1", features = ["derive"] }
Expand All @@ -136,6 +143,9 @@ feature_order = [
"actix",
"openapi",
"serde_json",
"tracing",
"metrics",
"backtrace",
"sqlx",
"sqlx-migrate",
"reqwest",
Expand Down Expand Up @@ -176,6 +186,15 @@ description = "Generate utoipa OpenAPI schema for ErrorResponse"
[package.metadata.masterror.readme.features.serde_json]
description = "Attach structured JSON details to AppError"

[package.metadata.masterror.readme.features.tracing]
description = "Emit structured tracing events when errors are constructed"

[package.metadata.masterror.readme.features.metrics]
description = "Increment `error_total{code,category}` counter for each AppError"

[package.metadata.masterror.readme.features.backtrace]
description = "Capture lazy `Backtrace` snapshots when telemetry is flushed"

[package.metadata.masterror.readme.features.sqlx]
description = "Classify sqlx_core::Error variants into AppError kinds"

Expand Down
31 changes: 18 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ guides, comparisons with `thiserror`/`anyhow`, and troubleshooting recipes.

~~~toml
[dependencies]
masterror = { version = "0.13.1", default-features = false }
masterror = { version = "0.14.0", default-features = false }
# or with features:
# masterror = { version = "0.13.1", features = [
# masterror = { version = "0.14.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
# "teloxide", "telegram-webapp-sdk", "frontend", "turnkey"
# "tracing", "metrics", "backtrace", "sqlx",
# "sqlx-migrate", "reqwest", "redis", "validator",
# "config", "tokio", "multipart", "teloxide",
# "telegram-webapp-sdk", "frontend", "turnkey"
# ] }
~~~

Expand Down Expand Up @@ -76,14 +77,15 @@ masterror = { version = "0.13.1", default-features = false }
~~~toml
[dependencies]
# lean core
masterror = { version = "0.13.1", default-features = false }
masterror = { version = "0.14.0", default-features = false }

# with Axum/Actix + JSON + integrations
# masterror = { version = "0.13.1", features = [
# masterror = { version = "0.14.0", features = [
# "axum", "actix", "openapi", "serde_json",
# "sqlx", "sqlx-migrate", "reqwest", "redis",
# "validator", "config", "tokio", "multipart",
# "teloxide", "telegram-webapp-sdk", "frontend", "turnkey"
# "tracing", "metrics", "backtrace", "sqlx",
# "sqlx-migrate", "reqwest", "redis", "validator",
# "config", "tokio", "multipart", "teloxide",
# "telegram-webapp-sdk", "frontend", "turnkey"
# ] }
~~~

Expand Down Expand Up @@ -671,6 +673,9 @@ assert_eq!(resp.status, 401);
- `actix` — Actix Web ResponseError and Responder implementations
- `openapi` — Generate utoipa OpenAPI schema for ErrorResponse
- `serde_json` — Attach structured JSON details to AppError
- `tracing` — Emit structured tracing events when errors are constructed
- `metrics` — Increment `error_total{code,category}` counter for each AppError
- `backtrace` — Capture lazy `Backtrace` snapshots when telemetry is flushed
- `sqlx` — Classify sqlx_core::Error variants into AppError kinds
- `sqlx-migrate` — Map sqlx::migrate::MigrateError into AppError (Database)
- `reqwest` — Classify reqwest::Error as timeout/network/external API
Expand Down Expand Up @@ -709,13 +714,13 @@ assert_eq!(resp.status, 401);
Minimal core:

~~~toml
masterror = { version = "0.13.1", default-features = false }
masterror = { version = "0.14.0", default-features = false }
~~~

API (Axum + JSON + deps):

~~~toml
masterror = { version = "0.13.1", features = [
masterror = { version = "0.14.0", features = [
"axum", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand All @@ -724,7 +729,7 @@ masterror = { version = "0.13.1", features = [
API (Actix + JSON + deps):

~~~toml
masterror = { version = "0.13.1", features = [
masterror = { version = "0.14.0", features = [
"actix", "serde_json", "openapi",
"sqlx", "reqwest", "redis", "validator", "config", "tokio"
] }
Expand Down
11 changes: 7 additions & 4 deletions src/app_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@
//! }
//! ```
//!
//! ## Logging
//! ## Telemetry
//!
//! [`AppError::log`] emits a single structured `tracing::error!` event with
//! `kind`, `code` and optional `message` fields. Prefer calling it at the
//! transport boundary (e.g. in `IntoResponse`) to avoid duplicate logs.
//! [`AppError::log`] flushes telemetry once: it emits a structured `tracing`
//! event (when the `tracing` feature is enabled), increments the
//! `error_total{code,category}` counter (with the `metrics` feature) and
//! captures a lazy [`Backtrace`] snapshot (with the `backtrace` feature).
//! Constructors and framework integrations call it automatically, so manual
//! usage is rarely required.

mod constructors;
mod context;
Expand Down
4 changes: 2 additions & 2 deletions src/app_error/constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ impl AppError {
/// assert!(err.message.is_none());
/// ```
pub fn database(msg: Option<Cow<'static, str>>) -> Self {
let mut err = Self::bare(AppErrorKind::Database);
err.message = msg;
let err = Self::new_raw(AppErrorKind::Database, msg);
err.emit_telemetry();
err
}

Expand Down
Loading
Loading