本文用到的技术栈有 typescript
、express
、winston
和 morgan
.
一个好的日志系统是检查应用程序
行为
的最简单的方法之一,也是我们排查 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.log
和 error.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 代码,更容易地发现隐藏的错误。🐛