open·source logoopen·source
All crates
Open source · Rust · MIT

moniof

v1.0.0

Monitor Over Fetch — N+1 detection for Actix

Actix Web middleware + instrumentation that detects N+1 / over-fetch patterns, tracks per-request DB latency, exposes Prometheus metrics, and sends Slack alerts. Works with MongoDB (command events) and SQLx (tracing spans). Inspired by Ruby’s bullet gem.

$cargo add moniof --features mongodb,sqlx
actixn+1observabilitymongodbsqlx
~/moniof
moniof
v1.0.0 · MIT · Rust Rust
$ cargo add moniof --features mongodb,sqlx
N+1 & over-fetch detection scoped per request
MongoDB command-event instrumentation
SQLx instrumentation via tracing spans (auto-installed)
Prometheus metrics + x-moniof-* timing headers
Slack alerts for slow / failed DB calls

// documentation

moniof docs

Overview

moniof (Monitor Over Fetch) is an Actix Web middleware and instrumentation crate that detects N+1 / over-fetch patterns, tracks per-request DB latency, exposes Prometheus metrics, and sends Slack alerts. It works with MongoDB (via command events) and SQLx (via tracing spans). Inspired by Ruby’s bullet gem — but built for Rust + Actix.

  • Actix middleware (MoniOF) that scopes DB stats to each request
  • N+1 & over-fetch detection with configurable thresholds
  • MongoDB instrumentation via CommandEventHandler
  • SQLx instrumentation via a tracing Layer (installed automatically)
  • Prometheus metrics and per-response x-moniof-* headers
  • Slack alerts for request-level anomalies and slow / failed DB commands
Note — Requires Rust 1.75+ and Actix Web 4. The only environment variable read is RUST_LOG (moniof appends its own debug directives on top).

Installation & Features

toml
[dependencies]
moniof = { version = "1.0.0", features = ["mongodb", "sqlx"] }
actix-web = "4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "registry"] }
mongodb = "2"
sqlx = { version = "0.8", features = ["postgres", "runtime-tokio-rustls"] }
FeatureDefaultEnables
mongodbONMOFMongoEvents (MongoDB command instrumentation)
sqlxoffMOFSqlEvents (SQLx tracing layer, installed by initiate())

Step 1 — Initialize globally

Call initiate() once in main(). It installs a tracing subscriber (reading RUST_LOG, then adding moniof + sqlx directives), installs the SQLx layer when the sqlx feature is on, and stores the global config.

rust
use moniof::{MoniOFGlobalConfig, initiate as moniof_initiate};

fn main() {
    moniof_initiate(MoniOFGlobalConfig {
        log_each_db_event: false,
        slow_db_threshold_ms: Some(100),
        low_db_threshold_ms: None,
        slack_webhook: None,        // Some("https://hooks.slack.com/...".into())
        ..Default::default()
    });

    // Start Actix...
}

MoniOFGlobalConfig

The process-wide config (all fields default to false / None). It governs Mongo command logging and Slack; it is not where N+1 thresholds live.

FieldTypeDefaultControls
log_each_db_eventboolfalseLog each Mongo command start/finish
slow_db_threshold_msOption<u64>Nonems ≥ threshold → WARN + optional Slack "slow" alert
low_db_threshold_msOption<u64>Nonems ≤ threshold → DEBUG (suspiciously fast?)
slack_webhookOption<String>NoneSlack incoming-webhook URL for all alerts

Step 2 — Add the middleware

Wrap your Actix app. MoniOF::new() uses the default per-request config; MoniOF::with_config(cfg) customizes it. Each request installs a task-local stats handle so DB instrumentation records into that request’s scope, then computes counts, latency, and N+1 suspects.

rust
use moniof::MoniOF;

HttpServer::new(|| {
    App::new()
        .wrap(MoniOF::new())
})

MoniOFConfig (per middleware)

A separate struct passed to MoniOF::with_config — it drives thresholds, headers, and N+1 detection.

FieldDefaultControls
max_total60total queries > this → "high DB query count" warning
max_same_key20most-repeated key count > this → "repeated same key" warning
add_response_headerstrueEmit the x-moniof-* headers
log_warningstrueMaster switch for warnings + Slack
warn_total_db_latency_msNoneCumulative DB latency ≥ this → warning
warn_low_total_db_latency_msNonequeries > 0 and latency ≤ this → warning
of_modetrueEnable N+1 suspect detection
n_plus_one_min_count5Min repeats for a key to be an N+1 suspect
n_plus_one_min_total_msSome(5)Min cumulative latency for a suspect to qualify

MongoDB Integration

Attach MOFMongoEvents as the command event handler so every command is counted and timed. Keys are namespaced as mongo/{collection}/{op}.

rust
use moniof::MOFMongoEvents;
use std::sync::Arc;
use mongodb::{Client, options::ClientOptions};

let mut opts = ClientOptions::parse(&mongo_uri).await?;
opts.command_event_handler = Some(Arc::new(MOFMongoEvents::default()));

let client = Client::with_options(opts)?;
let db = client.database("mydb");

SQLx Integration

With the sqlx feature enabled, initiate() installs the MOFSqlEvents tracing layer automatically — you do not attach anything manually. It hooks spans under the sqlx::query target, normalizes the SQL (whitespace-collapsed, lowercased, truncated to 200 chars) into a sql/{normalized} key, and records count + latency.

rust
let rows = sqlx::query!("SELECT id FROM users")
    .fetch_all(pool)
    .await?;
// moniof::sql: SQL completed key="select id from users" latency_ms=2

Prometheus Metrics

Register the async prom::metrics_handler as an Actix route (note: metrics_handler, not metrics). init_prometheus() is idempotent and called lazily.

rust
use actix_web::web;

App::new()
    .wrap(moniof::MoniOF::new())
    .route("/metrics", web::get().to(moniof::prom::metrics_handler))
MetricTypeLabels
moniof_http_requests_totalIntCounterVecmethod, status
moniof_http_inflight_requestsIntGauge
moniof_http_request_duration_secondsHistogramVecmethod
moniof_db_total_latency_secondsHistogramVeckind
moniof_mongo_command_duration_secondsHistogramVeccollection, op
Note — Histogram buckets (seconds): 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0. SQL latency flows into moniof_db_total_latency_seconds (there is no dedicated SQL histogram).

Response Headers

When add_response_headers is true, each response carries request-scoped timing (only the N+1 headers require of_mode and at least one suspect).

text
x-moniof-total: 5                  # total query count
x-moniof-elapsed-ms: 18            # request elapsed
x-moniof-db-total-ms: 12           # cumulative DB latency
x-moniof-slowest-key: users/find   # slowest single key
x-moniof-slowest-latency-ms: 9
x-moniof-n-plus-one-key: users/find
x-moniof-n-plus-one-count: 5
x-moniof-n-plus-one-total-ms: 7

N+1 Detection & Slack

When of_mode is on, find_suspects scans each key: it skips keys repeated fewer than n_plus_one_min_count (default 5) times, and (if n_plus_one_min_total_ms is set) those whose cumulative latency is below the minimum. Remaining keys are ranked by count then latency, and the top 3 become suspects (surfaced in the headers and warnings).

Slack alerts fire only when slack_webhook is set. Triggers: request-level anomalies (high total, repeated key, high/low cumulative latency, or N+1 suspects) when log_warnings is on; a slow MongoDB command (slow_db_threshold_ms); and every failed MongoDB command.

Ready to try moniof?

cargo add moniof --features mongodb,sqlx
View on crates.io

// more

moniof — help & policies