从零实现前端监控告警系统:SMTP + Node.js + 个人邮箱 完整免费方案

本文将详细介绍如何为前端监控平台设计并实现一套完整的邮件告警系统,包括架构设计、核心原理、代码实现和最佳实践。

一、为什么需要告警系统?

在前端监控平台中,我们通常会采集大量的错误和性能数据。但如果只是被动地等待开发者登录 Dashboard 查看,很多问题可能已经影响了大量用户。

告警系统的核心价值:

  • 🚨 实时感知:错误发生时第一时间通知相关人员
  • 🎯 精准触达:根据规则过滤,避免告警轰炸
  • 快速响应:缩短问题发现到修复的时间窗口

二、整体架构设计

scss 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        前端应用                                  │
│                    (SDK 错误采集)                                │
└─────────────────────┬───────────────────────────────────────────┘
                      │ HTTP POST /api/report
                      ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Server 层                                   │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐           │
│  │  数据接收    │──▶│  错误聚合   │──▶│  告警检查   │           │
│  │  (report)   │   │ (fingerprint)│   │  (rules)   │           │
│  └─────────────┘   └─────────────┘   └──────┬──────┘           │
│                                              │                   │
│                                              ▼                   │
│                                     ┌─────────────┐             │
│                                     │  规则评估   │             │
│                                     │ (evaluate)  │             │
│                                     └──────┬──────┘             │
│                                              │ 触发              │
│                                              ▼                   │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐           │
│  │  告警历史   │◀──│  邮件发送   │◀──│  冷却检查   │           │
│  │  (history)  │   │   (SMTP)    │   │ (cooldown)  │           │
│  └─────────────┘   └─────────────┘   └─────────────┘           │
└─────────────────────────────────────────────────────────────────┘
                      │
                      ▼ SMTP
┌─────────────────────────────────────────────────────────────────┐
│                    邮件服务器                                    │
│              (QQ邮箱/163/企业邮箱)                               │
└─────────────────────────────────────────────────────────────────┘
                      │
                      ▼ 📧
                  开发者邮箱

三、核心模块设计

3.1 告警规则模型

告警规则是整个系统的核心,定义了"什么情况下触发告警":

typescript 复制代码
interface AlertRule {
  id: number;
  dsn: string;           // 项目标识
  name: string;          // 规则名称
  type: AlertType;       // 告警类型
  enabled: boolean;      // 是否启用
  threshold?: number;    // 阈值
  timeWindow?: number;   // 时间窗口(分钟)
  recipients: string[];  // 收件人列表
  cooldown: number;      // 冷却时间(分钟)
}

type AlertType = 
  | 'new_error'        // 新错误首次出现
  | 'error_threshold'  // 错误累计次数超过阈值
  | 'error_spike';     // 时间窗口内错误激增

三种告警类型的适用场景:

类型 场景 示例
new_error 捕获未知错误 新上线功能出现 bug
error_threshold 监控已知问题 某接口错误超过 100 次
error_spike 检测异常波动 5 分钟内错误数突增

3.2 冷却机制

为了避免同一个错误短时间内重复告警(告警轰炸),我们引入了冷却机制:

typescript 复制代码
// 内存缓存:记录最近告警时间
const alertCooldowns = new Map<string, number>();

function shouldTrigger(rule: AlertRule, fingerprint: string): boolean {
  const cooldownKey = `${rule.id}-${fingerprint}`;
  const lastAlert = alertCooldowns.get(cooldownKey);
  
  // 检查是否在冷却期内
  if (lastAlert && Date.now() - lastAlert < rule.cooldown * 60 * 1000) {
    return false; // 冷却中,不触发
  }
  
  return true;
}

// 触发告警后更新冷却时间
function updateCooldown(rule: AlertRule, fingerprint: string) {
  const cooldownKey = `${rule.id}-${fingerprint}`;
  alertCooldowns.set(cooldownKey, Date.now());
}

