morgan、morgan-body 和 winston 都是 Node.js 生态中用于记录应用行为的工具,但定位和能力有显著差异。morgan 是 Express 专用的 HTTP 请求日志中间件,专注于记录传入请求的基本信息。morgan-body 是 morgan 的增强版,自动解析并记录请求和响应的完整 body 内容,适合调试 API 交互。winston 则是一个通用、可扩展的日志库,支持多传输目标(如文件、控制台、远程服务)、日志级别分类和结构化输出,适用于全应用范围的日志管理,不仅限于 HTTP 请求。
在构建 Node.js 应用时,日志记录是可观测性的基石。morgan、morgan-body 和 winston 虽然都涉及“日志”,但解决的问题域、适用场景和架构设计截然不同。本文从实战角度剖析三者的本质差异,帮助你做出精准的技术选型。
morgan 是专为 Express 设计的 HTTP 请求日志中间件。它只做一件事:在每次请求结束时,按指定格式输出一行日志,包含方法、路径、状态码、响应时间等元数据。它不处理应用内部的业务日志。
// morgan: 基础 Express 集成
const express = require('express');
const morgan = require('morgan');
const app = express();
app.use(morgan('combined')); // 使用 Apache combined 格式
app.get('/', (req, res) => {
res.send('Hello');
});
morgan-body 本质上是 morgan 的 body 增强插件。它复用 morgan 的中间件机制,但通过自动挂载 body-parser 来捕获并打印请求/响应的完整 body 内容,极大方便了 API 调试。
// morgan-body: 自动记录 body
const express = require('express');
const morganBody = require('morgan-body');
const app = express();
morganBody(app); // 自动注册中间件并解析 body
app.post('/api/data', (req, res) => {
res.json({ received: req.body });
});
winston 是一个 通用日志库,与 Web 框架无关。它提供日志级别(info、warn、error 等)、传输器(Transports)、格式化器(Formatters)等抽象,可将日志路由到文件、控制台、数据库或第三方服务。要记录 HTTP 请求,需手动集成。
// winston: 基础日志记录
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'app.log' })
]
});
logger.info('Application started');
logger.error('Database connection failed');
morgan 仅记录请求元数据。例如 combined 格式输出:
:remote-addr - - [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"
实际日志:
::1 - - [10/Oct/2023:12:00:00 +0000] "GET /api/users HTTP/1.1" 200 1234 "-" "curl/7.64.1"
morgan-body 在 morgan 基础上追加请求和响应的完整 body。例如发送 {"name":"Alice"} 到 /api/user,日志会包含:
BODY REQUESTED:
{"name":"Alice"}
BODY RESPONSE:
{"id":123,"name":"Alice"}
winston 需要开发者手动创建中间件来记录请求。你可以选择记录元数据或 body,完全可控:
// winston: 自定义请求日志中间件
const express = require('express');
const winston = require('winston');
const app = express();
const logger = winston.createLogger({ /* ... */ });
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration: Date.now() - start,
// 可选:记录 body(注意敏感数据!)
// requestBody: req.body,
// responseBody: res.locals.responseData
});
});
next();
});
morgan 和 morgan-body 无法记录非请求相关的日志(如定时任务、数据库操作)。它们的作用域严格限定在 Express 中间件链。winston 天然支持全应用日志。例如:// 记录数据库操作
logger.info('User created', { userId: 123, email: 'alice@example.com' });
// 记录错误
logger.error('Payment failed', { error: err.message, orderId: 'ORD-456' });
morgan 支持预定义格式(tiny, dev, combined)或自定义 token:// 自定义 morgan 格式
morgan.token('custom', (req, res) => `${req.method}:${req.url}`);
app.use(morgan(':custom :status :response-time ms'));
morgan-body 提供有限选项(如 logReqDateTime, maxBodyLength),但核心行为(记录完整 body)不可关闭。
winston 通过组合 format 实现高度定制:
const { combine, timestamp, printf } = winston.format;
const customFormat = printf(({ level, message, timestamp, ...meta }) => {
return `${timestamp} [${level}]: ${message} ${JSON.stringify(meta)}`;
});
const logger = winston.createLogger({
format: combine(timestamp(), customFormat)
});
morgan 和 morgan-body 默认输出到 process.stdout,可通过 stream 选项重定向:const fs = require('fs');
const accessLogStream = fs.createWriteStream('access.log', { flags: 'a' });
app.use(morgan('combined', { stream: accessLogStream }));
winston 原生支持多传输器(Transports),可同时写入多个目标:const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.Http({ host: 'logs.example.com' }) // 发送到远程服务
]
});
morgan 因只记录元数据,性能开销极低,适合高流量生产环境。morgan-body 记录完整 body 会带来显著开销(尤其大文件上传),且可能泄露敏感数据(密码、令牌)。强烈不建议在生产环境直接使用,除非配合严格的过滤逻辑。winston 性能取决于传输器配置。例如写入文件比控制台慢,但可通过异步传输器优化。结构化日志便于后续分析,但需注意避免记录敏感字段。morgan 和 morgan-body 在日志写入失败时(如磁盘满),默认会抛出错误导致进程崩溃。需通过 skip 或自定义 stream 处理异常。winston 的传输器可配置 handleExceptions 和 handleRejections,提供更健壮的错误隔离:const logger = winston.createLogger({
transports: [new winston.transports.Console()],
exceptionHandlers: [new winston.transports.File({ filename: 'exceptions.log' })]
});
morgan + 文件流// 生产环境 morgan 配置
const fs = require('fs');
const morgan = require('morgan');
const accessLogStream = fs.createWriteStream(
process.env.ACCESS_LOG_FILE || '/var/log/app/access.log',
{ flags: 'a' }
);
app.use(morgan('combined', { stream: accessLogStream }));
morgan-body// 开发环境 morgan-body
if (process.env.NODE_ENV === 'development') {
morganBody(app, { noColors: false });
}
winston + 自定义中间件// 统一日志方案
const express = require('express');
const winston = require('winston');
const app = express();
const logger = createUnifiedLogger(); // 封装 winston 配置
// 全局错误处理
app.use((err, req, res, next) => {
logger.error('Unhandled error', { error: err, url: req.url });
res.status(500).send('Internal Server Error');
});
// 请求日志中间件
app.use(requestLogger(logger));
| 特性 | morgan | morgan-body | winston |
|---|---|---|---|
| 核心用途 | Express 请求元数据日志 | Express 请求/响应 Body 日志 | 通用结构化日志库 |
| 框架依赖 | 仅 Express | 仅 Express | 无(任何 Node.js 应用) |
| 记录 Body | ❌ | ✅(自动) | ✅(需手动实现) |
| 日志级别 | ❌(仅请求日志) | ❌(仅请求日志) | ✅(info, warn, error...) |
| 多输出目标 | ⚠️(需自定义 stream) | ⚠️(需自定义 stream) | ✅(原生支持) |
| 生产环境适用性 | ✅(推荐) | ❌(谨慎使用) | ✅(推荐) |
| 非 HTTP 日志 | ❌ | ❌ | ✅ |
morgan/morgan-body 和 winston 做同一件事。如果已用 winston,就通过中间件统一记录请求日志,避免日志分散。morgan-body 是开发利器,但生产环境请用 morgan + 敏感字段过滤。若需记录 body,务必在 winston 中实现白名单机制。winston 的学习曲线稍陡,但长期收益巨大。对于中大型应用,统一的日志策略能显著降低运维成本。记住:日志不是越多越好,而是要有目的、可查询、无敏感信息。根据你的场景选择最匹配的工具,而非功能最多的工具。
选择 morgan 如果你只需要在 Express 应用中记录标准的 HTTP 请求日志(如方法、URL、状态码、耗时),且不需要查看请求或响应体内容。它轻量、稳定,是生产环境中记录访问日志的可靠选择,尤其适合对性能敏感或只需基础审计日志的场景。
选择 morgan-body 如果你在开发或测试阶段需要详细查看每个 HTTP 请求和响应的完整 body(包括 JSON、表单等),用于快速调试 API 接口。它基于 morgan 构建,自动处理 body 解析,但因记录完整 payload 可能带来性能开销和敏感数据泄露风险,不建议在生产环境直接使用。
选择 winston 如果你需要一个功能全面、可配置的日志系统,支持多种日志级别(info、error 等)、结构化输出、多目标写入(如同时写入文件和 Sentry),并希望将 HTTP 请求日志与其他应用日志(如数据库操作、业务逻辑)统一管理。它不局限于 Express,适用于任何 Node.js 应用,但需额外配置才能实现类似 morgan 的请求日志功能。
HTTP request logger middleware for node.js
Named after Dexter, a show you should not watch until completion.
This is a Node.js module available through the
npm registry. Installation is done using the
npm install command:
$ npm install morgan
For ES Modules:
import morgan from 'morgan'
For CommonJS:
var morgan = require('morgan')
Create a new morgan logger middleware function using the given format and options.
The format argument may be a string of a predefined name (see below for the names),
a string of a format string, or a function that will produce a log entry.
The format function will be called with three arguments tokens, req, and res,
where tokens is an object with all defined tokens, req is the HTTP request and res
is the HTTP response. The function is expected to return a string that will be the log
line, or undefined / null to skip logging.
morgan('tiny')
morgan(':method :url :status :res[content-length] - :response-time ms')
morgan(function (tokens, req, res) {
return [
tokens.method(req, res),
tokens.url(req, res),
tokens.status(req, res),
tokens.res(req, res, 'content-length'), '-',
tokens['response-time'](https://github.com/expressjs/morgan/blob/HEAD/req, res), 'ms'
].join(' ')
})
Morgan accepts these properties in the options object.
Write log line on request instead of response. This means that requests will be logged even if the server crashes, but data from the response (like the response code, content length, etc.) cannot be logged.
Function to determine if logging is skipped, defaults to false. This function
will be called as skip(req, res).
// EXAMPLE: only log error responses
morgan('combined', {
skip: function (req, res) { return res.statusCode < 400 }
})
Output stream for writing log lines, defaults to process.stdout.
If the stream is in object mode (stream.writableObjectMode is true) and the
format function returns an object, the object is written to the stream as-is,
without a trailing newline. This allows passing structured log entries to
loggers that accept objects:
morgan(function (tokens, req, res) {
return {
method: tokens.method(req, res),
url: tokens.url(req, res),
status: Number(tokens.status(req, res))
}
}, {
stream: {
writableObjectMode: true,
write: function (entry) {
// entry is the object returned by the format function
}
}
})
There are various pre-defined formats provided:
Standard Apache combined log output.
:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"
# will output
::1 - - [27/Nov/2024:06:21:42 +0000] "GET /combined HTTP/1.1" 200 2 "-" "curl/8.7.1"
Standard Apache common log output.
:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]
# will output
::1 - - [27/Nov/2024:06:21:46 +0000] "GET /common HTTP/1.1" 200 2
Concise output colored by response status for development use. The :status
token will be colored green for success codes, red for server error codes,
yellow for client error codes, cyan for redirection codes, and uncolored
for information codes.
:method :url :status :response-time ms - :res[content-length]
# will output
GET /dev 200 0.224 ms - 2
Coloring is disabled when the NO_COLOR environment
variable is set to any non-empty value, in which case the output above is
written without any escape sequences. The variable is read once, when morgan
is first required.
Shorter than default, also including response time.
:remote-addr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms
# will output
::1 - GET /short HTTP/1.1 200 2 - 0.283 ms
The minimal output.
:method :url :status :res[content-length] - :response-time ms
# will output
GET /tiny 200 2 - 0.188 ms
To define a token, simply invoke morgan.token() with the name and a callback function.
This callback function is expected to return a string value. The value returned is then
available as ":type" in this case:
morgan.token('type', function (req, res) { return req.headers['content-type'] })
Calling morgan.token() using the same name as an existing token will overwrite that
token definition.
The token function is expected to be called with the arguments req and res, representing
the HTTP request and HTTP response. Additionally, the token can accept further arguments of
its choosing to customize behavior.
The current date and time in UTC. The available formats are:
clf for the common log format ("10/Oct/2000:13:55:36 +0000")iso for the common ISO 8601 date time format (2000-10-10T13:55:36.000Z)web for the common RFC 1123 date time format (Tue, 10 Oct 2000 13:55:36 GMT)If no format is given, then the default is web.
The HTTP version of the request.
The HTTP method of the request.
The process ID of the Node.js process handling the request.
The Referrer header of the request. This will use the standard mis-spelled Referer header if it exists, otherwise Referrer.
The remote address of the request. This will use req.ip, otherwise the standard req.connection.remoteAddress value (socket address).
The user authenticated as part of Basic auth for the request.
The given header of the request. If the header is not present, the
value will be displayed as "-" in the log.
The given header of the response. If the header is not present, the
value will be displayed as "-" in the log.
The time between the request coming into morgan and when the response
headers are written, in milliseconds.
The digits argument is a number that specifies the number of digits to
include on the number, defaulting to 3, which provides microsecond precision.
The status code of the response.
If the request/response cycle completes before a response was sent to the
client (for example, the TCP socket closed prematurely by a client aborting
the request), then the status will be empty (displayed as "-" in the log).
The time between the request coming into morgan and when the response
has finished being written out to the connection, in milliseconds.
The digits argument is a number that specifies the number of digits to
include on the number, defaulting to 3, which provides microsecond precision.
The URL of the request. This will use req.originalUrl if it exists, otherwise req.url.
The contents of the User-Agent header of the request.
Compile a format string into a format function for use by morgan. A format string
is a string that represents a single log line and can utilize token syntax.
Tokens are referenced by :token-name. If tokens accept arguments, they can
be passed using [], for example: :token-name[pretty] would pass the string
'pretty' as an argument to the token token-name.
The function returned from morgan.compile takes three arguments tokens, req, and
res, where tokens is an object with all defined tokens, req is the HTTP request and
res is the HTTP response. The function will return a string that will be the log line,
or undefined / null to skip logging.
Normally formats are defined using morgan.format(name, format), but for certain
advanced uses, this compile function is directly available.
Sample app that will log all requests in the Apache combined format to STDOUT
var express = require('express')
var morgan = require('morgan')
var app = express()
app.use(morgan('combined'))
app.get('/', function (req, res) {
res.send('hello, world!')
})
Sample app that will log all requests in the Apache combined format to STDOUT
var finalhandler = require('finalhandler')
var http = require('http')
var morgan = require('morgan')
// create "middleware"
var logger = morgan('combined')
http.createServer(function (req, res) {
var done = finalhandler(req, res)
logger(req, res, function (err) {
if (err) return done(err)
// respond to request
res.setHeader('content-type', 'text/plain')
res.end('hello, world!')
})
})
Sample app that will log all requests in the Apache combined format to the file
access.log.
var express = require('express')
var fs = require('fs')
var morgan = require('morgan')
var path = require('path')
var app = express()
// create a write stream (in append mode)
var accessLogStream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' })
// setup the logger
app.use(morgan('combined', { stream: accessLogStream }))
app.get('/', function (req, res) {
res.send('hello, world!')
})
Sample app that will log all requests in the Apache combined format to one log
file per day in the log/ directory using the
rotating-file-stream module.
var express = require('express')
var morgan = require('morgan')
var path = require('path')
var rfs = require('rotating-file-stream') // version 2.x
var app = express()
// create a rotating write stream
var accessLogStream = rfs.createStream('access.log', {
interval: '1d', // rotate daily
path: path.join(__dirname, 'log')
})
// setup the logger
app.use(morgan('combined', { stream: accessLogStream }))
app.get('/', function (req, res) {
res.send('hello, world!')
})
The morgan middleware can be used as many times as needed, enabling
combinations like:
Sample app that will log all requests to a file using Apache format, but error responses are logged to the console:
var express = require('express')
var fs = require('fs')
var morgan = require('morgan')
var path = require('path')
var app = express()
// log only 4xx and 5xx responses to console
app.use(morgan('dev', {
skip: function (req, res) { return res.statusCode < 400 }
}))
// log all requests to access.log
app.use(morgan('common', {
stream: fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' })
}))
app.get('/', function (req, res) {
res.send('hello, world!')
})
Sample app that will use custom token formats. This adds an ID to all requests and displays it using the :id token.
var express = require('express')
var morgan = require('morgan')
var uuid = require('uuid')
morgan.token('id', function getId (req) {
return req.id
})
var app = express()
app.use(assignId)
app.use(morgan(':id :method :url :response-time'))
app.get('/', function (req, res) {
res.send('hello, world!')
})
function assignId (req, res, next) {
req.id = uuid.v4()
next()
}