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
7 changes: 7 additions & 0 deletions crates/azoth-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ chrono = { workspace = true }
rusqlite = { workspace = true }
parking_lot = { workspace = true }

# Optional metrics support
metrics = { version = "0.24", optional = true }

[features]
default = []
observe = ["metrics"]

[dev-dependencies]
tempfile = { workspace = true }
tracing-subscriber = { workspace = true }
38 changes: 38 additions & 0 deletions crates/azoth-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,44 @@ pub enum AzothError {

pub type Result<T> = std::result::Result<T, AzothError>;

impl AzothError {
/// Wrap this error with additional context.
///
/// The context string is prepended to the error message, producing a
/// chain like `"during balance update: Transaction error: ..."`.
///
/// # Example
/// ```ignore
/// db.write_txn()
/// .map_err(|e| e.context("during balance update"))?;
/// ```
pub fn context(self, msg: impl Into<String>) -> Self {
let ctx = msg.into();
AzothError::Internal(format!("{}: {}", ctx, self))
}
}

/// Extension trait to add `.context()` on `Result<T, AzothError>`.
///
/// Mirrors the ergonomics of `anyhow::Context`.
pub trait ResultExt<T> {
/// If the result is `Err`, wrap the error with additional context.
fn context(self, msg: impl Into<String>) -> Result<T>;

/// If the result is `Err`, wrap the error with a lazily-evaluated context.
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T>;
}

impl<T> ResultExt<T> for Result<T> {
fn context(self, msg: impl Into<String>) -> Result<T> {
self.map_err(|e| e.context(msg))
}

fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T> {
self.map_err(|e| e.context(f()))
}
}

// Custom Error Types:
//
// Azoth supports custom error types through the `#[from] anyhow::Error` variant.
Expand Down
1 change: 1 addition & 0 deletions crates/azoth-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod config;
pub mod error;
pub mod event_log;
pub mod lock_manager;
pub mod observe;
pub mod traits;
pub mod types;

Expand Down
128 changes: 128 additions & 0 deletions crates/azoth-core/src/observe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! Optional metrics instrumentation for Azoth.
//!
//! When the `observe` feature is enabled, key operations emit counters,
//! histograms, and gauges via the [`metrics`] crate. A downstream
//! application must install a metrics recorder (e.g. `metrics-exporter-prometheus`)
//! to collect the data.
//!
//! When the feature is **not** enabled every function in this module is a
//! zero-cost no-op.

/// Record a transaction commit (counter + latency histogram).
///
/// - `azoth.transaction.commits_total` – incremented on every commit
/// - `azoth.transaction.commit_duration_seconds` – histogram of commit latency
#[inline]
pub fn record_commit(duration: std::time::Duration) {
#[cfg(feature = "observe")]
{
metrics::counter!("azoth.transaction.commits_total").increment(1);
metrics::histogram!("azoth.transaction.commit_duration_seconds")
.record(duration.as_secs_f64());
}
#[cfg(not(feature = "observe"))]
{
let _ = duration;
}
}

/// Record a preflight validation (counter + duration).
///
/// - `azoth.preflight.total` – counter
/// - `azoth.preflight.duration_seconds` – histogram
#[inline]
pub fn record_preflight(duration: std::time::Duration, success: bool) {
#[cfg(feature = "observe")]
{
let outcome = if success { "ok" } else { "fail" };
metrics::counter!("azoth.preflight.total", "outcome" => outcome).increment(1);
metrics::histogram!("azoth.preflight.duration_seconds").record(duration.as_secs_f64());
}
#[cfg(not(feature = "observe"))]
{
let _ = (duration, success);
}
}

/// Record a preflight cache hit or miss.
///
/// - `azoth.preflight_cache.lookups_total` – counter with `result` label (`hit` / `miss`)
#[inline]
pub fn record_cache_lookup(hit: bool) {
#[cfg(feature = "observe")]
{
let result = if hit { "hit" } else { "miss" };
metrics::counter!("azoth.preflight_cache.lookups_total", "result" => result).increment(1);
}
#[cfg(not(feature = "observe"))]
{
let _ = hit;
}
}

/// Set the current preflight cache size gauge.
///
/// - `azoth.preflight_cache.size` – gauge
#[inline]
pub fn set_cache_size(size: usize) {
#[cfg(feature = "observe")]
{
metrics::gauge!("azoth.preflight_cache.size").set(size as f64);
}
#[cfg(not(feature = "observe"))]
{
let _ = size;
}
}

/// Record a projector run (counter + duration + events processed).
///
/// - `azoth.projector.runs_total` – counter
/// - `azoth.projector.run_duration_seconds` – histogram
/// - `azoth.projector.events_processed_total` – counter
#[inline]
pub fn record_projector_run(duration: std::time::Duration, events_processed: u64) {
#[cfg(feature = "observe")]
{
metrics::counter!("azoth.projector.runs_total").increment(1);
metrics::histogram!("azoth.projector.run_duration_seconds").record(duration.as_secs_f64());
metrics::counter!("azoth.projector.events_processed_total").increment(events_processed);
}
#[cfg(not(feature = "observe"))]
{
let _ = (duration, events_processed);
}
}

/// Record a lock acquisition wait time.
///
/// - `azoth.lock.wait_duration_seconds` – histogram
#[inline]
pub fn record_lock_wait(duration: std::time::Duration) {
#[cfg(feature = "observe")]
{
metrics::histogram!("azoth.lock.wait_duration_seconds").record(duration.as_secs_f64());
}
#[cfg(not(feature = "observe"))]
{
let _ = duration;
}
}

/// Record a backup operation.
///
/// - `azoth.backup.total` – counter with `outcome` label
/// - `azoth.backup.duration_seconds` – histogram
#[inline]
pub fn record_backup(duration: std::time::Duration, success: bool) {
#[cfg(feature = "observe")]
{
let outcome = if success { "ok" } else { "fail" };
metrics::counter!("azoth.backup.total", "outcome" => outcome).increment(1);
metrics::histogram!("azoth.backup.duration_seconds").record(duration.as_secs_f64());
}
#[cfg(not(feature = "observe"))]
{
let _ = (duration, success);
}
}
1 change: 1 addition & 0 deletions crates/azoth-lmdb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod state_iter;
pub mod store;
pub mod txn;

pub use preflight_cache::EvictionPolicy;
pub use read_pool::{LmdbReadPool, PooledLmdbReadTxn};
pub use store::LmdbCanonicalStore;
pub use txn::{LmdbReadTxn, LmdbWriteTxn};
Loading