为Express.js编写一个Logger

peacentury 8年前
   <p><a href="/misc/goto?guid=4958522262563415188" rel="nofollow,noindex">Express.js</a> 是Node.js下最基础最灵活的Web服务器。 Express的日志工具有很多,比如默认的访问日志工具 <a href="/misc/goto?guid=4959623923499520134" rel="nofollow,noindex">morgan</a> , 通用日志工具 <a href="/misc/goto?guid=4959716864134338523" rel="nofollow,noindex">winston</a> 等等。</p>    <p>本文便来发掘一下这些日志工具的优秀特性,并一一给出实现: 对象输出、日期前缀、访问日志,模块名前缀,以及彩色输出等。</p>    <h2><strong>JSON Stringify</strong></h2>    <p>JavaScript服务器输出JSON真是再平常不过了,输出可读的JSON在开发中非常有用。 为了输出可读的JSON,我们将所有对象类型的参数 <a href="/misc/goto?guid=4959716864233246770" rel="nofollow,noindex"> stringify </a> 即可。</p>    <pre>  <code class="language-java">function log(){      var args = Array.prototype.slice.call(arguments).map(stringify);      console.log.apply(console, args);  }  function stringify(arg) {      return typeof arg === 'object' ?          JSON.stringify(arg, null, 4) : arg;  }  log({foo: ["bar", "foo"]});  // 输出:  // {  //     foo: ["bar", "foo"]  // }</code></pre>    <p>我们希望 log 函数像 console.log 一样接受多个参数,因此我们需要对所有参数进行map。</p>    <h2><strong>日期前缀</strong></h2>    <p>当日志用于服务器环境时,我们希望知道日志的输出时间, 这在性能调试和调试因果关系时非常重要。 尤其是在使用像pm2这样的进程监视器时,错误和标准输出是分开存储的。 如果没有时间戳很难得知顺序关系。</p>    <pre>  <code class="language-java">function parse(str) {      // 为每行都添加时间戳      return str.split('\n')          .map(prefixify)          .join('\n');  }    function prefixify(str){      var now = new Date();      var prefix = `[${now.toLocaleString()}]`;      // 将时间戳放在行首      return prefix + ' ' + str;  }</code></pre>    <p>时间日期的格式化可以使用 <a href="/misc/goto?guid=4959716864324300229" rel="nofollow,noindex">strftime</a> 。</p>    <h2><strong>提供类console对象</strong></h2>    <p>我们的logger的使用方式最好与console相同以获得最好的兼容与可用性。 于是需要提供 .log() , .warn() , .error() , .info() 等方法, 同时也需要支持多参数的情形。我们需要做的是封装所有的 console 方法:</p>    <pre>  <code class="language-java">function parse(argvs) {      return Array.prototype.slice.call(argvs)          .map(stringify).join(' ')          .split('\n').map(prefixify).join('\n');  }  function prefixify(str){      var now = new Date();      var prefix = `[${now.toLocaleString()}]`;      return prefix + ' ' + str;  }  function stringify(arg) {      return typeof arg === 'object' ?          JSON.stringify(arg, null, 4) : arg;  }  module.exports = {      log:   function(){ console.log(parse(arguments)) },      warn:  function(){ console.warn(parse(arguments)) },      info:  function(){ console.info(parse(arguments)) },      error: function(){ console.error(parse(arguments)) }  };</code></pre>    <h2><strong>彩色的输出</strong></h2>    <p>在开发环境中输出彩色的日志可以让我们更快地获取信息,在终端中输出彩色需要使用特殊字符。 自定义过 <a href="/misc/goto?guid=4959716864417190588" rel="nofollow,noindex">PS1</a> 的童鞋一定会感受到手写这些字符的费劲,在Node.js中我们可以使用 <a href="/misc/goto?guid=4959625823264506682" rel="nofollow,noindex">colors</a> 库来完成这件事情。</p>    <pre>  <code class="language-java">npm install colors</code></pre>    <p>然后在输出时便可以先调用 colors 提供的API做字符串转换,现在来把日期前缀变成青色的:</p>    <pre>  <code class="language-java">const colors = require('colors/safe');    function prefixify(str){      var now = new Date();      var prefix = `[${now.toLocaleString()}]`;      // 将时间戳放在行首      return colors.cyan(prefix) + ' ' + str;  }</code></pre>    <p>带颜色的输出只是具有特殊字符的字符串,可以像普通字符串一样进行操作。</p>    <h2><strong>Express访问日志</strong></h2>    <p>现在我们Express访问日志与普通日志格式一致,这需要监听Express请求和响应。 需要用到 <a href="/misc/goto?guid=4959716864542529104" rel="nofollow,noindex"> on-headers </a> 来监听写Response Header事件。</p>    <pre>  <code class="language-java">const onHeaders = require('on-headers');  // 访问日志  app.use(function(req, res, next) {      req.receivedAt = Date.now();      onHeaders(res, function() {          var duration = Date.now() - req.receivedAt;          // 这里调用我们的logger          // 示例输出: [2016-07-28 10:23:02] GET / 200 21ms          logger.log(req.method.toUpperCase(),              req.originalUrl,              res.statusCode,              duration + 'ms');      });      next();  });  // use 路由</code></pre>    <p>可能你发现Express的默认访问日志工具 <a href="/misc/goto?guid=4959623923499520134" rel="nofollow,noindex">morgan</a> 会输出访问耗时。 这需要同时封装 console.time和 console.timeEnd`,本文就不赘述了。</p>    <h2><strong>模块名绑定</strong></h2>    <p>标准的Logger大多可以绑定一个模块名(或者Trace ID), 模块名会在该模块的每条日志的前缀部分出现,以方便跟踪Log是哪个模块(或业务线)输出的。 其用法大致如下:</p>    <pre>  <code class="language-java">var logger = Logger('account:factory');  logger.log('create account error');  // 输出:  // [account:factory] create account error</code></pre>    <p>如何实现呢?来一个简单的闭包即可:</p>    <pre>  <code class="language-java">function Logger(traceId){      return {          log: function(str){              console.log(`[${traceId}] ${str}`);          }      }  }</code></pre>    <p>你还可以将 [account:factory] 显示为灰色,并支持多参数的输出(见上文)。</p>    <p> </p>    <p>来自:http://harttle.com/2016/09/22/express-logger.html</p>    <p> </p>