第194集:Vue 3监控与告警系统

概述

在本集中,我们将深入探讨如何为Vue 3应用构建完整的监控与告警系统。监控系统是生产环境中不可或缺的组成部分,它能帮助我们实时了解应用性能、快速定位问题并进行预警。

一、性能指标体系

1. 核心Web Vitals指标

Web Vitals是Google提出的用于衡量网页用户体验的核心指标,包括:

  • **FCP (First Contentful Paint)**:首次内容绘制,衡量页面开始加载到首次渲染文本、图像、非白色画布或SVG的时间。
  • **LCP (Largest Contentful Paint)**:最大内容绘制,衡量页面主要内容加载完成的时间。
  • **FID (First Input Delay)**:首次输入延迟,衡量用户首次与页面交互到浏览器开始处理的时间。
  • **CLS (Cumulative Layout Shift)**:累积布局偏移,衡量页面元素意外移动的程度。
  • **TTI (Time to Interactive)**:可交互时间,衡量页面从开始加载到完全可交互的时间。

2. 自定义性能指标

除了Web Vitals,我们还可以监控自定义指标:

  • API请求耗时与成功率
  • 组件渲染时间
  • 内存使用情况
  • 错误率与错误类型
  • 用户行为轨迹

二、前端监控数据采集

1. 性能监控类实现

// src/utils/PerformanceMonitor.js
/**
 * 性能监控类,用于采集和上报前端性能数据
 */
export class PerformanceMonitor {
  constructor(options = {}) {
    this.options = {
      sampleRate: 1, // 采样率,1表示100%
     上报URL: '/api/monitor/performance',
      ...options
    };
    this.performanceData = {};
  }

  /**
   * 初始化性能监控
   */
  init() {
    // 采样控制
    if (Math.random() > this.options.sampleRate) return;

    // 监听页面加载完成
    if (document.readyState === 'complete') {
      this.collectPerformanceData();
    } else {
      window.addEventListener('load', () => this.collectPerformanceData());
    }

    // 监听页面卸载
    window.addEventListener('beforeunload', () => this.reportPerformanceData());
  }

  /**
   * 采集性能数据
   */
  collectPerformanceData() {
    if (!window.performance) return;

    const navigation = performance.getEntriesByType('navigation')[0];
    const resource = performance.getEntriesByType('resource');

    // 采集核心指标
    this.performanceData = {
      // 页面加载时间
      loadTime: navigation.loadEventEnd - navigation.fetchStart,
      // DOM解析时间
      domParseTime: navigation.domContentLoadedEventEnd - navigation.fetchStart,
      // 首次内容绘制
      fcp: this.getFCP(),
      // 最大内容绘制
      lcp: this.getLCP(),
      // 资源加载情况
      resourceCount: resource.length,
      resourceDetails: resource.map(item => ({
        name: item.name,
        type: item.initiatorType,
        duration: item.duration,
        size: item.transferSize
      })),
      // 时间戳
      timestamp: Date.now(),
      // 用户信息
      userAgent: navigator.userAgent,
      url: window.location.href
    };
  }

  /**
   * 获取FCP指标
   */
  async getFCP() {
    try {
      const observer = new PerformanceObserver((entryList) => {
        const entries = entryList.getEntriesByName('first-contentful-paint');
        if (entries.length > 0) {
          this.performanceData.fcp = entries[0].startTime;
        }
      });
      observer.observe({ entryTypes: ['paint'] });
    } catch (error) {
      console.error('获取FCP失败:', error);
    }
  }

  /**
   * 获取LCP指标
   */
  async getLCP() {
    try {
      const observer = new PerformanceObserver((entryList) => {
        const entries = entryList.getEntries();
        const lastEntry = entries[entries.length - 1];
        this.performanceData.lcp = lastEntry.startTime;
      });
      observer.observe({ entryTypes: ['largest-contentful-paint'] });
    } catch (error) {
      console.error('获取LCP失败:', error);
    }
  }

