生产报错不用怕——Next.js App Router集成日志模块

2,278 阅读5分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

经常做全栈项目或者做过正式后端项目的开发者都知道,日志记录是一个项目不可或缺的功能。不过,经常看Next.js开源项目的朋友会发现,几乎没什么项目集成了日志模块,我分析了一下,可能的原因有两个:

  • 国外使用Next.js开发的项目大多都部署在Vercel这样的Serverless平台,平台会记录 console.log 日志,开发者可能使用 console.log 替代正式的日志模块。
  • 很多Next.js开发的项目功能单一,调试和排查难度低,所以造成很多Next.js项目不会主动去集成日志模块。

但是,根据我的经验看,Next.js项目还是比较需要日志模块的,根据实际经验总结出来的原因如下有几个:

  • Serverless 平台和自己购买的服务器环境是有区别的,这就带来Next.js项目运行时不同,例如在Serverless 平台里,Next.js使用的是edge运行时,它相比于Node环境来说,缺少了一些API,这会导致偶尔出现本地运行正常,部署到Serverless 平台发现报错了。
  • 调用了第三方平台的API,当出现连接错误的情况,你去排查是第三方服务不稳定还是你的API额度用完,这都是比较麻烦了,但是如果有日志文件直接查看,定位问题就可以更快速。

所以,即使你现在不需要日志模块,但仍然要考虑预留功能,以备不时之需。

本文就来说说Next.js App Router下使用winston集成一个易于维护和易于帮助排查问题的日志模块。

日志模块需求

一个合格的日志模块应该符合以下要求:

  • 能够判断是否有日志保存路径,如果没有要自动创建文件夹
  • 可以指定日志文件保存路径,支持不同环境使用不同配置
  • 日志分级,支持记录info, warning, error这3个级别的日志
  • 能够按照日期和文件大小自动拆分文件
  • 能够限制日志保存天数,超出自动删除
  • 异步写入日志,不影响正常程序的执行。我们使用的是JavaScript,所以这一点无需额外操作。

开始整活!

集成winston日志

鲁迅我曾经说过:重复的劳动是对程序员的亵渎。

所以集成日志模块我直接在自己的发布的开源项目——clean-nextjs-starter 上添加新代码,clone下来就得到如下的项目:

├─ app                # 应用入口
│  ├─ layout.tsx
│  └─ page.tsx        # 首页
├─ components         # 组件
│  ├─ ……
├─ config             # 网站配置
│  └─ site.ts
├─ content            # mdx 文件统一放在这里
│  ├─ ……
├─ lib                # 公共工具类
│  ├─ logger.ts       # ⚠️新增:日志配置文件
│  ├─ ……
├─ public             # 公共静态资源
│  ├─ ……
├─ styles             # 样式
│  ├─ ……
│  ……

我已经在 lib 文件夹下新增了日志配置文件 logger.ts了。接下来正式开始开发!

首先,需要安装 winstonwinston-daily-rotate-file,后者是用于日志文件自动拆分的库:

npm install winston winston-daily-rotate-file @types/winston

添加日志文件路径配置和自动创建文件夹的代码:

import * as fs from 'fs';

const logDir: string = process.env.LOG_DIR || 'log'; // 使用环境变量或默认值

if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir, { recursive: true });
}

调用 winston 配置日志记录级别

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp({
      format: 'YYYY-MM-DD HH:mm:ss'
    }),
    winston.format.json()
  ),
});

winston 中,level 用来配置日志级别,日志级别按照严重性排序,顺序为(从最低到最高严重性):

  • silly: 最详细的日志级别,用于记录极为详尽的信息。
  • debug: 用于详细的系统内部信息,主要用于调试。
  • verbose: 提供比 debug 级别稍少的详细日志。
  • info: 一般信息,常用于跟踪程序的运行状态。
  • warn: 警告信息,表明可能的问题,但不一定是错误。
  • error: 错误信息,表明功能性的问题,通常需要用户或开发者介入解决。

在开发或调试阶段,你如果希望获得比较多的细节来帮助定位问题,可以将 level 设置问 debugverbose。生产环境建议使用info级别就可以,表示会记录 info、warn和error三个级别的日志。

