Mongoose
autotel-mongoose instruments Mongoose 8+ with stable semantic conventions, query text that carries no values, a statement hash to group by, and optional query plans.
Installation
Section titled “Installation”npm install autotel autotel-mongoose mongooseBasic Usage
Section titled “Basic Usage”import mongoose from 'mongoose';import { init } from 'autotel';import { instrumentMongoose } from 'autotel-mongoose';
init({ service: 'my-app', endpoint: 'http://localhost:4318' });
instrumentMongoose(mongoose, { dbName: 'myapp' });
const User = mongoose.model( 'User', new mongoose.Schema({ name: String, email: String }),);
await User.findOne({ email: 'alice@example.com' }).exec();// Span: "findOne users"// db.query.text: {"condition":{"email":"?"},"options":{}}// db.statement.hash: the same for every findOne on users by emailinstrumentMongoose() is safe to call more than once. Mongoose’s Model and
Query prototypes are shared by every new mongoose.Mongoose(), so a second
call recognises what the first installed and leaves it alone: one span per
operation either way.
Methods Mongoose implements with other methods produce one span, not one per
delegation: findById traces as findById, without a nested findOne for the
same round trip. A query a hook issues is a separate round trip and keeps its
own span.
Query text and grouping
Section titled “Query text and grouping”db.query.text is the query’s shape: field names and operators, with every
value replaced by ?. Nothing user-supplied reaches the span, so there is
nothing for a redactor to miss, and the same query with different values has
the same text. An insertMany records one entry per distinct document shape,
so batches of any size are one statement.
db.statement.hash is set on every operation, from the operation, the
collection and that shape, even when dbStatementSerializer is false or
captures values. Group a trace’s spans by it to find an N+1: one hash, many
spans. The find_repeated_queries tool in autotel-mcp does that for you.
To capture values anyway, pass a serializer. statementRedactor (the
'default' preset unless you say otherwise) still masks emails, phone numbers,
card numbers and SSNs in what it returns:
instrumentMongoose(mongoose, { dbStatementSerializer: (_operation, payload) => JSON.stringify(payload),});Query plans
Section titled “Query plans”instrumentMongoose(mongoose, { explain: 'analyze' });
await Order.find({ status: 'open', total: { $gte: 10 } }) .sort({ createdAt: -1 }) .exec();// db.plan.node: SORT// db.plan.full_scan: true// db.plan.rows_examined: 60// db.plan.rows_returned: 16// db.plan.index_suggestion: db.orders.createIndex({ status: 1, createdAt: -1, total: 1 })'analyze' runs explain('executionStats'), which reports documents examined
and time; 'plan' runs explain('queryPlanner'), which does not run the query.
Explain never applies a write, so updates and deletes are explained safely. It
is sent to the driver as a raw command, so your schema’s hooks run once per
query, never again for the explain.
The explain runs after the query settles: your code gets its result without waiting, and the span still ends when the query did, so its duration is the query’s own. It is one more round trip per query, so turn it on in development, in CI, or for a sample of traffic.
The attributes are the ones MongoDB documents, shared with Drizzle’s Postgres plans.
Hook Instrumentation
Section titled “Hook Instrumentation”Off by default. Enable it and the hooks your schemas register get their own
spans, named after the operation that ran: mongoose.users.pre.save.
instrumentMongoose(mongoose, { instrumentHooks: true });
userSchema.pre('save', async function () { this.set('name', this.get('name')?.trim()); // Span: mongoose.users.pre.save});Mongoose’s own hooks stay out of the way, so a schema using timestamps,
subdocuments or virtuals emits spans only for the hooks you wrote.
Pass a selector to choose which hooks to trace, the same shape customMethods
takes:
instrumentMongoose(mongoose, { instrumentHooks: ['save', 'validate'], // only these});
instrumentMongoose(mongoose, { instrumentHooks: { exclude: ['init'] }, // everything else});Reach for that with init, which runs once for every document a query
hydrates, where save and validate run once per operation. A find returning
500 documents means 500 post.init spans.
Selection works a hook at a time whichever way you register. Both
pre(['save', 'validate'], fn) and pre(/^find/, fn) are selected and named
per operation, so a find gets pre.find and a findOne gets pre.findOne.
Excluding a hook only stops the span; your handler still runs.
Configuration
Section titled “Configuration”| Option | Default | Description |
|---|---|---|
dbName |
— | Database name |
peerName |
— | MongoDB host |
peerPort |
— | MongoDB port |
captureCollectionName |
true |
Include collection in span |
instrumentHooks |
false |
Trace schema hooks; accepts a selector |
dbStatementSerializer |
value-free | Custom query serializer |
statementRedactor |
'default' |
PII redaction for a serializer’s output |
explain |
false |
'plan' or 'analyze': add db.plan.* |
Examples
Section titled “Examples”example-mongoose: Mongoose with query text capture and hook instrumentation.
Semantic Conventions
Section titled “Semantic Conventions”Uses stable OTel conventions:
db.system.namedb.operation.namedb.collection.namedb.namespacedb.query.textserver.address/server.port
Plus autotel’s own, shared across its database instrumentations:
db.statement.hash, and db.plan.* when explain is on.