禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 项目编排

  • 场景设计

  • CI-CD

  • 架构设计

  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
      • 写在文章开头
      • 详解Nightingale安装与配置
        • 前置准备
        • 二进制安装
        • 数据源配置
        • 日志查询
        • 告警规则配置与调测
        • 告警验收
      • 基于源码详解夜莺工作机制
        • 日志告警预览查询原理
        • 告警规则添加
        • 轮询监控告警工作过程
      • 小结
      • 参考
  • 软件工程实践
  • 监控方法论
sharkchili
2026-03-25
目录

硬核安利一个监控告警开源项目Nightingale

# 写在文章开头

夜莺是笔者近期了解到的一款比较强大的日志监控告警工具,按照官网的说法它可对接多种既有的主流数据源包括但不限于:

  1. ClickHouse
  2. elasticsearch
  3. Loki

然后我们只需按照夜莺配置模板即可完成监控指标、日志监控告警功能,而本文笔者所着重要介绍的,是这款系统中最强大的日志告警,笔者将通过一个es日志的示例逐步演示并结合源码分析的方式全方位带读者了解夜莺监控的使用和工作机制。

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。

为方便与读者交流,现已创建读者群。关注下方公众号获取我的联系方式,添加时备注加群即可加入。

# 详解Nightingale安装与配置

# 前置准备

主流的日志采集都是通过skywalking采集日志到elasticsearch,如下架构图所示:

而本文的案例则是通过elasticsearch采集系统程序运行日志,并按照Nightingale协定的规则配置定时采集任务,一旦感知到elasticsearch日志中存在错误日志则发出告警:

为了更好的演示夜莺如何根据采集日志并发出告警,笔者这里也简单介绍一下本文数据源elasticsearch的的配置。本文elasticsearch选用版本为7.12.0,在该版本的es上笔者刷入如下索引,可以看到这款索引很好的模拟了日常采集日志的常见数据:

  1. 微服务名称
  2. 日志消息
  3. 日志时间
  4. 日志级别
--  创建索引
curl -X PUT "localhost:9200/java_app_logs" -H 'Content-Type: application/json' -d'
{
  "mappings": {
    "properties": {
      "service_name": {
        "type": "keyword"
      },
      "log_message": {
        "type": "text"
      },
      "timestamp": {
        "type": "date"
      }
    }
  }
}
'

-- 添加日志级别
curl -X PUT "localhost:9200/java_app_logs/_mapping" -H 'Content-Type: application/json' -d'
{
  "properties": {
    "log_level": {
      "type": "keyword"
    }
  }
}
'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

完成必要的索引创建后,我们就可以按需刷入如下请求建立文档模拟用户登录成功和失败的消息:

-- 添加日志

curl -X POST "localhost:9200/java_app_logs/_doc" -H 'Content-Type: application/json' -d'
{
  "service_name": "user-service",
  "log_message": "用户登录成功,用户ID: 12345",
  "log_level": "INFO",
  "timestamp": "'$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")'"
}
'

-- 登录失败的日志
curl -X POST "localhost:9200/java_app_logs/_doc" -H 'Content-Type: application/json' -d'
{
  "service_name": "user-service",
  "log_message": "用户登录失败,用户名: testuser,原因: 密码错误",
  "log_level": "ERROR", 
  "timestamp": "'$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")'"
}
'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

自此,我们就有了一份可进行采集监控的es数据源,就可以开始通过夜莺进行日志监控告警配置了。

# 二进制安装

因为笔者采用Linux服务器部署且采用二进制的方式安装,所以我们需要到github下载最新版本,对应的下载地址为:https://github.com/ccfos/nightingale/releases (opens new window)

将下载好的压缩包进行解压:

tar zxvf n9e-v8.4.0-linux-amd64.tar.gz
1

然后我们直接进入bin目录键入如下指令就可以将夜莺启动了:

./n9e
1

随后我们通过17000端口即可访问Nightingale登录界面,默认情况下Nightingale的账户和密码分别是:

账户:root
密码:root.2020
1
2

明确可以正常启动,读者可以使用如下指令以后台的方式启动:

cd /opt/n9e && nohup ./n9e &> n9e.log &
1

# 数据源配置

