As33
@periodic/
arsenic

Mongoose Adapter

The Mongoose adapter instruments MongoDB operations via Mongoose ODM. Supports all query types including find, findOne, aggregate, save, update, and delete operations.

Setup

typescript
import mongoose from 'mongoose';
import { createMonitor, expressContext, mongooseAdapter } from '@periodic/arsenic';

const app = express();

const monitor = createMonitor({
  slowQueryThresholdMs: 200,
  exporter: (event) => {
    if (event.severity === 'critical') sendToPagerDuty(event);
    else if (event.severity === 'warning') sendToSlack(event);
  },
});

// 1. Attach Express context BEFORE routes
app.use(expressContext(monitor));

// 2. Connect to MongoDB
await mongoose.connect(process.env.MONGODB_URI);

// 3. Attach Mongoose adapter
mongooseAdapter(monitor, mongoose);

// Now all Mongoose queries are monitored automatically

Order matters

Call mongooseAdapter() AFTER mongoose.connect(). The adapter instruments the connection that is established, not future connections.

Supported operations

OperationMongoose methodSignals detected
findModel.find()hot_path, n_plus_one, unbounded_query, slow_query
findOneModel.findOne()hot_path, unbounded_query, slow_query
findByIdModel.findById()fast_query, indexed_lookup
savedoc.save()write_contention, slow_query
updateOneModel.updateOne()write_contention, retry_loop
aggregateModel.aggregate()slow_query, high_cpu, large_payload
countModel.countDocuments()slow_query

Event output

json
{
  "type": "db.query",
  "db": "mongodb",
  "adapter": "mongoose",
  "model": "User",
  "operation": "findOne",
  "durationMs": 312,
  "slow": true,
  "signals": ["hot_path", "unbounded_query"],
  "severity": "critical",
  "request": { "method": "GET", "route": "/api/users/:id" },
  "callsite": { "file": "src/routes/users.ts", "line": 14 },
  "metadata": { "limit": null },
  "timestamp": "2025-02-11T15: 30: 45.123Z"
}

N+1 detection with Mongoose

typescript
// BAD — triggers n_plus_one signal
const posts = await Post.find({ published: true });
for (const post of posts) {
  const author = await User.findById(post.authorId); // N queries!
}

// GOOD — single query with populate
const posts = await Post.find({ published: true }).populate('author');

Use .lean() for read-only queries

Adding .lean() to read-only queries returns plain JavaScript objects instead of Mongoose Documents, reducing memory usage significantly. Arsenic detects this as a positive signal.