  /**
   * 上报性能数据
   */
  reportPerformanceData() {
    if (Object.keys(this.performanceData).length === 0) return;

    // 使用navigator.sendBeacon发送数据,确保页面卸载时也能上报
    if (navigator.sendBeacon) {
      navigator.sendBeacon(
        this.options.上报URL,
        JSON.stringify(this.performanceData)
      );
    } else {
      // 降级方案
      fetch(this.options.上报URL, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(this.performanceData),
        keepalive: true
      });
    }
  }
}

2. 错误监控类实现

// src/utils/ErrorMonitor.js
/**
 * 错误监控类,用于捕获和上报前端错误
 */
export class ErrorMonitor {
  constructor(options = {}) {
    this.options = {
      上报URL: '/api/monitor/error',
      sampleRate: 1,
      ...options
    };
    this.errors = [];
  }

  /**
   * 初始化错误监控
   */
  init() {
    // 采样控制
    if (Math.random() > this.options.sampleRate) return;

    // 监听全局错误
    window.addEventListener('error', (event) => this.handleError(event));
    
    // 监听未捕获的Promise错误
    window.addEventListener('unhandledrejection', (event) => this.handlePromiseError(event));
    
    // 监听Vue组件错误
    this.setupVueErrorHandler();
  }

  /**
   * 配置Vue错误处理器
   */
  setupVueErrorHandler() {
    // 在main.js中使用:app.config.errorHandler = errorMonitor.handleVueError.bind(errorMonitor);
  }

  /**
   * 处理Vue组件错误
   */
  handleVueError(err, vm, info) {
    const errorData = {
      type: 'vue',
      message: err.message,
      stack: err.stack,
      info,
      componentName: vm.$options.name || vm.$options._componentTag,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    };
    this.errors.push(errorData);
    this.reportErrors();
  }

  /**
   * 处理全局错误
   */
  handleError(event) {
    const errorData = {
      type: 'js',
      message: event.message,
      filename: event.filename,
      lineno: event.lineno,
      colno: event.colno,
      stack: event.error?.stack,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    };
    this.errors.push(errorData);
    this.reportErrors();
  }

  /**
   * 处理Promise错误
   */
  handlePromiseError(event) {
    const errorData = {
      type: 'promise',
      message: event.reason?.message || 'Unhandled Promise Rejection',
      stack: event.reason?.stack,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    };
    this.errors.push(errorData);
    this.reportErrors();
  }

  /**
   * 上报错误数据
   */
  reportErrors() {
    if (this.errors.length === 0) return;

    // 批量上报,每次最多上报10条
    const reportErrors = this.errors.splice(0, 10);
    
    fetch(this.options.上报URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ errors: reportErrors })
    });
  }
}

三、实时监控面板实现

1. 使用ECharts实现可视化

<!-- src/components/MonitorDashboard.vue -->
<template>
  <div class="monitor-dashboard">
    <h2>实时监控面板</h2>
    
    <div class="dashboard-grid">
      <!-- 核心指标卡片 -->
      <div class="metric-card" v-for="metric in metrics" :key="metric.name">
        <div class="metric-name">{{ metric.name }}</div>
        <div class="metric-value">{{ metric.value }}</div>
        <div class="metric-trend" :class="metric.trend > 0 ? 'trend-up' : 'trend-down'">
          {{ metric.trend > 0 ? '↑' : '↓' }} {{ Math.abs(metric.trend) }}%
        </div>
      </div>
      
      <!-- 性能趋势图 -->
      <div class="chart-container">
        <h3>性能趋势</h3>
        <div ref="performanceChart" class="chart"></div>
      </div>
      
      <!-- 错误分布饼图 -->
      <div class="chart-container">
        <h3>错误分布</h3>
        <div ref="errorChart" class="chart"></div>
      </div>
      
      <!-- 最近错误列表 -->
      <div class="error-list-container">
        <h3>最近错误</h3>
        <div class="error-list">
          <div class="error-item" v-for="error in recentErrors" :key="error.id">
            <div class="error-message">{{ error.message }}</div>
            <div class="error-meta">{{ error.type }} - {{ formatTime(error.timestamp) }}</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, reactive } from 'vue';
import * as echarts from 'echarts';

