在传统的日志建设里,大家关注的重点几乎都放在服务端:Nginx 访问日志、应用运行日志、数据库慢查询日志等等。但一个真实的线上问题,往往需要前端信息才能完整还原——用户用的是什么浏览器、点击了哪个按钮、请求在哪个环节失败、页面渲染到哪一步卡住。Graylog 作为一款功能完善的开源日志管理平台,提供了标准的 GELF 输入协议和强大的查询界面,非常适合承接前端日志。本文就以 Vue 3 项目为例,讲解如何从零搭建一条完整的前端日志采集与上报链路。

整体架构:前端日志从采集到可检索的完整链路
前端日志和后端日志最大的区别在于:浏览器里的代码不受你控制,网络随时可能中断,页面随时可能被关闭。所以整条链路的设计核心是"尽力上报、绝不阻塞业务"。一个典型的架构分为四层:采集层负责在 Vue 3 应用内部捕获错误和埋点事件;缓冲层把日志暂存在内存队列或 localStorage 中,避免高频请求;传输层负责把日志以 GELF 格式发送到服务端;存储与查询层就是 Graylog 本身。
这里有一个需要提前想清楚的决策点:浏览器不能直连 Graylog 的 GELF UDP 输入(浏览器没有 UDP 能力),HTTP GELF 输入虽然存在但通常不建议直接暴露给公网。工程上更稳妥的做法是在自己服务端加一个轻量的日志接收接口,或者使用 Nginx 做一层转发,由服务端把日志转投给 Graylog。这样既规避了跨域问题,也能在转发层做鉴权、限流和敏感信息脱敏。
日志的字段设计也很关键。除了常规的 message,建议利用 GELF 的附加字段能力,把 app_version、user_id、route、error_stack 等结构化信息带上,这样后续在 Graylog 里可以直接按字段做聚合查询和告警,而不是靠全文检索碰运气。
在 Vue 3 中实现日志采集:错误钩子、拦截器与组合式 API
Vue 3 提供了官方的全局错误处理入口 app.config.errorHandler,这是采集组件渲染错误的首选位置。相比 Vue 2,Vue 3 还新增了 warnHandler 和针对异步组件、事件处理器的错误捕获选项,覆盖面更完整。基础用法如下:
import { createApp } from 'vue'
import App from './App.vue'
import { logger } from './logger'
const app = createApp(App)
// 全局错误捕获:组件渲染、侦听器、生命周期钩子中的异常都会进入这里
app.config.errorHandler = (err, instance, info) => {
logger.error('vue_global_error', {
message: err.message,
stack: err.stack,
// info 是 Vue 内部标注的错误来源,比如 "setup function" 或 "render function"
error_type: info,
route: instance?.$route?.fullPath
})
}
// 未被捕获的 Promise 异常,比如忘了 await 的调用
window.addEventListener('unhandledrejection', (event) => {
logger.error('unhandled_rejection', {
reason: String(event.reason)
})
})
app.mount('#app')
接口层面的日志适合放在 axios 拦截器里统一处理。请求拦截器记录发起时间和关键参数,响应拦截器计算耗时并判断业务状态码,超时、5xx、业务错误码都可以按不同级别上报。这样不需要在每个业务代码里手写日志,维护成本低很多。
import axios from 'axios'
import { logger } from './logger'
const api = axios.create({ baseURL: '/api', timeout: 10000 })
api.interceptors.request.use((config) => {
config.metadata = { startTime: Date.now() }
return config
})
api.interceptors.response.use(
(response) => {
const cost = Date.now() - response.config.metadata.startTime
// 慢接口才记录,避免日志量爆炸
if (cost > 2000) {
logger.warn('slow_request', {
url: response.config.url,
cost_ms: cost
})
}
return response
},
(error) => {
logger.error('request_failed', {
url: error.config?.url,
status: error.response?.status,
message: error.message
})
return Promise.reject(error)
}
)
除了被动捕获错误,主动埋点也很重要。可以封装一个 useLogger 组合式函数,让业务组件在关键动作(提交表单、点击支付按钮)处记录事件,同时自动附带当前路由、用户标识等上下文,用起来和 Composition API 的风格保持一致。
上报封装:GELF 格式、批量发送与可靠性保障
GELF 是 Graylog 的标准日志格式,本质上是一个 JSON 结构。version、host、short_message 是必填字段,所有附加字段必须以下划线开头,比如 _user_id。下面是一个支持分级、缓冲和批量刷新的上报封装:
const LEVELS = { debug: 0, info: 1, warn: 3, error: 4 }
class GraylogClient {
constructor(options) {
this.endpoint = options.endpoint
this.host = options.host
this.buffer = []
this.timer = null
this.maxBatch = 10 // 攒够 10 条立即发送
this.maxWait = 5000 // 最长等待 5 秒强制发送
}
push(level, message, extra = {}) {
this.buffer.push({
version: '1.1',
host: this.host,
short_message: message,
timestamp: Date.now() / 1000,
level: LEVELS[level],
...Object.fromEntries(
Object.entries(extra).map(([k, v]) => ['_' + k, v])
)
})
if (this.buffer.length >= this.maxBatch) {
this.flush()
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.maxWait)
}
}
async flush() {
clearTimeout(this.timer)
this.timer = null
if (!this.buffer.length) return
const payload = this.buffer.splice(0)
try {
// sendBeacon 在页面关闭时也能可靠发出,且不阻塞主线程
if (navigator.sendBeacon) {
navigator.sendBeacon(this.endpoint, JSON.stringify(payload))
} else {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true
})
}
} catch (e) {
// 上报失败绝不能影响业务,静默丢弃或写入 localStorage 稍后重试
console.warn('log send failed', e)
}
}
}
export const logger = {
client: null,
init(options) {
this.client = new GraylogClient(options)
},
info(msg, extra) { this.client?.push('info', msg, extra) },
warn(msg, extra) { this.client?.push('warn', msg, extra) },
error(msg, extra) { this.client?.push('error', msg, extra) }
}
可靠性方面有几个细节值得强调。第一,错误堆栈和用户输入中可能包含超长内容,GELF 单条消息默认上限是 8KB,发送前应当截断。第二,日志失败本身不能再抛错,否则会出现"日志报错又触发日志"的死循环,所以整个发送逻辑必须用 try-catch 包住。第三,对于 error 级别的日志可以立即 flush,而 debug 和 info 则交给批量机制,减少请求数量。
采样和降级同样不可忽视。如果产品用户量大,info 级别的行为日志可以按用户 ID 哈希做 10% 采样;一旦接收端压力大,可以通过配置开关动态关闭 debug 日志。这些开关建议在应用初始化时从配置接口拉取,形成一套可远程控制的日志策略。
生产环境的治理:安全、成本与查询体验
日志体系上线后,治理问题会很快浮现。安全上,前端代码是公开的,绝不能把手机号、token、密码等敏感信息打进日志,可以在发送前用正则做一轮脱敏,服务端转发层再做二次过滤兜底。成本上,日志量直接决定 Graylog 的存储压力,建议按 level 设置不同的保留周期:error 保留 90 天,info 只保留 7 天。
查询体验决定了这套体系能否真正被用起来。在 Graylog 中为 _route、_app_version、_error_type 等字段建立索引字段,配置常用搜索的仪表盘,比如"近 1 小时 error 趋势"、"慢接口 TOP10"。再配合 Graylog 的告警功能,当 error 日志每分钟超过阈值时自动通知到企业微信或钉钉,就能把被动排查变成主动发现。
最后建议把整套日志能力抽成独立的 npm 包,包含采集、缓冲、上报、脱敏四个模块,通过初始化配置接入任意 Vue 3 项目。这样日志体系本身就是工程化的产物,后续接入新项目只需几行配置,维护成本会远低于每个项目各写一套。通过这套方案,前端日志将从零散的 console 输出,升级为可检索、可告警、可追溯的生产级基础设施。