- Published on
Express 项目的日志处理 | pino
- Authors
- Name
- Shelton Ma
1. Pino
简介
Pino 是一个快速、低开销的 Node.js 日志记录库,专为性能而设计,特别适合高并发和大流量场景.它的特点包括:
- 性能卓越:Pino 使用异步 I/O 且默认输出 JSON 格式,减少字符串拼接和额外的序列化成本,性能优于 Winston、- Morgan 等传统日志工具.
- 高效的 JSON 格式:默认输出 JSON 日志,便于结构化数据分析,适合 ELK (Elasticsearch + Logstash + - Kibana) 等日志平台.
- 异步日志处理:Pino 使用子进程 (pino.transport) 将日志处理移出主线程,进一步提升性能.
- 内置日志级别:trace、debug、info、warn、error、fatal.
- 强大的扩展性:Pino 提供丰富的插件,支持日志轮转、传输到远程服务、格式转换等.
1. 使用
import pino from 'pino';
const logger = pino({ level: 'info' });
logger.info('Server started'); // {"level":30,"time":1710000000000,"pid":12345,"hostname":"localhost","msg":"Server started"}
logger.error({ err: new Error('Something failed') }, 'Unexpected error');
2. pino-pretty 专为开发环境设计,支持更易读的日志格式
pino-pretty 专为开发环境设计,支持更易读的日志格式.
- 美化 JSON 格式日志,输出更直观
- 支持彩色输出,便于调试
- 不推荐在生产环境使用 (性能损耗)
使用
import pino from 'pino'; const logger = pino({ level: 'info', transport: process.env.NODE_ENV === 'development' ? { target: 'pino-pretty', options: { colorize: true } } : undefined // 生产环境避免使用 pino-pretty }); logger.info('Server started'); logger.error({ err: new Error('Something failed') }, 'Unexpected error');
3. 日志输出携带call-site
在 Pino 日志里,lineno(行号)和 filename 默认是不会自动输出的,因为 Pino 是一个高性能 JSON logger,它不自带 call-site 跟踪(这样会严重影响性能).使用 pino-caller 实现, 但仅限测试环境, 生产环境可以关闭
// pnpm add pino-caller
import pino from 'pino';
import pinoCaller from 'pino-caller';
const logger = pino();
const log = pinoCaller(logger);
log.info('Hello'); // 会自动带上文件名和行号
4. pino-http (或 pino-server) — 专为 HTTP 请求日志设计
pino-http (或 pino-server) 是 Pino 专门为 HTTP 服务 (如 Express、Fastify 等) 提供的日志中间件.
- 自动记录 HTTP 请求日志 (请求方法、URL、响应时间、状态码等)
- 无需手动编写日志代码
- 轻量高效,推荐用于生产环境
使用
import express from 'express'; import pinoHttp from 'pino-http'; const app = express(); // 使用 pino-http 作为中间件,自动记录请求日志 app.use(pinoHttp()); app.get('/', (req, res) => { req.log.info('Home page visited'); // req.log 继承自 pino 实例 res.send('Hello World'); }); app.listen(3000, () => console.log('Server running on port 3000'));
4. Pino 接入 Elasticsearch (ES)
pino-elasticsearch
— 官方推荐,轻量、专为 Pino 设计logstash
— 更灵活的日志处理管道,支持复杂的日志转换、筛选、路由
pino-elasticsearch
是专为 Pino 设计的插件优点:
- 直接将 Pino 日志流式传输至 Elasticsearch
- 内置批量写入,性能高效
- 支持 JSON 格式,适合 ELK/Loki 等日志分析平台
使用
// npm install pino pino-elasticsearch import pino from 'pino'; import { createWriteStream } from 'pino-elasticsearch'; // 创建 Elasticsearch 输出流 const stream = createWriteStream({ index: 'app-logs', // 日志索引 consistency: 'one', // 数据一致性 node: 'http://localhost:9200', // ES 地址 'es-version': 8, // ES 版本 (确保与 ES 版本匹配) 'flush-bytes': 1000 // 批量发送大小 (优化性能) }); // 创建 Pino 实例 const logger = pino({ level: 'info' }, stream); logger.info('Server started successfully'); logger.error({ error: new Error('Something went wrong') }, 'Critical error');
使用 pino + logstash (更灵活), 什么时候使用 logstash?
- 复杂日志处理,如数据清洗、重命名、脱敏、字段转换等
- 日志来源多样 (如多服务、多容器、第三方系统)
- 需要将日志发送到多个目标 (如同时推送到 Elasticsearch、Kafka、S3 等)
- 需要根据日志内容动态路由 (如按日志级别、来源等分类)
- 日志格式非 JSON (如传统文本日志、Nginx/Apache 日志)
5. 日志输出最佳实践
使用 pino 和 pino-http 提供了两类日志记录方式:
使用 logger 记录与业务逻辑相关的信息(如任务状态、定时任务、外部服务调用等)
使用 req.log 记录与 HTTP 请求相关的信息(如参数、请求状态)
// req.log 输出自定义字段 app.use(pinoHttp({ customProps: (req, res) => ({ userId: req.headers['x-user-id'] || 'unknown', // 自定义字段 traceId: req.headers['x-trace-id'] || 'N/A' // 追踪 ID (推荐) }) })); // {"level":30,"time":1710500000000,"pid":12345,"hostname":"localhost","req":{"method":"GET","url":"/","headers":{...}},"res":{"statusCode":200},"responseTime":15,"userId":"1234","traceId":"abc-xyz","msg":"User accessed home page"} // {"level":30,"time":1710500001000,"pid":12345,"hostname":"localhost","req":{"method":"GET","url":"/order/42","headers":{...}},"res":{"statusCode":200},"responseTime":18,"orderId":"42","msg":"Fetching order details"} req.log.info({ orderId: req.params.id }, 'Fetching order details');
测试环境输出更多信息, 使用
pino-pretty/pino-caller
, 生产环境关闭 caller 信息,只输出 JSON,避免影响性能const baseLogger = pino({ level: 'info', transport: process.env.NODE_ENV === 'production' ? undefined // 生产环境避免使用 pino-pretty : { target: 'pino-pretty', options: { colorize: true } } }); export const logger = process.env.NODE_ENV === 'production' ? baseLogger : pinoCaller(baseLogger); // 生产环境避免使用 pinoCaller
日志打印
import express from 'express'; import pino from 'pino'; import pinoHttp from 'pino-http'; const logger = pino({ level: 'info', transport: { target: 'pino-pretty', options: { colorize: true } } }); const app = express(); app.use(pinoHttp({ logger })); app.get('/order/:id', (req, res) => { const orderId = req.params.id; req.log.info(`Fetching order details for ID: ${orderId}`); // 模拟业务逻辑 if (orderId === '0') { req.log.warn('Order ID is zero, possible data error'); return res.status(400).json({ error: 'Invalid order ID' }); } logger.info(`Order ${orderId} processed successfully`); // 业务层日志 res.json({ orderId, status: 'Processed' }); }); app.listen(3000, () => logger.info('Server running on port 3000'));
结合 Express 错误处理中间件
app.use((err, req, res, next) => { req.log.error({ err }, 'Unhandled error'); res.status(500).json({ error: 'Internal Server Error' }); });
2. Winston
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' })
]
});
logger.info('Server started on port 3000');
logger.error('Unexpected error occurred');
3. Morgan
morgan 是一个常用的 HTTP 请求日志中间件,默认情况下,它会将日志输出到控制台.为了写入到外部文件,我们可以结合 fs.createWriteStream 来存储日志到文件中,或者将其集成到更强大的日志管理工具(如 Winston 或 ELK).
0. 安装
npm install morgan
1. 基本写入日志到文件
// npm install morgan
const express = require("express");
const fs = require("fs");
const path = require("path");
const morgan = require("morgan");
const app = express();
// 创建一个写入流(append 模式)
const logStream = fs.createWriteStream(path.join(__dirname, "access.log"), {
flags: "a", // 'a' 追加模式,不会覆盖已有日志
});
// 使用 morgan 记录日志到文件
app.use(morgan("combined", { stream: logStream }));
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
2. 自定义日志格式
app.use(
morgan(":method :url :status :response-time ms - :res[content-length]", {
stream: logStream,
})
);
3. 按日期自动生成日志文件
// npm install rotating-file-stream
const rfs = require("rotating-file-stream");
const accessLogStream = rfs.createStream("access.log", {
interval: "1d", // 每天生成一个新日志
path: path.join(__dirname, "logs"),
});
app.use(morgan("combined", { stream: accessLogStream }));
4. 结合 Winston 进行日志管理
如果想要更高级的日志管理(比如存储到数据库或 JSON 格式存储),可以结合 winston.
// npm install winston
const winston = require("winston");
const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
new winston.transports.File({ filename: "logs/combined.log" }),
],
});
app.use(
morgan("combined", {
stream: { write: (message) => logger.info(message.trim()) },
})
);
5. 发送日志到远程存储
app.use(
morgan("combined", {
stream: {
write: (message) => {
// 发送日志到远程服务(假设有日志收集 API)
fetch("http://log-server.example.com/api/logs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ log: message.trim() }),
});
},
},
})
);
Pino vs Winston vs Morgan 对比
特点 | Pino | Winston | Morgan |
---|---|---|---|
性能 | ✅ 高性能,速度远超 Winston、Morgan | ❗性能较慢,序列化和格式化较重 | ✅ 轻量级,专为 HTTP 请求日志设计 |
默认格式 | ✅ JSON,结构化数据 | ❗ 多格式(JSON、文本等) | ❗ 仅输出 HTTP 请求日志(非结构化) |
异步处理 | ✅ 内置异步日志传输 | ❌ 无内置异步机制 | ❌ 无内置异步机制 |
日志级别 | ✅ 内置丰富的日志级别 | ✅ 内置丰富的日志级别 | ❌ 无日志级别,仅关注 HTTP 请求 |
生态系统 | ✅ 提供丰富插件和扩展支持 | ✅ 插件丰富,但较重 | ❗ 以中间件形式存在,扩展性一般 |
易用性 | ✅ 简洁 API,配置灵活 | ❗ 配置复杂,学习曲线较陡 | ✅ 简单易用,专注 HTTP 日志 |
适用场景 | ✅ 高性能 API 服务、微服务 | ✅ 复杂项目、日志多样化场景 | ✅ 纯粹的 HTTP 请求日志记录 |
推荐选择
- 性能优先 ➔ 使用 Pino
- 复杂日志管理(如多传输目标) ➔ 使用 Winston
- HTTP 请求日志 ➔ 使用 Morgan