Published on

Express 项目的日志处理 | pino

Authors
  • avatar
    Name
    Shelton Ma
    Twitter

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 专为开发环境设计,支持更易读的日志格式

  1. pino-pretty 专为开发环境设计,支持更易读的日志格式.

    • 美化 JSON 格式日志,输出更直观
    • 支持彩色输出,便于调试
    • 不推荐在生产环境使用 (性能损耗)
  2. 使用

    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 请求日志设计

  1. pino-http (或 pino-server) 是 Pino 专门为 HTTP 服务 (如 Express、Fastify 等) 提供的日志中间件.

    • 自动记录 HTTP 请求日志 (请求方法、URL、响应时间、状态码等)
    • 无需手动编写日志代码
    • 轻量高效,推荐用于生产环境
  2. 使用

    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 — 更灵活的日志处理管道,支持复杂的日志转换、筛选、路由
  1. pino-elasticsearch 是专为 Pino 设计的插件

    1. 优点:

      • 直接将 Pino 日志流式传输至 Elasticsearch
      • 内置批量写入,性能高效
      • 支持 JSON 格式,适合 ELK/Loki 等日志分析平台
    2. 使用

      // 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');
      
  2. 使用 pino + logstash (更灵活), 什么时候使用 logstash?

    • 复杂日志处理,如数据清洗、重命名、脱敏、字段转换等
    • 日志来源多样 (如多服务、多容器、第三方系统)
    • 需要将日志发送到多个目标 (如同时推送到 Elasticsearch、Kafka、S3 等)
    • 需要根据日志内容动态路由 (如按日志级别、来源等分类)
    • 日志格式非 JSON (如传统文本日志、Nginx/Apache 日志)

5. 日志输出最佳实践

  1. 使用 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');
      
  2. 测试环境输出更多信息, 使用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
    
  3. 日志打印

    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'));
    
  4. 结合 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 对比

特点PinoWinstonMorgan
性能✅ 高性能,速度远超 Winston、Morgan❗性能较慢,序列化和格式化较重✅ 轻量级,专为 HTTP 请求日志设计
默认格式✅ JSON,结构化数据❗ 多格式(JSON、文本等)❗ 仅输出 HTTP 请求日志(非结构化)
异步处理✅ 内置异步日志传输❌ 无内置异步机制❌ 无内置异步机制
日志级别✅ 内置丰富的日志级别✅ 内置丰富的日志级别❌ 无日志级别,仅关注 HTTP 请求
生态系统✅ 提供丰富插件和扩展支持✅ 插件丰富,但较重❗ 以中间件形式存在,扩展性一般
易用性✅ 简洁 API,配置灵活❗ 配置复杂,学习曲线较陡✅ 简单易用,专注 HTTP 日志
适用场景✅ 高性能 API 服务、微服务✅ 复杂项目、日志多样化场景✅ 纯粹的 HTTP 请求日志记录

推荐选择

  • 性能优先 ➔ 使用 Pino
  • 复杂日志管理(如多传输目标) ➔ 使用 Winston
  • HTTP 请求日志 ➔ 使用 Morgan