Node.JS 应用最佳实践:日志

做者:Mahesh Haldarjavascript

翻译:疯狂的技术宅前端

原文:blog.bitsrc.io/logging-bes…java

未经容许严禁转载node

img

日志记录是每一个开发人员从第一天编写代码时就要作的事情,但不多有人知道它能够产生的价值和最佳实践。git

在本文中,咱们将讨论如下主题:github

  • 什么是日志,为何很重要性?
  • 记录日志的最佳作法
  • 日志的重要部分
  • 正确使用日志级别
  • 为何选择 Winston?

什么是日志,为何很重要?

日志是反映程序各个方面的事件,若是可以正确编写,那么它就是最简单的故障排除和诊断程序的模式。数据库

当你启动 Node.js 服务器时,若是数据库因为某些问题而没有运行,或服务器端口已经被占用时,若是没有日志,你将永远不知道服务器失败的缘由。npm

做为开发人员,你常常须要调试一些问题,咱们很喜欢用调试器和断点来定位故障的位置和内容。json

当你的程序在生产环境中运行时,你会作些什么?你能在那里附加调试器并重现 bug 吗?显然没有。所以,这是日志记录可以帮助你的地方。前端工程化

在不使用调试器的状况下,你能够经过浏览日志找到问题并了解出现问题的缘由和位置。

最佳实践

1)日志的三个重要部分

程序日志既适用于人类,也适用于机器。人类参考日志来调试问题,机器用日志生成各类图表,并经过数据分析来产生关于客户使用的各类结论。

每一个日志都应包含三个最重要的部分:

  • 日志源

    当咱们有一个微服务架构时,这对于了解日志的来源、服务名称、区域、主机名等信息很是重要(有关管理微服务中的公共代码的更多信息请在此处阅读

    有关源的详细元数据主要由日志 agent 进行处理,日志 agent 将日志从全部微服务推送到集中式日志系统。 ELK 栈的 Filebeat 是日志 agent 的最佳选择之一。

  • 时间戳

    事件发生或生成日志的时间很是重要。因此要确保每一个日志都有时间戳,以便咱们进行排序和筛选。

  • 级别和上下文

    在经过查看日志查找错误时,若是日志没有提供足够的信息,你就必须回到代码中,那将很是使人沮丧。所以在记录时咱们应该传递足够的上下文

    例如。没有上下文的日志将以下所示:

    The operation failed!

    有意义的上下文应该是是:

    Failed to create user, as the user id already exist

2)日志的使用方法

  • 日志方法和输入:

    在调试的同时,若是咱们知道调用了哪一个函数以及传递了哪些参数,它就能发挥真正的做用。

