NextJS 12.x如何通过请求头传递correlationId实现Pino日志关联?
Next.js 12.x 结合 Pino 实现带 Correlation ID 的服务端日志
核心思路
靠 Next.js 12 的 middleware.ts 统一处理 Correlation ID,用Cookie把ID传递到服务端数据获取方法(getServerSideProps/getStaticProps),再封装 Pino 实例,让它自动把这个ID注入到每条日志里,不用每次手动加。
1. 完善 Middleware 的 Correlation ID 处理
在 middleware.ts 里,先检查请求头的correlation-id,没有的话生成一个UUID,然后把ID写入Cookie,同时回写到响应头方便客户端后续请求携带:
import { NextRequest, NextResponse } from 'next/server'; import { v4 as uuidv4 } from 'uuid'; export function middleware(req: NextRequest) { // 从请求头取ID,没有就生成新的 const correlationId = req.headers.get('correlation-id') || uuidv4(); const res = NextResponse.next(); // 写入Cookie,服务端方法能通过req.cookies读取 res.cookies.set('correlation-id', correlationId, { httpOnly: true, sameSite: 'strict', path: '/', }); // 回写响应头,方便客户端后续请求带上这个ID res.headers.set('correlation-id', correlationId); return res; } // 匹配所有路由,确保每个请求都经过处理 export const config = { matcher: '/:path*', };
2. 封装 Pino 日志实例,自动带 Correlation ID
新建一个logger.ts文件,封装Pino,让它能从Cookie或传入的ID自动生成带关联ID的日志实例:
import pino from 'pino'; // 基础Pino配置,适配Kibana的JSON格式 const baseLogger = pino({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', formatters: { level: (label) => ({ level: label.toUpperCase() }), }, base: { service: '你的Next.js服务名', }, }); // 创建带Correlation ID的日志实例 export const createLogger = (correlationId?: string) => { return baseLogger.child({ correlationId: correlationId || 'unknown', }); }; // 从请求的Cookie里提取Correlation ID的工具函数 export const getCorrelationIdFromReq = (req: { cookies: Record<string, string> }) => { return req.cookies['correlation-id']; };
3. 在 getServerSideProps/getStaticProps 里用日志
在页面的服务端数据方法中,从context.req.cookies里取出ID,创建带ID的日志实例,之后打日志就会自动带上这个ID:
// pages/example.tsx import { createLogger, getCorrelationIdFromReq } from '../utils/logger'; export async function getServerSideProps(context) { const correlationId = getCorrelationIdFromReq(context.req); const logger = createLogger(correlationId); logger.info('开始获取示例页面数据'); // 业务逻辑... try { const data = await fetchSomeData(); logger.debug('数据获取成功', { dataLength: data.length }); return { props: { data } }; } catch (err) { logger.error('数据获取失败', { error: err.message }); return { props: { data: [] } }; } } // getStaticProps 用法类似(注意:只有开启revalidate时,getStaticProps才会在服务端运行,此时能拿到请求Cookie;纯静态构建时没有请求上下文,ID会是'unknown') export async function getStaticProps(context) { const correlationId = getCorrelationIdFromReq(context.req); const logger = createLogger(correlationId); logger.info('生成带revalidate的静态页面'); // 业务逻辑... return { props: {}, revalidate: 60 }; }
4. API路由的日志增强(可选)
如果想让API路由也自动带上Correlation ID,可以写一个简单的wrapper:
// utils/apiWrapper.ts import { NextApiRequest, NextApiResponse } from 'next'; import { createLogger, getCorrelationIdFromReq } from './logger'; export const withLogger = (handler: (req: NextApiRequest, res: NextApiResponse, logger: pino.Logger) => Promise<void>) => { return async (req: NextApiRequest, res: NextApiResponse) => { const correlationId = getCorrelationIdFromReq(req); const logger = createLogger(correlationId); try { await handler(req, res, logger); } catch (err) { logger.error('API处理失败', { error: err.message }); res.status(500).json({ error: '服务器内部错误' }); } }; };
用的时候直接套在API路由外面:
// pages/api/data.ts import { withLogger } from '../../utils/apiWrapper'; export default withLogger(async (req, res, logger) => { logger.info('开始处理数据API请求'); // 业务逻辑... res.status(200).json({ data: '示例数据' }); });
几个关键注意点
- getStaticProps的特殊性:纯静态页面(无revalidate)的getStaticProps是在构建时执行的,此时没有请求上下文,Correlation ID会显示为
unknown。如果需要构建时也有ID,可以在构建脚本里生成一个临时ID,或者忽略这类日志的ID。 - Cookie安全性:设置
httpOnly: true防止前端篡改,sameSite: 'strict'降低CSRF风险。 - Kibana适配:确保Pino输出的是JSON格式日志,Kibana能直接解析
correlationId字段,方便后续做链路追踪。
内容的提问来源于stack exchange,提问作者Matthew Trow
相关产品推荐
相关产品推荐

