如何在Next.js(TypeScript) Monorepo中实现Pino客户端与服务端日志?
Next.js (TypeScript) Monorepo 中配置 Pino 实现全端日志记录
核心方案概述
在Monorepo架构下,我们可以将日志配置抽为共享包,分别实现服务端直接输出到Graylog和客户端日志通过API路由转发到Graylog的逻辑,同时保证TypeScript类型一致性。
1. Monorepo 目录结构规划
首先将日志模块独立为共享包,方便主应用及其他子包复用:
your-monorepo/ ├── apps/ │ └── next-app/ # Next.js 主应用 │ ├── app/ │ │ ├── api/ │ │ │ └── log/route.ts # 客户端日志转发API │ │ └── ... │ └── package.json ├── packages/ │ └── logging/ # 共享日志包 │ ├── src/ │ │ ├── server.ts # 服务端日志配置 │ │ ├── client.ts # 客户端日志配置 │ │ ├── graylogStream.ts # 你的Graylog流实现 │ │ └── index.ts # 统一导出入口 │ ├── package.json │ └── tsconfig.json └── package.json
2. 服务端 Pino 配置优化(共享包)
基于你提供的代码,补充TypeScript类型支持及Next.js服务端场景适配:
packages/logging/src/graylogStream.ts
保留你的现有实现,确保它符合Pino的Stream接口:
import type { Stream } from 'pino'; class GraylogStream implements Stream { private host: string; private port: number; constructor(host: string, port: number) { this.host = host; this.port = port; // 初始化Graylog连接逻辑 } write(chunk: string | Buffer): boolean { // 实现将日志发送到Graylog的逻辑 console.log('Sending to Graylog:', chunk.toString()); return true; } } export default GraylogStream;
packages/logging/src/server.ts
import pino from 'pino'; import GraylogStream from './graylogStream'; // 从环境变量读取配置,TS类型断言 const graylogHost = process.env.GRAYLOG_HOST as string; const graylogPort = parseInt(process.env.GRAYLOG_PORT || '12201', 10); const logLevel = process.env.LOG_LEVEL || 'info'; export const serverLogger = pino( { level: logLevel, serializers: { err: pino.stdSerializers.err, req: pino.stdSerializers.req, res: pino.stdSerializers.res, // 自定义序列化Next.js的Request对象(Server Components/Middleware) nextReq: (req: Request) => ({ method: req.method, url: req.url, headers: Object.fromEntries(req.headers.entries()), }), }, // 服务端日志添加环境标识 base: { env: process.env.NODE_ENV, service: 'next-app' }, }, new GraylogStream(graylogHost, graylogPort) );
3. 客户端 Pino 配置(共享包)
客户端无法直接连接Graylog(跨域限制),因此通过Next.js API路由转发日志:
packages/logging/src/client.ts
import pino from 'pino/browser'; // 客户端日志发送到API路由 const sendLogToServer = async (logEntry: string) => { try { await fetch('/api/log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: logEntry, keepalive: true, // 页面卸载时仍能发送日志 }); } catch (err) { // 降级处理:控制台输出失败的日志 console.error('Failed to send client log:', err); } }; // 创建客户端Pino实例 export const clientLogger = pino({ level: process.env.NEXT_PUBLIC_LOG_LEVEL || 'info', // 客户端日志添加标识 base: { env: process.env.NODE_ENV, service: 'next-app-client' }, // 自定义传输:发送到API路由 transport: { target: 'pino-browser', options: { write: sendLogToServer, }, }, // 禁用客户端默认的控制台输出(可选,根据需求调整) browser: { write: () => {}, }, });
4. 共享包导出入口
packages/logging/src/index.ts
export { serverLogger } from './server'; export { clientLogger } from './client';
5. Next.js 主应用配置与使用
5.1 客户端日志转发API路由
创建apps/next-app/app/api/log/route.ts:
import { serverLogger } from '@your-monorepo/logging'; import { NextRequest } from 'next/server'; export async function POST(req: NextRequest) { try { const logEntry = await req.json(); // 将客户端日志转发到Graylog serverLogger.write(logEntry); return new Response(null, { status: 204 }); } catch (err) { serverLogger.error(err, 'Failed to process client log'); return new Response('Error', { status: 500 }); } }
5.2 服务端场景使用示例
- API路由:
import { serverLogger } from '@your-monorepo/logging'; import { NextRequest, NextResponse } from 'next/server'; export async function GET(req: NextRequest) { serverLogger.info({ req: req }, 'API route called'); try { // 业务逻辑 return NextResponse.json({ data: 'success' }); } catch (err) { serverLogger.error({ err, req }, 'API route failed'); return NextResponse.json({ error: 'failed' }, { status: 500 }); } }
- Server Components:
import { serverLogger } from '@your-monorepo/logging'; export default async function ServerComponent() { serverLogger.info('Server Component rendered'); // 业务逻辑 return <div>Server Component</div>; }
- Middleware:
import { serverLogger } from '@your-monorepo/logging'; import { NextRequest, NextResponse } from 'next/server'; export function middleware(req: NextRequest) { serverLogger.info({ nextReq: req }, 'Middleware triggered'); return NextResponse.next(); }
5.3 客户端场景使用示例
在Client Components中使用:
'use client'; import { clientLogger } from '@your-monorepo/logging'; export default function ClientComponent() { const handleClick = () => { clientLogger.info('Button clicked'); // 业务逻辑 }; return <button onClick={handleClick}>Click Me</button>; }
6. 关键注意事项
- 环境变量:客户端使用的日志级别需要加
NEXT_PUBLIC_前缀,确保能被Next.js暴露到客户端。 - 日志批量发送:客户端可以优化
sendLogToServer,实现批量发送日志,减少API请求次数。 - TypeScript路径配置:在Monorepo的根
tsconfig.json中配置路径别名,方便引用共享包:{ "compilerOptions": { "paths": { "@your-monorepo/logging": ["./packages/logging/src"] } } } - 日志级别一致性:尽量保持客户端和服务端的日志级别一致,避免日志遗漏或冗余。
内容的提问来源于stack exchange,提问作者Kavindu Wimalasooriya
相关产品推荐
相关产品推荐

