【无标题】ECharts 实现日志量异常检测:基线 ±Kσ 基带 + 灵敏度切换(Vue2 实战)

实现日志量异常检测:基线 ±Kσ 基带 + 灵敏度切换(Vue2 实战)

前言:为什么固定阈值不好用

做日志监控的同学大多配置过这样的告警:日志量超过 5000 条/分钟就报警。上线第一天就会发现,业务流量本身是有周期性的------白天高、夜里低、午间有个小高峰。固定阈值要么在白天业务高峰时疯狂误报,要么在深夜把真正的异常漏掉。

一、数据模型

先约定数据结构,后面所有逻辑都围绕它展开:

js 复制代码
{
  labels:   ['10:00', '10:05', ...],  // 时间轴,5 分钟粒度
  raw:      [2015, 1892, 2130, ...],  // 原始值(每分钟日志量)
  baseline: [1938, 1951, 1907, ...],  // 基线(历史同期规律)
  sigma:    60                         // 波动标准差
}

生产环境中 baselinesigma 一般由后端(或 Flink 任务)按历史同期数据计算好返回;前端拿到后只做一件事:

js 复制代码
const k = this.sensitivity            // 1 / 2 / 3
const upper = baseline.map(b => b + k * sigma)   // 基带上限
const lower = baseline.map(b => Math.max(0, b - k * sigma))  // 下限,防止为负

K 越小基带越窄、报警越灵敏;K 越大基带越宽、越保守。这就是"灵敏度调节"的全部原理。

二、基带怎么画:堆叠面积

ECharts 没有原生的"置信区间带",标准做法是用两个堆叠的 line 系列

  1. 第一个系列画 lower,线条和面积全部透明------它只是"垫底";
  2. 第二个系列画 upper - lower(差值),叠上去之后,它的面积就正好落在 lower 和 upper 之间。
js 复制代码
series: [
  // 垫底的 lower,不可见、不响应鼠标
  {
    name: '基带下限',
    type: 'line',
    stack: 'band',
    data: lower,
    symbol: 'none',
    silent: true,
    lineStyle: { opacity: 0 },
    areaStyle: { opacity: 0 }
  },
  // 差值 (upper - lower),半透明蓝色就是基带
  {
    name: '基带',
    type: 'line',
    stack: 'band',
    data: upper.map((u, i) => u - lower[i]),
    symbol: 'none',
    silent: true,
    lineStyle: { opacity: 0 },
    areaStyle: { color: 'rgba(37, 99, 235, 0.10)' }
  },
  // 基线:浅蓝虚线
  {
    name: '基线',
    type: 'line',
    data: baseline,
    smooth: true,
    symbol: 'none',
    lineStyle: { color: '#93c5fd', type: 'dashed', width: 2 }
  },
  // 原始值:蓝色实线
  {
    name: '原始值',
    type: 'line',
    data: raw,
    smooth: true,
    symbol: 'none',
    lineStyle: { color: '#2563eb', width: 2 },
    z: 5
  }
]

三、异常点:遍历 + scatter 标红

异常判定本身只有一行:v > upper[i] || v < lower[i]。把所有越带点收集成 [下标, 值] 数组,交给散点图:

js 复制代码
const anomalyPoints = []
raw.forEach((v, i) => {
  if (v > upper[i] || v < lower[i]) anomalyPoints.push([i, v])
})

// 追加到 series
{
  name: '异常点',
  type: 'scatter',
  data: anomalyPoints,        // [[54, 208], [55, 195], ...]
  symbolSize: 8,
  itemStyle: { color: '#dc2626' },
  z: 10
}

注意 scatter 的 data 用 [index, value] 二元组,它会自动对齐 category 轴的下标,不需要自己拼时间字符串。

四、悬浮提示:越基带时标红

tooltip.formatter 里可以拿到 dataIndex,反查四个数组就能同时展示原始值、基线、上下限,并按是否越带切换颜色:

js 复制代码
tooltip: {
  trigger: 'axis',
  formatter: (ps) => {
    const idx = ps[0].dataIndex
    const v = raw[idx]
    const out = v > upper[idx] || v < lower[idx]
    const flag = out ? ' <span style="color:#dc2626;font-weight:700">(越基带)</span>' : ''
    return `<div style="font-size:12px;line-height:1.8">
      <div style="font-weight:600">${labels[idx]}${flag}</div>
      <div style="color:${out ? '#dc2626' : '#1e293b'}">原始值:${v.toLocaleString()}</div>
      <div>基线:${baseline[idx].toLocaleString()}</div>
      <div>基带上限:${upper[idx].toLocaleString()}</div>
      <div>基带下限:${lower[idx].toLocaleString()}</div>
    </div>`
  }
}