import logger from '../logSetup';
getInstallment(month: number, count: number ): number {
    logger.debug(`>>>> Entering getInstallment(month = ${month}, count= ${count}"); // process const installment: number = 3; log.debug("<<<< Exiting getIntallment()"); return installment; } 复制代码

经过日志 >>>><<<< 将给出函数输入和退出的信息。这是受到了 git merge 冲突的启发。

  • 日志不该该评估抛出异常

    在第7行中,userService.getUser() 能够返回 null,且 .getId() 能够抛出异常,因此要避免这些状况。

import logger from '../logSetup';
processLoan(...) {
    logger.debug(">>>> Entering processLoan()");
    
    // ... process

    logger.debug(`Processing user loan with id ${userService.getUser().getId()}`);
    // this might throw error, when getUser returns undefined

    logger.debug("<<<< Exiting processLoan()");
    return true;
}
复制代码

你应该用 Aspect js 自动执行函数级日志。

  • 日志不该产生反作用

    日志应该是无状态的,不该产生任何反作用。例如,下面第 7 行的日志将在数据库中建立新资源。

import logger from '../logSetup';
createUser() {
  logger.debug(">>>> Entering createUser");

  // ... process

  logger.debug("Saving user loan {}", userInfoRepository.save(userInfo)) // don't do this

  return true;
}
复制代码
  • 记录错误和详细信息

当描述错误时,请说起尝试的内容及其失败的缘由。

记录哪些是失败的你接下来作什么

import logger from '../logSetup';
processLoan(id: number, userId: number) {
    try {
        getLoanDeatilsById()
    } catch(error) {
        log.error(`Failed to do getLoanDetails with id ${id}, ignoring it and trying to getLoanDetailsByUserId`, error);
        // good example: provide what failed, and how you are handling. 
        // e.g here on fail I am trying to call other function
        getLoanDetailsByUserId();
    }
}
复制代码

若是你在 catch 部分中丢弃错误,请记录哪一个操做失败并说起你正在抛出错误。

import logger from '../logSetup';
processLoan(id: number, userId: number) {
    try {
        getLoanDeatilsById()
    } catch(error) {
        log.error(`Failed to do getLoanDetails with id ${id} hence throwing error`, error);
        // good example: provide what failed, and how you are handling. 
        // e.g here on fail I am throwing
        throw error;
    }
}
复制代码

3)敏感信息

该系列日志应该反映用户在程序中的活动以便调试更容易,而且应该记录错误以便尽快采起措施。日志包含一些信息,例如调用哪些函数,输入的内容,发生的位置和错误等。

记录时咱们必须确保不去记录用户名和密码等敏感信息,例如信用卡号、CVV 号码等财务信息。

做为开发人员,咱们应该经过与产品团队沟通,来准备敏感信息的列表并在记录以前将其屏蔽。

4)正确使用日志级别

若是生产环境下的程序具备至关多的用户事务,那么理想的日志设置可能天天会生成 GB 级别的日志,所以咱们须要将日志分组为多个组。根据受众,咱们能够在运行时切换日志级别,并仅获取适当的日志。

例如,若是产品经理但愿在咱们的日志记录仪表板中查看有多少客户交易成功或失败,则不该向他展现各类功能调用的杂乱信息,这些信息仅供开发人员使用。当生产环境中存在错误时,开发人员应该看到各类函数成功执行和失败的详细日志。这样就能够尽快发现并修复问题。

要实现这种设置,咱们须要更好地了解每一个日志级别。

让咱们讨论最重要的级别及其用法:

  • **INFO:**一些重要的消息,描述一个任务完成时的事件消息。例如:New User created with id xxx

这表示仅记录进度信息

  • **DEBUG:**此级别适用于开发人员,这相似于记录你在使用调试器或断点时看到的信息,例如调用了哪一个函数以及传递了哪些参数等。它应该记录当前状态,这样在调试和查找确切问题时会颇有用。

  • **WARN:**这些日志是警告而且不阻止应用程序继续运行,这些日志会在出现问题并使用变通方法时发出警报。例如错误的用户输入、重试等。管理员未来应该修复这些警告。

  • **ERROR:**发生了错误时,应在优先在这里进行调查。例如数据库与其余微服务的通讯失败,或所须要的输入未定义。

主要受众是系统操做员或监控系统。

理想状况下,生产环境下的程序应该具备接近零的错误日志。

5)不要使用console.log

大多数开发人员使用控制台模块做为获取日志或调试代码的第一个工具,由于它简单容易且全局可用,无需设置。在 Node.Js 中,控制台的实现方式与浏览器不一样,控制台模块在使用 console.log 时会在 stdout 中打印消息,若是使用 console.error 它将打印到 stderr。

console.logconsole.debugconsole.info 都在 stdout 中打印,所以咱们将没法关闭或打开调试和及信息。一样,``console.warnconsole.error` 都在 stderr 中打印。

生产环境程序很难切换各类级别。

咱们还须要不一样类型的配置,如标准格式、把JSON 输出格式发送到 ELK 栈,这些在开箱即用的控制台中不可用。

要克服全部这些问题,可使用 Winston 日志框架,还有其余一些选项,如BunyanPino等。

为何须要像 Winston 这样的日志库?

在上一节中咱们讨论了控制台的一些缺陷,让咱们列出 Winston 提供的一些重要功能:

  • 级别: Winston 提供了几组日志级别,而且还将级别打印为日志的一部分,这可使咱们可以在集中式仪表板中过滤日志。 例如 {message: “something wrong”, level: “error"}

    若是须要,你也能够建立自定义级别。

  • 格式: Winston 有一些高级配置,好比给日志着色,输出 JSON 格式等等。

  • **动态更改日志级别:**咱们将在生产环境程序中启用警告和错误,并能够根据须要将日志级别更改成调试并返回错误,而无需从新启动程序。 Winston 具备这种开箱即用的功能。

// log setup
import winston from 'winston';
const transports = {
  console: new winston.transports.Console({ level: 'warn' }),
};

const logger = winston.createLogger({
  transports: [transports.console, transports.file]
});

logger.info('This will not be logged in console transport because warn is set!');

transports.console.level = 'info'; // changed the level

logger.info('This will be logged in now!');

export default {logger, transport}
复制代码

咱们还能够公开 API 动态更改级别,公开 REST API 并在处理程序中执行第 13 行以更改级别。

  • **传输:**对于生产环境,咱们但愿有一个集中式日志记录系统,全部的微服务都会推送日志,咱们将经过仪表板过滤和搜索日志。这是标准的 ELK 设置或等效设置。
import winston from 'winston';
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    //
    new winston.transports.File({ filename: 'stdout.log' })
  ]
});

export default logger;
复制代码

经过配置 Winston 将咱们的日志写入文件,以便任何日志托运代理均可以将日志推送到集中式系统。可是,这超出了本文的范围,咱们会在另外一篇文章中详细讨论。

6)性能影响

若是程序写日志的频率很高,则可能直接影响程序性能。

DEBUG 和 INFO 级别的日志可占到总体的 95% 以上,这就是为何应该只启用 ERROR 和 WARN 级别,并在想要找出问题时将级别更改成DEBUG,以后再将其切换回 ERROR 。

当应用程序出现问题时,日志就是救星。若是你当前尚未很好的使用日志,请实施日志记录实践并将日志添加到代码审查核对表中。

欢迎关注前端公众号:前端先锋,领取前端工程化实用工具包。

相关文章
相关标签/搜索