wy-log
Version:
An express middleware to record logs for Weiyi Application
251 lines (239 loc) • 11 kB
JavaScript
/*
options: {
level: 日志级别,可选'all','info','error',
file: 分割时,需要输入文件夹名,
split: 是否分割,可选false,'day','month',
ignoreList: [] 访问忽略名单,可以在路由后写入/*代表该路由下的所有子路由
routerList: [] 路由白名单,名单中没有出现的访问路径会被当作404处理
}
*/
const fs = require('fs')
const moment = require('moment')
const schedule = require('node-schedule')
const defaultOptions = {
level: 'all',
file: './log',
split: false,
ignoreList: [],
routerList: []
}
const writeLogStream = (file,text) => {
let logStream
logStream = fs.createWriteStream(`${file}`, {flags: 'a'})
logStream.write(text)
logStream.end()
}
const log = options => {
//参数错误提示
options = Object.assign({}, defaultOptions, options)
let {level, file, split, ignoreList, routerList} = options
return (req, res, next) => {
let stime = process.hrtime(), // 运行开始时间
allLog, infoLog, errorLog,
statusReg = /^4\d{2}|5\d{2}$/,
isNext = false
// 获取ignoreList参数里带有*的值,以匹配子路由
const getIgnoreParentList = () => {
return new Promise((resolve, reject) => {
let ignoreParentList = []
ignoreList && ignoreList.forEach((value, index) => {
const routerReg = /^\/(.*?)\/\*$/
if(routerReg.test(value)) {
ignoreParentList.push('/' + value.split('/')[1])
}
if(index === (ignoreList.length - 1)) {
resolve(ignoreParentList)
}
})
})
}
// 核心方法,最后会返回一个JSON对象,包含路由对象,日志级别,错误信息等
const getAccessJson = url => {
let json = {
url
} // ignore, level, url
const RouterPipe = new Promise((resolve, reject) => {
// 判断当前访问路由是不是忽略列表中的一个,如果是就跳入下个中间件
if(ignoreList.length) {
let routerIgnoreParentReg,
regStr = ''
getIgnoreParentList().then(list => {
if(list.length) {
list.map(value => {
regStr += '^\\' + value + '\\/.*?$|'
})
regStr = regStr.substring(0,regStr.length - 1)
}
routerIgnoreParentReg = new RegExp(regStr)
if(routerIgnoreParentReg.test(url) || ignoreList.includes(url)) {
isNext = true
json.ignore = true
}
if(level === 'all') {
if(statusReg.test(res.statusCode) || !routerList.includes(req.url)) {
json.level = 'error'
} else {
json.level = 'info'
}
} else {
json.level = level
}
console.log(json)
resolve(json)
})
}
})
return RouterPipe
}
allLog = () => {
if(statusReg.test(res.statusCode) || !routerList.includes(req.url)) {
errorLog()
} else {
infoLog()
}
}
infoLog = () => {
if(!split) {
!fs.existsSync(file) && fs.writeFile(file,'', err => {
if(err) {
console.error(err)
}
})
logText = `[access-log] --> url: ${req.url} ip: ${req.header('x-forwarded-for') || req.header('x-real-ip') || req.connection.remoteAddress || req.ip}; code: ${req.status}; ua: ${JSON.stringify(req.headers)}; <-- [/all] \n\n`
writeLogStream(file, logText)
} else {
let logFile
!fs.existsSync(file) && fs.mkdirSync(file)
if(split === 'day') {
logFile = `${file}/access.log`
// 每天凌晨过30秒的时候拷贝 access.log --> access-YYYYMMDD.log 日志文件并重新生成一个access.log文件
const rule = '30 0 0 * * *'
var j = schedule.scheduleJob(rule, function(){
if (fs.existsSync(logFile)) {
// 拷贝
let readable, writable
// 创建读取流
readable = fs.createReadStream(logFile);
// 创建写入流
writable = fs.createWriteStream(`${file}/access${moment().add(-1, 'days').format('YYYYMMDD')}.log`)
// 通过管道来传输流
readable.pipe(writable)
let copyError = false
readable.on('error', (err) => {
console.log(err)
copyError = true
})
readable.on('close', () => {
// 流写完之后再执行回调清空处理
if (!copyError) {
// 清空
let removeStream = fs.createWriteStream(`${file}/access.log`, {flags: 'w'})
removeStream.write('')
removeStream.end()
}
});
}
})
}
let etime = process.hrtime(), // 运行结束时间
duration = (etime[1] - stime[1]) * 1e-3,
dataJson = {
url: req.url,
ip: req.header('x-forwarded-for') || req.header('x-real-ip') || req.connection.remoteAddress || req.ip,
ua: JSON.stringify(req.header('user-agent')),
code: res.statusCode,
time: duration
}
logText = `[access-log] date: ${moment().format('YYYY MM DD hh:mm:ss')} ###start###${JSON.stringify(dataJson)}###end### [/access-log]\n\n`
writeLogStream(logFile, logText)
}
}
errorLog = () => {
//访问错误
if(routerList && !routerList.length) {
console.error('必须要传入至少一个路由地址')
isNext = true
return
}
let errMessage
if(statusReg.test(res.statusCode) || !routerList.includes(req.url)) {
!routerList.includes(req.url) && (res.statusCode = 404)
switch (res.statusCode) {
case 404:
errMessage = '未找到页面'
break
case 500:
errMessage = '内部服务器错误'
break
case 502:
errMessage = 'Web服务器用作网关或代理服务器时收到了无效响应'
break
case 403:
errMessage = '访问被禁止'
break
}
if(!split) {
!fs.existsSync(file) && fs.writeFile(file,'', err => {
if(err) {
console.log(err)
}
})
logText = `[error-log] --> date: ${moment().format('YYYY MM DD hh:mm:ss')} url: ${req.url}; code: ${res.statusCode}; errMessage: ${errMessage}; <-- [/error-log] \n\n`
writeLogStream(file, logText)
} else {
let logFile
!fs.existsSync(file) && fs.mkdirSync(file)
if(split === 'day') {
logFile = `${file}/error.log`
// 每天凌晨过30秒的时候拷贝 error.log --> error-YYYYMMDD.log 日志文件并重新生成一个error.log文件
const rule = '30 0 0 * * *'
var j = schedule.scheduleJob(rule, function(){
if (fs.existsSync(logFile)) {
// 拷贝
let readable, writable
// 创建读取流
readable = fs.createReadStream(logFile);
// 创建写入流
writable = fs.createWriteStream(`${file}/error${moment().add(-1, 'days').format('YYYYMMDD')}.log`)
// 通过管道来传输流
readable.pipe(writable)
let copyError = false
readable.on('error', (err) => {
console.log(err)
copyError = true
})
readable.on('close', () => {
// 流写完之后再执行回调清空处理
if (!copyError) {
// 清空
let removeStream = fs.createWriteStream(`${file}/error.log`, {flags: 'w'})
removeStream.write('')
removeStream.end()
}
});
}
})
}
logText = `[error-log] --> date: ${moment().format('YYYY MM DD hh:mm:ss')} url: ${req.url}; code: ${res.statusCode}; errMessage: ${errMessage}; <-- [/error-log] \n\n`
writeLogStream(logFile, logText)
}
}
}
!isNext && getAccessJson(req.url).then(json => {
if(json.ignore) {
console.log('the router is ignored')
return
}
switch(json.level) {
case 'info':
infoLog()
break
case 'error':
errorLog()
break
}
})
next()
}
}
exports = module.exports = log