format 用来配置日志格式,winston.format.combine 方法用于组合多种日志格式化方法:

  • winston.format.timestamp:添加一个时间戳到日志消息中,这里配置的格式是 YYYY-MM-DD HH:mm:ss,即年-月-日 时:分:秒。
  • winston.format.json:将日志消息格式化为JSON格式,这样日志更容易被结构化存储和解析。

接下来配置日志输出格式:

const winston = require('winston');
require('winston-daily-rotate-file');

const fileTransport = new winston.transports.DailyRotateFile({
  filename: `${logDir}/%DATE%-results.log`,
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,
  maxSize: '20m',
  maxFiles: '3d',  // 保留3天的日志
  level: 'info',   // 此传输层记录info及以上级别的日志(info, warning, error)
});

const logger = winston.createLogger({
  // ……

  transports: [
    fileTransport,
    new winston.transports.Console({
      level: 'info', // 控制台同时也输出所有级别的日志
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    })
  ]
});

先介绍 fileTransport 的部分,这部分配置了文件命名和保存规则,其中:

  • filenamedatePattern:定义了日志文件命名格式为 2024-05-24-results.log 这样的形式
  • zippedArchive:设置为 true 表示日志文件在滚动时(即新的日志文件开始使用时)会自动被压缩成 .zip 文件。这有助于减少存储空间的占用。
  • maxSize:定义了单个日志文件的最大大小,这里设置为 20m(20兆字节)。当文件大小达到这个限制时,将创建一个新的日志文件继续记录,而旧的文件则根据 maxFiles 设置进行处理。
  • maxFiles:控制日志文件的保留数量或时间。这里设置为 3d,意味着只保留最近三天的日志文件。超过这个时间范围的日志文件将被自动删除。

然后看一下 transports,这个参数是用来定义日志消息的输出目标。每个 transport 是一个输出通道,可以是文件、控制台或其他自定义目标。

  • fileTransport:之前定义的文件输出配置,会保存在 logDir 指定的文件夹下
  • new winston.transports.Console:这是一个控制台输出配置,设置为输出 debug 级别及以上的所有日志。有了这个配置,你会看到控制台也会同步输出日志,在开发的时候会更方便,当然,如果你觉得这样看着很繁杂,可以删掉这部分配置。

完整配置如下,你也可以到我的开源仓库:clean-nextjs-starter查看:

import * as fs from 'fs';
import * as winston from 'winston';
import 'winston-daily-rotate-file';

const logDir: string = process.env.LOG_DIR || 'log'; // 使用环境变量或默认值

if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir, { recursive: true });
}

const fileTransport = new winston.transports.DailyRotateFile({
  filename: `${logDir}/%DATE%-results.log`,
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,
  maxSize: '20m',
  maxFiles: '3d',  // 保留3天的日志
  level: 'info',   // 此传输层记录info及以上级别的日志(info, warning, error)
});

const logger: winston.Logger = winston.createLogger({
  level: 'info', // 最低级别
  format: winston.format.combine(
    winston.format.timestamp({
      format: 'YYYY-MM-DD HH:mm:ss'
    }),
    winston.format.json()
  ),
  transports: [
    fileTransport,
    new winston.transports.Console({
      level: 'info', // 控制台同时也输出所有级别的日志
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    })
  ]
});

export default logger;

使用 Logger

上一节就是 集成winston所需的配置了,现在我们在服务器组件调用日志模块试一下

// app/page.tsx

import logger from "@/lib/logger";

export default function Home() {
  logger.debug("Debug message");
  logger.info("Info message");
  logger.warn("Warning message");
  logger.error("Error message");

  // ……
}

现在打开首页(如果你clone了上面的项目,打开 http://localhost:3000 ),能够在vscode控制台看到日志,也可以看到根目录下生成了log文件夹,里面有跟我们配置对应的日志文件。

如果你不需要日志模块,只要不去调用 logger.ts 就可以,让logger模块静静地躺在角落吧!

开源地址:clean-nextjs-starter

关于我

我是一名前端工程师,Next.js 手艺人,AI降临派。

今年致力于 Next.js 和 Node.js 领域的开源项目开发和知识分享。

欢迎关注我的 掘金 和 Github