Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NodeJS日志解决方案 #10

Open
linweiwei123 opened this issue Sep 28, 2018 · 0 comments
Open

NodeJS日志解决方案 #10

linweiwei123 opened this issue Sep 28, 2018 · 0 comments

Comments

@linweiwei123
Copy link
Owner

NodeJS在web领域常常作为服务器运行。NodeJS可以像其他服务器一样进行数据库操作、逻辑运算、HTTP和websocket通信、文件读写、模板渲染等等许多功能。在美柚NodeJS更多的用来做中间层,用来做API的请求结合模板引擎进行服务端渲染,还有一些轻微的逻辑处理。本文对比最佳实践例子与反面例子,系统阐述NodeJS日志的解决方案。

1. 划分不同的错误类型

要用使用NodeJS环境自带的Error对象进行抛错,它含有错误的堆栈信息和其他有用属性。根据业务需要设计不同类型的Error,根据场景选择正确的Error输出。

正确使用

app.get('/api', (req, res, next) => {
    Http.fetch(url)
    .then(resp => {
        throw new Error('错误信息')
    })
    .catch(err => next(err))
})

http://oflt40zxf.bkt.clouddn.com/log_1.png

堆栈信息将被收集,方便开发调试,也用于记录生产错误信息。

反面例子

app.get('/api', (req, res, next) => {
    Http.fetch(url)
    .then(resp => {
        throw '错误信息' // 或者抛一个对象也是不佳的
    })
    .catch(err => next(err))
})

最佳的实践

在美柚的业务场景中我们设计了这些错误类型:

  • MeetError - 原型继承Error,并具有自身属性和方法
  • CommonError - 程序错误
  • DataNotFoundError - API返回的404错误
  • RespDataError - API 返回的数据格式错误
  • JSONParseError - API的repsonse json解析错误
  • EJSRenderError - EJS模板解析错误
  • RedisError - redis数据库连接错误
  • HttpTimeOutError - http超时错误

这些CustomError都继承Error,具有Error的属性和方法,还自定义了自己属性如code、name等

http://ovn18u9yn.bkt.clouddn.com/WX20180928-073534.png

MeetError 原型继承Error

function MeetError(message){
    this.message = message;
    Error.call(this);
    Error.captureStackTrace(this,this.constructor);
}

MeetError.prototype = Object.create(Error.prototype);
MeetError.prototype.constructor = MeetError;

module.exports = MeetError;

自定义业务Error原型继承MeetError

const MeetError = require('./MeetError');
const { errorConstants } = require('./codeConstants');

function JSONParseError(message){
    MeetError.call(this, message);
    this.name = 'JSONParseError';
    this.code = errorConstants.JSONParseError
}

JSONParseError.prototype = Object.create(MeetError.prototype);
JSONParseError.prototype.constructor = JSONParseError;

这样自定义错误信息不仅有Error的属性和方法,还有自身的code,name,在统一处理错误的中间件errorHandler中可以根据错误类型进行分类处理。业务开发时只需要根据业务场景抛相应的错误即可。

2.日志分级打印

不同的环境对日志有不同的需求,比如开发环境喜欢看到更多的调试信息(debug level),测试、预发环境喜欢看到更多的警告信息(info level),线上环境由于量特别大,像美柚网页日pv超过1000万,随便一个日志信息的打印可能导致大量的日志产生,所以线上只用于打印有用的错误信息(error level)

正确使用

使用成熟的日志模块(这里举例winston)

const winston = require('winston');

var level = 'debug';
var ENV = process.env.NODE_ENV;

switch (ENV) {
    case 'development' : level = 'debug';break;
    case 'development' : level = 'info';break;
    case 'development' : level = 'info';break;
    case 'production' : level = 'error';break;
    default : level = 'debug';break;
}

const logger = new winston.Logger({
    transports: [
        new winston.transports.Console({
            level: level,
            timestamp: function () {
                return (new Date()).toISOString();
            }
            // 开发环境可以输出文件,生产环境用pm2管理日志
        })
    ]
});

module.exports = logger;

日志分级打印

logger.debug(err)  // 调试
logger.info(err)   // 提示或者警告 
logger.error(err)  // 错误

分级之后根据不同环境会有不同的输出。

反面例子

    app.get('/api', (req, res, next) => {
    Http.fetch(url)
    .then(resp => {
        console.log(resp); // 随便输出打印调试日志 ❌。
        throw new Error('错误信息') 
    })
    .catch(err => {
    })
})

3. 错误统一处理,业务代码不消化错误

错误放在errorHandler中间件统一处理,业务代码无需根据错误类型进行消化来判断跳错误页面还是提示内容不存在页面(除非业务需要个性化的这些页面)

errorHandler

