前端应用上线之后,最让人头疼的事情莫过于用户反馈页面白屏或按钮点击没反应,但你翻遍自己的开发环境都复现不出来。没有日志系统的前端项目就像一个黑盒,线上问题只能靠猜。Vue 3 提供了完善的全局错误处理钩子,配合 ELK Stack 强大的日志收集与分析能力,可以低成本搭建一套完整的工程化日志体系。本文将从前端采集、传输、存储到可视化分析,完整走一遍方案落地的全过程。
一、Vue 3 端:统一封装日志采集模块
搭建日志系统的第一步是在前端建立统一的采集入口。Vue 3 中最核心的错误捕获点是 app.config.errorHandler,任何组件内部抛出的未捕获异常都会流经这里。我们不应该在业务代码里到处写 try-catch,而是把采集逻辑集中到一处,业务代码保持干净。
先设计一个简单的日志上报类,它负责给每条日志附加公共元信息,比如应用版本、用户标识、页面路由、设备信息等,并控制上报频率,避免日志风暴拖垮服务端。
class Logger {
constructor(options) {
this.endpoint = options.endpoint;
this.appVersion = options.appVersion;
this.queue = [];
this.timer = null;
}
// 组装公共字段
buildPayload(level, message, extra = {}) {
return {
level,
message,
appVersion: this.appVersion,
userAgent: navigator.userAgent,
pageUrl: location.href,
timestamp: new Date().toISOString(),
...extra
};
}
log(level, message, extra) {
this.queue.push(this.buildPayload(level, message, extra));
// 队列攒够 10 条或 3 秒后批量上报,减少请求数
if (this.queue.length >= 10) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), 3000);
}
}
async flush() {
clearTimeout(this.timer);
this.timer = null;
if (this.queue.length === 0) return;
const logs = this.queue.splice(0);
try {
// 使用 sendBeacon 优先,页面关闭时也能保证发出
if (navigator.sendBeacon) {
navigator.sendBeacon(this.endpoint, JSON.stringify(logs));
} else {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(logs),
keepalive: true
});
}
} catch (e) {
// 上报失败不能影响主流程,更不能递归报错
console.warn('日志上报失败', e);
}
}
}
export const logger = new Logger({
endpoint: '/api/logs',
appVersion: __APP_VERSION__
});封装好之后,在应用入口把 Vue 的全局错误处理接进来。除了 errorHandler,还要注意 window.onerror 和 unhandledrejection 这两个全局事件,它们能兜住 Vue 体系之外的错误,比如异步 Promise 拒绝、第三方脚本抛错等。Source Map 的处理也不能忽视,线上代码通常经过压缩混淆,需要在构建时保留 map 文件,并在服务端上传到 Sentry 之类的解析服务,或者自己写脚本反解,否则收到的堆栈信息基本没有可读性。
import { createApp } from 'vue';
import App from './App.vue';
import { logger } from './logger';
const app = createApp(App);
// Vue 组件内的错误统一走这里
app.config.errorHandler = (err, instance, info) => {
logger.log('error', err.message, {
stack: err.stack,
// info 是 Vue 提供的错误来源信息,如 "setup function"
errorInfo: info,
component: instance?.$options?.name || 'Anonymous'
});
};
// 捕获未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', (event) => {
logger.log('error', `UnhandledRejection: ${event.reason}`, {
stack: event.reason?.stack
});
});
// 捕获资源加载失败与运行时错误
window.addEventListener('error', (event) => {
if (event.target && event.target.tagName) {
// 资源加载失败,比如图片、脚本 404
logger.log('warn', `ResourceError: ${event.target.tagName}`, {
src: event.target.src || event.target.href
});
} else {
logger.log('error', event.message, { stack: event.error?.stack });
}
}, true);
app.mount('#app');除了错误日志,业务埋点日志也建议走同一个通道。比如用户点击关键按钮、页面停留时长、路由切换记录,统一以 info 级别上报。这样在 Kibana 里既能看错误也能看用户行为,排查问题时可以把错误和用户操作轨迹串起来还原现场,价值远大于单纯收集报错。
二、接口异常采集与日志传输链路
实际项目中相当一部分前端问题是接口层面的:超时、返回异常状态码、数据结构不符合预期等。在 axios 拦截器里做统一采集是最优雅的方式,请求和响应两个拦截器配合,能拿到完整的上下文。请求拦截器记录请求方法、URL 和耗时起点,响应拦截器计算耗时并判断是否需要上报。
import axios from 'axios';
import { logger } from './logger';
const http = axios.create({ timeout: 15000 });
http.interceptors.request.use((config) => {
config.metadata = { startTime: Date.now() };
return config;
});
http.interceptors.response.use(
(response) => {
const cost = Date.now() - response.config.metadata.startTime;
// 慢接口也算健康问题,超过 3 秒记为 warn
if (cost > 3000) {
logger.log('warn', 'Slow API detected', {
url: response.config.url,
cost,
status: response.status
});
}
return response;
},
(error) => {
logger.log('error', 'API Error', {
url: error.config?.url,
method: error.config?.method,
status: error.response?.status,
message: error.message,
// 请求被取消的不算错误,避免误报
canceled: axios.isCancel(error)
});
return Promise.reject(error);
}
);日志从前端到达 Elasticsearch 通常有两种链路。第一种是前端直接上报到一个后端接口,后端写入本地日志文件或直接调 Elasticsearch 的 REST API。这种方式实现简单,但日志格式需要后端严格配合。第二种是更标准的做法:后端把日志写成文件,由 Filebeat 采集后送入 Logstash 做解析过滤,最后写入 Elasticsearch。Logstash 在这条链路里承担清洗工作,比如把 JSON 格式的日志体解析成独立字段、剔除不需要的字段、统一时间戳格式。
需要注意上报接口本身的性能和稳定性。日志接口应该尽量轻量,不执行复杂业务逻辑,失败时直接丢弃即可,绝不能让日志上报反过来影响用户体验。如果采用 sendBeacon,浏览器会在页面卸载时异步发送,不会阻塞跳转,这是移动端弱网环境下非常实用的特性。另外建议给上报接口做采样控制,比如 PV 级别的日志采样 100%,纯行为日志采样 10%,可以有效控制存储成本。
三、ELK 端配置与 Kibana 可视化分析
ELK Stack 由 Elasticsearch、Logstash、Kibana 三部分组成,Elasticsearch 负责存储与检索,Logstash 负责解析转换,Kibana 负责可视化。下面给出一段 Logstash 的配置,把前端上报的 JSON 日志拆解成结构化字段,并按日志级别打标签。
input {
beats {
port => 5044
}
}
filter {
json {
source => "message"
}
# 把纯文本日志按换行拆成多条
mutate {
split => { "message" => "\n" }
}
# 只保留有意义的字段,减轻索引负担
mutate {
remove_field => ["host", "agent", "ecs", "tags"]
}
# 解析 ISO8601 时间戳作为索引时间
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
}
}
output {
elasticsearch {
hosts => ["http://127.0.0.1:9200"]
index => "vue-frontend-logs-%{+YYYY.MM.dd}"
}
}索引按天滚动是日志系统的通用实践,配合索引生命周期管理策略,可以设置日志保留 30 天后自动删除,避免磁盘无限膨胀。进入 Kibana 后,先创建索引模式匹配 vue-frontend-logs-*,之后就可以用 Kibibana 的 Discover 页面对日志做全文检索。常用的查询比如 level:error AND pageUrl:*checkout*,可以快速定位支付结算页的所有错误。
可视化层面建议搭建三个核心面板:一是错误趋势图,按天聚合 error 级别日志数量,用于判断新版本发布后错误是否上升;二是错误类型排行,按 message 聚合的 Top 10,快速找到最高频问题;三是接口健康面板,展示慢接口占比和失败率。更进一步可以配置 Kibana 的告警规则,比如五分钟内 error 日志超过 50 条就通过 Webhook 通知企业微信或钉钉群,实现从被动排查到主动响应的转变。
四、方案细节优化与常见坑
落地过程中有几个容易踩的坑值得提前规避。第一是循环上报问题:如果日志上报接口本身走的是被拦截的 axios 实例,接口失败会触发错误日志,错误日志又去请求接口,形成死循环。解决办法是给日志请求单独创建一个不走拦截器的 axios 实例,或者直接用原生 fetch。
第二是敏感信息过滤。日志里可能混入用户输入的密码、token 等内容,上报前必须在 buildPayload 里做字段白名单过滤,Logstash 侧也可以再加一层 gsub 正则清洗,双保险更稳妥。第三是版本关联,每条日志都带上 appVersion 后,在 Kibana 里可以按版本维度拆分错误数据,发布新版本时对比新旧版本的错误率,一旦异常立即回滚,这就是工程化日志系统真正的价值所在。
最后提醒一点,日志系统的建设应该循序渐进。初期先把错误采集和 Elasticsearch 存储跑通,确认数据可用后再补充行为日志和告警体系,不要一开始就追求大而全,否则容易在复杂链路里迷失,反而延迟了核心价值的交付。