You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

在Express中使用Pino Logger为所有日志附加自定义属性的实现问题

解决Pino-HTTP与Express结合时日志冗余及注入基础属性的问题

我完全懂你现在的困扰——非Node.js栈背景,好不容易把Pino和Express搭起来,结果用child logger传进去后日志变得臃肿不堪,全是没用的冗余内容。其实问题核心在于Pino-HTTP的默认行为和logger的正确使用方式上,咱们一步步来解决:

先搞清楚两个关键问题

  1. 为什么日志会冗长?:Pino-HTTP默认会序列化整个reqres对象,包含大量你不需要的细节(比如socket信息、完整headers、请求体结构等),再加上child logger的层级继承,自然就堆出了冗余内容。
  2. 你用错了日志调用方式:代码里直接调用logger.info()是错误的——logger是Pino-HTTP返回的中间件函数,不是直接的logger实例,正确的做法是用req.log,它是Pino-HTTP为每个请求自动挂载的、继承了基础属性的child logger。

方案一:优化Child Logger的使用(保留你原本的思路)

我们可以通过配置序列化器精简输出,同时修正日志调用方式:

const express = require('express');
const pino = require('pino');
const pinoHttp = require('pino-http');

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

// 1. 创建根logger
const rootLog = pino({ level: process.env.LOG_LEVEL || 'info' });

// 2. 创建带基础属性的child logger,同时配置序列化器精简输出
const childLogger = rootLog.child(
  { 'exampleProp': 'example-value' },
  { 
    serializers: {
      // 只保留请求的核心信息:方法、URL
      req: (req) => ({ method: req.method, url: req.url }),
      // 只保留响应的状态码
      res: (res) => ({ statusCode: res.statusCode })
    }
  }
);

// 3. 初始化Pino-HTTP中间件,传入child logger并关闭冗余默认配置
const loggerMiddleware = pinoHttp({ 
  logger: childLogger,
  // 不需要自动生成reqId的话可以关闭
  genReqId: () => undefined,
  // 自定义成功/失败日志的消息,替代默认的冗长格式
  customSuccessMessage: (req, res) => `Request ${req.method} ${req.url} done`,
  customErrorMessage: (req, res) => `Request ${req.method} ${req.url} failed (${res.statusCode})`
});

app.use(loggerMiddleware);

app.get('/', (req, res) => {
  // 4. 用req.log记录日志,它会自动带上基础属性和请求上下文
  req.log.info('Hello World from root route');
  res.send('Hello World!');
});

app.listen(port, () => {
  rootLog.info(`Example app listening on port ${port}`);
});

方案二:直接在Pino-HTTP中注入基础属性(更简洁)

其实不用单独创建child logger,Pino-HTTP本身提供了base选项可以直接添加全局基础属性,避免额外的logger层级:

const express = require('express');
const pinoHttp = require('pino-http');

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

// 直接在Pino-HTTP配置中搞定基础属性+精简输出
const loggerMiddleware = pinoHttp({
  level: process.env.LOG_LEVEL || 'info',
  // 这里直接添加你的全局基础属性
  base: { 'exampleProp': 'example-value' },
  // 自定义序列化器,砍掉冗余内容
  serializers: {
    req: (req) => ({ method: req.method, url: req.url }),
    res: (res) => ({ statusCode: res.statusCode })
  },
  genReqId: () => undefined,
  customSuccessMessage: (req, res) => `Request ${req.method} ${req.url} completed`
});

app.use(loggerMiddleware);

app.get('/', (req, res) => {
  req.log.info('Hello World from root route');
  res.send('Hello World!');
});

app.listen(port, () => {
  // 启动日志可以从中间件的logger属性获取根logger
  loggerMiddleware.logger.info(`Example app listening on port ${port}`);
});

额外技巧:给特定路由添加专属属性

如果需要给某一类路由添加额外的基础属性,可以在路由中间件里创建新的child logger:

// 给/api开头的请求添加routeType属性
app.use('/api', (req, res, next) => {
  req.log = req.log.child({ routeType: 'api' });
  next();
});

app.get('/api/users', (req, res) => {
  // 这条日志会同时包含exampleProp和routeType属性
  req.log.info('Fetching user list');
  res.json({ users: [] });
});

内容的提问来源于stack exchange,提问作者David

火山引擎 最新活动