Node.js 的日志系统指南

3,434 阅读3分钟

本文用到的技术栈有 typescriptexpresswinstonmorgan.

一个好的日志系统是检查应用程序 行为 的最简单的方法之一,也是我们排查 bug 的第一武器。

如果你有一个 ExpressJS 的应用程序,你可能会想,如何创建一个优秀的、组织良好的日志系统?

问题是,很多应用程序没有一个全面的日志系统,甚至更糟,它们到处使用简单的console.log

在本文中,你将了解如何使用 Winston 和 Morgan 配置日志。

TL;DR;

你可以在 GitHub 中查看完整的配置(complete 分支)。

我没有在本文中添加单元测试,但下面的代码已经经过了全面测试。你可以在上面的存储库中找到所有测试。

开始吧

首先,你需要一个 express 的应用程序。你可以下载这个项目:

git clone https://github.com/vassalloandrea/medium-morgan-winston-example.git node-logging

启动服务:

cd node-logging
npm install
npm run dev

安装与配置 Winston

Winston 是一个很棒的库,它可以配置应用的日志,并具有可定制化的特性。

若在没有第三方库的情况下使用 console.log,我们需要编写大量的代码,并且 Winston 这些年来已经涵盖了所有的边缘情况,just use it。

以下是我们在项目中要实现的主要功能:

  • 日志级别:error, warn, info, HTTP, debug
  • 日志级别颜色
  • 依据 ENV 展示或隐藏不同级别日志:例如,在生产环境我们不会展示所有的日志信息。
  • 添加日志时间戳
  • 日志存储到文件

接下来,安装 winston:

npm install winston

在下面的代码中,是一个 logger 的简单配置。复制并粘贴到项目中。你可以使用这个路径 :src/lib/logger.ts

一会儿我会解释代码。

import winston from 'winston'

const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
}

const level = () => {
  const env = process.env.NODE_ENV || 'development'
  const isDevelopment = env === 'development'
  return isDevelopment ? 'debug' : 'warn'
}

const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'white',
}

winston.addColors(colors)

const format = winston.format.combine(
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  winston.format.colorize({ all: true }),
  winston.format.printf(
    (info) => `${info.timestamp} ${info.level}: ${info.message}`,
  ),
)

const transports = [
  new winston.transports.Console(),
  new winston.transports.File({
    filename: 'logs/error.log',
    level: 'error',
  }),
  new winston.transports.File({ filename: 'logs/all.log' }),
]

const Logger = winston.createLogger({
  level: level(),
  levels,
  format,
  transports,
})

export default Logger

现在,你可以在应用程序中使用 Logger 函数。

进入 index.ts 文件,这是 express 服务定义的位置,使用 Logger 函数来替换所有的 console.log

import express from "express";

import Logger from "./lib/logger";

const app = express();
const PORT = 3000;

app.get("/logger", (_, res) => {
  Logger.error("This is an error log");
  Logger.warn("This is a warn log");
  Logger.info("This is a info log");
  Logger.http("This is a http log");
  Logger.debug("This is a debug log");

  res.send("Hello world");
});

app.listen(PORT, () => {
  Logger.debug(`Server is up and running @ http://localhost:${PORT}`);
});

重启服务,通过请求logger 服务查看日志内容:

正如你所看到的,依据日志的严重性打印出不同的颜色,并且另个重点是,这些日志都输出到 logs 文件夹下的 all.logerror.log

接下来,我们来介绍刚刚的 winston 配置,你可以查看其中的注释:

import winston from 'winston'

// Define your severity levels. 
// With them, You can create log files, 
// see or hide levels based on the running ENV.
const levels = {
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  debug: 4,
}

// This method set the current severity based on 
// the current NODE_ENV: show all the log levels 
// if the server was run in development mode; otherwise, 
// if it was run in production, show only warn and error messages.
const level = () => {
  const env = process.env.NODE_ENV || 'development'
  const isDevelopment = env === 'development'
  return isDevelopment ? 'debug' : 'warn'
}

// Define different colors for each level. 
// Colors make the log message more visible,
// adding the ability to focus or ignore messages.
const colors = {
  error: 'red',
  warn: 'yellow',
  info: 'green',
  http: 'magenta',
  debug: 'white',
}

// Tell winston that you want to link the colors 
// defined above to the severity levels.
winston.addColors(colors)

