https://github.com/maku85/mongoose-lens
Slow query interceptor and index advisor for Mongoose 8+. Automatically runs explain() on slow queries, detects COLLSCAN/SORT stages, and suggests optimal indexes following the ESR rule - zero runtime dependencies
https://github.com/maku85/mongoose-lens
database-performance esm index-advisor mongodb mongodb-explain mongoose mongoose-plugin nodejs query-optimization slow-query typescript
Last synced: 15 days ago
JSON representation
Slow query interceptor and index advisor for Mongoose 8+. Automatically runs explain() on slow queries, detects COLLSCAN/SORT stages, and suggests optimal indexes following the ESR rule - zero runtime dependencies
- Host: GitHub
- URL: https://github.com/maku85/mongoose-lens
- Owner: maku85
- License: mit
- Created: 2026-05-05T07:27:30.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-05T08:11:48.000Z (about 1 month ago)
- Last Synced: 2026-05-05T10:15:50.002Z (about 1 month ago)
- Topics: database-performance, esm, index-advisor, mongodb, mongodb-explain, mongoose, mongoose-plugin, nodejs, query-optimization, slow-query, typescript
- Language: TypeScript
- Homepage:
- Size: 71.3 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# mongoose-lens
[](https://github.com/maku85/mongoose-lens/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/mongoose-lens)
[](LICENSE)
Slow query interceptor and index advisor for Mongoose 8+.
Automatically runs `explain('executionStats')` on queries that exceed configurable thresholds, detects the dominant execution stage (COLLSCAN, SORT, high-ratio FETCH …), and suggests an optimal index following the **ESR rule** (Equality → Sort → Range).
## Features
- Zero-overhead on fast queries — sampling, circuit breaker, and deduplication keep explain calls rare
- Non-blocking — explain runs asynchronously via `setImmediate` + bounded concurrency queue
- Human-readable advice with a ready-to-paste `db.collection.createIndex(…)` command
- Three built-in transports: `console`, JSON lines file, custom handler
- On-demand `.lens()` helper for ad-hoc analysis in development
- Dual ESM + CJS output, TypeScript declarations included
## Installation
```sh
npm install mongoose-lens
# or
pnpm add mongoose-lens
```
`mongoose` ≥ 8.0.0 is a peer dependency.
## Quick start
```ts
import mongoose from 'mongoose'
import { mongooseLens } from 'mongoose-lens'
// Register once, before defining models
mongoose.plugin(mongooseLens({
thresholds: { executionTimeMs: 200 },
transport: [
{ type: 'console' },
{ type: 'json', path: './logs/slow-queries.jsonl' },
],
}))
```
From that point every query or aggregation slower than 200 ms (or that examines too many documents) will be automatically analyzed and reported.
## Configuration
```ts
mongooseLens({
// A query triggers analysis when ANY threshold is exceeded.
thresholds: {
executionTimeMs: 200, // wall-clock ms (default: 200)
docsExamined: 1000, // nExaminedDocuments (default: 1000)
ratio: 10, // examined / returned ratio (default: 10)
},
// Probabilistic sampling — 1 = always, 0 = never.
sampling: { rate: 1 },
// Sliding-window circuit breaker.
circuitBreaker: {
maxExplainsPerWindow: 10, // max explains per window
windowMs: 10_000,
},
// Skip re-analysis of the same model+filter within this window.
deduplication: { windowMs: 60_000 },
// Max simultaneous explain() calls in flight.
explainConcurrency: 2,
// 'human' (default) — readable summary + details text
// 'raw' — empty text fields, LensReport.raw contains the full explain
// 'both' — readable text AND LensReport.raw
advice: 'human',
transport: [
{ type: 'console' },
{ type: 'json', path: './logs/queries.jsonl' },
{ type: 'custom', handler: async (report) => { /* … */ } },
],
})
```
All fields are optional. Missing fields fall back to defaults.
## LensReport shape
```ts
interface LensReport {
model: string; // e.g. "User"
operation: string; // e.g. "find"
filter: Record;
sort?: Record;
stage: QueryStage; // "COLLSCAN" | "IXSCAN" | "SORT" | "FETCH" | …
severity: "warning" | "critical";
executionTimeMs: number;
docsExamined: number;
docsReturned: number;
ratio: number;
timestamp: string; // ISO 8601
advice: {
summary: string;
details: string;
suggestedIndex: Record | null;
indexCommand: string | null;
};
raw?: object; // full explain output (opt-in)
}
```
## On-demand `.lens()` helper
Use `.lens()` during development to inspect a specific query without waiting for it to exceed a threshold:
```ts
const report = await User.find({ status: 'active' }).sort({ createdAt: -1 }).lens()
console.log(report.stage) // "COLLSCAN"
console.log(report.advice.indexCommand) // db.users.createIndex({"status":1,"createdAt":-1})
```
`.lens()` runs `explain('executionStats')` directly — no sampling, dedup, or circuit-breaker gates apply. The query itself is **not** executed as a data fetch.
## Per-query opt-out with `.skipLens()`
Use `.skipLens()` to exclude a specific query from automatic lens analysis — useful for internal or system queries you do not want to monitor:
```ts
// This query will not trigger explain or emit any LensReport
await User.findById(systemId).skipLens()
```
`.skipLens()` is chainable and returns the query unchanged. It only suppresses the automatic middleware; `.lens()` is unaffected.
## Custom transport
```ts
import type { LensReport } from 'mongoose-lens'
mongoose.plugin(mongooseLens({
transport: [{
type: 'custom',
handler: async (report: LensReport) => {
await mySlackClient.post('#alerts', report.advice.summary)
},
}],
}))
```
## License
MIT