// 核心指标数据
const metrics = reactive([
  { name: 'FCP', value: '1.2s', trend: -5 },
  { name: 'LCP', value: '2.5s', trend: -2 },
  { name: 'FID', value: '10ms', trend: 3 },
  { name: 'CLS', value: '0.02', trend: -10 },
  { name: '错误率', value: '0.1%', trend: -20 },
  { name: 'API成功率', value: '99.9%', trend: 0.5 }
]);

// 最近错误数据
const recentErrors = ref([
  { id: 1, message: 'ReferenceError: xxx is not defined', type: 'js', timestamp: Date.now() - 10000 },
  { id: 2, message: 'Failed to fetch', type: 'api', timestamp: Date.now() - 20000 },
  { id: 3, message: 'TypeError: Cannot read property of undefined', type: 'js', timestamp: Date.now() - 30000 }
]);

// 图表实例
let performanceChart = null;
let errorChart = null;

// 性能趋势图引用
const performanceChartRef = ref(null);
// 错误分布饼图引用
const errorChartRef = ref(null);

// 格式化时间
const formatTime = (timestamp) => {
  const date = new Date(timestamp);
  return date.toLocaleTimeString();
};

// 初始化性能趋势图
const initPerformanceChart = () => {
  performanceChart = echarts.init(performanceChartRef.value);
  
  const option = {
    xAxis: {
      type: 'category',
      data: ['00:00', '01:00', '02:00', '03:00', '04:00', '05:00', '06:00']
    },
    yAxis: {
      type: 'value',
      name: '时间(ms)'
    },
    series: [
      {
        name: 'FCP',
        type: 'line',
        data: [1200, 1100, 1300, 1250, 1150, 1350, 1200]
      },
      {
        name: 'LCP',
        type: 'line',
        data: [2500, 2400, 2600, 2550, 2450, 2650, 2500]
      }
    ]
  };
  
  performanceChart.setOption(option);
};

// 初始化错误分布饼图
const initErrorChart = () => {
  errorChart = echarts.init(errorChartRef.value);
  
  const option = {
    series: [
      {
        type: 'pie',
        data: [
          { value: 60, name: 'JavaScript错误' },
          { value: 20, name: 'API错误' },
          { value: 10, name: 'Vue组件错误' },
          { value: 10, name: '其他错误' }
        ]
      }
    ]
  };
  
  errorChart.setOption(option);
};

// 监听窗口大小变化,自适应图表
const handleResize = () => {
  performanceChart?.resize();
  errorChart?.resize();
};

onMounted(() => {
  initPerformanceChart();
  initErrorChart();
  window.addEventListener('resize', handleResize);
});

onUnmounted(() => {
  performanceChart?.dispose();
  errorChart?.dispose();
  window.removeEventListener('resize', handleResize);
});
</script>

<style scoped>
.monitor-dashboard {
  padding: 20px;
  background-color: #f5f7fa;
  border-radius: 8px;
}

.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

.metric-card {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  text-align: center;
}

.metric-name {
  font-size: 14px;
  color: #666;
  margin-bottom: 10px;
}

.metric-value {
  font-size: 24px;
  font-weight: bold;
  color: #333;
  margin-bottom: 5px;
}

.metric-trend {
  font-size: 12px;
  font-weight: bold;
}

.trend-up {
  color: #f56c6c;
}

.trend-down {
  color: #67c23a;
}

.chart-container {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  grid-column: span 2;
}

.chart {
  width: 100%;
  height: 300px;
  margin-top: 10px;
}

.error-list-container {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  grid-column: span 2;
}

.error-list {
  max-height: 300px;
  overflow-y: auto;
  margin-top: 10px;
}

.error-item {
  padding: 15px;
  border-bottom: 1px solid #eee;
}

.error-item:last-child {
  border-bottom: none;
}

.error-message {
  font-weight: bold;
  margin-bottom: 5px;
  color: #f56c6c;
}

.error-meta {
  font-size: 12px;
  color: #999;
}
</style>

四、告警系统实现

1. 告警规则配置

