日志与监控
日志系统
请求日志中间件
ts
// server/middleware/logger.ts
export default defineEventHandler((event) => {
const start = Date.now()
event.node.res.on('finish', () => {
const duration = Date.now() - start
const status = event.node.res.statusCode
const method = event.method
const url = event.path
console.log(JSON.stringify({
type: 'request',
method,
url,
status,
duration,
timestamp: new Date().toISOString(),
userId: event.context.userId,
ip: getRequestHeader(event, 'x-forwarded-for'),
userAgent: getRequestHeader(event, 'user-agent'),
}))
})
})结构化日志
ts
// server/utils/logger.ts
export const logger = {
info: (message: string, data?: any) => {
console.log(JSON.stringify({
level: 'info',
message,
data,
timestamp: new Date().toISOString(),
}))
},
error: (message: string, error?: any) => {
console.error(JSON.stringify({
level: 'error',
message,
error: error?.message || error,
stack: error?.stack,
timestamp: new Date().toISOString(),
}))
},
warn: (message: string, data?: any) => {
console.warn(JSON.stringify({
level: 'warn',
message,
data,
timestamp: new Date().toISOString(),
}))
},
}日志级别指南
| 级别 | 使用场景 | 示例 |
|---|---|---|
info | 正常业务流程 | 用户登录、订单创建、缓存命中 |
warn | 可恢复的异常 | 慢请求、缓存未命中、限流触发 |
error | 需要关注的错误 | 数据库连接失败、支付回调验签失败 |
什么不该记日志?
- ❌ 密码、Token、密钥等敏感信息
- ❌ 完整的请求体(可能包含用户隐私)
- ❌ 高频操作的详细日志(如每秒数百次的健康检查)
为什么用 JSON 格式?
JSON 格式便于日志收集工具(如 ELK、Loki、CloudWatch)解析和搜索。console.log('用户登录: 13800138000') 不好搜索,{"message":"用户登录","phone":"138****8000"} 更好。
使用日志
ts
// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
try {
const { phone } = await readBody(event)
logger.info('用户登录', { phone: phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') })
// ... 登录逻辑
logger.info('登录成功', { phone: phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'), userId: user.id })
return { token, user }
} catch (error) {
logger.error('登录失败', error)
throw error
}
})手机号脱敏
日志中不记录完整手机号,使用正则替换为 138****8000。这是数据保护的基本要求。
错误追踪
全局错误处理
ts
// server/plugins/error-tracking.ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('error', async (error, event) => {
logger.error('服务端错误', {
error: error.message,
stack: error.stack,
url: event.path,
method: event.method,
userId: event.context.userId,
})
// 上报到 Sentry(可选)
// Sentry.captureException(error)
})
})客户端错误处理
ts
// app/plugins/error-handler.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('vue:error', (error) => {
console.error('Vue Error:', error)
// 上报到错误监控
})
if (import.meta.client) {
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled Promise:', event.reason)
})
}
})服务端 vs 客户端错误处理对比
| 对比项 | 服务端 | 客户端 |
|---|---|---|
| 触发位置 | server/plugins/ | app/plugins/ |
| 捕获的错误 | API 错误、数据库错误 | Vue 渲染错误、未捕获的 Promise |
| 日志位置 | 服务器终端 / 日志文件 | 浏览器控制台 |
| 上报方式 | Sentry(服务端集成) | Sentry(浏览器 SDK) |
| 用户影响 | 500 错误页面 | 白屏 / 组件错误 |
性能监控
API 响应时间
ts
// server/middleware/performance.ts
export default defineEventHandler((event) => {
const start = Date.now()
event.node.res.on('finish', () => {
const duration = Date.now() - start
if (duration > 1000) {
logger.warn('慢请求', {
url: event.path,
duration,
method: event.method,
})
}
})
})慢请求阈值参考
| 响应时间 | 级别 | 原因 |
|---|---|---|
| < 200ms | ✅ 正常 | 大部分 API 应在这个范围 |
| 200ms - 1s | ⚠️ 偏慢 | 可能有 N+1 查询或缺少索引 |
| > 1s | ❌ 慢请求 | 需要优化,影响用户体验 |
| > 5s | 🚨 严重 | 可能数据库死锁或外部服务超时 |
健康检查 API
ts
// server/routes/health.ts
export default defineEventHandler(async () => {
const start = Date.now()
// 检查数据库连接
try {
await db.execute(sql`SELECT 1`)
} catch {
return { status: 'unhealthy', database: 'disconnected', uptime: process.uptime() }
}
// 检查 Redis 连接
try {
await useStorage('redis').getItem('health-check')
} catch {
return { status: 'unhealthy', redis: 'disconnected', uptime: process.uptime() }
}
return {
status: 'ok',
uptime: process.uptime(),
responseTime: Date.now() - start,
timestamp: new Date().toISOString(),
}
})健康检查不需要认证
/health 路径在 server/routes/ 下(不是 /api/ 前缀),不会经过认证中间件。这是正确的设计——监控工具和负载均衡器需要无认证访问。
process.uptime() 的作用
返回 Node.js 进程运行时间(秒)。如果频繁重启(uptime 很短),说明应用不稳定。
Sentry 集成(推荐)
服务端 Sentry
bash
npm install @sentry/nodets
// server/plugins/sentry.ts
import * as Sentry from '@sentry/node'
export default defineNitroPlugin((nitroApp) => {
Sentry.init({
dsn: useRuntimeConfig().sentryDsn,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1, // 10% 的请求追踪性能
})
nitroApp.hooks.hook('error', (error) => {
Sentry.captureException(error)
})
nitroApp.hooks.hook('request', (event) => {
Sentry.setUser({ id: event.context.userId })
})
})客户端 Sentry
bash
npm install @sentry/vuets
// app/plugins/sentry.client.ts
import * as Sentry from '@sentry/vue'
export default defineNuxtPlugin((nuxtApp) => {
Sentry.init({
app: nuxtApp.vueApp,
dsn: useRuntimeConfig().public.sentryDsn,
environment: process.env.NODE_ENV,
})
})为什么客户端插件加 .client 后缀?
sentry.client.ts 只在客户端执行。Sentry 的 Vue SDK 依赖浏览器 API,在 SSR 环境会报错。
Sentry vs 自建日志
| 对比项 | 自建 console.log | Sentry |
|---|---|---|
| 错误聚合 | 需要自己实现 | 自动去重、分组 |
| 告警通知 | 需自己写 Webhook | 邮件/Slack/钉钉开箱即用 |
| 错误上下文 | 需手动记录 | 自动捕获面包屑、用户信息 |
| Source Map | 不支持 | 支持还原压缩代码的原始位置 |
| 费用 | 免费 | 免费额度 + 付费 |
建议:开发阶段用自建日志,上线后接 Sentry。
告警
简单告警(Webhook)
ts
// server/utils/alert.ts
export const sendAlert = async (message: string, level: 'warning' | 'critical' = 'warning') => {
const webhookUrl = useRuntimeConfig().alertWebhookUrl
if (!webhookUrl) return
await $fetch(webhookUrl, {
method: 'POST',
body: {
msgtype: 'markdown',
markdown: {
content: `## ${level === 'critical' ? '🚨' : '⚠️'} 服务告警\n\n${message}\n\n时间:${new Date().toISOString()}`,
},
},
})
}使用告警
ts
nitroApp.hooks.hook('error', async (error, event) => {
if (error.statusCode >= 500) {
await sendAlert(`服务端错误: ${error.message}`, 'critical')
}
})告警级别与触发条件
| 级别 | 触发条件 | 通知方式 | 示例 |
|---|---|---|---|
warning | 可恢复的异常 | 企业微信/钉钉 | 慢请求 > 3s、缓存未命中率高 |
critical | 服务不可用 | 企业微信 + 电话 | 数据库断连、5xx 错误飙升 |
告警疲劳
告警太多会导致开发者忽略。建议:(1) 只对需要人为介入的问题告警;(2) 自恢复问题不发 critical;(3) 同一问题 5 分钟内不重复告警。
常见问题
生产环境日志去哪看?
| 部署方式 | 查看日志命令 |
|---|---|
| PM2 | pm2 logs flutter-api |
| Docker | docker compose logs -f app |
| Kubernetes | kubectl logs -f deployment/flutter-api |
日志太多磁盘满了?
- 使用
logrotate自动轮转和压缩旧日志 - 日志只保留最近 30 天
- 考虑使用日志收集服务(如 Loki、ELK)替代本地文件
console.log 在生产环境性能差吗?
console.log 是同步阻塞的(特别是在写文件时)。高并发场景下,建议使用异步日志库或 event.waitUntil() 延迟日志写入。