最近公司让做操作日志记录(koa2+mongoose),听到这个消息我撸起袖子就开始干了,但有几个地方还需要注意下:
- 功能部分已经做完了,不能修改别人的代码(小部分还是可以的)
- 每个请求的操作可能会操作多张不同的表
- 需要记录每个请求操作数据(操作前和操作后的数据,可用于恢复数据)
要追踪到每次操作数据库的操作,肯定不能再每次具体操作方法的地方都去把操作前数据和操作后的数据保存起来,这样工作量很大,其他功能的每个地方基本上都会被动一次,那么怎么样才能做到追踪到每次操作而不用具体到每个操作方法呢?
中间件+mongoose的hook函数
中间件是个很好的东西,什么是中间件这里不做说明,不明白的可以自行百度,这里贴下中间件的代码:
module.exports = async (ctx, next) => {
try {
// get请求跳过不记录操作日志
if (ctx.method === 'GET') {
await next()
} else {
// 建立全局对象用于中间价与mongoose传递参数
if (!global.userLogs) global.userLogs = {}
// 区分每次请求用到了 请求方法+url 作为key (我们用的restfull风格)
const _logctx = ctx.method + '_' + ctx.url
// 在全局变量中 添加该属性
global.userLogs[_logctx] = {
url: ctx.url,
method: ctx.method,
ip: ctx.ip,
beforeData: '',
afterData: '',
time: Math.floor(new Date().getTime() / 1000)
}
await next()
// 打印需要处理的数据(这里是保存等操作)
console.log(global.userLogs[_logctx])
}
} catch (error) {
console.log(error)
}
}
mongoose 是个orm 框架,与我们应用的逻辑程序没有直接关系,所以我这里用到了全局变量用来传递参数,后面再做具体的每种操作时会给global.userLogs赋值。
由于公司之前用mongoose 建立的model都是每个各自建立自己的model,所有在再做统一处理是就很麻烦了,需要自每个model都要修改,为了避免上的麻烦我建立一个base.model.js 一些公共的东西都放在这里,让所有的model都去引用这个model,代码如下:
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const schema = new Schema()
// 监听方法名称
const methods = [
// 'init',
// 'validate',
// 'count',
// 'find',
// 'findOne',
'save',
'remove',
'findOneAndRemove',
'findOneAndUpdate',
'insertMany',
'update'
]
methods.forEach(n => {
// 操作前处理方法
schema.pre(n, function(next) {
if (n === 'save') {
global.userLogs[this._logctx].afterData += JSON.stringify(delete this._logctx)
} else {
const _logctx = this.options._logctx
global.userLogs[_logctx].afterData += this._update ? JSON.stringify(this._update) : ''
}
next()
})
// 操作后处理方法
schema.post(n, function(data) {
if (n !== 'save') {
const _logctx = this.options._logctx
global.userLogs[_logctx].beforeData += JSON.stringify(data)
}
})
})
// 保存方法特殊处理(保存方法option 数据传递不过来)
schema.method({
'create': (data, model, ctx) => {
data._logctx = {logctx: ctx.method + '_' + ctx.url}
this.mongoose.models[model].create(data)
}
})
exports.schema = schema
exports.mongoose = mongoose
methods 对象里面放的是所有mongoose所有hook函数(pre(操作前), post(操作后))能监听到的方法名称,里面注释掉的部分不属于我们操作记录范围内(增、删、改),有人可能回问这里的方法不全,比如,保存数据除了save()还有create()方法,其实create的方法内部还是走的是save(),所以监听save,同时可以监听create和save方法。
上面我们说过,mongoose是个独立orm框架,只处理数据库相关操作,不会牵扯逻辑相关数据,那我们怎样才能让mongoose知道这次操作,是哪个请求方法在操作呢?
mongoose在每次操作数据数据是会有个配置项(options), 这个配置项会被传递到具体操作方法(蛋疼的save方法除外),这样我们就可以在操作数据的时候拿到请求相关信息了,把请求数据放在global.userLogs对象里key 这里_logctx需要和前面中间件中命名一致(请求类型+url),在这里修改前面中间中定义相应的属性值。
mongoose中save方法比较特殊,因为没有配置项(option)或参数传递不过来,在pre和post函数中,参数只有保存的对象,取不到别的参数,由于我们公司全部使用的是create方法保存对象,所以我只需要重写create方法,在create方法中添加了_logctx 属性,属相值为我配拼写的(请求类型+url),然后调用原始的create方法,需要注意的是在重写方法中this是取不到当前model的,这里取到的是所有的model,需要把当前model 传递过来,所以我吧model 名称也传过来,方便获取当前model(这个方法比较笨,肯定还有更好的方法,有的话请留言指出),由于mongoose 的特性,在model 中没有定义相应的字段,所以在保存时_logctx 字段是保存不进去的。
最后在上面中间件的 await next() 后面根据key 值得不同取到相应的数据,执行存储或者其他操作。
我写完上面的方法之后,领导告诉我,只要处理逻辑层面就行(只记录请求接口和请求结果就行了),不用这么复杂,╮(╯▽╰)╭……