// src/config/alertRules.js
/**
 * 告警规则配置
 */
export const alertRules = [
  {
    id: 1,
    name: 'FCP超时',
    metric: 'fcp',
    operator: '>',
    threshold: 2000, // 2秒
    duration: 60, // 持续60秒
    level: 'warning', // warning, error, critical
    message: 'FCP超过2秒,当前值: {{value}}ms',
    通知渠道: ['email', 'slack']
  },
  {
    id: 2,
    name: '错误率过高',
    metric: 'errorRate',
    operator: '>',
    threshold: 1, // 1%
    duration: 300, // 持续5分钟
    level: 'error',
    message: '错误率超过1%,当前值: {{value}}%',
    通知渠道: ['email', 'slack', 'sms']
  },
  {
    id: 3,
    name: 'API成功率过低',
    metric: 'apiSuccessRate',
    operator: '<',
    threshold: 99, // 99%
    duration: 120, // 持续2分钟
    level: 'critical',
    message: 'API成功率低于99%,当前值: {{value}}%',
    通知渠道: ['email', 'slack', 'sms', 'phone']
  }
];

2. 告警引擎实现

// src/services/AlertEngine.js
import { alertRules } from '../config/alertRules';

/**
 * 告警引擎,用于根据监控数据触发告警
 */
export class AlertEngine {
  constructor() {
    this.alertStates = new Map(); // 存储告警状态
    this.dataBuffer = new Map(); // 存储最近的数据
    this.checkInterval = 10000; // 每10秒检查一次
    this.start();
  }

  /**
   * 启动告警引擎
   */
  start() {
    setInterval(() => this.checkAlerts(), this.checkInterval);
  }

  /**
   * 添加监控数据
   */
  addData(metric, value, timestamp = Date.now()) {
    if (!this.dataBuffer.has(metric)) {
      this.dataBuffer.set(metric, []);
    }
    
    const buffer = this.dataBuffer.get(metric);
    buffer.push({ value, timestamp });
    
    // 只保留最近5分钟的数据
    const fiveMinutesAgo = timestamp - 5 * 60 * 1000;
    this.dataBuffer.set(metric, buffer.filter(item => item.timestamp > fiveMinutesAgo));
  }

  /**
   * 检查告警规则
   */
  checkAlerts() {
    const now = Date.now();
    
    alertRules.forEach(rule => {
      const buffer = this.dataBuffer.get(rule.metric) || [];
      
      // 过滤出最近duration时间内的数据
      const recentData = buffer.filter(item => {
        return now - item.timestamp <= rule.duration * 1000;
      });
      
      if (recentData.length === 0) return;
      
      // 计算平均值
      const avgValue = recentData.reduce((sum, item) => sum + item.value, 0) / recentData.length;
      
      // 检查是否触发告警
      const isTriggered = this.checkCondition(avgValue, rule.operator, rule.threshold);
      const currentState = this.alertStates.get(rule.id) || { triggered: false, lastTriggered: 0 };
      
      if (isTriggered && !currentState.triggered) {
        // 触发告警
        this.triggerAlert(rule, avgValue);
        this.alertStates.set(rule.id, { triggered: true, lastTriggered: now });
      } else if (!isTriggered && currentState.triggered) {
        // 恢复告警
        this.resolveAlert(rule, avgValue);
        this.alertStates.set(rule.id, { triggered: false, lastTriggered: 0 });
      }
    });
  }

  /**
   * 检查条件是否满足
   */
  checkCondition(value, operator, threshold) {
    switch (operator) {
      case '>':
        return value > threshold;
      case '<':
        return value < threshold;
      case '>=':
        return value >= threshold;
      case '<=':
        return value <= threshold;
      case '==':
        return value === threshold;
      case '!=':
        return value !== threshold;
      default:
        return false;
    }
  }

  /**
   * 触发告警
   */
  triggerAlert(rule, value) {
    const message = rule.message.replace('{{value}}', value.toFixed(2));
    const alertData = {
      ruleId: rule.id,
      name: rule.name,
      level: rule.level,
      message,
      metric: rule.metric,
      value,
      timestamp: Date.now()
    };
    
    // 发送告警
    this.sendAlert(alertData, rule.通知渠道);
    
    console.warn(`[告警触发] ${message}`);
  }