冷却机制的关键点:

  1. 按规则+错误指纹组合作为冷却 key,而不是全局冷却
  2. 使用内存 Map 存储,重启后冷却状态重置(可接受)
  3. 冷却时间可配置,建议默认 30 分钟

3.3 规则评估引擎

规则评估是告警系统的"大脑",决定是否触发告警:

typescript 复制代码
async function evaluateRule(rule: AlertRule, errorData: ErrorData): Promise<boolean> {
  // 1. 先检查冷却
  if (!shouldTrigger(rule, errorData.fingerprint)) {
    return false;
  }

  // 2. 根据规则类型评估
  switch (rule.type) {
    case 'new_error':
      // 新错误:检查是否首次出现
      return errorData.isNew;

    case 'error_threshold':
      // 阈值:检查累计次数
      return errorData.count >= rule.threshold;

    case 'error_spike': {
      // 激增:查询时间窗口内的错误数
      const windowStart = Date.now() - rule.timeWindow * 60 * 1000;
      const recentCount = await getErrorCountSince(
        errorData.dsn, 
        errorData.fingerprint, 
        windowStart
      );
      return recentCount >= rule.threshold;
    }

    default:
      return false;
  }
}

四、邮件服务实现

4.1 Nodemailer 配置

使用 nodemailer 库发送邮件,支持主流 SMTP 服务:

typescript 复制代码
import nodemailer, { Transporter } from 'nodemailer';

let transporter: Transporter | null = null;

function initEmailService(config: EmailConfig) {
  transporter = nodemailer.createTransport({
    host: config.host,      // smtp.qq.com
    port: config.port,      // 465
    secure: true,           // 使用 SSL
    auth: {
      user: config.user,    // 邮箱账号
      pass: config.pass     // 授权码(不是登录密码!)
    }
  });
}

常见 SMTP 配置:

服务商 Host Port 备注
QQ邮箱 smtp.qq.com 465 需开启 SMTP 服务,使用授权码
163邮箱 smtp.163.com 465 需开启 SMTP 服务
Gmail smtp.gmail.com 587 需开启两步验证,使用应用密码
企业微信 smtp.exmail.qq.com 465 企业邮箱

4.2 邮件模板设计

告警邮件需要清晰展示关键信息:

邮件设计要点:

  1. 使用内联样式(邮件客户端不支持外部 CSS)
  2. 关键信息突出显示(错误消息、次数)
  3. 提供快捷操作入口(查看详情按钮)
  4. 移动端适配(响应式布局)

4.3 发送邮件

typescript 复制代码
async function sendAlertEmail(data: AlertEmailData): Promise<boolean> {
  if (!transporter) {
    console.warn('[Email] Service not initialized');
    return false;
  }

  try {
    const info = await transporter.sendMail({
      from: '"Sentinel 监控" <monitor@example.com>',
      to: data.recipients.join(', '),
      subject: `🚨 [${data.ruleName}] ${data.errorMessage.slice(0, 50)}`,
      html: generateAlertEmailHtml(data)
    });
    
    console.log('[Email] Sent:', info.messageId);
    return true;
  } catch (error) {
    console.error('[Email] Failed:', error);
    return false;
  }
}

五、完整告警流程

5.1 错误上报时触发检查

在错误数据入库后,异步检查告警规则:

typescript 复制代码
// routes/report.ts
async function saveErrorEvent(dsn: string, event: ErrorEvent) {
  const { fingerprint } = generateFingerprint(event);
  
  // 1. 检查是否新错误
  const existing = await db.query(
    'SELECT id, count FROM errors WHERE fingerprint = $1',
    [fingerprint]
  );
  const isNew = existing.rows.length === 0;
  const count = isNew ? 1 : existing.rows[0].count + 1;
  
  // 2. 保存/更新错误记录
  if (isNew) {
    await db.query('INSERT INTO errors ...', [...]);
  } else {
    await db.query('UPDATE errors SET count = $1 ...', [count]);
  }
  
  // 3. 异步检查告警(不阻塞响应)
  checkAndTriggerAlerts({
    dsn,
    type: event.type,
    message: event.message,
    fingerprint,
    url: event.url,
    isNew,
    count
  }).catch(err => console.error('[Alert] Check failed:', err));
}

