单页应用的前端性能与稳定性数据往往分散在浏览器控制台和用户反馈里,开发团队很难在故障初期获得结构化指标。Prometheus 虽然原生面向服务端,但通过工程化设计同样可以采集 Vue 3 应用的关键运行数据。核心思路是前端采集指标、后端聚合暴露、Prometheus 拉取存储、Alertmanager 触发告警。本文从指标设计、插件封装、抓取与告警配置、实践注意事项四个层面展开。

一、前端监控指标设计:明确采集对象与数据模型
前端监控与后端监控的差异在于数据来源是真实用户的浏览器环境,而不是长期运行的进程。因此指标设计要覆盖页面加载、路由切换、接口调用、资源加载和运行时异常这五类核心场景。Prometheus 提供了 Counter、Gauge、Histogram 和 Summary 四种原生指标类型,前端工程中通常使用 Histogram 来统计耗时分布,用 Counter 来累计错误次数,用 Gauge 记录当前活跃会话数。延迟类指标如果只记录平均值会掩盖尾部延迟,使用 Histogram 配合分位数可以更真实地反映用户体验。
下面是一组推荐的前端监控指标,它们既符合 Prometheus 命名规范,也能覆盖大部分 Vue 3 应用的观测需求。
| 指标名称 | 类型 | 标签 | 采集来源 |
|---|---|---|---|
| vue_page_load_duration_seconds | Histogram | route, device_type | PerformanceObserver |
| vue_route_change_duration_seconds | Histogram | from, to | Vue Router 守卫 |
| vue_http_request_duration_seconds | Histogram | method, route, status | Axios 拦截器 |
| vue_http_request_errors_total | Counter | method, route, error_type | Axios 拦截器 |
| vue_resource_load_failures_total | Counter | resource_type | 资源加载错误监听 |
| vue_js_error_total | Counter | error_type, route | 全局错误监听 |
| vue_page_view_total | Counter | route | 路由切换 |
| vue_active_sessions | Gauge | 无 | 页面展示与卸载 |
标签设计是决定 Prometheus 存储压力与查询效率的关键。务必避免把完整 URL、用户 ID、原始 User-Agent 这类高基数值放入标签。路由标签应使用规范化后的路由名称,例如 /user/:id 而不是 /user/1024。接口标签同理,只保留方法、路由模式和状态码,不要带查询参数。这样既能控制时间序列数量,又能让聚合查询具有业务意义。
下面是一段基于 PerformanceObserver 的采集示例,它在浏览器空闲时记录核心 Web Vitals 数据,并通过自定义事件交给监控插件处理。
// 采集 LCP、FID、CLS 等核心 Web Vitals
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'largest-contentful-paint') {
window.dispatchEvent(new CustomEvent('monitor:metric', {
detail: { name: 'vue_page_load_duration_seconds', value: entry.startTime / 1000 }
}));
}
if (entry.entryType === 'first-input') {
window.dispatchEvent(new CustomEvent('monitor:metric', {
detail: { name: 'vue_page_input_delay_seconds', value: entry.processingStart - entry.startTime }
}));
}
}
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.observe({ type: 'first-input', buffered: true });
二、Vue 3 工程化封装:监控插件与指标上报
Vue 3 的插件机制非常适合承载监控逻辑。通过 app.use 注册一个监控插件,可以在应用启动时初始化指标注册表,并利用全局属性向组件暴露轻量级 API。路由守卫是接入路由切换耗时的天然位置:在 beforeEach 记录起始时间,在 afterEach 中计算差值并写入 Histogram。对于 HTTP 请求,Axios 拦截器可以统一捕获请求耗时、状态码和网络错误,避免在每个服务函数中手动埋点。
下面是一个 Vue 3 监控插件的简化实现,它负责采集路由切换耗时、运行时错误和页面浏览计数,并通过 HTTP POST 将指标增量发送到聚合层。
// monitorPlugin.js
export default {
install(app, options) {
const endpoint = options.endpoint || '/api/metrics';
const routeDuration = {};
// 路由切换耗时
app.config.globalProperties.$router.beforeEach((to, from) => {
routeDuration.start = performance.now();
});
app.config.globalProperties.$router.afterEach((to, from) => {
if (routeDuration.start) {
const duration = (performance.now() - routeDuration.start) / 1000;
sendMetric(endpoint, 'vue_route_change_duration_seconds', duration, {
from: from.name || 'unknown',
to: to.name || 'unknown'
});
}
});
// 全局 JS 错误
window.addEventListener('error', (event) => {
sendMetric(endpoint, 'vue_js_error_total', 1, {
error_type: event.error ? event.error.name : 'unknown',
route: app.config.globalProperties.$router.currentRoute.value.name || 'unknown'
});
});
// 页面浏览计数
app.config.globalProperties.$router.afterEach((to) => {
sendMetric(endpoint, 'vue_page_view_total', 1, { route: to.name || 'unknown' });
});
function sendMetric(url, name, value, labels) {
if (navigator.sendBeacon) {
navigator.sendBeacon(url, JSON.stringify({ name, value, labels }));
} else {
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, value, labels })
});
}
}
}
};
由于浏览器实例生命周期短且无法被 Prometheus 直接抓取,通常需要引入一个聚合层。聚合层接收前端上报的指标增量,在服务端维护 Prometheus 指标注册表,并暴露标准 /metrics 端点。这种方式比 Pushgateway 更适合高频前端指标,因为聚合层可以对重复标签进行聚合,且不需要手动清理过期数据。下面使用 Express 和 prom-client 实现一个极简聚合层。
// metricsServer.js
const express = require('express');
const promClient = require('prom-client');
const app = express();
app.use(express.json());
const registry = new promClient.Registry();
promClient.collectDefaultMetrics({ register: registry });
const httpDuration = new promClient.Histogram({
name: 'vue_http_request_duration_seconds',
help: 'Duration of Vue HTTP requests',
labelNames: ['method', 'route', 'status'],
buckets: [0.05, 0.1, 0.3, 0.5, 1, 2, 5, 10],
registers: [registry]
});
const jsErrorTotal = new promClient.Counter({
name: 'vue_js_error_total',
help: 'Total number of Vue JS errors',
labelNames: ['error_type', 'route'],
registers: [registry]
});
const pageViewTotal = new promClient.Counter({
name: 'vue_page_view_total',
help: 'Total page views',
labelNames: ['route'],
registers: [registry]
});
app.post('/api/metrics', (req, res) => {
const { name, value, labels } = req.body || {};
try {
if (name === 'vue_http_request_duration_seconds') {
httpDuration.observe(labels, value);
} else if (name === 'vue_js_error_total') {
jsErrorTotal.inc(labels, value);
} else if (name === 'vue_page_view_total') {
pageViewTotal.inc(labels, value);
}
res.status(204).end();
} catch (err) {
res.status
Vue_3Prometheus前端监控修改时间:2026-08-13 05:04:57