moniof
v1.0.0Monitor 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// 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
Installation & Features
[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"] }| Feature | Default | Enables |
|---|---|---|
| mongodb | ON | MOFMongoEvents (MongoDB command instrumentation) |
| sqlx | off | MOFSqlEvents (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.
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.
| Field | Type | Default | Controls |
|---|---|---|---|
| log_each_db_event | bool | false | Log each Mongo command start/finish |
| slow_db_threshold_ms | Option<u64> | None | ms ≥ threshold → WARN + optional Slack "slow" alert |
| low_db_threshold_ms | Option<u64> | None | ms ≤ threshold → DEBUG (suspiciously fast?) |
| slack_webhook | Option<String> | None | Slack 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.
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.
| Field | Default | Controls |
|---|---|---|
| max_total | 60 | total queries > this → "high DB query count" warning |
| max_same_key | 20 | most-repeated key count > this → "repeated same key" warning |
| add_response_headers | true | Emit the x-moniof-* headers |
| log_warnings | true | Master switch for warnings + Slack |
| warn_total_db_latency_ms | None | Cumulative DB latency ≥ this → warning |
| warn_low_total_db_latency_ms | None | queries > 0 and latency ≤ this → warning |
| of_mode | true | Enable N+1 suspect detection |
| n_plus_one_min_count | 5 | Min repeats for a key to be an N+1 suspect |
| n_plus_one_min_total_ms | Some(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}.
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.
let rows = sqlx::query!("SELECT id FROM users")
.fetch_all(pool)
.await?;
// moniof::sql: SQL completed key="select id from users" latency_ms=2Prometheus Metrics
Register the async prom::metrics_handler as an Actix route (note: metrics_handler, not metrics). init_prometheus() is idempotent and called lazily.
use actix_web::web;
App::new()
.wrap(moniof::MoniOF::new())
.route("/metrics", web::get().to(moniof::prom::metrics_handler))| Metric | Type | Labels |
|---|---|---|
| moniof_http_requests_total | IntCounterVec | method, status |
| moniof_http_inflight_requests | IntGauge | — |
| moniof_http_request_duration_seconds | HistogramVec | method |
| moniof_db_total_latency_seconds | HistogramVec | kind |
| moniof_mongo_command_duration_seconds | HistogramVec | collection, op |
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).
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: 7N+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// more