登录管理界面之后,就可以将es数据源引入进行监控告警管理了,通过集成中心定位到数据源,点击进入数据源配置界面:

随后点击新增并选择ElasticSearch数据源:

根据提示依次配置:

  1. 数据源名称
  2. 访问地址
  3. 版本信息

明确无误后,点击测试连通性并保存:

如下图所示,这样就说明数据源添加成功了,自此我们的Nightingale就可以针对日志数据源进行监控告警规则配置了:

# 日志查询

为了明确我们es写入的日志能够被准确的查询到,我们可以先通过Nightingale的日志页面针对针对该数据源进行日志查询,以笔者为例,对应的配置为:

  1. 数据源选择elasticsearch数据源
  2. 选择索引模式即indices
  3. 索引使用java_app_logs
  4. 日志过滤条件为日志级别为错误级别的即log_level:"ERROR"

对应的配置和输出结果如下,由此可确定笔者的配置没有任何问题:

# 告警规则配置与调测

重头戏来了,明确错误日志查询无误后,我们就可以按照我们上述的调测针对error级别的日志配置相应告警规则了,按照我们需求的说法,即一旦查询到错误级别的日志超过1条则直接发出告警,我们可以到告警面板选择规则管理配置监控告警,以笔者为例首先指明规则为错误日志监控告警:

针对规则配置,相应配置为:

  1. 选择数据源类型为elasticsearch数据源
  2. 选择精确匹配指定数据源为我们的elasticsearch数据源
  3. 查询统计项指明索引为java_app_logs且过滤条件为过滤出错误类型日志log_level:"ERROR"
  4. 时间间隔设置为30min以内的数据

明确这个时间后,我们可以在es上刷几条错误日志看看规则配置是否可以正确执行:

curl -X POST "localhost:9200/java_app_logs/_doc" -H 'Content-Type: application/json' -d'
{
  "service_name": "user-service",
  "log_message": "用户登录失败,用户名: testuser,原因: 密码错误",
  "log_level": "ERROR", 
  "timestamp": "'$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ")'"
}
'
1
2
3
4
5
6
7
8

可以看到通过表达式我们拿到了30条数据,说明这个规则配置没有问题:

针对阈值判断,笔者指定规则为上述规则大于1也就是错误日志大于0条则触发告警,对应这个监控频率为1min一次,持续时长设置为0即代表只要出现一次直接告警。

这里我们也补充说明一下持续时长的概念,按照官网的说法持续时长即代表该规则为真时且持续配置的时间后才触发告警,例如我们1min执行一次,持续时长配置为120s即代表定时任务两次采集都收到错误级别日志大于1才触发告警。

当然笔者这里也是出于简单,直接配置为0:

其他配置全部默认保存即可。

# 告警验收

此时,一旦触发告警我们就可以在告警事件上看到事件,因为夜莺默认情况下都会将事件存储在缓存中,基于这种设计理念我们可以将实时告警在夜莺平台上对接各种方式通知用户:

  1. 企业微信
  2. 阿里云短信
  3. 邮件

# 基于源码详解夜莺工作机制

# 日志告警预览查询原理

通过上述的实践,我们基本了解了夜莺的基本使用方式,为了更好的帮助读者理解夜莺这个开源告警的工作原理,笔者也将Nightingale的源码克隆到本地针对几个比较核心的部分进入深入的拆解分析。

首先我们先来说说数据预览这一块,因为Nightingale的数值提取针对支持对es日志进行count、max、min等多种日志数据收敛提取方式,然后通过数据源预览即可得到查询结果:

通过浏览器控台,笔者定位到对应的请求映射为http://127.0.0.1:17000/api/n9e/ds-query,同时我们也可以看到请求参数,如下所示,这里笔者也针对说明一下如下几个参数的含义:

  1. cate:指明数据源类型为elasticsearch
  2. datasource_id:规则配置使用的数据源为id为1,也就是我们首次配置的elasticsearch数据源
  3. query:这个json块比较重要,它说明我们配置的规则名为A,索引类型index_type为索引类型而非索引匹配模式,然后index指明为java_app_logs,然后就是kql配置规则和提取方式为count
  4. date_field:指明采集的时间字段用索引中的timestamp

