如何在Node.js中统计响应时间存MongoDB及解决mongoose.query报错
报错根因
触发TypeError: mongoose.query is not a function的核心原因是Mongoose没有提供顶层的query()方法,和你之前使用PostgreSQL时直接调用连接池pool.query的逻辑不同:Mongoose作为MongoDB的ODM工具,所有数据库操作都必须基于提前定义好的数据模型(Model) 实例发起,直接调用mongoose.query()不存在对应的方法实现,必然抛错。
手把手实现全链路耗时统计+MongoDB持久化方案
步骤1:定义耗时记录专用存储模型
首先新建专门存储链路耗时数据的Schema,用于持久化所有请求的耗时指标,可根据业务需要扩展字段:
const mongoose = require('mongoose'); const { Schema } = mongoose; const ApiMetricSchema = new Schema({ apiPath: { type: String, required: true }, // 请求接口路径 method: { type: String, required: true }, // 请求方法 GET/POST等 executionTime: { type: Number, required: true }, // 全链路耗时,单位秒 isSuccess: { type: Boolean, required: true }, // 请求是否成功 errorMsg: { type: String, default: '' }, // 失败时的错误信息 createdAt: { type: Date, default: Date.now, expires: '30d' } // 自动记录生成时间,配置30天自动过期,避免集合数据膨胀 }); // 初始化耗时统计模型 const ApiMetric = mongoose.model('ApiMetric', ApiMetricSchema);
步骤2:选择适配的耗时统计实现
根据你要统计的范围,二选一实现即可:
方案A:全局中间件统计(推荐,真正覆盖全链路)
直接在服务入口加全局中间件,覆盖所有HTTP请求从进入服务到响应返回的全流程耗时,包含业务逻辑、数据库操作、第三方调用等所有环节的总耗时,不需要修改每个业务接口代码,以Express框架为例:// 全局耗时统计中间件,注册在所有业务路由之前 app.use((req, res, next) => { const startTime = Date.now(); // 监听响应结束事件,此时请求全流程已经执行完成 res.on('finish', () => { const executionTime = (Date.now() - startTime) / 1000; logger.info(`接口 ${req.method} ${req.path} 耗时: ${executionTime}s`); // 异步写入MongoDB,不要加await阻塞主流程 ApiMetric.create({ apiPath: req.path, method: req.method, executionTime, isSuccess: res.statusCode < 400, errorMsg: res.statusCode >= 400 ? res.statusMessage : '' }).catch(err => { // 统计逻辑报错仅打日志,绝对不能影响主业务 logger.error('耗时记录写入Mongo失败:', err); }); }); next(); });方案B:数据库查询层封装(仅统计数据库操作耗时)
如果你只需要统计MongoDB查询本身的耗时,可以封装通用查询方法,对齐你之前使用pool.query的调用习惯:/** * 带耗时统计的通用查询方法 * @param {mongoose.Model} model 要操作的业务模型,对应PG里的不同表 * @param {Function} queryExec 具体查询逻辑 * @param {Object} metricMeta 请求元信息 * @returns 查询结果 */ const queryWithTiming = async (model, queryExec, metricMeta = {}) => { const startTime = Date.now(); let result = null; let error = null; let isSuccess = false; try { result = await queryExec(model); isSuccess = true; return result; } catch (err) { error = err; throw err; } finally { // 无论成功失败都统一计算耗时、写日志、存库,避免重复代码 const executionTime = (Date.now() - startTime) / 1000; logger.info(`DB查询耗时: ${executionTime}s`); ApiMetric.create({ apiPath: metricMeta.apiPath || 'unknown', method: metricMeta.method || 'unknown', executionTime, isSuccess, errorMsg: error ? error.message : '' }).catch(err => logger.error('耗时记录写入Mongo失败:', err)); } };实际业务调用方式和你之前写PG的逻辑基本一致:
// 提前定义业务模型,以用户表为例 const User = mongoose.model('User', new Schema({ name: String, age: Number })); // 业务接口中调用 const getUserList = async (req, res) => { try { const userList = await queryWithTiming( User, // 传入具体查询逻辑,和直接调用Model方法写法完全一致 (model) => model.find({ age: { $gte: 18 } }).limit(20), { apiPath: req.path, method: req.method } ); res.json({ code: 0, data: userList }); } catch (err) { res.status(500).json({ code: 500, msg: err.message }); } };
实现注意事项
- 耗时数据写入MongoDB时不要使用
await等待写入完成,用异步非阻塞方式执行,避免统计逻辑拖慢正常业务响应 - 所有统计逻辑的异常必须单独捕获,不能因为监控日志写入失败导致主业务请求报错
- 存储耗时数据的集合一定要配置TTL过期索引,避免服务长期运行后日志数据占满磁盘
- 如果使用Koa、Egg等其他Node框架,全局中间件的实现逻辑一致,仅响应监听的写法略有差异
内容的提问来源于stack exchange,提问作者Rumpelstiltskin
相关产品推荐
相关产品推荐

