Menu

Pino 日志与 Sentry 错误监控

NEXTY.DEV 基于 Pino + Sentry 的生产级日志与错误监控方案

架构概览

代码仓库

  └── getLogger("module-name")

        ├── Pino(结构化日志)
        │     ├── 开发环境 → 彩色格式化输出(pino-pretty)
        │     ├── 生产环境 → JSON 写 stdout(云平台自动收集)
        │     └── VPS + LOG_DIR → 文件轮转(pino-roll)

        └── Sentry(错误上报)
              ├── warn  → 添加 Breadcrumb(面包屑追踪)
              ├── error → captureException(上报错误)
              └── fatal → captureException(level: fatal)

两个工具各司其职:

  • Pino 负责记录所有日志,高性能、结构化 JSON 输出
  • Sentry 负责错误聚合、告警、堆栈追踪、用户影响分析

配置环境变量

所有变量均为可选,未配置时 Sentry 所有调用静默忽略,Pino 默认 info 级别输出到 stdout。

Sentry 相关

NEXT_PUBLIC_SENTRY_DSN

获取步骤:

  1. 登录 sentry.io,进入项目(或新建 Next.js 类型项目)
  2. 左侧菜单 → SettingsProjects → 选择项目
  3. 左侧 → Client Keys (DSN)
  4. 复制 DSN 字段的值,格式为 https://[email protected]/xxx
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/789
dsn
dsn

SENTRY_AUTH_TOKEN(可选,用于 source map 上传)

获取步骤:

  1. 登录 sentry.io → 左侧 SettingsOrganization Tokens
  2. 点击 Create New Token
  3. 复制生成的 token(以 sntrys_ 开头)
SENTRY_AUTH_TOKEN=sntrys_eyJ0eXBlIjoiYXV0aF90b2tlbiJ9...
org token

SENTRY_ORGSENTRY_PROJECT(配合 AUTH_TOKEN 使用)

