uniquejava / blog

My notes regarding the vibrating frontend :boom and the plain old java :rofl.
Creative Commons Zero v1.0 Universal
11 stars 5 forks source link

log4js #91

Open uniquejava opened 7 years ago

uniquejava commented 7 years ago

本文针对目前最新的log4js 2.x系列

2.x内置了对cluster的支持(不必写任何代码), 并且作者说2.x的性能更好(特别是file appender).

安装

npm install log4js --save

输出

[2017-09-17 12:56:44.378] [INFO] startup - Listening on port 3000
[2017-09-17 12:56:49.029] [INFO] console - This is from console.log
[2017-09-17 12:56:49.339] [WARN] http - GET /admin/import

第一行是通过logger.info打印出来的, 第二行是console.log, 第三行是expresss.js内部打出来的access log. 稍后将解释怎么处理这三种日志.

最小用法

var log4js = require('log4js');
var logger = log4js.getLogger();
logger.level = 'debug'; // default level is OFF - which means no logs at all.
logger.debug("Some debug messages");

两种Configure方式

我喜欢第二种, 就像java中的log4j有log4j.xml或log4j.properties.

// 方式一
log4js.configure({
  appenders: { console: { type: 'console' } },
  categories: { default: { appenders: [ 'console' ], level: 'info' } }
});

// 方式二
log4js.configure('./config/log4js.json');

最小配置

见: Console Appender, 作者不推荐使用console, (因为它的内部实现全部是用console.log的形式输出的), 推荐使用stdout, 性能更好. 如果用stdout, 如下:

log4js.configure({
  appenders: { 'out': { type: 'stdout' } },
  categories: { default: { appenders: ['out'], level: 'info' } }
});

中等配置

{
  "appenders": {
    "console": { "type": "console" },
    "cheeseLogs": { "type": "file", "filename": "logs/cheese.log" },
    "no-debugs": { "type": "logLevelFilter", "appender":"cheeseLogs", "level": "info"}
  },
  "categories": {
    "cheese": { "appenders": ["cheeseLogs"], "level": "info" },
    "another": { "appenders": ["console"], "level": "debug" },
    "default": { "appenders": ["console", "no-debugs"], "level": "debug"}
  }
}

以中等配置为例, 理解一下最前面输出中的startup, console, http都是些什么. (这些都是日志的category), 那么在log4j.json中定义的是cheese, another, default这三种啊. (哦, log4js.getLogger("startup");这个参数传什么名字, 日志中就打印出来什么), 如果传的category在log4j.json中没有定义, 就会用default那个categories对应的配置. (明白了)

另外在categories中定义了每类日志的输出 level, 小于这个level的日志将不予处理.

在appenders中也可以定义level, 但是只有type为logLevelFilter的appender才能定义level.

并且logLevelFilter类型的appender只能基于某个已存在的appender.

以上中等配置中. 如果项目中用console.debug('this is some debug message');, 那么这句话只会出现在控制台, 并不会出现在日志文件中. (这是因为虽然总体的输出level为debug, 所以console类型的appender会打印出这句话, 但是no-debugs(名字乱取的)这个类型的appender限制了该类型的appender只输出info级别的日志)

startup 在./bin/www中, 有如下代码

/**
 * Initialise log4js first, so we don't miss any log messages
 */
const log4js = require('log4js');
log4js.configure('./config/log4js.json');

const log = log4js.getLogger("startup");
log.info("Listing on port 3000");

其中log4js.getLogger(CATEGORY_NAME); 这是新建一个logger的实例, 会自动去找配置中categories中同名的category, 如果找不到就用default那个category, 比如这里的startup并不存在, 所以实际用的default category.

自动处理console.log/console.error等

项目中大家都习惯了用console.log来记录日志 , 如何不更改已有的代码, 让log4js自动接手console.log? 作者在FAQ中有回答这个问题.

/**
 * Initialise log4js first, so we don't miss any log messages
 */
const log4js = require('log4js');
log4js.configure('./config/log4js.json');

const log = log4js.getLogger("startup");