5.2 告警检查主流程

typescript 复制代码
async function checkAndTriggerAlerts(errorData: ErrorData) {
  // 1. 检查邮件服务是否可用
  if (!isEmailConfigured()) return;

  // 2. 获取该项目的所有启用规则
  const rules = await getAlertRules(errorData.dsn);
  const enabledRules = rules.filter(r => r.enabled);

  // 3. 逐个评估规则
  for (const rule of enabledRules) {
    const shouldTrigger = await evaluateRule(rule, errorData);
    
    if (shouldTrigger) {
      // 4. 触发告警
      await triggerAlert(rule, errorData);
    }
  }
}

async function triggerAlert(rule: AlertRule, errorData: ErrorData) {
  // 1. 发送邮件
  const emailSent = await sendAlertEmail({
    to: rule.recipients,
    subject: `🚨 [${rule.name}] ${errorData.message.slice(0, 50)}`,
    errorMessage: errorData.message,
    errorType: errorData.type,
    errorCount: errorData.count,
    url: errorData.url,
    timestamp: Date.now()
  });

  // 2. 记录告警历史
  await saveAlertHistory(rule.id, errorData, emailSent);

  // 3. 更新冷却时间
  updateCooldown(rule, errorData.fingerprint);
  
  console.log(`[Alert] Triggered: ${rule.name}, sent: ${emailSent}`);
}

六、数据库设计

6.1 告警规则表