获取步骤:

  • SENTRY_ORGSettingsGeneral SettingsOrganization Slug(也是 sentry.io URL 中的组织名,如 https://my-company.sentry.io 中的 my-company
  • SENTRY_PROJECTSettingsProjects → 点击项目 → Project Slug
SENTRY_ORG=my-company
SENTRY_PROJECT=my-nextjs-app

SENTRY_DEBUG(可选)

开发环境下默认 Sentry 不上报(避免污染数据)。设置为 true 后在开发环境也会真实发送事件,适合调试 Sentry 集成本身。

SENTRY_DEBUG=true

Pino 相关

LOG_LEVEL

控制输出哪些级别的日志,低于此级别的日志会被忽略。可选值:trace | debug | info | warn | error | fatal

LOG_LEVEL=info   # 生产环境推荐
LOG_LEVEL=debug  # 排查问题时临时使用

LOG_DIR(可选,仅 VPS 需要)

设置后启用 pino-roll 文件轮转,日志写入到该目录。Vercel、Cloudflare Workers 留空即可,由平台收集 stdout。

LOG_DIR=./logs          # 相对路径,相对于项目根目录
LOG_DIR=/var/log/myapp  # 或绝对路径

使用方法

基础用法

import { getLogger } from "@/lib/logger";
 
const logger = getLogger("payment-service");
 
// trace / debug:开发调试,生产默认不输出
logger.trace({ sql: "SELECT ..." }, "Executing query");
logger.debug({ params }, "Request received");
 
// info:正常业务事件
logger.info({ userId, planId }, "User subscribed");
 
// warn:可恢复的异常(同时添加 Sentry Breadcrumb)
logger.warn({ retryCount: 3, url }, "Webhook delivery failed, retrying");
 
// error:错误(自动上报 Sentry)
logger.error(new Error("Stripe API timeout"), "Payment failed");
 
// fatal:严重错误(自动上报 Sentry,level: fatal)
logger.fatal(new Error("DB connection lost"), "Service unavailable");

带上下文的错误

// 传入 Error 对象
logger.error(new Error("Invalid signature"), "Webhook verification failed");
 
// 传入对象(包含 err 字段时自动提取上报到 Sentry)
logger.error({ err: error, webhookId, payload }, "Webhook processing error");
 
// 手动捕获:等同于 error(),但语义更明确
logger.captureError(error, { userId, action: "checkout" });

访问底层 Pino logger

需要创建子 logger 或使用 Pino 高级功能时:

const logger = getLogger("api");
const childLogger = logger.pino.child({ requestId: "req-123" });
childLogger.info("Request started");

在 Server Action 中使用

// app/actions/payment.ts
import { getLogger } from "@/lib/logger";
 
const logger = getLogger("payment-action");
 
export async function createCheckout(planId: string) {
  try {
    const session = await stripe.checkout.sessions.create({ ... });
    logger.info({ planId, sessionId: session.id }, "Checkout session created");
    return { url: session.url };
  } catch (error) {
    logger.error(error instanceof Error ? error : new Error(String(error)), "Checkout failed");
    throw error;
  }
}

在 Route Handler 中使用

// app/api/webhooks/stripe/route.ts
import { getLogger } from "@/lib/logger";
 
const logger = getLogger("stripe-webhook");
 
export async function POST(request: Request) {
  try {
    // ...
    logger.info({ event: event.type }, "Webhook processed");
    return Response.json({ ok: true });
  } catch (error) {
    logger.error(error instanceof Error ? error : new Error(String(error)), "Webhook error");
    return Response.json({ error: "Internal error" }, { status: 500 });
  }
}

日志级别说明

级别使用场景Sentry 行为
traceSQL 查询、详细执行路径
debug请求参数、中间状态
info用户操作、订单创建、登录
warn重试、降级、速率限制触发添加 Breadcrumb
errorAPI 失败、数据库错误、付款失败上报异常
fatal服务不可用、初始化失败上报异常(fatal 级别)

生产环境默认级别为 info,即 tracedebug 日志不输出。调试时临时设置 LOG_LEVEL=debug 即可。

各平台部署指南

Vercel

无需任何额外配置。Next.js 函数的 stdout 自动发送到 Vercel 日志系统。

查看日志: Vercel Dashboard → 项目 → LogsFunctions

SENTRY_DSN=https://[email protected]/xxx
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
LOG_LEVEL=info
# LOG_DIR 留空

注意: Vercel 函数日志免费计划只保留 1 小时,生产环境建议同时配置 Sentry 作为主要的错误追踪手段。

Cloudflare Workers

Workers 运行在 Edge 运行时,无文件系统。logger 自动检测 Edge 环境,使用纯 JSON stdout 模式(跳过 pino-roll 和 pino-pretty)。

查看日志: Cloudflare Dashboard → Workers & Pages → 项目 → Logs

SENTRY_DSN=https://[email protected]/xxx
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
LOG_LEVEL=info
# LOG_DIR 必须留空,Cloudflare Workers 无文件系统

实时日志: 开发调试时可通过 wrangler tail 在终端查看实时输出。

VPS — Coolify

Coolify 以 Docker 容器方式运行应用,stdout 自动被 Docker 捕获并可在面板中查看。

查看 stdout 日志: Coolify Dashboard → 应用 → Logs 标签页(实时流式输出)

SENTRY_DSN=https://[email protected]/xxx
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
LOG_LEVEL=info
# LOG_DIR 留空,使用 stdout,Coolify 面板直接查看

如需持久化文件日志:

  1. Coolify Dashboard → 应用 → StoragesAdd Storage
  2. 填写:
    • Namelogs
    • Source Path(Host):宿主机路径,如 /var/lib/coolify/myapp/logs
    • Destination Path(Container):容器内路径,如 /app/logs
  3. 在环境变量中设置:
LOG_DIR=/app/logs
  1. 重新部署后,日志文件写入宿主机 /var/lib/coolify/myapp/logs/ 目录,可通过 SSH 查看:
tail -f /var/lib/coolify/myapp/logs/payment-service.2025-03-08.1.log

VPS — Dokploy

Dokploy 同样基于 Docker,stdout 日志在面板中可直接查看。

查看 stdout 日志: Dokploy Dashboard → 应用 → Logs 标签页

SENTRY_DSN=https://[email protected]/xxx
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/xxx
LOG_LEVEL=info
# LOG_DIR 留空,使用 stdout

如需持久化文件日志:

  1. Dokploy Dashboard → 应用 → AdvancedMountsAdd Mount
  2. 选择 Volume Mount,填写:
    • Host Path:宿主机路径,如 /var/dokploy/myapp/logs
    • Container Path:容器内路径,如 /app/logs
  3. 在环境变量中设置:
LOG_DIR=/app/logs
  1. 重新部署后通过 SSH 查看:
tail -f /var/dokploy/myapp/logs/payment-service.2025-03-08.1.log

VPS — Node.js / PM2

方案 A:PM2(推荐,无需配置 LOG_DIR)

PM2 自动把 stdout 收集到文件,无需改任何代码:

pm2 start npm --name "myapp" -- start
pm2 logs myapp                         # 实时查看
# 日志文件位置:~/.pm2/logs/myapp-out.log

方案 B:LOG_DIR 文件轮转

适合不使用 PM2 的场景:

LOG_DIR=./logs
# 日志文件:logs/payment-service.2025-03-08.1.log(按天轮转,超 10MB 自动切分)

建议配合 cron 定期清理旧日志:

# 保留最近 30 天
0 2 * * * find /path/to/app/logs -name "*.log" -mtime +30 -delete

最佳实践

结构化字段,避免字符串拼接

// ✅ 推荐:字段可被日志平台索引和过滤
logger.info({ userId, amount, currency }, "Payment completed");
 
// ❌ 避免:无法检索
logger.info({}, `User ${userId} paid ${amount} ${currency}`);

敏感信息不进日志

// ❌ 绝对不要记录密码、token、完整卡号
logger.info({ password, stripeKey }, "User data");
 
// ✅ 只记录必要的非敏感标识
logger.info({ userId, last4: card.last4 }, "Payment method added");

每个模块用独立 logger

const logger = getLogger("stripe-webhook");   // 支付 webhook
const logger = getLogger("ai-chat");          // AI 功能
const logger = getLogger("auth");             // 认证

不要在客户端组件中使用

logger 依赖 Node.js/Edge 运行时,只能在服务端使用:

// ✅ Server Component、Server Action、Route Handler、middleware
import { getLogger } from "@/lib/logger";
 
// ❌ "use client" 组件中不能使用

客户端错误由 sentry.client.config.ts 中的 Sentry SDK 自动捕获,无需手动调用 logger。

相关文件

lib/
  logger/
    index.ts              # Logger 核心实现
sentry.client.config.ts   # 客户端 Sentry 配置(含 Session Replay)
sentry.server.config.ts   # 服务端 Sentry 配置
sentry.edge.config.ts     # Edge 运行时 Sentry 配置
instrumentation.ts        # Next.js 初始化钩子
next.config.mjs           # serverExternalPackages + withSentryConfig