首页 / 资讯中心 / 文章详情

基于日志的监控告警:我们是怎么用一套轻量方案把线上问题“盯“住的

基于日志的监控告警:我们是怎么用一套轻量方案把线上问题“盯“住的 ★ FEATURED ARTICLE
业务接口反馈慢但从监控面板CPU、内存、QPS 全都绿绿的看不出哪里不对。最后查日志发现某个下游服务调用耗时飙到了 5 秒但因为它没报错只是慢所以传统的指标监控根本没捕捉到。指标监控擅长告诉你系统是不是挂了但对于系统是不是慢了、流程是不是卡住了这类灰度异常往往力不从心。我们团队之前也一直被这个问题困扰后来干脆自己搞了一套基于日志内容的监控告警服务。思路很直接既然日志里什么信息都有那就直接从日志里把关键数值抠出来跟预设阈值做对比超了就报警。当然市面上有很多日志监控但很大的问题是只能基于关键字匹配存在来告警而无法按模板提取其中的数值来告警 这就是痛点。当然有人可能会问为什么不基于prometheus来做 一是基于prometheus的话就需要服务全部集成prometheus的sdk打点,再将指标全部搜集从服务数量和时间上来看周期比较长成本比较高。二是我们有很多服务都是陈年老服务 百把年都不会更新的 而且之前已经打了日志那基于日志分析就是最省事最快的方式而且监控只是个旁路监控不影响业务对新老服务都兼容。所以基于此方案跑了一段时间下来效果还不错这里聊一聊整个方案的思路和部分实现细节。整体思路定时拉日志 → 模板匹配 → 阈值判断 → 告警通知先说大流程。整个服务的工作方式可以用一句话概括每隔一段时间从 ES 里捞出最近 N 分钟的日志用 Grok 模板从日志文本中提取关键变量再用表达式引擎判断是否超过阈值超了就通过钉钉机器人推一条消息。画成流程图大概是这样的定时调度 → 从 Redis 读取告警规则配置 → 遍历每条规则异步查询 ES 日志 → 拿到日志后多线程进行 Grok 模板匹配 阈值表达式计算 → 需要告警检查 Redis 去重 → 发送钉钉消息 → 记录已告警没有引入什么重型组件核心依赖就是 ES Redis 钉钉部署起来也很轻量。规则配置一条告警规则长什么样我们把告警规则做成了 JSON 数组的形式存在 Redis 里key 是logmonitor:conf。运维同事可以直接在后台管理界面上增删改规则不需要重启服务。一条典型的规则大概长这样{ servername: base—outstore, is_alarm: 1, alarm_threshold: costtime2000, where: run-costTime, template: run-costTime:%{DATA:costtime} consume, alarmDingDingCode: dingtalk-log, limit_time: 5, title: 运行耗时超过阈值, link: http://10.3.87.1:3001/goto/4uNzKjkNz?orgId1, atMobiles: 15512345678 }几个关键字段解释一下servername要监控哪个服务。我们会用这个字段去 ES 里按服务名过滤日志。where日志里必须包含的关键字。相当于一层粗过滤先把无关日志筛掉。template这是核心——一个 Grok 表达式。%{DATA:costtime}的意思是从日志文本中匹配出一段内容赋值给变量costtime。这个变量后面会参与阈值计算。alarm_threshold阈值表达式直接写 Java 的条件表达式就行比如costtime2000意思是匹配出来的耗时超过 2000 毫秒就触发告警。limit_time往前查多少分钟的日志。设 5 就查最近 5 分钟的。is_alarm开关。设成 0 这条规则就静默了方便灰度上线或者临时关闭。atMobiles钉钉 谁的手机号多个用逗号隔开。说实话这套配置设计谈不上多优雅但胜在直观。新同事看一眼就知道这条规则在干什么监控base-outstore服务找到包含run-costTime的日志把耗时提取出来超过 2 秒就告警。日志从哪来ES 查询的 DSL 长什么样我们的日志采集链路是标准的那套应用打日志 → Filebeat/Logstash 采集 → 写入 ES。索引按join-app-*的方式命名告警服务直接从 ES 里查。具体查询的 DSL 放在esmapper.xml里用类似 MyBatis 的方式管理。拿一条规则的查询来说{ query: { bool: { filter: [ { range: { timestamp.keyword: { gte: startTime, lte: endTime } } } ], must: [ { term: { servername.keyword: { value: base-outstore } } }, { match_phrase: { msg: run-costTime } } ] } }, sort: [{ timestamp.keyword: { order: asc } }], size: 10000 }逻辑很简单时间范围 服务名 关键字匹配。match_phrase保证日志消息中必须包含where字段指定的短语。这样一轮查下来基本就能把目标日志精准地筛出来。代码层面查询条件被封装成了EsLogQueryCondition对象由SearchLogForMonitorAlarmService根据规则自动组装时间范围和服务名然后通过 bboss一个 ES 客户端框架发起查询。核心处理Grok 模板匹配 QLExpress 表达式引擎这是整个方案里我觉得最有意思的部分。拿到日志之后怎么从一行文本里把耗时这个数值提取出来我们用的是Grok。这个东西搞过 ELK 的人应该不陌生Logstash 里用它来解析日志格式本质上就是一组命名正则表达式。比如模板run-costTime:%{DATA:costtime} consume面对这样一行日志run-costTime:3500 consume orderFlowGrok 会把它解析成一个 Map{costtime: 3500}。代码实现上用的是io.krakens这个 Java 版 Grok 库。先编译模板再用 match 方法去匹配日志内容最后通过 capture 拿到捕获的变量GrokCompiler grokCompiler GrokCompiler.newInstance(); grokCompiler.registerDefaultPatterns(); Grok grok grokCompiler.compile(template); Match grokMatch grok.match(message); MapString, Object result grokMatch.capture();拿到变量之后下一步就是判断是否超阈值。这里我们引入了阿里的QLExpress表达式引擎。它的好处是可以直接执行 Java 语法的表达式而且支持动态注入变量。我们把 Grok 匹配出来的变量塞进上下文然后直接执行alarm_threshold里配的表达式ExpressRunner runner new ExpressRunner(); DefaultContextString, Object context new DefaultContext(); // 把 Grok 匹配出的变量放进去能转数字就转数字 for (Map.EntryString, Object entry : grokResultMap.entrySet()) { try { context.put(entry.getKey(), Double.parseDouble((String) entry.getValue())); } catch (Exception e) { context.put(entry.getKey(), entry.getValue()); } } // 执行表达式返回布尔值 Boolean isNeedAlarm (Boolean) runner.execute(costtime2000, context, null, true, false);这么做的灵活性在于alarm_threshold可以写得很复杂。比如costtime2000 retryCount3甚至调用外部 Java 方法QLExpress 都支持。不需要改代码只需要在配置里调整表达式就行。去重机制同一条日志不要反复告警这个问题其实很关键。你想想定时任务每隔几分钟跑一次如果一条日志已经触发过告警了下一轮跑的时候它还在时间窗口内难道再报一次运维同事估计会疯掉。我们的做法是在 Redis 里用日志 ID 做一个去重标记。key 的格式是logmonitor:id:{日志ID}value 随便填个 1 就行关键是设置过期时间——跟规则的limit_time保持一致。// 钉钉发送成功后记录已告警 stringRedisTemplate.opsForValue().set( logmonitor:id: esLogInfo.getId(), esLogInfo.getId(), logMonitorRule.getLimit_time() * 60, TimeUnit.SECONDS );而且在进入 Grok 匹配之前会先做一轮预过滤——如果 Redis 里已经有这个日志 ID 的标记直接跳过连线程资源都不占if (stringRedisTemplate.opsForValue().get(logmonitor:id: esLogInfo.getId()) ! null) { continue; }这个设计虽然简单但确实管用。既避免了重复告警的骚扰又减少了不必要的计算开销。当然我们的匹配规则有好几种和阈值比较的操作类型目前支持数值型: ,,,,,rang字符型eq、neq、包含比如alarm_threshold:data.contains(异常数超阈值) 其中还可以有条件组合。多线程架构两层线程池分工考虑到告警规则可能有很多条每条规则查出来的日志也可能有成百上千条如果串行处理一轮跑下来可能十几分钟都跑不完。所以我们用了两层线程池asyncLogSearchTaskPool负责 ES 查询。每条规则一个异步任务并行去 ES 拉数据。asyncLogCheckTaskPool负责日志匹配和告警判断。每条日志一个异步任务并行做 Grok 匹配 表达式计算。线程池参数可以通过配置文件调整默认核心线程数 50最大线程数 100队列容量 200拒绝策略用的CallerRunsPolicy——队列满了就让调用线程自己干至少不会丢任务。这种两层异步的设计让整轮告警检查通常能在 1-2 分钟内跑完即使有十几条规则、上万条日志也不至于卡住。告警通知钉钉机器人推送告警的最后一环是把消息送出去。我们对接的是钉钉机器人通过内部的消息服务FeignJoinMeshMsgServer发送。告警内容会拼成一段文本包含告警标题、阈值表达式的实际计算值、服务名、时间戳、日志摘要以及一个跳转链接一般指向 Grafana 面板或者 Kibana 的日志详情页xx服务运行耗时超过阈值,告警规则匹配值:35002000;服务名:base-outstore;时间:2025-09-24T10:30:15;日志内容:run-costTime:3500 consume orderFlow...;详情链接:http://10.3.87.1:3001/goto/4uNzKjkNz如果日志内容超过 200 个字符会自动截断避免钉钉消息太长影响阅读。atMobiles字段指定要 的人运维值班同学的手机号直接配在规则里。几点实践中的体会1. 规则不要一上来就配一堆。刚开始做的时候我们恨不得把每种异常日志都配一条规则结果每天收几十条告警到后来大家直接屏蔽了告警群。后来我们做了精简只保留真正需要关注的场景告警数量降下来了响应速度反而上去了。2.limit_time和定时调度间隔要配合好。如果定时任务 10 分钟跑一次但limit_time只设了 5 分钟中间就有 5 分钟的盲区。我们一般把limit_time设成大于等于调度间隔宁可有一点重叠也别漏。3. Grok 模板的写法需要调试。Grok 表达式写起来不难但有时候日志格式变了会导致匹配失败。建议加规则之前先用几条真实日志跑一下匹配确认能正确捕获变量再上线。4. 阈值表达式别搞太复杂。QLExpress 虽然支持很复杂的表达式但规则太复杂了不容易维护。简单的就够覆盖大部分场景了实在需要组合条件的用拼两个就行。5. Redis 配置的缓存超时别忘了设。logmonitor:conf这个 key 我们一般设 1 小时过期。如果配置平台出了问题没法及时更新 Redis告警服务最多空跑一小时就会自动停下来不至于拿着过期的规则一直跑。这套日志告警服务没有什么高深的技术Grok、表达式引擎、Redis 去重、钉钉通知每个单拎出来都不是什么新东西。但把它们串在一起配合合理的线程池设计和去重策略就能组成一个够用、好维护、能快速响应线上问题的工具。对于中小团队来说不一定所有监控都要上 Prometheus Grafana 那套重型方案。有时候直接从日志里抓关键信息配合几条简单的规则就能解决 80% 的问题。关键是——让日志不只是躺在 ES 里等你来搜而是主动把异常推到你面前。
阅读完成 · 觉得有帮助?
咨询建站