// Chose the aspect of your log customizing the log format.
const format = winston.format.combine(
  // Add the message timestamp with the preferred format
  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
  // Tell Winston that the logs must be colored
  winston.format.colorize({ all: true }),
  // Define the format of the message showing the timestamp, the level and the message
  winston.format.printf(
    (info) => `${info.timestamp} ${info.level}: ${info.message}`,
  ),
)

// Define which transports the logger must use to print out messages. 
// In this example, we are using three different transports 
const transports = [
  // Allow the use the console to print the messages
  new winston.transports.Console(),
  // Allow to print all the error level messages inside the error.log file
  new winston.transports.File({
    filename: 'logs/error.log',
    level: 'error',
  }),
  // Allow to print all the error message inside the all.log file
  // (also the error log that are also printed inside the error.log(
  new winston.transports.File({ filename: 'logs/all.log' }),
]

// Create the logger instance that has to be exported 
// and used to log messages.
const Logger = winston.createLogger({
  level: level(),
  levels,
  format,
  transports,
})

export default Logger

现在,我们能够为应用程序代码添加特性的日志。并且通过 Winston,还可以在运行时使用 ENV 变量更改日志的严重性。

因为 ExpressJS 是用来处理请求的,所以我们应该添加一个请求 logger,它会自动记录每个请求信息。应该使用一个可以轻松地与 Winston 集成的库来实现这个目标。它就是 Morgan!

安装与配置 Morgan

Morgan 是一个 Node.js 的中间件,用于定制请求日志。

与 Winston 集成非常简单。刚刚上文我们还为 Winston 配置 http 级别的日志,其实它只能在 Morgan 中间件中使用。

npm install morgan @types/morgan

下面代码是对 Morgan 的简单配置。复制并粘贴到你的项目中。你可以放在 src/config/morganMiddleware.ts 目录。

配置的相关注释已在其中。

import morgan, { StreamOptions } from "morgan";

import Logger from "../lib/logger";

// Override the stream method by telling
// Morgan to use our custom logger instead of the console.log.
const stream: StreamOptions = {
  // Use the http severity
  write: (message) => Logger.http(message),
};

// Skip all the Morgan http log if the 
// application is not running in development mode.
// This method is not really needed here since 
// we already told to the logger that it should print
// only warning and error messages in production.
const skip = () => {
  const env = process.env.NODE_ENV || "development";
  return env !== "development";
};

// Build the morgan middleware
const morganMiddleware = morgan(
  // Define message format string (this is the default one).
  // The message format is made from tokens, and each token is
  // defined inside the Morgan library.
  // You can create your custom token to show what do you want from a request.
  ":method :url :status :res[content-length] - :response-time ms",
  // Options: in this case, I overwrote the stream and the skip logic.
  // See the methods above.
  { stream, skip }
);

export default morganMiddleware;

然后,在 index.ts 文件中,加入中间件 morganMiddleware:

import morganMiddleware from './config/morganMiddleware'
...
...
const PORT = 3000;
app.use(morganMiddleware)
app.get("/logger", (_, res) => {
...

重启服务,再次请求logger 服务查看日志内容:

GraphQL Morgan 配置

如果你应用的是 GraphQL APIs,请继续阅读。

在默认情况下,GraphQL 只有一个路由,因此我们需要更改 Morgan 配置使其更有意义。

import morgan, { StreamOptions } from "morgan";

import { IncomingMessage } from "http";

import Logger from "../lib/logger";

interface Request extends IncomingMessage {
  body: {
    query: String;
  };
}

const stream: StreamOptions = {
  write: (message) =>
    Logger.http(message.substring(0, message.lastIndexOf("\n"))),
};

const skip = () => {
  const env = process.env.NODE_ENV || "development";
  return env !== "development";
};

const registerGraphQLToken = () => {
  morgan.token("graphql-query", (req: Request) => `GraphQL ${req.body.query}`);
};

registerGraphQLToken();

const morganMiddleware = morgan(
  ":method :url :status :res[content-length] - :response-time ms\n:graphql-query",
  { stream, skip }
);

export default morganMiddleware;

若是你想用一个很棒的 ExpressJS GraphQL APIs 模板,看下这个链接:github.com/vassalloand…

Enjoy it

That's all! 期望这个配置将帮助你 debug 代码,更容易地发现隐藏的错误。🐛

参考