{"id":50320091,"url":"https://github.com/maku85/mongoose-lens","last_synced_at":"2026-05-29T03:03:25.128Z","repository":{"id":355789889,"uuid":"1229579585","full_name":"maku85/mongoose-lens","owner":"maku85","description":"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","archived":false,"fork":false,"pushed_at":"2026-05-05T08:11:48.000Z","size":73,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-05T10:15:50.002Z","etag":null,"topics":["database-performance","esm","index-advisor","mongodb","mongodb-explain","mongoose","mongoose-plugin","nodejs","query-optimization","slow-query","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maku85.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-05T07:27:30.000Z","updated_at":"2026-05-05T08:18:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/maku85/mongoose-lens","commit_stats":null,"previous_names":["maku85/mongoose-lens"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/maku85/mongoose-lens","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maku85%2Fmongoose-lens","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maku85%2Fmongoose-lens/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maku85%2Fmongoose-lens/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maku85%2Fmongoose-lens/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maku85","download_url":"https://codeload.github.com/maku85/mongoose-lens/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maku85%2Fmongoose-lens/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33634611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database-performance","esm","index-advisor","mongodb","mongodb-explain","mongoose","mongoose-plugin","nodejs","query-optimization","slow-query","typescript"],"created_at":"2026-05-29T03:03:24.360Z","updated_at":"2026-05-29T03:03:25.115Z","avatar_url":"https://github.com/maku85.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mongoose-lens\n\n[![CI](https://github.com/maku85/mongoose-lens/actions/workflows/ci.yml/badge.svg)](https://github.com/maku85/mongoose-lens/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/mongoose-lens)](https://www.npmjs.com/package/mongoose-lens)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\nSlow query interceptor and index advisor for Mongoose 8+.\n\nAutomatically 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).\n\n## Features\n\n- Zero-overhead on fast queries — sampling, circuit breaker, and deduplication keep explain calls rare\n- Non-blocking — explain runs asynchronously via `setImmediate` + bounded concurrency queue\n- Human-readable advice with a ready-to-paste `db.collection.createIndex(…)` command\n- Three built-in transports: `console`, JSON lines file, custom handler\n- On-demand `.lens()` helper for ad-hoc analysis in development\n- Dual ESM + CJS output, TypeScript declarations included\n\n## Installation\n\n```sh\nnpm install mongoose-lens\n# or\npnpm add mongoose-lens\n```\n\n`mongoose` ≥ 8.0.0 is a peer dependency.\n\n## Quick start\n\n```ts\nimport mongoose from 'mongoose'\nimport { mongooseLens } from 'mongoose-lens'\n\n// Register once, before defining models\nmongoose.plugin(mongooseLens({\n  thresholds: { executionTimeMs: 200 },\n  transport: [\n    { type: 'console' },\n    { type: 'json', path: './logs/slow-queries.jsonl' },\n  ],\n}))\n```\n\nFrom that point every query or aggregation slower than 200 ms (or that examines too many documents) will be automatically analyzed and reported.\n\n## Configuration\n\n```ts\nmongooseLens({\n  // A query triggers analysis when ANY threshold is exceeded.\n  thresholds: {\n    executionTimeMs: 200,   // wall-clock ms (default: 200)\n    docsExamined:   1000,   // nExaminedDocuments (default: 1000)\n    ratio:          10,     // examined / returned ratio (default: 10)\n  },\n\n  // Probabilistic sampling — 1 = always, 0 = never.\n  sampling: { rate: 1 },\n\n  // Sliding-window circuit breaker.\n  circuitBreaker: {\n    maxExplainsPerWindow: 10,   // max explains per window\n    windowMs:             10_000,\n  },\n\n  // Skip re-analysis of the same model+filter within this window.\n  deduplication: { windowMs: 60_000 },\n\n  // Max simultaneous explain() calls in flight.\n  explainConcurrency: 2,\n\n  // 'human' (default) — readable summary + details text\n  // 'raw'   — empty text fields, LensReport.raw contains the full explain\n  // 'both'  — readable text AND LensReport.raw\n  advice: 'human',\n\n  transport: [\n    { type: 'console' },\n    { type: 'json', path: './logs/queries.jsonl' },\n    { type: 'custom', handler: async (report) =\u003e { /* … */ } },\n  ],\n})\n```\n\nAll fields are optional. Missing fields fall back to defaults.\n\n## LensReport shape\n\n```ts\ninterface LensReport {\n  model:           string;          // e.g. \"User\"\n  operation:       string;          // e.g. \"find\"\n  filter:          Record\u003cstring, unknown\u003e;\n  sort?:           Record\u003cstring, unknown\u003e;\n  stage:           QueryStage;      // \"COLLSCAN\" | \"IXSCAN\" | \"SORT\" | \"FETCH\" | …\n  severity:        \"warning\" | \"critical\";\n  executionTimeMs: number;\n  docsExamined:    number;\n  docsReturned:    number;\n  ratio:           number;\n  timestamp:       string;          // ISO 8601\n  advice: {\n    summary:        string;\n    details:        string;\n    suggestedIndex: Record\u003cstring, 1 | -1\u003e | null;\n    indexCommand:   string | null;\n  };\n  raw?: object;                     // full explain output (opt-in)\n}\n```\n\n## On-demand `.lens()` helper\n\nUse `.lens()` during development to inspect a specific query without waiting for it to exceed a threshold:\n\n```ts\nconst report = await User.find({ status: 'active' }).sort({ createdAt: -1 }).lens()\n\nconsole.log(report.stage)               // \"COLLSCAN\"\nconsole.log(report.advice.indexCommand) // db.users.createIndex({\"status\":1,\"createdAt\":-1})\n```\n\n`.lens()` runs `explain('executionStats')` directly — no sampling, dedup, or circuit-breaker gates apply. The query itself is **not** executed as a data fetch.\n\n## Per-query opt-out with `.skipLens()`\n\nUse `.skipLens()` to exclude a specific query from automatic lens analysis — useful for internal or system queries you do not want to monitor:\n\n```ts\n// This query will not trigger explain or emit any LensReport\nawait User.findById(systemId).skipLens()\n```\n\n`.skipLens()` is chainable and returns the query unchanged. It only suppresses the automatic middleware; `.lens()` is unaffected.\n\n## Custom transport\n\n```ts\nimport type { LensReport } from 'mongoose-lens'\n\nmongoose.plugin(mongooseLens({\n  transport: [{\n    type: 'custom',\n    handler: async (report: LensReport) =\u003e {\n      await mySlackClient.post('#alerts', report.advice.summary)\n    },\n  }],\n}))\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaku85%2Fmongoose-lens","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaku85%2Fmongoose-lens","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaku85%2Fmongoose-lens/lists"}