五、灵敏度切换:一个变量驱动全部重算

把灵敏度 K 放在 data 里,radio 切换时重新调用渲染函数即可。顶部统计卡(越基带点数、最新状态)用 computed 联动:

js 复制代码
computed: {
  overviewStats() {
    const k = this.sensitivity
    const n = raw.length
    let total = 0, max = 0, outCount = 0
    raw.forEach((v, i) => {
      total += v
      if (v > max) max = v
      if (Math.abs(v - baseline[i]) > k * sigma) outCount++
    })
    const current = raw[n - 1]
    const lastOut = Math.abs(current - baseline[n - 1]) > k * sigma
    return { total, avg: Math.round(total / n), max, current,
             outCount, status: lastOut ? '越基带' : '正常' }
  }
}

同一个 K,既控制图表上基带的宽窄,也控制统计口径,两边天然一致。

六、几个踩坑点

1. 下限为负会拉长 y 轴。 日志量不可能为负,lower 计算时务必 Math.max(0, b - k * sigma),否则灵敏 ±1σ 模式下 y 轴会出现一大段无意义的负值区域。

2. 切换指标时旧系列残留。 从"日志量"切到"QPS"时 series 数量可能变化(比如异常点从 10 个变 0 个),要用 chart.setOption(option, true) 开启 notMerge,否则上一次的散点会残留在图上。

3. 垫底系列要 silent: true 堆叠基带的 lower 系列如果响应鼠标,tooltip 里会多出一条"基带下限"干扰信息,设为 silent 一了百了。

4. 记得 dispose。 单页应用里切走路由前 chart.dispose(),并移除 window.resize 监听,否则内存泄漏 + 重复初始化警告。

5. 数据缓存。 演示/调试时如果用随机数造数据,每次重算 sensitivity 都重新 generate 会导致曲线"抖动",按指标缓存一份数据,切换 K 值时只重算带和异常点,不重新造数。

七、演示数据怎么造

没有后端时,用"余弦周期 + 三角分布噪声 + 尾部骤降"可以造出非常接近真实流量的曲线:

js 复制代码
for (let i = 0; i <= 66; i++) {
  // 基线:周期性波动(12 个点一个周期 ≈ 1 小时)
  const b = base - amp * Math.cos((2 * Math.PI * i) / 12)
  baseline.push(Math.round(b))
  // 三角分布噪声:三个均匀随机数之和,中间值概率高,更像真实噪声
  const noise = (Math.random() + Math.random() + Math.random() - 1.5) / 1.5
  let v = b + noise * sigma
  if (i >= 54) v = anomalyValue + noise * sigma * 0.3   // 14:30 起骤降
  raw.push(Math.max(0, Math.round(v)))
}

三个 Math.random() 相加再归一化,得到的是近似正态的三角分布,比单一 Math.random() 的均匀噪声自然得多。

结语

整套实现的核心其实就三件事:堆叠面积画基带、scatter 标异常点、一个 K 值驱动全部重算。剩下的都是 ECharts 的常规配置。建议把它做成独立的图表组件,基线计算交给后端按"历史同期"(上周同时段、昨的同时段)滚动计算,前端只负责渲染和灵敏度交互,职责会很清晰。

你们的基线是用历史同期算的还是滑动平均?遇到周期性业务(比如整点抢购)时有没有更稳的基线算法?欢迎评论区聊聊。

相关推荐
用户3169353811833 小时前
SpringBoot + Vue 项目生产环境部署完整指南
前端·后端
Smile_ping3 小时前
自定义tooltip,解决图表数据重叠,无法准确点击问题!
echarts·自定义tooltip
Csvn3 小时前
🔍 TypeScript `satisfies` 操作符:既要类型安全,又要推断的原始字面量类型
前端
Csvn3 小时前
🚦 手写 Promise 并发控制:如何优雅限制异步请求的并发数?
前端
gb42152873 小时前
python中Web应用服务器
开发语言·前端·python
默_笙3 小时前
💌 为了把"主题色"传给孙子组件,我翻了五层楼——直到遇见了 useContext
前端·javascript
颜进强3 小时前
04 - 一张图看懂 OpenSpec 变更的一生:从创建到归档
前端·后端·ai编程
the_answer3 小时前
JS 垃圾回收机制
前端
小KK_3 小时前
JS 事件循环从小白视角入门:宏任务、微任务与 async/await 一网打尽
前端·javascript