// see https://nomiddlename.github.io/log4js-node/faq.html
const consoleLogger = log4js.getLogger('console');
console.debug = consoleLogger.debug.bind(consoleLogger);
console.log = consoleLogger.info.bind(consoleLogger);
console.error = consoleLogger.error.bind(consoleLogger);

如何处理express.js自带的access log

这次从./bin/www转移到app.js中来.

var log4js = require('log4js');
var express = require('express');

//We won't need this.
//var logger = require('morgan');
var log = log4js.getLogger("app");

// replace this with the log4js connect-logger
// app.use(logger('dev'));
app.use(log4js.connectLogger(log4js.getLogger("http"), {level: 'auto', format: ':method :url', nolog: '\\.js|\\.css|\\.png'}));

看代码中的注释, 我们可以用npm uninstall morgan --save卸载掉express generator包含的morgan组件. 因为在以上的代码中我们用log4js(而非morgan)接管了connect-logger. auto, format及nolog的含义见: Connect / Express Logger

cyper实战

经过权衡, 我暂时配置了如下这款, 除了一点: 不知道怎么去掉时间的毫秒数, 查了文档, 对%d没有像log4j那样提供更精细的控制, 其它都足够满意.

{
  "appenders": {
    "stdout": {
      "type": "stdout",
      "layout": {
        "type": "pattern",
        "pattern": "[%d %p] %m"
      }
    },
    "daily": {
      "type": "dateFile",
      "filename": "logs/app",
      "pattern": ".yyyy-MM-dd.log",
      "alwaysIncludePattern": true,
      "compress": true,
      "daysToKeep": 30,
      "layout": {
        "type": "pattern",
        "pattern": "[%d %p] %m"
      }
    },
    "file": {
      "type": "file",
      "filename": "logs/error.log",
      "maxLogSize": 10485760,
      "backups": 3,
      "compress": true
    },
    "error": {
      "type": "logLevelFilter",
      "appender": "file",
      "level": "error"
    }
  },
  "categories": {
    "default": {
      "appenders": [
        "stdout",
        "daily",
        "error"
      ],
      "level": "info"
    }
  }
}

同时, 我会在开发模式下, 把日志级别动态调整为debug, 在./bin/www中还添加了如下代码:

/**
 * Initialise log4js first, so we don't miss any log messages
 */
const log4js = require('log4js');
log4js.configure('./config/log4js.json');

const logger = log4js.getLogger("startup");

const consoleLogger = log4js.getLogger('console');
console.debug = consoleLogger.debug.bind(consoleLogger);
console.log = consoleLogger.info.bind(consoleLogger);
console.error = consoleLogger.error.bind(consoleLogger);

logger.info('env=', app.get('env'));

if(app.get('env') === 'development') {
  logger.level = 'debug';
  consoleLogger.level = 'debug';
}

会打印出如下格式的日志:

[2017-09-18 00:32:33.578 INFO] env= development
[2017-09-18 00:32:33.584 INFO] Listening on port 3000
uniquejava commented 7 years ago

log4js 1.x

log4js的1.x版本和最新的2.x版本变化很大, 以下都是1.x时的笔记, 仅供参考.

1.x的能找到一篇不错的博客: http://www.lkhweb.com/node-js-zhi-log4js-wan-quan-jiang-jie/

简单

简单版中要用logger.info才能将日志打印到文件中, 使用console.log还是只会打印在console, 不知道是哪里设置不对, cluster版没有问题, 使用console.log/info/error都会打印到日志文件.

原因: 作者去掉了对replaceConsole的支持, 见https://nomiddlename.github.io/log4js-node/faq.html

log.js

var log4js = require('log4js');
log4js.configure({
    appenders: [
        {type: "console"},
        {
            type: "dateFile",
            filename: 'logs/wss.log',
            pattern: "_yyyy-MM-dd.log",
            category: 'normal'
        }
    ],
    levels: {
        "[all]": "INFO"
    },
    replaceConsole: true
});

var logger = log4js.getLogger('normal');
exports.logger = logger;

exports.use = function (app) {
    app.use(log4js.connectLogger(logger, {level: log4js.levels.INFO, format: ':method :url'}));
};

在app.js中use

var log = require('./log');
var logger = log.logger;

var app = express();

//日志
log.use(app);

其它地方使用

var logger = require('./log').logger;
logger.info('hello world');

cluster版

./bin/www

var app = require('../app');
var debug = require('debug')('wefact:server');
var http = require('http');
var cluster = require('cluster');
var cpuNums = require('os').cpus().length;
var util = require('util');
var log4js = require('log4js');
var env = process.env.NODE_ENV || 'development';
/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

var appWrap = function (worker, app) {
    return function (req, res, next) {
        if (!req.url.startsWith('/assets/')) {
            console.log(util.format('WorkerId:%d, url:%s', worker.id, req.url));
        }
        app(req, res, next);
    }
}

if (cluster.isMaster) {
    // configuration for log
    log4js.configure({
        appenders: [
            {
                type: "clustered",
                appenders: [
                    {type: 'dateFile', filename: "logs/xxxxx.log", "pattern": "-yyyy-MM-dd.log"}
                ]
            }
        ],
        replaceConsole: true
    });

    for (var i = 0; i < cpuNums; i++) {
        cluster.fork();
    }
    cluster.on('exit', function (worker, exitCode, signal) {
        console.log(util.format('Worker-%d Exit', worker.id));
        cluster.fork();
    });
} else {
    var myApp = appWrap(cluster.worker, app);

    /**
     * Create HTTP server.
     */

    var server = http.createServer(myApp);
    // Init worker loggers, adding only the clustered appender here.
    var appenders = [{type: "clustered"}];

    if (env === 'development') {
        appenders.unshift({type: "console"});
    }
    log4js.configure({
        appenders: appenders,
        replaceConsole: true
    });
    var logger = log4js.getLogger('app');
    logger.setLevel('INFO');
    app.use(log4js.connectLogger(logger, {level: log4js.levels.INFO}));

    /**
     * Listen on provided port, on all network interfaces.
     */

    server.listen(port);
    server.on('error', onError);
    server.on('listening', onListening);

    console.log(util.format('Worker-%d Listening On Port %d', cluster.worker.id, port));

    process.on('uncaughtException', function (err) {
        console.error("Uncaught exception", err.message);
        console.error(err.stack);
        process.exit(1);
    });
}
uniquejava commented 7 years ago

在页面上查看logs

我设置了个取日志文件列表, 以及查看日志内容的工具类models/Logs.js, 如下

var Q = require('q');
var fs = require('fs');
var path = require('path');
var moment = require('moment');
var LOGS_PATH = '../logs/';

module.exports.getLog = getLog;

/**
 * return {names: ['error.log', 'app.yyyy-MM-dd.log', ..], data: 'log file content for that specific filename'}
 * @param date an optional string yyyy-MM-dd
 */
function getLog(fileName) {
  var d = Q.defer();
  var logDir = path.join(__dirname, LOGS_PATH);
  fileName = fileName || ('app.' + moment().format('YYYY-MM-DD') + '.log');

  // list all log file names
  fs.readdir(logDir, 'utf8', function (err, files) {
    if (err) {
      d.resolve({names: [], fileName: fileName, content: err});
    } else {
      var names = [];
      files.forEach(function (name) {
        if (name.endsWith('.log')) {
          names.push(name);
        }
      });

      // user only allowed to access .log file within the logs directory
      if (!fileName.endsWith('.log') || fileName.indexOf("/") !== -1) {
        d.resolve({names: names, fileName: fileName, content: "Illegal log file name: " + fileName});

      } else {
        // get specified file content
        var logFile = path.join(__dirname, LOGS_PATH, fileName);
        fs.readFile(logFile, 'utf8', function (err, data) {
          if (err) {
            d.resolve({names: names, fileName: fileName, content: err});
          } else {
            d.resolve({names: names, fileName: fileName, content: data});
          }
        });
      }

    }
  });

  return d.promise;
}

在routes中这样使用:

router.get('/logs', function (req, res, next) {
  var fileName = req.query.fileName;
  Logs.getLog(fileName).then(function (log) {
    res.render('admin/logs', {log: log});
  }, function (error) {
    res.render('admin/logs', {log: error.message});
  });

});