  /**
   * 恢复告警
   */
  resolveAlert(rule, value) {
    const message = `${rule.name}已恢复正常,当前值: ${value.toFixed(2)}`;
    const alertData = {
      ruleId: rule.id,
      name: rule.name,
      level: 'resolved',
      message,
      metric: rule.metric,
      value,
      timestamp: Date.now()
    };
    
    // 发送恢复通知
    this.sendAlert(alertData, rule.通知渠道);
    
    console.info(`[告警恢复] ${message}`);
  }

  /**
   * 发送告警通知
   */
  sendAlert(alertData, channels) {
    // 实现各种通知渠道的发送逻辑
    channels.forEach(channel => {
      switch (channel) {
        case 'email':
          this.sendEmail(alertData);
          break;
        case 'slack':
          this.sendSlack(alertData);
          break;
        case 'sms':
          this.sendSMS(alertData);
          break;
        case 'phone':
          this.sendPhoneCall(alertData);
          break;
        default:
          break;
      }
    });
  }

  // 各种通知渠道的实现
  sendEmail(alertData) {
    // 邮件发送逻辑
  }
  
  sendSlack(alertData) {
    // Slack发送逻辑
  }
  
  sendSMS(alertData) {
    // SMS发送逻辑
  }
  
  sendPhoneCall(alertData) {
    // 电话呼叫逻辑
  }
}

五、集成到Vue 3应用

1. 在main.js中初始化监控

// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import { PerformanceMonitor } from './utils/PerformanceMonitor';
import { ErrorMonitor } from './utils/ErrorMonitor';

const app = createApp(App);

// 初始化性能监控
const performanceMonitor = new PerformanceMonitor({
  sampleRate: 0.5, // 50%采样率
  上报URL: '/api/monitor/performance'
});
performanceMonitor.init();

// 初始化错误监控
const errorMonitor = new ErrorMonitor({
  sampleRate: 1, // 100%采样率
  上报URL: '/api/monitor/error'
});
errorMonitor.init();
app.config.errorHandler = errorMonitor.handleVueError.bind(errorMonitor);

app.mount('#app');

2. 在管理后台使用监控面板

<!-- src/views/MonitorView.vue -->
<template>
  <div class="monitor-view">
    <h1>应用监控中心</h1>
    <MonitorDashboard />
  </div>
</template>

<script setup>
import MonitorDashboard from '../components/MonitorDashboard.vue';
</script>

<style scoped>
.monitor-view {
  padding: 20px;
}

h1 {
  font-size: 24px;
  margin-bottom: 20px;
  color: #333;
}
</style>

六、最佳实践

  1. 合理设置采样率:根据应用规模和服务器负载,设置合适的采样率,避免过多数据占用资源。
  2. 分级告警:根据问题严重程度设置不同级别的告警,避免告警风暴。
  3. 告警抑制:对于同一问题,设置合理的告警间隔,避免重复告警。
  4. 数据可视化:使用图表直观展示监控数据,便于分析和定位问题。
  5. 定期分析:定期分析监控数据,找出性能瓶颈和潜在问题。
  6. 持续优化:根据监控结果,持续优化应用性能和稳定性。

总结

在本集中,我们构建了一个完整的Vue 3监控与告警系统,包括:

  1. 性能指标体系设计,涵盖Web Vitals和自定义指标
  2. 前端监控数据采集类,用于收集性能数据和错误信息
  3. 实时监控面板,使用ECharts可视化展示监控数据
  4. 告警规则配置和告警引擎实现
  5. 集成到Vue 3应用的方法

通过这套监控与告警系统,我们可以实时了解应用性能状况,快速定位和解决问题,提高应用的可靠性和用户体验。

在下一集中,我们将探讨日志收集与分析系统的实现。

« 上一篇 Vue 3 与 WebRTC 高级应用:实时音视频通信实践 下一篇 » Vue 3 日志收集与分析:系统监控与问题排查