sql 复制代码
CREATE TABLE alert_rules (
  id SERIAL PRIMARY KEY,
  dsn TEXT NOT NULL,                    -- 项目标识
  name VARCHAR(100) NOT NULL,           -- 规则名称
  type VARCHAR(20) NOT NULL,            -- 告警类型
  enabled BOOLEAN DEFAULT true,         -- 是否启用
  threshold INTEGER,                    -- 阈值
  time_window INTEGER DEFAULT 60,       -- 时间窗口(分钟)
  recipients TEXT[] NOT NULL,           -- 收件人数组
  cooldown INTEGER DEFAULT 30,          -- 冷却时间(分钟)
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_alert_rules_dsn ON alert_rules(dsn);

6.2 告警历史表

sql 复制代码
CREATE TABLE alert_history (
  id SERIAL PRIMARY KEY,
  rule_id INTEGER REFERENCES alert_rules(id),
  dsn TEXT NOT NULL,
  fingerprint TEXT,                     -- 错误指纹
  error_message TEXT,                   -- 错误消息
  triggered_at TIMESTAMP DEFAULT NOW(), -- 触发时间
  email_sent BOOLEAN DEFAULT false      -- 邮件是否发送成功
);

CREATE INDEX idx_alert_history_dsn ON alert_history(dsn, triggered_at);

七、API 设计

7.1 告警规则 CRUD

typescript 复制代码
// 获取规则列表
GET /api/alerts/rules?dsn=demo-app

// 创建规则
POST /api/alerts/rules
{
  "dsn": "demo-app",
  "name": "生产环境错误告警",
  "type": "new_error",
  "recipients": ["dev@example.com"],
  "cooldown": 30
}

// 更新规则
PATCH /api/alerts/rules/:id
{
  "enabled": false,
  "threshold": 50
}

// 删除规则
DELETE /api/alerts/rules/:id

7.2 告警历史查询

typescript 复制代码
// 获取告警历史
GET /api/alerts/history?dsn=demo-app&limit=50

// 响应
{
  "history": [
    {
      "id": 1,
      "ruleId": 1,
      "errorMessage": "Cannot read property 'x' of undefined",
      "triggeredAt": "2024-01-15T10:30:00Z",
      "emailSent": true
    }
  ]
}

7.3 邮件服务状态

typescript 复制代码
// 检查邮件服务状态
GET /api/alerts/email-status
// { "configured": true, "connected": true }

// 发送测试邮件
POST /api/alerts/test-email
{ "email": "test@example.com" }

八、最佳实践

8.1 告警规则配置建议

typescript 复制代码
// 推荐的规则组合
const recommendedRules = [
  {
    name: '新错误告警',
    type: 'new_error',
    cooldown: 60,        // 1小时内同一错误不重复告警
    recipients: ['oncall@team.com']
  },
  {
    name: '错误激增告警',
    type: 'error_spike',
    threshold: 100,      // 5分钟内超过100次
    timeWindow: 5,
    cooldown: 30,
    recipients: ['oncall@team.com', 'manager@team.com']
  },
  {
    name: '关键错误阈值',
    type: 'error_threshold',
    threshold: 1000,     // 累计超过1000次
    cooldown: 120,       // 2小时冷却
    recipients: ['dev@team.com']
  }
];

8.2 避免告警疲劳

  1. 合理设置冷却时间:避免同一问题反复告警
  2. 分级告警:不同严重程度发送给不同人员
  3. 聚合告警:相似错误合并为一条告警
  4. 静默时段:非工作时间降低告警频率

8.3 邮件发送优化

typescript 复制代码
// 使用队列异步发送,避免阻塞主流程
import { Queue } from 'bull';

const emailQueue = new Queue('email-alerts');

emailQueue.process(async (job) => {
  await sendAlertEmail(job.data);
});

// 触发告警时加入队列
async function triggerAlert(rule, errorData) {
  await emailQueue.add({
    to: rule.recipients,
    subject: `🚨 ${errorData.message}`,
    // ...
  });
}

九、扩展方向

当前实现了邮件告警,后续可以扩展:

  1. 多渠道通知

    • 钉钉/飞书 Webhook
    • 企业微信机器人
    • Slack 集成
    • 短信通知(严重告警)
  2. 智能告警

    • 基于历史数据的异常检测
    • 告警收敛和去重
    • 根因分析关联
  3. 告警升级

    • 未处理告警自动升级
    • 值班表集成
    • 告警认领机制

十、总结

本文介绍了前端监控告警系统的完整实现方案:

  • 架构设计:错误上报 → 规则评估 → 冷却检查 → 邮件发送
  • 核心机制:三种告警类型 + 冷却防抖 + 异步处理
  • 技术选型:Node.js + Nodemailer + PostgreSQL

告警系统是监控平台的"最后一公里",让被动查看变为主动通知,大大提升了问题响应效率。


完整代码已开源:GitHub - Sentinel 前端监控平台

如果觉得有帮助,欢迎 Star ⭐️

相关推荐
重铸码农荣光2 小时前
🎯 从零搭建一个 React Todo 应用:父子通信、状态管理与本地持久化全解析!
前端·react.js·架构
用户4099322502122 小时前
Vue3 v-if与v-show:销毁还是隐藏,如何抉择?
前端·vue.js·后端
Mr_chiu2 小时前
🚀 效率暴增!Vue.js开发必知的15个神级提效工具
前端
shanLion2 小时前
Vite项目中process报红问题的深层原因与解决方案
前端·javascript
烟袅2 小时前
从零构建一个待办事项应用:一次关于组件化与状态管理的深度思考
前端·javascript·react.js
前端小万2 小时前
草稿
前端
闲云一鹤2 小时前
将地图上的 poi 点位导出为 excel,并转换为 shp 文件
前端·cesium
岁月宁静3 小时前
MasterGo AI 实战教程:10分钟生成网页设计图(附案例演示)
前端·aigc·视觉设计
狗头大军之江苏分军3 小时前
快手12·22事故原因的合理猜测
前端·后端