You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 08:07:46