其他参数则是轮询间隔和基于这个间隔生成的起止时间:

{
  "cate": "elasticsearch",
  "datasource_id": 1,
  "query": [
    {
      "ref": "A",
      "index_type": "index",
      "index": "java_app_logs",
      "filter": "log_level:\"ERROR\"",
      "value": {
        "func": "count"
      },
      "date_field": "timestamp",
      "interval": 1800,
      "start": 1762532762,
      "end": 1762534562
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

返回结果如下,通过整体结构我们可以看出,对应A规则的在1762531200即2025年11月8日 00:00:00查出count为22:

{
    "dat": [
        {
            "ref": "A",
            "metric": {
                "__name__": "A__count"
            },
            "values": [
                [
                    1762531200,
                    22
                ]
            ],
            "query": "map[date_field:timestamp end:1.762534562e+09 filter:log_level:\"ERROR\" index:java_app_logs index_type:index interval:1800 ref:A start:1.762532762e+09 value:map[func:count]]"
        }
    ],
    "err": ""
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

明确这个接口出参和入参之后,我们就可以针对该接口进行深入分析了,本质上Nightingale数值提取查询就是通过上述请求参数生成es的restful请求参数,并通过返回结果中的聚合通提取到规则对应的count、max、min等信息然后返回给用户:

对应的我们可以在router.go文件中看到这个请求的入口,可以看到该请求映射本质上都是通过QueryData这个函数处理的:

		//告警规则配置请求入口
			pages.POST("/ds-query", rt.QueryData)
1
2

步入QueryData,即可看到如下几个步骤:

  1. 将请求参数绑定到变量f这个QueryParam结构体上,本质上就是将上述的入参进行一个一一对应的封装
  2. 通过QueryDataConcurrently发起es查询请求
  3. 将查询结果返回
func (rt *Router) QueryData(c *gin.Context) {
	
	var f models.QueryParam
	//解析绑定参数
	ginx.BindJSON(c, &f)
	//发起es restful请求然后返回结果resp
	resp, err := QueryDataConcurrently(rt.Center.AnonymousAccess.PromQuerier, c, f)
	
	//......
	//将结果返回给用户
	ginx.NewRender(c).Data(resp, nil)
}
1
2
3
4
5
6
7
8
9
10
11
12

而QueryDataConcurrently内部逻辑也比较简单,因为我们的http请求入参中的query是个数组,所以该函数会拿到QueryParam中的query数组进行遍历,生成一个个协程发起请求并通过倒计时门栓阻塞等待,当所有请求结果写入切片后,直接将切片resp返回:

func QueryDataConcurrently(anonymousAccess bool, ctx *gin.Context, f models.QueryParam) ([]models.DataResp, error) {
	//声明一个结果切片
	var resp []models.DataResp
	//......
	for _, q := range f.Querys {
		//......

	
		//针对查询请求添加一个倒计时门栓
		wg.Add(1)
		//起个协程发起es查询
		go func(query interface{}) {
			defer wg.Done()
			//执行查询,实际调用es的地方,将数组内部的query参数传入
			datas, err := plug.QueryData(ctx.Request.Context(), query)
			//......

		
			//将结果写入resp这个切片中
			resp = append(resp, datas...)
			mu.Unlock()
		}(q)
	}
	//等待所有协程查询结束
	wg.Wait()

	if len(errs) > 0 {
		return nil, errs[0]
	}

	//......
	//返回执行结果的切片
	return resp, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

通过上述源码可以看到每一个协程都是通过plug.QueryData(ctx.Request.Context(), query)发起es请求的,实际上这段逻辑都是在eslike.go上完成的,对应的执行步骤为就是:

  1. 基于filter生成es的must表达式
  2. 基于timestamp和interval生成range查询
  3. 基于1、2构建一个bool查询
  4. 通过参数value指明的count聚合,构建出一个桶聚合,指明不同时间区间的符合要求的文档数

对应的参数解析映射如下:

最后es会返回类似如下的一个结构体,Nightingale就会拿出这个doc_count作为结果封装返回:

"aggregations" : {
    "ts" : {
      "buckets" : [
        {
          "key_as_string" : "2025-11-05T14:30:00.000Z",
          "key" : 1762353000000,
          "doc_count" : 10
        }
      ]
    }
  }
1
2
3
4
5
6
7
8
9
10
11

对应的逻辑参见如下eslike.go的QueryData,和上述说明一致,读者可结合注释阅读理解:

func QueryData(ctx context.Context, queryParam interface{}, cliTimeout int64, version string, search SearchFunc) ([]models.DataResp, error) {
	//......
	//生成range对象,结构体类似于
	/**
	range": {
	          "timestamp": {
	            "format": "epoch_millis",
	            "from": 1762353000000,
	            "include_lower": true,
	            "include_upper": true,
	            "to": 1762353600000
	          }
	        }
	*/
	q := elastic.NewRangeQuery(param.DateField)
	//......
	//生成时间范围并给出返回对应的时间单位
	q.Gte(time.Unix(start, 0).UnixMilli())
	q.Lte(time.Unix(end, 0).UnixMilli())
	q.Format("epoch_millis")

		//......
	//生成bool查询并基于fiter构建出must子句,传入q即将range查询存入filter子句中
	/**
	"query": {
	    "bool": {
	      "filter": {
	        "range": {
	          "timestamp": {
	            "format": "epoch_millis",
	            "from": 1762353000000,
	            "include_lower": true,
	            "include_upper": true,
	            "to": 1762353600000
	          }
	        }
	      },
	      "must": {
	        "query_string": {
	          "query": "log_level:\"ERROR\""
	        }
	      }
	    }
	  }
	 */
	queryString := GetQueryString(param.Filter, q)

	var aggr elastic.Aggregation
	switch param.MetricAggr.Func {
	case "avg":
		aggr = elastic.NewAvgAggregation().Field(field)
	//......
		aggr = elastic.NewSumAggregation().Field(field)
	case "count":
		
		aggr = elastic.NewValueCountAggregation().Field(field)
//......
	default:
		return nil, fmt.Errorf("func %s not support", param.MetricAggr.Func)
	}
	//生成聚合桶查询,每个桶至少要有一个文档,少了就不显示
	tsAggr := elastic.NewDateHistogramAggregation().
		Field(param.DateField).
		MinDocCount(1)

	//......
	//构建查询参数
	searchSource := elastic.NewSearchSource().
		Query(queryString).
		Aggregation("ts", tsAggr) //设置自定义聚合名称为ts
	//......
	//发起请求
	result, err := search(ctx, indexArr, searchSource, param.Timeout, param.MaxShard)
	//......
	/** 提取ts中的bucket桶提取count和key也就是时间生成item返回
	"aggregations" : {
	    "ts" : {
	      "buckets" : [
	        {
	          "key_as_string" : "2025-11-05T14:30:00.000Z",
	          "key" : 1762353000000,
	          "doc_count" : 10
	        }
	      ]
	    }
	  }
	 */
	js, err := simplejson.NewJson(result.Aggregations["ts"])
	//......
	bucketsData, err := js.Get("buckets").Array()
	return items, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

# 告警规则添加

了解了规则配置解析之后,我们再来了解后续步骤,即如何将规则持久化到Nightingale底层的数据库中,让告警监控的工作协程能够获取到这个规则进行定时查询和告警消息缓存。

对应的我们通过浏览器控台定位到接口映射为http://127.0.0.1:17000/api/n9e/busi-group/1/alert-rules,请求参数为一个比较大的JSON数组,为了更好的说明和解析,这里笔者也给出几个比较核心的部分以便于读者更准确的理解规则的存储过程。

先来看看参数的第一部分,可以看到这个规则通过cate即category指明数据源类型为elasticsearch,并通过datasource这个数组块指明等价匹配数据源-1也就是elasticsearch

"cate": "elasticsearch",
  "datasource_queries": [{
    "match_type": 0, //精确匹配
    "op": "in", //包含关系
    "values": [
      1 //数据源1也就是我们配置的elasticsearch
    ]
  }],
1
2
3
4
5
6
7
8

然后就是规则配置,对应的rule_config的JSON参数如下,这里笔者抽出核心的两个部分,第一个部分也就是上面规则查询的参数,对应的明细笔者上文已经详细解释过了,这里就不多做赘述,这里我们着重说明一下trigger,其内部有个expressions结合3个参数语义和浏览器界面即知晓这个就是触发条件的配置,他告知Nightingale在查询count大于0的时候即可触发告警:

"rule_config": {
    "queries": [{ //查询条件参数
      "prom_ql": "",
      "severity": 2,
      "ref": "A",
      "index_type": "index",
      "value": {
        "func": "count"
      },
      "unit": "none",
      "index": "java_app_logs",
      "date_field": "timestamp",
      "filter": "log_level:\"ERROR\"",
      "interval": 1800
    }],
  	//.......
    "triggers": [{
      "mode": 0,
      "expressions": [{ //当规则A查出来的值大于0时触发告警
        "ref": "A",
        "comparisonOperator": ">",
        "value": 0,
        "logicalOperator": "&&"
      }],
      "severity": 1, //一级告警
      "recover_config": {
        "judge_type": 0
      },
      "join_ref": "A",
      "exp": "$A > 0"
    }],
    //.......
  },
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

最后一个部分也就是规则配置的调度算法,对应参数如下:

  1. cron_pattern指明每15s执行一次
  2. prom_for_duration:持续时间为60s也就是4次调度都触发告警与之则告警
  3. enable_days_of_weeks:执行周期为一整周即周一到周日都有
  4. enable_stimes: 开始时间
  5. enable_etimes:结束时间
 "cron_pattern": "@every 15s",
  "prom_for_duration": 60,
  "enable_days_of_weeks": [
    [
      "0",
      "1",
      "2",
      "3",
      "4",
      "5",
      "6"
    ]
  ],
  "enable_stimes": [
    "00:00"
  ],
  "enable_etimes": [
    "00:00"
  ],
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

明确参数后,我们通过接口映射定位到后端的执行函数,也就是alertRuleAddByFE方法,这里笔者也贴出接口对应的映射配置和实际执行函数,保存规则的逻辑比较简单,即:

  1. 解析参数并判空
  2. 调用alertRuleAdd保存规则
  3. 返回执行结果
pages.POST("/busi-group/:id/alert-rules", rt.auth(), rt.user(), rt.perm("/alert-rules/add"), rt.bgrw(), rt.alertRuleAddByFE)


func (rt *Router) alertRuleAddByFE(c *gin.Context) {
	username := c.MustGet("username").(string)

	var lst []models.AlertRule
	//解析参数列表存储到lst这个规则结构体中
	ginx.BindJSON(c, &lst)
	//如果参数为空直接返回异常
	count := len(lst)
	if count == 0 {
		ginx.Bomb(http.StatusBadRequest, "input json is empty")
	}

	bgid := ginx.UrlParamInt64(c, "id") //获取对应group id
	//调用alertRuleAdd保存规则
	reterr := rt.alertRuleAdd(lst, username, bgid, c.GetHeader("X-Language"))
	//将结果渲染返回
	ginx.NewRender(c).Data(reterr, nil)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

宏观的了解整个流程之后,我们还是需要深入细节了解alertRuleAdd实现细节,其实这段逻辑和我们java开发日常的crud接口都差不多,本质上就是将参数绑定到go语言的dto上保存到数据库(默认为sqllite),对应的映射转换细节如下:

  1. EnableStime和EnableEtime作为任务起止时间直接平迁
  2. 执行周期转为空格分隔
  3. 核心的告警规则查询条件和调度时间配置json即rule config直接序列化为JSON写入

几个核心转换过程笔者也已图解的方式展示了一下,读者结合说明了解一下:

对应笔者也给出router_alert_rule.go中alertRuleAdd的实现,可以看到如下步骤:

  1. 这个函数内部设置了alertRule对象的CreateBy和UpdateBy
  2. 调用FE2DB生执行前端参数转为上图所示的数据结构
  3. 然后AlertRule调用Add写入db中,很明显这种设计让AlertRule具备持久化的能力,是一种具备充血模型的设计理念
func (rt *Router) alertRuleAdd(lst []models.AlertRule, username string, bgid int64, lang string) map[string]string {
	count := len(lst) //获取对应的规则切片长度
	// alert rule name -> error string
	reterr := make(map[string]string)
	for i := 0; i < count; i++ {
		lst[i].Id = 0 //设置规则id和配置用户名信息
		lst[i].GroupId = bgid
		if username != "" {
			lst[i].CreateBy = username //绑定创建人username也就是root
			lst[i].UpdateBy = username //绑定修改人username也就是root
		}
		//价格lst转为封装db字段信息,例如起止时间 执行规则配置等信息,如果某条报错直接结束循环
		if err := lst[i].FE2DB(); err != nil {
			reterr[lst[i].Name] = i18n.Sprintf(lang, err.Error())
			continue
		}
		//存入数据库,返回写入结果
		if err := lst[i].Add(rt.Ctx); err != nil {
			reterr[lst[i].Name] = i18n.Sprintf(lang, err.Error())
		} else {
			reterr[lst[i].Name] = ""
		}
	}
	return reterr
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

结合上述的整体说明笔者也给出FE2DB的函数实现细节,大体就是上图所示的映射转换,读者可结合注释回顾一下:

func (ar *AlertRule) FE2DB() error {
	//如果起止时间存在则将起止时间设置到调用的ar上,也就是我们配置的enable_stimes和enable_etimes
	if len(ar.EnableStimesJSON) > 0 {
		ar.EnableStime = strings.Join(ar.EnableStimesJSON, " ")
		ar.EnableEtime = strings.Join(ar.EnableEtimesJSON, " ")
	} else {
		ar.EnableStime = ar.EnableStimeJSON
		ar.EnableEtime = ar.EnableEtimeJSON
	}
	//按照空格设置启用的星期,按照空格进行拼接,对应参数为 "enable_days_of_weeks": [
	//      [
	//        "0",
	//        "1",
	//        "2",
	//        "3",
	//        "4",
	//        "5",
	//        "6"
	//      ]
	//    ],
	if len(ar.EnableDaysOfWeeksJSON) > 0 {
		for i := 0; i < len(ar.EnableDaysOfWeeksJSON); i++ {
			if len(ar.EnableDaysOfWeeksJSON) == 1 {
				ar.EnableDaysOfWeek = strings.Join(ar.EnableDaysOfWeeksJSON[i], " ")
			} else {
				if i == len(ar.EnableDaysOfWeeksJSON)-1 {
					ar.EnableDaysOfWeek += strings.Join(ar.EnableDaysOfWeeksJSON[i], " ")
				} else {
					ar.EnableDaysOfWeek += strings.Join(ar.EnableDaysOfWeeksJSON[i], " ") + ";"
				}
			}
		}
	} else {
		ar.EnableDaysOfWeek = strings.Join(ar.EnableDaysOfWeekJSON, " ")
	}

	//......

	

	//将rule_config转为json串绑定到RuleConfig上
	if ar.RuleConfigJson != nil {
		b, err := json.Marshal(ar.RuleConfigJson)
		if err != nil {
			return fmt.Errorf("marshal rule_config err:%v", err)
		}
		//绑定rule规则 "rule_config": {
		//    "queries": [{
		//      "prom_ql": "",
		//      "severity": 2,
		//      "ref": "A",
		//      "index_type": "index",
		//      "value": {
		//        "func": "count"
		//      },
		//      "unit": "none",
		//      "index": "java_app_logs",
		//      "date_field": "timestamp",
		//      "filter": "log_level:\"ERROR\"",
		//      "interval": 1800
		//    }],
		ar.RuleConfig = string(b)
		ar.PromQl = ""
	}

	//......

	return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

基于FE2DB生成数据映射对象后,就需要进行持久化,按照笔者的说法该模型会调用内置的Add将结构体持久化,对应的方法也位于alert_rule.go文件的Add:

func (ar *AlertRule) Add(ctx *ctx.Context) error {
	//......
	
	//设置创建时间和更新时间
	now := time.Now().Unix()
	ar.CreateAt = now
	ar.UpdateAt = now
	//写入数据库 写入到alert_rule表
	return Insert(ctx, ar)
}
1
2
3
4
5
6
7
8
9
10

最终我们也可以在alert_rule看到这份信息:

# 轮询监控告警工作过程

完成的查询的任务创建之后,就到了Nightingale中最重要的一环,即基于规则进行告警监控,这个过程本质就是上述两个步骤的综合配置结果,即通过规则配置和插入,结合用户调测配置的查询规则进行周期性轮询并,针对查到的数据进行阈值判断,一旦发现结果超出阈值,则将结果写入缓存队列中,后续各种告警手段都会基于这个缓存进行告警输出。

这里笔者也给出Nightingale告警的宏观流程:

  1. 基于之前写入数据库且缓存的rule对象生成job提交到调取器scheduler中
  2. scheduler定期执行这个job
  3. job向elasticsearch发起请求获取结果
  4. 将触发阈值的结果封装成DataResp写入缓存seriesStore中
  5. 遍历seriesStore将其封装成事件写入一个协程安全的队列eventQueue中
  6. 后续Nightingale就会基于这个队列缓存进行数据库持久化或者告警操作

对应的我们先给出从缓存中获取rule并将其提交到调度器scheduler的代码,即位于alert_rule.go下的syncAlertRules函数,逻辑比较简单,从alertRuleCache拉取到规则后调用NewAlertRuleWorker提交到调度器即可:

func (s *Scheduler) syncAlertRules() {
	//从缓存中拿到rule
	ids := s.alertRuleCache.GetRuleIds() 
	//.....
	//基于id拿到rule具体信息
	for _, id := range ids { 
		rule := s.alertRuleCache.Get(id)
		//.....

		ruleType := rule.GetRuleType()
		if rule.IsPrometheusRule() || rule.IsInnerRule() {
				//.....
			for _, dsId := range datasourceIds {
				//.....
				//封装成cronjob添加
				alertRule := NewAlertRuleWorker(rule, dsId, processor, s.promClients, s.ctx) 
				alertRuleWorkers[alertRule.Hash()] = alertRule
			}
		} else if rule.IsHostRule() {
			//.....
		} else {
			//.....
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

后续就会有一个协程定时调用eval.go的Eval方法,通过GetAnomalyPoint发起es查询,如果存在异常端点事件,则将其通过Processor.Handle封装成event缓存起来,等待Nightingale底层各种协程进行持久化、告警操作,对应笔者给出这几个步骤的核心代码断,读者可以结合注释了解一下:

func (arw *AlertRuleWorker) Eval() {
	//......
	//根据数据源类型获取异常点anomalyPoints
	switch typ {
	case models.PROMETHEUS:
	//......
	default:
		//触发es查询,anomalyPoints就是异常的端点信息(如果存在的话)
		anomalyPoints, recoverPoints, err = arw.GetAnomalyPoint(cachedRule, arw.Processor.DatasourceId())
	}

	//......

	

	
	} else {
	//......
	}
	//处理异端点数据anomalyPoints,将其封装成event缓存,等待后续告警 持久化等各种操作
	arw.Processor.Handle(anomalyPoints, "inner", arw.Inhibit)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

对应的笔者也给出GetAnomalyPoint的实现细节,逻辑比较简单,整体就是触发es查询即QueryData(上文告警规则调测介绍过,其底层es查询的逻辑),然后将其写入seriesStore中,然后结合rule对象通过parser.CalcWithRid查看结果是否触发阈值,如果触发则存入异常端点的切片points中返回:

func (arw *AlertRuleWorker) GetAnomalyPoint(rule *models.AlertRule, dsId int64) ([]models.AnomalyPoint, []models.AnomalyPoint, error) {
	//......

			//触发es查询即可得到 异常端点的时间点和count
			series, err := plug.QueryData(ctx, query)
			
		//......

		
			for i := 0; i < len(series); i++ {
				//计算es结果的hash
				serieHash := hash.GetHash(series[i].Metric, series[i].Ref)
				//计算tag hash
				tagHash := hash.GetTagHash(series[i].Metric)
				//将本次count查询结果存储到序列桶中
				seriesStore[serieHash] = series[i]

				//......
				//将series的hash写入到seriesTagIndex中
				seriesTagIndex[tagHash] = append(seriesTagIndex[tagHash], serieHash) 
			}
			//......
		}

		//......

		if !ruleQuery.ExpTriggerDisable {
			for _, trigger := range ruleQuery.Triggers {
				//......
				//从标签seriesTagIndex中获取series的hash值
				for _, seriesHash := range seriesTagIndex {
					//......
					
					//通过表达式结合查询结果查看是否满足条件
					isTriggered := parser.CalcWithRid(trigger.Exp, m, rule.Id)
					//......
					//将结果封装成point,并追加到切片中
					point := models.AnomalyPoint{
						Key:           sample.MetricName(),
						Labels:        sample.Metric,
						Timestamp:     int64(ts),
						Value:         value,
						Values:        values,
						Severity:      trigger.Severity,
						Triggered:     isTriggered,
						Query:         fmt.Sprintf("query:%+v trigger:%+v", queries, trigger),
						RecoverConfig: trigger.RecoverConfig,
						ValuesUnit:    valuesUnitMap,
					}
					//如果符合条件则将其追加到points中
					if isTriggered {
						points = append(points, point)
					} else {
					//......
					}
				}
			}
		}

		//......
	//返回point
	return points, recoverPoints, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

最后就是事件告警了,逻辑比较简单,基于上一步的异常端点anomalyPoints遍历生成event写入缓存中:

func (p *Processor) Handle(anomalyPoints []models.AnomalyPoint, from string, inhibit bool) {
		//......

	// 遍历anomalyPoints,根据 event 的 tag 将 events 分组,处理告警抑制的情况
	eventsMap := make(map[string][]*models.AlertCurEvent)
	for _, anomalyPoint := range anomalyPoints {
		//封装成事件
		event := p.BuildEvent(anomalyPoint, from, now, ruleHash) //基于异常点生成告警事件
		//......

		

		
		}

		//......
		//将anomalyPoint哈希运算存入eventsMap
		tagHash := TagHash(anomalyPoint)
		//追加到对应tagHash(也就是我们的规则标签的hash)
		eventsMap[tagHash] = append(eventsMap[tagHash], event)
	}
	//遍历告警事件的map并处理事件,其底层就是将其存入到一个协程安全的缓存队列中
	for _, events := range eventsMap {
		p.handleEvent(events)
	}

		//......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 小结

本文详细的介绍了Nightingale的日志告警规则配置和实现细节,基于这个契机,笔者也来谈谈读者常问的一个问题——如何较好的去掌握一门技术,作为一个计算机从业者,私以为学习一门技术的本质无非是想尽一切去了解它,让自己拥有白盒的视角去看待这些原本黑盒的技术。

以本次Nightingale这个告警框架为例,读者可以非常直观的看到笔者的学习过程,本质上就是:

  1. 结合一手官网的文档去学习和应用
  2. 通过这些应用找到请求入口
  3. 结合入口定位到源码入口并针对每个接口的实现细节进行阅读和具象化梳理

最终读者眼中的这些工具,在笔者眼里就变为一个http请求在go应用框架的协程中的各种数据流扭转和es交互请求和响应,后续无论是运用还是问题排查也都是以这种视角游刃有余的去使用和排查。

总结源码 结合接口推断保存查询 数据流向哪里来 推测告警或者动作的源头 从而了解源码原理

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。

为方便与读者交流,现已创建读者群。关注下方公众号获取我的联系方式,添加时备注加群即可加入。

# 参考

夜莺监控官网:https://n9e.github.io/zh/docs/prologue/introduction/ (opens new window)

夜莺告警规则配置:https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v7/usage/alarm-management/alert-rules/rule-configuration/business/es-alarm-rules/ (opens new window)

夜莺日志告警:https://n9e.github.io/zh/docs/usage/logs-alerting/ (opens new window)

使用SkyWalking和Elasticsearch实现全链路监控 :https://help.aliyun.com/zh/es/use-cases/use-skywalking-to-implement-end-to-end-monitoring-on-elasticsearch (opens new window)

《Elasticsearch实战第二版》

一篇文章学会黑盒测试、白盒测试(含实操) :https://blog.csdn.net/u010924879/article/details/146502076 (opens new window)

DDD领域驱动设计:贫血模型和充血模型 :https://zhuanlan.zhihu.com/p/464914100 (opens new window)

JSON文件加注释的7种方法 :https://blog.csdn.net/Dream_fengyuefei/article/details/92626804 (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
从micrometer计量器角度快速上手promQL

← 从micrometer计量器角度快速上手promQL

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×