提到 Logstash,大多数前端开发者的第一反应是这是运维或者后端同事的工具。但真实业务场景中,前端往往是日志数据的源头:页面停留时长、接口异常、白屏时间、用户点击路径,这些数据最终都要经过一道清洗管道才能落库分析。如果在 Vue 3 项目里随意地用 console.log 加一个 fetch 上报,日志格式混乱、丢失率高、排查困难的问题很快就会暴露出来。本文将围绕如何在 Vue 3 工程化体系中设计一套完整的前端日志采集与 Logstash 处理管道展开,从 SDK 封装到 Logstash 配置,完整走通整条链路。

一、整体链路设计:前端采集到 Logstash 再到 Elasticsearch
在动手写代码之前,先梳理清楚整条数据链路。典型的前端日志管道分为四层:第一层是 Vue 3 应用内的采集层,负责捕获错误、性能指标和业务埋点;第二层是传输层,通常是一个轻量的上报接口或直接对接 Logstash 的 HTTP 输入插件;第三层是 Logstash 的处理层,完成解析、过滤、富化和路由;第四层是存储与消费层,一般是 Elasticsearch 加 Kibana 做可视化。
这样分层的好处是职责清晰。前端只关心把结构化的事件发出去,不关心后端怎么存储;Logstash 只关心数据加工,不做业务判断;Elasticsearch 只负责存储和检索。任何一层出问题都不会波及其他层,比如 Logstash 重启期间,前端的上报队列可以在本地暂存,恢复后重发。
需要特别注意的是,Logstash 提供了 http input 插件,可以直接接收前端请求,但在生产环境更推荐中间加一层 Nginx 或网关做缓冲与鉴权,避免 Logstash 端口直接暴露在公网被恶意灌数据。下面是一份最小可用的 Logstash 管道配置:
input {
http {
port => 5044
codec => json
response_code => 204
}
}
filter {
mutate {
convert => {
"duration" => "integer"
"timestamp" => "float"
}
}
date {
match => ["timestamp", "UNIX_MS"]
target => "@timestamp"
}
}
output {
elasticsearch {
hosts => ["http://127.0.0.1:9200"]
index => "frontend-logs-%{+YYYY.MM.dd}"
}
}这份配置里,codec => json 让 Logstash 自动把请求体解析成 JSON 对象,mutate 负责类型转换,date 插件把前端传来的毫秒时间戳规范化为 @timestamp 字段,最后按天建索引写入 Elasticsearch。整个管道没有复杂的正则解析,因为格式化的工作应该尽量在前端完成,这能显著降低 Logstash 的 CPU 消耗。
二、在 Vue 3 中封装类型安全的日志采集 SDK
前端采集层的核心是一个独立的 SDK 模块。借助 Vue 3 推崇的 Composition API 和 TypeScript,可以把日志事件定义成严格的联合类型,从源头杜绝脏数据。首先定义事件模型:
// types/log.ts
export interface BaseEvent {
eventId: string
eventType: 'error' | 'performance' | 'behavior' | 'api'
timestamp: number
page: string
userId?: string
sessionId: string
}
export interface ErrorEvent extends BaseEvent {
eventType: 'error'
message: string
stack?: string
component?: string
}
export interface PerformanceEvent extends BaseEvent {
eventType: 'performance'
metric: 'FCP' | 'LCP' | 'CLS' | 'TTFB'
value: number
}
export interface ApiEvent extends BaseEvent {
eventType: 'api'
url: string
method: string
status: number
duration: number
}
export type LogEvent = ErrorEvent | PerformanceEvent | ApiEvent类型定义好之后,封装一个发送器。关键设计点是批量发送和失败重试:如果每产生一条日志就发一次 HTTP 请求,既浪费带宽,也容易在高频操作场景下把浏览器连接数打满。合理的做法是在内存里维护一个队列,达到阈值或定时器到期时批量提交:
// utils/logger.ts
import type { LogEvent } from '@/types/log'
const QUEUE_LIMIT = 10
const FLUSH_INTERVAL = 5000
const ENDPOINT = '/log-collect'
class LogPipeline {
private queue: LogEvent[] = []
private timer: number | null = null
push(event: LogEvent) {
this.queue.push(event)
if (this.queue.length >= QUEUE_LIMIT) {
this.flush()
} else if (this.timer === null) {
this.timer = window.setTimeout(() => this.flush(), FLUSH_INTERVAL)
}
}
async flush() {
if (this.timer !== null) {
clearTimeout(this.timer)
this.timer = null
}
if (this.queue.length === 0) return
const batch = this.queue.splice(0, this.queue.length)
try {
await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events: batch }),
keepalive: true
})
} catch {
// 失败时重新入队,下次一起发送,并限制重试总量防止内存膨胀
if (this.queue.length + batch.length < 200) {
this.queue.unshift(...batch)
}
}
}
}
export const logger = new LogPipeline()这里的 keepalive: true 值得一提,它允许请求在页面卸载后仍然继续发送,解决了用户关闭标签页导致最后一批日志丢失的经典问题。另外重试逻辑设置了总量上限,避免 Logstash 长时间宕机时队列无限增长把页面内存撑爆。
接下来把采集能力接入 Vue 3 的生命周期。错误捕获可以用 app.config.errorHandler 拦组件异常,用 window.addEventListener('unhandledrejection') 抓未处理的 Promise 拒绝:
// plugins/logPlugin.ts
import type { App } from 'vue'
import { logger } from '@/utils/logger'
function genId() {
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
}
export function setupLogPlugin(app: App, sessionId: string) {
app.config.errorHandler = (err, instance, info) => {
logger.push({
eventId: genId(),
eventType: 'error',
timestamp: Date.now(),
page: location.pathname,
sessionId,
message: String(err),
stack: (err as Error)?.stack,
component: instance?.$options?.name || 'Anonymous'
})
}
window.addEventListener('unhandledrejection', (e) => {
logger.push({
eventId: genId(),
eventType: 'error',
timestamp: Date.now(),
page: location.pathname,
sessionId,
message: `UnhandledRejection: ${e.reason}`
})
})
}在 main.ts 中注册这个插件即可全局生效。这种集中式的错误拦截方式比在每个组件里写 try-catch 干净得多,而且能拿到组件名信息,方便 Logstash 侧按组件维度聚合统计错误分布。
三、Logstash 过滤器进阶:条件路由与数据富化
当管道里的数据量上来之后,单一 filter 就不够用了。不同类型的事件需要不同的加工策略:错误事件要提取堆栈首行、标记严重级别;性能事件要和阈值比较生成告警标记;API 事件要按耗时打分。Logstash 的条件语法可以很好支撑这种分流:
filter {
if [eventType] == 'error' {
# 提取错误堆栈的第一行作为摘要字段
grok {
match => { "message" => "(?<errorSummary>^[^\r\n]+)" }
}
mutate {
add_field => { "severity" => "high" }
}
} else if [eventType] == 'performance' {
if [metric] == 'LCP' and [value] > 2500 {
mutate { add_field => { "severity" => "medium" } }
} else {
mutate { add_field => { "severity" => "low" } }
}
} else if [eventType] == 'api' {
if [duration] >= 1000 {
mutate { add_tag => ["slow-api"] }
}
if [status] >= 500 {
mutate { add_tag => ["server-error"] }
}
}
# 利用 translate 插件做数据富化,例如根据页面路径补充业务模块名
translate {
source => "page"
target => "module"
dictionary => {
"/home" => "首页"
"/order" => "订单中心"
"/user" => "个人中心"
}
fallback => "其他页面"
}
}条件路由的价值在于让 output 阶段可以做精细化分发。比如打上 server-error 标签的事件可以额外输出一份到告警系统,普通行为日志只进 Elasticsearch。这种按需分流的架构比所有数据全量写一个大索引要高效得多,查询时也不用在海量低价值数据里捞关键错误。
output 阶段同样支持条件判断,配合多路输出可以让告警链路和存档链路解耦:
output {
if "server-error" in [tags] {
http {
url => "https://alert.internal.ipipp.com/api/webhook"
http_method => post
format => "json"
}
}
elasticsearch {
hosts => ["http://127.0.0.1:9200"]
index => "frontend-%{eventType}-%{+YYYY.MM.dd}"
}
}注意索引名里用了 %{eventType},这样错误、性能、行为数据各自落到独立索引,Elasticsearch 的分片管理 和 生命周期策略都可以按类型独立配置,长期行为日志可以设置更激进的保留周期,错误日志则保留更久。
四、开发联调与常见坑点排查
前端和 Logstash 联调时,最常见的坑是跨域问题。本地开发时 Vue DevServer 跑在 localhost:5173,Logstash 的 http input 默认不返回 CORS 头,浏览器会直接拦截请求。解决方法有两个:一是在 vite.config.ts 里配置代理,把上报路径转发到 Logstash;二是给 Logstash 配置 response_headers 补充跨域头。开发环境推荐用代理,代码示例如下:
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/log-collect': {
target: 'http://127.0.0.1:5044',
changeOrigin: true
}
}
}
})第二个坑是时间时区问题。前端用 Date.now() 传毫秒时间戳本身没有时区概念,但一旦有人在采集层改用 new Date().toISOString() 字符串,Logstash 的 date 插件如果按 UNIX_MS 解析就会失败,事件会被打上 _dateparsefailure 标签。建议全链路统一用毫秒时间戳,解析失败的事件在 Kibana 里检索这个标签就能快速定位。
第三个坑是批量上报的请求体大小。一次发送几十条带堆栈的错误事件,JSON 可能达到几百 KB,如果中间有 Nginx,默认的 client_max_body_size 只有 1MB,容易在高错误率场景触发 413。除了调大限制,更好的做法是在前端对超长堆栈做截断,只保留前若干行,配合 Logstash 侧的完整信息查日志即可。
最后是日志量控制。开发环境下如果不做区分,热更新一次页面就产生大量噪音数据。可以通过环境变量区分采集端点,开发环境指向本地的一个测试 Logstash 实例,或者在 SDK 内部加采样率开关,生产环境对行为类事件按 10% 采样,错误和性能事件保持全量上报。这样既保证了核心数据的完整性,又控制了 Elasticsearch 的存储成本,是这套管道长期稳定运行的关键保障。