在NextJS中用中间件实现类似Morgan的请求追踪
在Next.js中实现类似Morgan的请求追踪日志
一、解决Middleware无法追踪响应结果的问题
Next.js Middleware默认执行时机在路由匹配后、页面/API路由处理前,无法直接等待路由处理完成并获取完整响应,可通过以下两种方式实现追踪:
1. 克隆响应获取状态码与内容
通过克隆NextResponse实例读取响应信息,克隆操作不会影响返回给客户端的原始响应:
// middleware.ts import { NextRequest, NextResponse } from 'next/server'; export function middleware(request: NextRequest) { const start = Date.now(); const response = NextResponse.next(); const clonedResponse = response.clone(); // 异步读取并记录响应信息 (async () => { const status = clonedResponse.status; // 根据响应类型选择text()或json() const content = await clonedResponse.text(); const duration = Date.now() - start; console.log(`[${new Date().toISOString()}] ${request.method} ${request.nextUrl.pathname} ${status} ${duration}ms`); // 可选:记录响应内容(注意过滤敏感数据) // console.log('Response:', content); })(); return response; }
2. 结合路由边界的日志逻辑
在App Router的layout.tsx或单个路由文件中添加日志,能直接获取路由处理的完整上下文:
- 全局布局中记录页面路由日志:
// app/layout.tsx import { useEffect } from 'react'; import { headers } from 'next/headers'; import { usePathname, useSearchParams } from 'next/navigation'; export default function RootLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const searchParams = useSearchParams(); const start = Date.now(); useEffect(() => { const duration = Date.now() - start; console.log(`[${new Date().toISOString()}] GET ${pathname}?${searchParams} 200 ${duration}ms`); }, []); return ( <html lang="en"> <body>{children}</body> </html> ); }
- API路由中直接记录请求响应:
// app/api/hello/route.ts import { NextRequest, NextResponse } from 'next/server'; export async function GET(request: NextRequest) { const start = Date.now(); const response = NextResponse.json({ message: 'Hello World' }, { status: 200 }); const duration = Date.now() - start; console.log(`[${new Date().toISOString()}] ${request.method} ${request.nextUrl.pathname} ${response.status} ${duration}ms`); return response; }
二、解决Edge Runtime的限制问题
1. 终端颜色输出
Edge Runtime不支持chalk等Node.js颜色库,可手动使用ANSI转义码实现:
// 定义颜色常量 const COLORS = { green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', reset: '\x1b[0m', gray: '\x1b[90m', }; // 根据状态码匹配颜色 function getStatusColor(status: number) { if (status >= 200 && status < 300) return COLORS.green; if (status >= 400 && status < 500) return COLORS.yellow; if (status >= 500) return COLORS.red; return COLORS.reset; } // 带颜色的日志输出示例 console.log( `${COLORS.gray}[${new Date().toISOString()}]${COLORS.reset} ${request.method} ${request.nextUrl.pathname} ${getStatusColor(status)}${status}${COLORS.reset} ${duration}ms` );
2. 替代Node.js原生模块
若日志逻辑依赖fs等Node.js原生模块,可将这部分逻辑移至使用Node.js Runtime的API路由:
- 创建日志记录API:
// app/api/log/route.ts // 默认使用Node.js Runtime,可直接调用原生模块 import { NextRequest, NextResponse } from 'next/server'; import fs from 'fs/promises'; import path from 'path'; export async function POST(request: NextRequest) { const logData = await request.json(); const logPath = path.join(process.cwd(), 'logs', 'requests.log'); // 确保日志目录存在 await fs.mkdir(path.dirname(logPath), { recursive: true }); // 追加日志内容 await fs.appendFile(logPath, `${JSON.stringify(logData)}\n`, 'utf-8'); return NextResponse.json({ success: true }); }
- 在Middleware或路由中调用该API:
// middleware.ts中调用日志API (async () => { const logData = { timestamp: new Date().toISOString(), method: request.method, path: request.nextUrl.pathname, status: clonedResponse.status, duration: Date.now() - start, }; await fetch(new URL('/api/log', request.nextUrl.origin), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logData), }); })();
三、完整Middleware示例(带颜色日志)
// middleware.ts import { NextRequest, NextResponse } from 'next/server'; const COLORS = { green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', reset: '\x1b[0m', gray: '\x1b[90m', }; function getStatusColor(status: number) { if (status >= 200 && status < 300) return COLORS.green; if (status >= 400 && status < 500) return COLORS.yellow; if (status >= 500) return COLORS.red; return COLORS.reset; } export function middleware(request: NextRequest) { const start = Date.now(); const response = NextResponse.next(); const clonedResponse = response.clone(); (async () => { const status = clonedResponse.status; const duration = Date.now() - start; const timestamp = new Date().toISOString(); console.log( `${COLORS.gray}[${timestamp}]${COLORS.reset} ${request.method} ${request.nextUrl.pathname} ${getStatusColor(status)}${status}${COLORS.reset} ${duration}ms` ); })(); return response; } export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
内容的提问来源于stack exchange,提问作者Sigma Octantis
相关产品推荐
相关产品推荐