const logger = require('../config/logger');
const { infoLevelArray } = require('../errors/codeConstants');
const ENV = process.env.NODE_ENV;

module.exports = (err, req, res, next) => {

    // 如果是router 404错误,则直接进入404页面
    if(err.status === 404){
        return next();
    }

    // 判断错误类型,分级记录日志
    LoggerError(err);

    // render the error page
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: ENV !== 'production' ? err : {}
    });
};


 // 判断error级别记录
function LoggerError(err){
   if(err.name === undefined || err.name === 'Error'){
       logger.error('捕获到未预期的错误');
       logger.error(err)
   }
   else if(infoLevelArray.indexOf(err.name)>-1){  // 某些错误只在info级别,如API返回的数据不存在。
       logger.info(err);
   }
   else {
       logger.error(err);
   }

}

根据err的类型进行统一处理,如果时router 404 则直接进入404页面。而后根据错误类型进行不同的记录操作。如日志记录,500页面,数据不存在而页面等。

infoLevelArray存放了一些info级别的错误类型,如服务端数据不存在,已删除等,这种业务上大量存在,如果记录导致大量无用信息记录

router404

const ENV = process.env.NODE_ENV;

module.exports = (req, res, next) => {
    const err = new Error('哎哟喂!页面被小柚子给弄丢了!');
    res.status(404).render('404', {
        message: err.message,
        error: ENV !== 'production' ? err : {}
    });
}
// error handler
app.use(errorHandler);
app.use(router404);

统一的errorHandler可以让业务代码与错误处理代码剥离、业务代码只需关心业务,也不用每个router去render错误页面。层级分明,分工明确。

反面例子

业务router 消化error,render error页面

exports.index = (req, res) => {

  // ...

  http.fetch(`${base}?${search}`, {headers:{_headers:''}})
    .then(response => response.json())
    .then(json => res.render('topic-templete/index', medalData(json, req, pagesize)))
    .catch(error => res.render('error', tools.failData(error, req)));
};

业务代码消化error,error页面放业务的html代码中,根据数据情况显示

  http.fetch(`${base}?${search}`, { headers:{_headers:''}})
    .then(response => {
      return response.json();
    })
    .then(json => {
      return res.render('xiaoyou/circle-detail', process(json, req))
    })
    .catch(error => res.render('xiaoyou/circle-detail', tools.failData(error, req)));
exports.failData = (error, req) => {
  // log(error.stack);
  errLog.log(error, req);
  const query = req.query;
  const qs = exports.stringifyQs(query, qsArray);
  const myclient = req.headers['myclient'] || query.myclient;
  const host = '';
  return { status: 'error', data: {}, query, qs, host, message: error.message, appid: req._appid, myclient: myclient };
};
<!doctype html>

<html lang="zh-CN">
<% if(data.code == 0 && data.data) { %>
  <head...>
  <body class="share-body share-body<%= appid %>"...>
<% } else { %>
  <% include ../error.html %>
<% } %>
</html>

上面可以看出业务代码写了很多错误处理的内容,代码冗余。
正确做法应该是catch的error中解析错误内容,next(error),交给errorHander来统一处理

4.使用举例

const errors = require('../errors/errors');

module.exports.index = function(req, res){
  
    global.fetch(api, { headers })
        .then(response => {    // 这里比较繁琐,而可以封装
            let respJson;
            try{ 
                respJson = response.json()
            }
            catch(e){
                throw new errors.JSONParseError(e)  //解析出错 ✔
            }
            return respJson
        ) 
        .then(data => {
          if (data && data.data && data.code == 0) { // 数据正常
            res.render('index', data, function(error, html) {
                if (error) {
                  throw new errors.RenderError(error); // 渲染出错 ✔
                } else {
                  res.send(html); // 正常返回页面
                }
           )
          } else { //API给的数据不对,会导致页面无法正常渲染,可能是404,403
            next(errors.RespDataError('数据错误')) // 数据异常 ✔
          }
        })
        .catch(error => {
          next(error)
        });
}
  • 异步的使用promise捕获异常,同步的使用你try catch捕获
  • 区分了错误类型,方面errorHandler进行区分记录,有类型信息方便排查
  • 业务代码不消化错误,不做render error页面
  • 全面的考虑了可能存在的错误
  • 未考虑的异常跟这些错误一起在catch中传递给了errorHandler做处理

注意:还应该考虑API返回的一切可能情况,确保无懈可击

5. 调试日志的打印

如果是用于调试的日志引用日志区分环境进行输出

const logger = require('../../config/logger');
 
...     
.then(response => {
  logger.info('微信返回data',response); // 获取debug级别
  ...
})

有些必须在外网调试的,比如微信认证授权等的,用日志调试至关重要。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant