iKcamp|基于Koa2搭建Node.js实战(含视频)? 记录日志

沪江CCtalk视频地址:https://www.cctalk.com/v/15114923883523

log 日志中间件

最困难的事情就是认识自己。

在一个真实的项目中,开发只是整个投入的一小部分,版本迭代和后期维护占了极其重要的部分。项目上线运转起来之后,我们如何知道项目运转的状态呢?如何发现线上存在的问题,如何及时进行补救呢?记录日志就是解决困扰的关键方案。正如我们每天写日记一样,不仅能够记录项目每天都做了什么,便于日后回顾,也可以将做错的事情记录下来,进行自我反省。完善的日志记录不仅能够还原问题场景,还有助于统计访问数据,分析用户行为。

日志的作用

  • 显示程序运行状态
  • 帮助开发者排除问题故障
  • 结合专业的日志分析工具(如 ELK )给出预警

关于编写 log 中间件的预备知识

log4js

本项目中的 log 中间件是基于 log4js 2.x 的封装,Log4jsNode.js 中一个成熟的记录日志的第三方模块,下文也会根据中间件的使用介绍一些 log4js 的使用方法。

日志分类

日志可以大体上分为访问日志和应用日志。访问日志一般记录客户端对项目的访问,主要是 http 请求。这些数据属于运营数据,也可以反过来帮助改进和提升网站的性能和用户体验;应用日志是项目中需要特殊标记和记录的位置打印的日志,包括出现异常的情况,方便开发人员查询项目的运行状态和定位 bug 。应用日志包含了debuginfowarnerror等级别的日志。

日志等级

log4js 中的日志输出可分为如下7个等级:

在应用中按照级别记录了日志之后,可以按照指定级别输出高于指定级别的日志。

日志切割

当我们的项目在线上环境稳定运行后,访问量会越来越大,日志文件也会越来越大。日益增大的文件对查看和跟踪问题带来了诸多不便,同时增大了服务器的压力。虽然可以按照类型将日志分为两个文件,但并不会有太大的改善。所以我们按照日期将日志文件进行分割。比如:今天将日志输出到 task-2017-10-16.log 文件,明天会输出到 task-2017-10-17.log 文件。减小单个文件的大小不仅方便开发人员按照日期排查问题,还方便对日志文件进行迁移。

代码实现

安装 log4js 模块

npm i log4js -S

log4js 官方简单示例

middleware/ 目录下创建 mi-log/demo.js,并贴入官方示例代码:

var log4js = require(‘log4js‘);
var logger = log4js.getLogger();
logger.level = ‘debug‘;
logger.debug("Some debug messages");

然后在 /middleware/mi-log/ 目录下运行:

cd ./middleware/mi-log/ && node demo.js

可以在终端看到如下输出:

[2017-10-24 15:45:30.770] [DEBUG] default - Some debug messages

一段带有日期、时间、日志级别和调用 debug 方法时传入的字符串的文本日志。实现了简单的终端日志输出。

log4js 官方复杂示例

替换 mi-log/demo.js 中的代码为如下:

const log4js = require(‘log4js‘);
log4js.configure({
  appenders: { cheese: { type: ‘file‘, filename: ‘cheese.log‘ } },
  categories: { default: { appenders: [‘cheese‘], level: ‘error‘ } }
});

const logger = log4js.getLogger(‘cheese‘);
logger.trace(‘Entering cheese testing‘);
logger.debug(‘Got cheese.‘);
logger.info(‘Cheese is Gouda.‘);
logger.warn(‘Cheese is quite smelly.‘);
logger.error(‘Cheese is too ripe!‘);
logger.fatal(‘Cheese was breeding ground for listeria.‘);

再次在 /middleware/mi-log/ 目录下运行:

node demo.js

运行之后,在当前的目录下会生成一个日志文件 cheese.log文件,文件中有两条日志并记录了 error 及以上级别的信息,也就是如下内容:

[2017-10-24 15:51:30.770] [ERROR] cheese - Cheese is too ripe!
[2017-10-24 15:51:30.774] [FATAL] cheese - Cheese was breeding ground for listeria.

注意: 日志文件产生的位置就是当前启动环境的位置。

分析以上代码就会发现,configure 函数配置了日志的基本信息

{
  /**
   * 指定要记录的日志分类 cheese
   * 展示方式为文件类型 file
   * 日志输出的文件名 cheese.log
   */
  appenders: { cheese: { type: ‘file‘, filename: ‘cheese.log‘ } },

  /**
   * 指定日志的默认配置项
   * 如果 log4js.getLogger 中没有指定,默认为 cheese 日志的配置项
   * 指定 cheese 日志的记录内容为 error 及 error 以上级别的信息
   */
  categories: { default: { appenders: [‘cheese‘], level: ‘error‘ } }
}

改写为log中间件

创建 /mi-log/logger.js 文件,并增加如下代码:

const log4js = require(‘log4js‘);
module.exports = ( options ) => {
  return async (ctx, next) => {
    const start = Date.now()
    log4js.configure({
      appenders: { cheese: { type: ‘file‘, filename: ‘cheese.log‘ } },
      categories: { default: { appenders: [‘cheese‘], level: ‘info‘ } }
    });
    const logger = log4js.getLogger(‘cheese‘);
    await next()
    const end = Date.now()
    const responseTime = end - start;
    logger.info(`响应时间为${responseTime/1000}s`);
  }
}

创建 /mi-log/index.js 文件,并增加如下代码:

const logger = require("./logger")
module.exports = () => {
   return logger()
}

修改 middleware/index.js 文件,并增加对 log 中间件的注册, 如下代码:

const path = require(‘path‘)
const bodyParser = require(‘koa-bodyparser‘)
const nunjucks = require(‘koa-nunjucks-2‘)
const staticFiles = require(‘koa-static‘)

const miSend = require(‘./mi-send‘)
// 引入日志中间件
const miLog = require(‘./mi-log‘)
module.exports = (app) => {
  // 注册中间件
  app.use(miLog())

  app.use(staticFiles(path.resolve(__dirname, "../public")))
  app.use(nunjucks({
    ext: ‘html‘,
    path: path.join(__dirname, ‘../views‘),
    nunjucksConfig: {
      trimBlocks: true
    }
  }));
  app.use(bodyParser())
  app.use(miSend())
}

打开浏览器并访问 http://localhost:3000, 来发送一个http 请求。

如上,按照前几节课程中讲解的中间件的写法,将以上代码改写为中间件。 基于 koa 的洋葱模型,当 http 请求经过此中间件时便会在 cheese.log 文件中打印一条日志级别为 info 的日志并记录了请求的响应时间。如此,便实现了访问日志的记录。

实现应用日志,将其挂载到 ctx

若要在其他中间件或代码中通过 ctx 上的方法打印日志,首先需要在上下文中挂载 log 函数。打开 /mi-log/logger.js 文件:

const log4js = require(‘log4js‘);
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]

module.exports = () => {
  const contextLogger = {}
  log4js.configure({
    appenders: { cheese: { type: ‘file‘, filename: ‘cheese.log‘ } },
    categories: { default: { appenders: [‘cheese‘], level: ‘info‘ } }
  }); 

  const logger = log4js.getLogger(‘cheese‘);

  return async (ctx, next) => {
     // 记录请求开始的时间
    const start = Date.now()
     // 循环methods将所有方法挂载到ctx 上
    methods.forEach((method, i) => {
       contextLogger[method] = (message) => {
         logger[method](message)
       }
    })
    ctx.log = contextLogger;

    await next()
    // 记录完成的时间 作差 计算响应时间
    const responseTime = Date.now() - start;
    logger.info(`响应时间为${responseTime/1000}s`);
  }
}

创建 contextLogger 对象,将所有的日志级别方法赋给对应的 contextLogger 对象方法。在将循环后的包含所有方法的 contextLogger 对象赋给 ctx 上的 log 方法。

打开 /mi-send/index.js 文件, 并调用 ctx 上的 log 方法:

module.exports = () => {
  function render(json) {
      this.set("Content-Type", "application/json")
      this.body = JSON.stringify(json)
  }
  return async (ctx, next) => {
      ctx.send = render.bind(ctx)
      // 调用ctx上的log方法下的error方法打印日志
      ctx.log.error(‘ikcamp‘);
      await next()
  }
}

在其他中间件中通过调用 ctx 上的 log 方法,从而实现打印应用日志。

const log4js = require(‘log4js‘);
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]

module.exports = () => {
  const contextLogger = {}
  const config = {
    appenders: {
        cheese: {
         type: ‘dateFile‘, // 日志类型
         filename: `logs/task`,  // 输出的文件名
         pattern: ‘-yyyy-MM-dd.log‘,  // 文件名增加后缀
         alwaysIncludePattern: true   // 是否总是有后缀名
       }
    },
    categories: {
      default: {
        appenders: [‘cheese‘],
        level:‘info‘
      }
    }
  }

  const logger = log4js.getLogger(‘cheese‘);

  return async (ctx, next) => {
    const start = Date.now()

    log4js.configure(config)
    methods.forEach((method, i) => {
      contextLogger[method] = (message) => {
        logger[method](message)
      }
    })
    ctx.log = contextLogger;

    await next()
    const responseTime = Date.now() - start;
    logger.info(`响应时间为${responseTime/1000}s`);
  }
}

修改日志类型为日期文件,按照日期切割日志输出,以减小单个日志文件的大小。这时候打开浏览器并访问 http://localhost:3000,这时会自动生成一个 logs 目录,并生成一个 cheese-2017-10-24.log 文件, 中间件执行便会在其中中记录下访问日志。

├── node_modules/
├── logs/
│     ├── cheese-2017-10-24.log
├── ……
├── app.js

抽出可配置量

const log4js = require(‘log4js‘);
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]

// 提取默认公用参数对象
const baseInfo = {
  appLogLevel: ‘debug‘,  // 指定记录的日志级别
  dir: ‘logs‘,      // 指定日志存放的目录名
  env: ‘dev‘,   // 指定当前环境,当为开发环境时,在控制台也输出,方便调试
  projectName: ‘koa2-tutorial‘,  // 项目名,记录在日志中的项目信息
  serverIp: ‘0.0.0.0‘       // 默认情况下服务器 ip 地址
}

const { env, appLogLevel, dir } = baseInfo
module.exports = () => {
  const contextLogger = {}
  const appenders = {}

  appenders.cheese = {
    type: ‘dateFile‘,
    filename: `${dir}/task`,
    pattern: ‘-yyyy-MM-dd.log‘,
    alwaysIncludePattern: true
  }
  // 环境变量为dev local development 认为是开发环境
  if (env === "dev" || env === "local" || env === "development") {
    appenders.out = {
      type: "console"
    }
  }
  let config = {
    appenders,
    categories: {
      default: {
        appenders: Object.keys(appenders),
        level: appLogLevel
      }
    }
  }

  const logger = log4js.getLogger(‘cheese‘);

  return async (ctx, next) => {
    const start = Date.now()

    log4js.configure(config)
    methods.forEach((method, i) => {
      contextLogger[method] = (message) => {
        logger[method](message)
      }
    })
    ctx.log = contextLogger;

    await next()
    const responseTime = Date.now() - start;
    logger.info(`响应时间为${responseTime/1000}s`);
  }
}

代码中,我们指定了几个常量以方便后面提取,比如 appLogLeveldirenv 等。 。并判断当前环境为开发环境则将日志同时输出到终端, 以便开发人员在开发是查看运行状态和查询异常。

丰富日志信息

ctx 对象中,有一些客户端信息是我们数据统计及排查问题所需要的,所以完全可以利用这些信息来丰富日志内容。在这里,我们只需要修改挂载 ctx 对象的 log 函数的传入参数:

logger[method](message)

参数 message 是一个字符串,所以我们封装一个函数,用来把信息与上下文 ctx 中的客户端信息相结合,并返回字符串。

增加日志信息的封装文件 mi-log/access.js

module.exports = (ctx, message, commonInfo) => {
  const {
    method,  // 请求方法 get post或其他
    url,          // 请求链接
    host,     // 发送请求的客户端的host
    headers   // 请求中的headers
  } = ctx.request;
  const client = {
    method,
    url,
    host,
    message,
    referer: headers[‘referer‘],  // 请求的源地址
    userAgent: headers[‘user-agent‘]  // 客户端信息 设备及浏览器信息
  }
  return JSON.stringify(Object.assign(commonInfo, client));
}

注意: 最终返回的是字符串。

取出 ctx 对象中请求相关信息及客户端 userAgent 等信息并转为字符串。

mi-log/logger.js 文件中调用:

const log4js = require(‘log4js‘);
// 引入日志输出信息的封装文件
const access = require("./access.js");
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]

const baseInfo = {
  appLogLevel: ‘debug‘,
  dir: ‘logs‘,
  env: ‘dev‘,
  projectName: ‘koa2-tutorial‘,
  serverIp: ‘0.0.0.0‘
}
const { env, appLogLevel, dir, serverIp, projectName } = baseInfo
// 增加常量,用来存储公用的日志信息
const commonInfo = { projectName, serverIp }
module.exports = () => {
  const contextLogger = {}
  const appenders = {}

  appenders.cheese = {
    type: ‘dateFile‘,
    filename: `${dir}/task`,
    pattern: ‘-yyyy-MM-dd.log‘,
    alwaysIncludePattern: true
  }

  if (env === "dev" || env === "local" || env === "development") {
    appenders.out = {
      type: "console"
    }
  }
  let config = {
    appenders,
    categories: {
      default: {
        appenders: Object.keys(appenders),
        level: appLogLevel
      }
    }
  }

  const logger = log4js.getLogger(‘cheese‘);

  return async (ctx, next) => {
    const start = Date.now()

    log4js.configure(config)
    methods.forEach((method, i) => {
      contextLogger[method] = (message) => {
       // 将入参换为函数返回的字符串
        logger[method](access(ctx, message, commonInfo))
      }
    })
    ctx.log = contextLogger;

    await next()
    const responseTime = Date.now() - start;
    logger.info(access(ctx, {
      responseTime: `响应时间为${responseTime/1000}s`
    }, commonInfo))
  }
}

重启服务器并访问 http://localhost:3000 就会发现,日志文件的记录内容已经变化。代码到这里,已经完成了大部分的日志功能。下面我们完善下其他功能:自定义配置参数和捕捉错误。

项目自定义内容

安装依赖文件 ip:

npm i ip -S

修改 middleware/index.js 中的调用方法

const path = require(‘path‘)
const ip = require(‘ip‘)
const bodyParser = require(‘koa-bodyparser‘)
const nunjucks = require(‘koa-nunjucks-2‘)
const staticFiles = require(‘koa-static‘)

const miSend = require(‘./mi-send‘)
const miLog = require(‘./mi-log/logger‘)
module.exports = (app) => {
  // 将配置中间件的参数在注册中间件时作为参数传入
  app.use(miLog({
    env: app.env,  // koa 提供的环境变量
    projectName: ‘koa2-tutorial‘,
    appLogLevel: ‘debug‘,
    dir: ‘logs‘,
    serverIp: ip.address()
  }))

  app.use(staticFiles(path.resolve(__dirname, "../public")))

  app.use(nunjucks({
    ext: ‘html‘,
    path: path.join(__dirname, ‘../views‘),
    nunjucksConfig: {
      trimBlocks: true
    }
  }));

  app.use(bodyParser())
  app.use(miSend())
}

再次修改 mi-log/logger.js 文件:

const log4js = require(‘log4js‘);
const access = require("./access.js");
const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]

const baseInfo = {
  appLogLevel: ‘debug‘,
  dir: ‘logs‘,
  env: ‘dev‘,
  projectName: ‘koa2-tutorial‘,
  serverIp: ‘0.0.0.0‘
}

module.exports = (options) => {
  const contextLogger = {}
  const appenders = {}

  // 继承自 baseInfo 默认参数
  const opts = Object.assign({}, baseInfo, options || {})
  // 需要的变量解构 方便使用
  const { env, appLogLevel, dir, serverIp, projectName } = opts
  const commonInfo = { projectName, serverIp }

  appenders.cheese = {
    type: ‘dateFile‘,
    filename: `${dir}/task`,
    pattern: ‘-yyyy-MM-dd.log‘,
    alwaysIncludePattern: true
  }

  if (env === "dev" || env === "local" || env === "development") {
    appenders.out = {
      type: "console"
    }
  }
  let config = {
    appenders,
    categories: {
      default: {
        appenders: Object.keys(appenders),
        level: appLogLevel
      }
    }
  }

  const logger = log4js.getLogger(‘cheese‘);

  return async (ctx, next) => {
    const start = Date.now()

    log4js.configure(config)
    methods.forEach((method, i) => {
      contextLogger[method] = (message) => {
        logger[method](access(ctx, message, commonInfo))
      }
    })
    ctx.log = contextLogger;

    await next()
    const responseTime = Date.now() - start;
    logger.info(access(ctx, {
      responseTime: `响应时间为${responseTime/1000}s`
    }, commonInfo))
  }
}

将项目中自定义的量覆盖默认值,解构使用。以达到项目自定义的目的。

对日志中间件进行错误处理

对于日志中间件里面的错误,我们也需要捕获并处理。在这里,我们提取一层进行封装。

打开 mi-log/index.js 文件,修改代码如下:

const logger = require("./logger")
module.exports = (options) => {
  const loggerMiddleware = logger(options)

  return (ctx, next) => {
    return loggerMiddleware(ctx, next)
    .catch((e) => {
        if (ctx.status < 500) {
            ctx.status = 500;
        }
        ctx.log.error(e.stack);
        ctx.state.logged = true;
        ctx.throw(e);
    })
  }
}

如果中间件里面有抛出错误,这里将通过 catch 函数捕捉到并处理,将状态码小于 500 的错误统一按照 500 错误码处理,以方便后面的 http-error 中间件显示错误页面。 调用 log 中间件打印堆栈信息并将错误抛出到最外层的全局错误监听进行处理。

到这里我们的日志中间件已经制作完成。当然,还有很多的情况我们需要根据项目情况来继续扩展,比如结合『监控系统』、『日志分析预警』和『自动排查跟踪机制』等。可以参考一下官方文档

下一节中,我们将学习下如何处理请求错误。

上一篇:iKcamp新课程推出啦~~~~~iKcamp|基于Koa2搭建Node.js实战(含视频)? 处理静态资源

推荐: 翻译项目Master的自述:

1. 干货|人人都是翻译项目的Master

2. iKcamp出品微信小程序教学共5章16小节汇总(含视频)

原文地址:https://www.cnblogs.com/ikcamp/p/8328490.html

时间: 2024-10-29 19:41:16

iKcamp|基于Koa2搭建Node.js实战(含视频)? 记录日志的相关文章

iKcamp|基于Koa2搭建Node.js实战(含视频)? 代码分层

视频地址:https://www.cctalk.com/v/15114923889408 文章 在前面几节中,我们已经实现了项目中的几个常见操作:启动服务器.路由中间件.Get 和 Post 形式的请求处理等.现在你已经迈出了走向成功的第一步. 目前,整个示例中所有的代码都写在 app.js 中.然而在业务代码持续增大,场景更加复杂的情况下,这种做法无论是对后期维护还是对患有强迫症的同学来说都不是好事.所以我们现在要做的就是:『分梨』. 分离 router 路由部分的代码可以分离成一个独立的文件

iKcamp|基于Koa2搭建Node.js实战(含视频)? 视图Nunjucks

视频地址:https://www.cctalk.com/v/15114923888328 视图 Nunjucks 彩虹是上帝和人类立的约,上帝不会再用洪水灭人. 客户端和服务端之间相互通信,传递的数据最终都会展示在视图中,这时候就需要用到『模板引擎』. 什么是模板引擎? 模板引擎是为了使用户界面与业务数据分离而产生的,可以生成特定格式的文档.例如,用于网站的模板引擎会生成一个标准的 HTML 文档. 市面上常见的模板引擎很多,例如:Smarty.Jade.Ejs.Nunjucks 等,可以根据个

iKcamp|基于Koa2搭建Node.js实战(含视频)? 错误处理

沪江CCtalk视频地址:https://www.cctalk.com/v/15114923887518 处理错误请求 爱能遮掩一切过错. 当我们在访问一个站点的时候,如果访问的地址不存在(404),或服务器内部发生了错误(500),站点会展示出某个特定的页面,比如: 那么如何在 Koa 中实现这种功能呢?其实,一个简单的中间件即可实现,我们把它称为 http-error.实现过程并不复杂,拆分为三步来看: 第一步:确认需求 第二步:整理思路 第三步:代码实现 确认需求 打造一个事物前,需要先确

用node.js 搭建的博客程序心得(node.js实战读书笔记1)

学习node已经有一段时间了,之前把了不起的node.js看完了,基本算了解了一些node的基本的用法还有一些概念了,然后就开始看第二本node.js实战,第一章就是搭建一个博客程序.但是不得不吐槽一下node,发展得太块了,很多库已经和之前的用法不一样了,就要一直去百度google来查询最新的用法,其实我觉得这样并不见得是一件好事,因为不稳定,所以就不好学习,就要一直保持对于node的关注.不废话了,这篇文章就大概说一些在这章里面所学习到的一些东西,经验总结吧 1.express - 基于 N

SSH2+LigerUI+JBPM5+Node.js实战开发视频教程

基于SSH2+LigerUI+JBPM5+Node.js技术实现大型J2EE金融行业财务预算系统 (第三季适合二年以上的开发者)课程分类:Java框架适合人群:中级课时数量:51课时用到技术:SSH2.LigerUI.JBPM5.Node.js技术涉及项目:大型J2EE金融行业财务预算系统咨询qq:1840215592课程项目所采用的技术架构为:struts2+spring+hibernate+LigerUI+jbpm5+mysql+自定义表单+node.js+webservice+思维导图+j

腾讯云:搭建 Node.js 环境

搭建 Node.js 环境 安装 Node.js 环境 任务时间:5min ~ 10min Node.js 是运行在服务端的 JavaScript, 是基于 Chrome JavaScript V8 引擎建立的平台. 下载并安装 Node.js 下载最新的稳定版 v6.10.3 到本地 wget https://nodejs.org/dist/v6.10.3/node-v6.10.3-linux-x64.tar.xz 下载完成后, 将其解压 tar xvJf node-v6.10.3-linux

快速搭建 Node.js 开发环境以及加速 npm

在公交车上刷微博,还是有很多同学在咨询: 如何快速搭建 node 开发环境 npm 超慢 github 无法打开的问题 于是我觉得应该写一篇文章解答所有这些起步问题,让新同学也能顺顺利利入门. 快速搭建 Node.js 开发环境 如果你想长期做 node 开发, 或者想快速更新 node 版本, 或者想快速切换 node 版本, 那么在非 Windows(如 osx, linux) 环境下, 请使用 nvm 来安装你的 node 开发环境, 保持系统的干净. 如果你使用 Windows 做开发,

基于hapi的Node.js后端开发

基于hapi的Node.js后端开发 1.背景今年下半年公司线上程序频繁出现问题.不是内存跑满,就是CPU跑满就是程序自己挂掉了.严重影响了现有的小程序业务.目前线上主要架构是dubbo-x搭建的分布式.之前主要用来为app做数据服务.解决思路1:优化现有架构,找到问题(奈何本人虽然写了不少java代码.但是对java基本还是处于一窍不通的状态.)解决思路2:业务分离,为小程序从新搭建一套服务器系统.与原有的互不影响.这样既解决了小程序服务稳定性问题,也流出了时间来处理"思路1",不会

《Node.js实战》博客实例 express4.x

大致看了一阵子nodejs的书,对语法有初步的了解.但是还是写不出个实例来.最近学长给我的这本书感觉挺入门的<Node.js实战>电子工业出版社.毕竟图书馆借的,出版时间已经是两年前了.很多代码都更新了.特别是express 4.x相对与express 3.x有很多的修改.比如把中间件独立出来.好处是有.这样express的更新就不用受这些中间件的影响 坏处也许主要是对我这种新手吧.大部分教材都是express 3.x甚至是 2.x 的.这样学习起来就有困难.也许不会有人看到这个.即使解决不了