Compare commits

...
Author SHA1 Message Date
tomsun28 09103ae89c [docs] update dockerfile
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-09-20 19:04:30 +08:00
5cdfe545b5 [doc] add japanese i18n in app-idrac.yml (#3766)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-20 00:48:03 +08:00
+16 9be7295084 [fix]bugfix:Prevent Long.parseLong() error when search param is a float (#3483)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Signed-off-by: Cyanty <153884653+Cyanty@users.noreply.github.com>
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: Wenqi Luo <495872803@qq.com>
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Signed-off-by: lynx009 <2030509072@qq.com>
Signed-off-by: Tom <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: Cyanty <153884653+Cyanty@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
Co-authored-by: VampireAchao <achao@apache.org>
Co-authored-by: Wenqi Luo <495872803@qq.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
Co-authored-by: tuzuy <95274591+tuzuy@users.noreply.github.com>
Co-authored-by: tuzy <tuzy.cq@chinaccs.cn>
Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
Co-authored-by: liutianyou <liutianyou@apache.org>
Co-authored-by: carl pinto <carlpinto25@gmail.com>
Co-authored-by: TJxiaobao <2922035952@qq.com>
Co-authored-by: cxhello <49056040+cxhello@users.noreply.github.com>
2025-09-20 00:24:51 +08:00
6905bcc986 [feat] Support monitoring center indicator favorites feature (#3735)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-19 23:53:26 +08:00
Duansg 1050032a13 [improve] SSE exception handling improvements (#3775) 2025-09-19 00:24:49 +08:00
DeleiGuo 9b223dd217 [doc] replace with the new ASF logo (#3770) 2025-09-17 01:16:25 +08:00
Duansg 0a1b5961e9 [feat] Support apollo configuration center monitoring (#3768) 2025-09-15 08:48:52 +08:00
tuzuyandtuzy a8a70b76d7 [feat] Add configuration file for Dell iDRAC monitoring type (#3763)
Co-authored-by: tuzy <tuzy.cq@chinaccs.cn>
2025-09-13 00:08:46 +08:00
+1 05b63903b5 [GSOC] Added all the necessary tools across services (#3722)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: lynx009 <2030509072@qq.com>
2025-09-10 08:31:05 +08:00
Calvin 7d8b55dfb8 [docs] Update contributors (#3759) 2025-09-09 08:10:40 +08:00
DuansgandTom 72a3e59ca6 [improve] Prometheus streaming parsing optimization (#3752)
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 23:20:23 +08:00
3ab94a751b [Improve] optimize the dashboard page and adapt to the new theme (#3730)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 23:00:12 +08:00
66049f189b [OSPP] Add RISC-V related help document. (#3712)
Signed-off-by: lynx009 <2030509072@qq.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 22:45:21 +08:00
DeleiGuoandTom 69ec3f8751 [bugfix] synchronise AI-related configuration for Docker Compose (#3751)
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-07 22:42:43 +08:00
feb43c5678 [GSOC] Chat UI, Conversation Management, OpenAI Chat Client Support, More monitoring tools. (#3679)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: DeleiGuo <deleiguo@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-07 22:36:04 +08:00
liutianyou 1fae67282d [doc] update doc for how to release and add release note for 1.7.3 (#3749) 2025-09-06 21:00:04 +08:00
886f0b74f6 chore: optimize pom content (#3732)
Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-06 09:02:01 +08:00
cxhelloandTom 32e1a43aa5 fix: update CNCF link (#3746)
Co-authored-by: Tom <tomsun28@outlook.com>
2025-09-05 22:28:05 +08:00
liutianyou dcd806802e [doc] update new version download link (#3747) 2025-09-05 21:29:40 +08:00
Duansg 5746cbbbd7 [improve] Prometheus streaming parsing supports CRLF (#3745) 2025-09-05 00:52:33 +08:00
Duansg e23903fd0c [fix] Fixed issue where default path did not match whitelist (#3740) 2025-09-03 21:52:44 +08:00
铁甲小宝andCalvin 780710f226 Integration QuestDB as Time Series Database Storage (#3731)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-09-02 14:42:51 +08:00
Duansg 4607ede541 [improve] Improve styling issues (#3734) 2025-09-02 01:25:58 +08:00
tomsun28andTJxiaobao 67ad4b04ef docs: update doc and add graduation blog (#3729)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: TJxiaobao <2922035952@qq.com>
2025-08-30 17:34:50 +08:00
DeleiGuoandCalvin 6432d689dd [improve] optimize the incident list on the status page to support paginated queries (#3727)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-30 11:23:10 +08:00
carl pinto 2eb856fffb [Improve] changed scroll of intervals in New Monitor page (#3726) 2025-08-30 00:02:29 +08:00
liutianyouandtomsun28 56312831ca [release] release new version 1.7.3 (#3724)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-29 00:07:49 +08:00
Duansg fe54d42bc0 [improve] Improve Prometheus label PromQL parsing logic (#3725) 2025-08-28 23:56:22 +08:00
DeleiGuoandtomsun28 2ea24c1b3a [feature] add Synology NAS monitoring support (#3721)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-28 23:41:47 +08:00
DeleiGuoandCalvin e0040e2ede [feature] add macOS monitoring support (#3715)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-28 23:26:57 +08:00
Calvinandliutianyou 0ac0fd4a8d [doc] add japanese i18n in app-tdengine.yml (#3717)
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-08-27 22:11:51 +08:00
DuansgandCalvin 0ef87bcc08 [improve] Improve performance issues when loading large numbers of metric cards in Monitors (#3719)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-27 12:41:41 +08:00
Calvinandlynx009 660bda6e14 [doc] add japanese i18n in app-valkey.yml (#3706)
Co-authored-by: lynx009 <2030509072@qq.com>
2025-08-27 11:41:22 +08:00
Calvinandtomsun28 242cf40135 [doc] add japanese i18n in app-ubuntu.yml, app-windows.yml and app-yarn.yml (#3703)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-25 23:20:17 +08:00
lynx009 7319c55430 [doc] modify github link. (#3714) 2025-08-25 21:44:56 +08:00
5ea29f610b [improve] Improve the parsing logic of prometheus label (#3662)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
2025-08-25 19:26:31 +08:00
b74aff0500 refactor: Optimize the monitoring data chart component (#3668)
Co-authored-by: tuzy <tuzy.cq@chinaccs.cn>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Kerwin Bryant <kerwin612@qq.com>
2025-08-24 22:10:27 +08:00
d2e8f2f4aa [bugfix] fixed jexl keyword issue in vastbase monitoring template (#3708)
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-24 20:51:41 +08:00
DeleiGuoandkangli 28681a1f9e [bugfix] fixed jexl keyword issue in greenplum monitoring template (#3707)
Co-authored-by: kangli <likang@apache.org>
2025-08-24 20:49:25 +08:00
DuansgandCalvin 6814c7c357 [feat] Support TDengine monitoring (#3678)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-24 14:23:23 +08:00
DeleiGuoandCalvin f475540508 [bugfix] fixed jexl keyword issue in kingbase monitoring template (#3705)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-24 13:04:09 +08:00
Calvin 44660a5042 [doc] add japanese i18n in app-vastbase.yml (#3704) 2025-08-24 12:02:44 +08:00
339386fd2b [bugfix] resolve translation value fetching for uppercase app names (#3690)
Signed-off-by: Wenqi Luo <495872803@qq.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Yang Chen <1597081640@qq.com>
2025-08-24 00:07:55 +08:00
tomsun28andCalvin faa7cb71db [doc] update website, doc while graduate (#3701)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-23 22:11:37 +08:00
014a376c30 [refactor] modify message column to TEXT type (#3698)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-23 16:57:04 +08:00
Calvinandtomsun28 377172e44a [doc] add japanese i18n in app-tomcat.yml (#3700)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-23 16:52:36 +08:00
Calvinandtomsun28 5b9e19cc30 [doc] add japanese i18n in app-seatunnel.yml (#3691)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-23 16:50:24 +08:00
Calvinandtomsun28 0457ec7d16 [doc] add japanese i18n in app-rockylinux.yml (#3688)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-23 16:48:08 +08:00
tomsun28andCalvin 2947ee218e chore: update ui theme (#3682)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-22 22:37:51 +08:00
DuansgandCalvin 4e841a9fb1 [doc] Fixed menu path errors and MD document format anomalies. (#3695)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-22 10:08:35 +08:00
tomsun28andCalvin b7ecb9b2ec [doc] update doc while graduate (#3692)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-22 01:48:48 +08:00
DeleiGuo 7e1873bc10 [bugfix] fixed jexl keyword issue in spring_gateway monitoring template (#3693) 2025-08-22 01:12:04 +08:00
DeleiGuoandCalvin 8eb60210a3 [bugfix] fixed jexl keyword issue in windows monitoring template (#3687)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-21 22:38:49 +08:00
DeleiGuoandCalvin d608f732f7 [bugfix] fixed jexl keyword issue in hertzbeat monitoring template (#3686)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-21 22:36:53 +08:00
DeleiGuoandCalvin 2d9a2955ef [bugfix] fixed jexl keyword issue in postgresql monitoring template (#3685)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-21 22:19:28 +08:00
cae4423f30 [doc] graduate and remove incubating (#3584)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-08-21 21:22:49 +08:00
f0e53a9fb3 [doc] add japanese i18n in app-redis_sentinel.yml (#3681)
Co-authored-by: liutianyou <tianyou.liu@qq.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-20 00:21:51 +08:00
79da1a198b [improve] optimize status page layout and UI (#3680)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-20 00:17:03 +08:00
64cb804147 [doc] add japanese i18n in app-dolphinscheduler.yml (#3677)
Co-authored-by: liutianyou <tianyou.liu@qq.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-20 00:03:12 +08:00
Calvinandtomsun28 344cb25229 [doc] add japanese i18n in app-redis_cluster.yml (#3672)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-19 23:30:11 +08:00
DeleiGuo 74d21a7acd [feature] add user password monitoring metrics in oracle monitor (#3674) 2025-08-19 22:08:05 +08:00
f315bf9607 [fix] Fixed Grafana visualization integration display issue (#3666)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-17 14:51:14 +08:00
810d54bef5 [improve] Optimize the scheduling logic for batch flush tasks (#3660)
Signed-off-by: Cyanty <153884653+Cyanty@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-17 14:24:48 +08:00
DeleiGuoandCalvin 48eafa1382 [Feature] add Apache DolphinScheduler monitoring support (#3656)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-17 10:58:46 +08:00
Calvinandtomsun28 4b6c1e0f08 [doc] add japanese i18n in app-redis.yml (#3669)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-17 09:59:11 +08:00
edfd857030 [GSOC] MCP server setup, authorization, and basic tool support (#3610)
Signed-off-by: Sarthak Arora <f20200060@pilani.bits-pilani.ac.in>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-16 19:08:13 +08:00
tomsun28andLogic 408a06d3eb [doc] update contribution doc (#3667)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-08-16 16:07:41 +08:00
Calvinandtomsun28 fd3a2f0569 [doc] add japanese i18n and remove chinese in script folder (#3665)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-16 15:43:24 +08:00
7d8ba72717 [doc] add japanese i18n in app-redhat.yml (#3655)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-16 15:25:39 +08:00
Calvinandtomsun28 f936b50ce9 [doc] add japanese i18n in app-spring_gateway.yml, app-sqlserver.yml (#3661)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-15 20:25:01 +08:00
Calvinandtomsun28 93f5da2d02 [doc] add japanese i18n in app-process.yml (#3654)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-15 20:17:14 +08:00
Calvinandshown bc06c268ca [doc] add japanese i18n in app-prestodb.yml, app-spark.yml (#3651)
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-08-15 20:12:07 +08:00
DeleiGuoandCalvin 9e6f2762c0 [doc] improve documentation formatting and readability (#3659)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-13 00:40:37 +08:00
Duansg 6ac8a42bd3 [doc] add japanese i18n in app-springboot3.yml (#3658) 2025-08-13 00:37:49 +08:00
Calvinandtomsun28 89647d96c7 [doc] add japanese i18n in app-tidb.yml, app-windows_script.yml (#3650)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-11 00:57:21 +08:00
tomsun28 0b0fac4a41 [bugfix] fix the collector aes decode with secret error (#3652)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-08-10 22:34:10 +08:00
Duansg f59e15bbbe [fix] Fixed an issue where template configuration was overwritten (#3649) 2025-08-09 23:37:10 +08:00
b7398b1bf9 [feature] Support HertzBeat self monitor (#3641)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-09 17:14:17 +08:00
Calvinandtomsun28 0082e724bc [doc] add japanese i18n in app-s7.yml, app-tplink_switch.yml, app-shenyu.yml (#3648)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-09 14:25:46 +08:00
57a2daf30d [doc] add japanese i18n in app-prometheus.yml, app-pulsar.yml and app-registry.yml (#3639)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-09 14:23:39 +08:00
Calvinandtomsun28 db90f29cd0 [doc] add japanese i18n in app-qq_mailbox.yml and app-redfish.yml (#3635)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-09 14:21:28 +08:00
DeleiGuoandtomsun28 00fd8fa1da [bugfix] fixed jexl keyword issue in springboot2 monitoring template (#3632)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-09 14:18:46 +08:00
60100eb980 [doc] add English version of linux operating system practice usecase (#3631)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-08-09 14:16:09 +08:00
DuansgandLogic a6eb6be1ab [feat] Monitoring templates support JSON nested parsing. (#3645)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-08-08 21:54:38 +08:00
DeleiGuoandLogic 5eff094af6 [bugfix] fix potential StringIndexOutOfBoundsException (#3642)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-08-08 21:35:25 +08:00
CalvinandLogic 3b126f53d2 [doc] add japanese i18n in app-smtp.yml, app-ssl_cert.yml, app-storm.yml, app-udp_port.yml, app-uniview.yml, app-website.yml, app-websocket.yml (#3643)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-08-08 21:15:37 +08:00
Calvinandaias00 1a5b722994 [doc] add japanese i18n in app-openai.yml and app-opengauss.yml (#3617)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-08-08 20:56:45 +08:00
Duansg 5fe20dff05 [fix] remove optional metrics (#3636) 2025-08-07 00:44:35 +08:00
04cfdae1e5 [fix] Fixed issue where jdbc url was replaced (#3625)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: 铁甲小宝 <2922035952@qq.com>
2025-08-05 09:57:04 +08:00
8776f98cf5 [Feature] add customized labels to VM metrics (#3622)
Co-authored-by: zhochi <zhochi@cisco.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
2025-08-05 09:37:12 +08:00
Duansgandtomsun28 da484aef26 [fix] Fixed springboot3 template yaml has the jexl inner conflict keyword(#3629)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-04 23:32:02 +08:00
DeleiGuoandtomsun28 e8ebc65053 [doc] add linux operating system practice usecase (#3628)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-04 23:12:12 +08:00
Calvinandtomsun28 bd08de41d5 [doc] add japanese i18n in app-postgresql.yml (#3624)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-04 23:09:05 +08:00
Calvinandtomsun28 18f8a541f0 [doc] add japanese i18n in app-oracle.yml (#3621)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-04 23:06:40 +08:00
Calvinandtomsun28 007ecd2db1 [doc] add japanese i18n in app-opensuse.yml (#3619)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-08-04 23:04:33 +08:00
VampireAchaoandCalvin 89aaad2401 [doc] Optimize the home website navbar layout UI for medium-sized screens (#3616)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-04 22:47:13 +08:00
17d836752e [feat] Http collect supports jsonpath parsing of numeric type (#3612)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
2025-08-01 19:05:08 +08:00
VampireAchaoandCalvin 3949b73751 [doc] add japanese i18n in app-oceanbase.yml (#3614)
Signed-off-by: VampireAchao <achao@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-08-01 00:05:48 +08:00
Calvinandtomsun28 2731504278 [doc] add japanese i18n in app-netease_mailbox.yml and app-ntp.yml (#3607)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-30 22:02:09 +08:00
babbfaea68 [bugfix] the tagValue may be null in the determineNewLabels method (#3606)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Yang Chen <1597081640@qq.com>
2025-07-30 22:00:19 +08:00
9f7b9994b8 [doc] add japanese i18n in app-nebula_graph_cluster.yml and update japanese of client (#3603)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-29 09:26:51 +08:00
d156c10aed [doc] update japanese i18n in app-nacos_sd.yml, app-nebula_graph.yml, app-zookeeper.yml (#3602)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-29 09:22:32 +08:00
Calvinandtomsun28 9f3329c85f [doc] add japanese i18n in app-nvidia.yml, app-ping.yml, app-pop3.yml, app-port.yml and app-push.yml (#3611)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-29 09:20:14 +08:00
cto-huhangandCalvin fd1c58f68d [doc] add japanese i18n in app-nacos.yml (#3599)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-07-28 12:04:27 +08:00
DeleiGuo 6d1aa503df [doc] fix image reference issues in documents (#3608) 2025-07-28 01:54:05 +08:00
a58ad35c8b [fix] Fix collector offline without sending offline notification (#3601)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-25 21:53:15 +08:00
Calvinandshown 64cd5f9658 [doc] update japanese i18n in app-mqtt.yml and app-mysql.yml (#3597)
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-07-25 08:19:08 +08:00
Yang Chenandshown a13dd89fd2 [feat] Auto-generate AES key if not configured (#3604)
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-07-24 21:06:45 +08:00
2f5cfa9d5d [improve] VM write request sets up gzip compression and adds saveData unit tests (#3595)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
2025-07-24 19:18:17 +08:00
LunaRain_079andCalvin a9dc7bf7f3 [doc] add japanese i18n in app-rocketmq.yml and app-rabbitmq.yml (#3598)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-07-23 10:59:41 +08:00
DuansgandCalvin c1492deaa0 [doc] add japanese i18n in app-nginx.yml (#3600)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-07-23 00:18:28 +08:00
CyantyandCalvin 2252b2a769 [doc] add japanese i18n in app-zookeeper.yml and app-zookeeper_sd.yml (#3596)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-07-22 00:32:41 +08:00
cto-huhang 73f71503f5 [doc] add japanese i18n in app-mysql.yml (#3594) 2025-07-20 21:17:21 +08:00
Calvinandtomsun28 2e224ce626 [doc] add japanese i18n in app-mongodb_atlas.yml (#3593)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-20 09:38:27 +08:00
e5406d57c6 [fix] Fix collector online metadata filling (#3579)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-20 09:35:14 +08:00
433a83303f [refactor] Refactor the comparison logic (#3574)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-20 01:34:03 +08:00
Calvinandtomsun28 f309d5eafd [doc] add japanese i18n in app-mongodb.yml (#3590)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-19 18:20:36 +08:00
986103395b [feature] System time zone optimization. (#3588)
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-19 18:18:25 +08:00
Calvinandtomsun28 d569721e01 [doc] add japanese i18n in app-modbus.yml (#3580)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-19 00:44:03 +08:00
Calvinandtomsun28 08cac3c6cc [doc] add japanese i18n in app-memcached.yml (#3578)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-19 00:42:16 +08:00
Calvinandtomsun28 f80d066851 [doc] add japanese i18n in app-mariadb.yml (#3577)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-19 00:39:02 +08:00
d6584093f5 [doc] add japanese i18n in app-linux_script.yml (#3575)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-19 00:37:03 +08:00
DuansgandCalvin fb03a57725 [feature] Support i18n of collector online notification (#3585)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-07-18 19:51:08 +08:00
tomsun28 779272d65f [infra] upgrade openjdk image to fix Debian source failed (#3587)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-07-18 15:32:34 +08:00
Logic c21a2952a5 [doc]: add FAQs about task limits (#3581) 2025-07-17 21:52:18 +08:00
Logic b46d049158 [fix]: Fix a problem where the inputstream was closed prematurely when parsing Prometheus (#3567) 2025-07-14 21:21:01 +08:00
Calvin dc52c88d88 [doc] japanese linux (#3571) 2025-07-13 23:45:46 +08:00
Calvin df4e947c8a [doc] japanese kvrocks (#3570) 2025-07-12 14:24:47 +08:00
a4ec6c1f66 [bugfix] Fix MySQL WrongArgumentException (#3564)
Co-authored-by: kangli <1363539513@qq.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-07-12 00:26:14 +08:00
Yang Chen a8dd57d009 [docs] Add new committer blog (#3569) 2025-07-11 19:33:33 +08:00
af3eabdd1c [mqtt] refact the MQTT based on the Paho SDK and support both unidirectional and bidirectional MQTT over TLS connections. (#3474)
Signed-off-by: yy549159265 <40821310+yy549159265@users.noreply.github.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-11 13:36:10 +08:00
Calvin d6b300d3df [doc] japanese kubernetes (#3568) 2025-07-11 06:18:35 +08:00
Duansg b5789538e6 [feat] Add QueryExecutor does not support internationalization. (#3565) 2025-07-10 20:04:15 +08:00
Sherlock Yin d6c5b2b8a2 [doc] add new committer (#3563) 2025-07-10 09:23:03 +08:00
Calvin 3a04647c3c [doc] japanese kingbase (#3562) 2025-07-09 00:37:04 +08:00
tomsun28 d2999c1aa6 [doc] add new pmc and committer (#3561) 2025-07-09 00:30:43 +08:00
Duansg b3dbc7092a [doc] Fix documentation error and adjust Chinese semantics (#3559) 2025-07-08 08:17:40 +08:00
Calvinandtomsun28 edb3cb6b6a [doc] japanese kafka promql (#3553)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-07 23:25:25 +08:00
淞筱 4dd78b2e98 [doc] V1.7.2 version blog (#3558) 2025-07-07 23:21:43 +08:00
2f91f56b7b [bugfix] fix npe due the victoria insert not config when startup (#3552)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-07-07 09:18:33 +08:00
tomsun28andkangli be3dac488b [improve] make jackson serialize all field visibility (#3551)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: kangli <likang@apache.org>
2025-07-07 00:17:39 +08:00
553bdbaf9f [refactor] AlarmCacheManager refactoring processing logic (#3525)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: kangli <likang@apache.org>
2025-07-06 22:06:30 +08:00
40d3f1243b feat: Add LogUtil wrapper and optimize logging comments (#3489)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-06 20:57:39 +08:00
740e3f8385 [feature] Support export all allmonitors (#3509)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: kangli <likang@apache.org>
2025-07-06 19:06:19 +08:00
Calvin 47d089dff7 [doc] japanese kafka client (#3550) 2025-07-06 06:51:49 +08:00
Calvin c6f1f07155 [doc] japanese jvm (#3545) 2025-07-05 12:35:45 +08:00
Calvinandtomsun28 0ebd3499b7 [doc] japanese kafka (#3548)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-05 12:04:24 +08:00
淞筱 c88d4a7b0e [doc] update download page (#3549) 2025-07-05 11:56:39 +08:00
tomsun28 8bebae9db6 [doc] add hertzbeat maturity and fix doc (#3544) 2025-07-04 19:55:27 +08:00
b6b1a48a5c [doc] recommend newer mysql connector jar (#3541)
Signed-off-by: PJ Fanning <pjfanning@users.noreply.github.com>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-03 23:35:53 +08:00
63ed50ef45 [doc] Fix some issues in the documentation (#3526)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-07-03 23:04:31 +08:00
PJ Fanning e63715d1b0 enable GitHub Dependabot to create PRs for security alerts but not for every dependency update (#3543) 2025-07-03 21:27:48 +08:00
Calvin edf3eac6d6 [doc] japanese jetty (#3539) 2025-07-03 11:03:54 +08:00
Calvin 743f285c8d [doc] japanese ipmi (#3537) 2025-07-02 22:55:09 +08:00
Logicandaias00 458433464c [doc]update download page text (#3532)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-07-02 17:45:28 +08:00
淞筱 64740a4f85 [doc] update outdated links (#3535) 2025-07-02 17:15:19 +08:00
淞筱andaias00 b5a2bce261 download version deal (#3531)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-07-01 13:11:31 +08:00
Calvin 5e351c854b [doc] japanese iotdb (#3530) 2025-07-01 12:29:54 +08:00
0875bea046 fix: fix something (#3524)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
2025-06-30 10:56:55 +08:00
Calvin 9ad5f77816 [doc] japanese influxdb promql (#3527) 2025-06-30 09:46:56 +08:00
aias00andCalvin 998f9059bc [feat]parallel ci (#3523)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-06-30 00:04:10 +08:00
a1439c3ec2 [fix] fix i18n error and append two metrics about disk infos (#3514)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-06-29 20:20:05 +08:00
742b6f6755 [doc] Add new commiter blog (#3522)
Co-authored-by: yinyijun <yingey2011>
Co-authored-by: 淞筱 <2030509072@qq.com>
2025-06-29 15:15:50 +08:00
5fdb34598c [doc] japanese hikvision (#3499)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
2025-06-29 12:16:17 +08:00
Calvin 4f982d1ab0 [doc] japanese influxdb (#3521) 2025-06-29 11:50:42 +08:00
4a28056d9f [fix] Update the correct replacement of outdated functions and placeholders (#3516)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-28 22:31:18 +08:00
Calvinandaias00 3f2fa71e22 [doc] japanese iceberg (#3519)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-28 22:25:22 +08:00
Jastandaias00 08c81bf62f [Improve] Improve kafka e2e (#3520)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-28 22:23:07 +08:00
e16d6cb4ba [feature] Support for Periodic Threshold preview (#3505)
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-06-28 20:38:38 +08:00
Calvin a54d14e803 [doc] japanese hugeGraph (#3518) 2025-06-28 07:24:44 +08:00
Calvinandaias00 01ae281769 [doc] japanese hive (#3506)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-28 00:10:22 +08:00
Calvin 9cb10ff14d [doc] japanese huawei switch (#3515) 2025-06-27 20:55:27 +08:00
Calvin 62f40d3ad1 [doc] japanese hpe switch (#3511) 2025-06-26 00:07:54 +08:00
tomsun28 57601ef989 [release] release new version 1.7.2 (#3510)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-06-25 22:01:18 +08:00
puruidong 0a05edbec5 [bugfix]Fixed an error in the format of the flying book notification (#3508)
Signed-off-by: puruidong <pruidong@gmail.com>
2025-06-25 19:18:21 +08:00
Yang Chenandtomsun28 fcef24fdbf [bugfix] Correctly parse binary comparison expressions (#3504)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-25 11:01:12 +08:00
e45c31abfb [bugfix] Fix incorrect expression parsing in alert setting component (#3497)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-24 22:56:16 +08:00
198c190ca7 fixed:a minor issue change (#3428)
Signed-off-by: 淞筱 <2030509072@qq.com>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
Co-authored-by: 淞筱 <2030509072@qq.com>
2025-06-24 22:50:22 +08:00
铁甲小宝anda-little-fool 392a59f9a9 add:a small jdbc modified. (#3500)
Co-authored-by: a-little-fool <2030509072@qq.com>
2025-06-24 22:07:18 +08:00
aias00 c01b7f7bce [feat] update_mvnd_version (#3498) 2025-06-23 21:18:04 +08:00
Calvin 0323b044b8 [doc] japanese hertzbeat token (#3493) 2025-06-23 08:54:47 +08:00
256302fdcf [fix] Fix antlr4 parsing of or and unless logical and set binary operators (#3488)
Co-authored-by: wordofwordof <black.jack.kaiqi@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-22 19:46:34 +08:00
Calvin 3f3330af41 [doc] japanese hertzbeat (#3492) 2025-06-22 19:13:18 +08:00
Calvin 33342baa3f [doc] japanese hdfs namenode (#3490) 2025-06-22 06:43:10 +08:00
Logic 563cbd7943 [ci]: add mvnd support and update backend build (#3491) 2025-06-21 20:07:10 +08:00
Duansgandaias00 ddb1601290 [fix] antlr4 vectors and parse semantic fixes and optimizations (#3482)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-21 07:55:54 +08:00
Calvin 8e9d3c09f3 [doc] japanese hdfs datanode (#3487) 2025-06-21 00:07:53 +08:00
Calvin ade04d2cf2 [doc] japanese hbase region server (#3479) 2025-06-20 09:03:09 +08:00
Calvinandaias00 53555c88c7 [doc] japanese hbase master (#3477)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-19 19:14:43 +08:00
Logic 03bf31269d [docs](webhook): update Chinese documentation for alert integration (#3478) 2025-06-19 19:05:21 +08:00
Calvin 5c329a8a66 [doc] japanese hadoop (#3476) 2025-06-18 23:34:44 +08:00
dda9250243 [feature] Add statusCode metrics data. (#3446)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-06-17 22:32:31 +08:00
Calvin 65a97aed8f [doc] japanese h3c switch (#3471) 2025-06-17 08:07:17 +08:00
Calvin 56528f4fc0 [doc] japanese greptime (#3469) 2025-06-15 22:12:43 +08:00
Duansgandliutianyou f218ba018a [fix] Fix Monitors paging display. (#3467)
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-06-15 11:44:23 +08:00
d6ce0c153a [improvement] change column definitions for commonAnnotations and alertFingerprints to TEXT type (#3463)
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-06-14 21:52:27 +08:00
Calvin a1b23cacdc [doc] japanese greenplum (#3468) 2025-06-14 17:40:04 +08:00
ed6214d0c1 [doc] Alert notification template (#3466)
Co-authored-by: yinyijun <yingey2011>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-14 13:04:32 +08:00
Duansgandtomsun28 9a3fc5911d [fix] fix jacoco can not generate test reports. (#3455)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-14 00:21:07 +08:00
d5d01459e7 [Fix] fix custom monitoring bulletin NullPointerException (#3448)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-06-13 08:48:51 +08:00
liutianyouandtomsun28 d077c72211 [doc] add doc for integrate volcengine alerts (#3460)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-13 00:32:15 +08:00
Yang Chen 11e3e4cc22 [improvement] disable default OpenTelemetry exporters to prevent connection errors (#3461) 2025-06-12 23:40:28 +08:00
Calvin c24c6ab7aa [doc] japanese freebsd (#3456) 2025-06-12 08:50:13 +08:00
Calvinandaias00 a183176e83 [doc] japanese flink on yarn (#3452)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-11 08:56:10 +08:00
tomsun28 a2e8739fec [doc] add publish version 1.7.1 blog (#3449) 2025-06-11 06:57:02 +08:00
liutianyouandCalvin ebb6e3d4ff [feature] support volcengine alert source (#3451)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-06-11 00:48:07 +08:00
tomsun28 fc3f26368d [security] update hertzbeat security model (#3450) 2025-06-11 00:22:26 +08:00
Calvin 7967cfabf0 [doc] japanese flink (#3447) 2025-06-10 07:31:31 +08:00
199466cd59 [feat] add support for OpenRouter AI provider (#3439)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
2025-06-09 23:35:08 +08:00
8065b63057 [docs] Add one-click installation for Rainbond Cloud (#3440)
Signed-off-by: songyg <songyg@goodrain.com>
Co-authored-by: songyg <songyg@goodrain.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-09 23:32:14 +08:00
Calvinandaias00 88655fcf19 [doc] japanese euleros (#3442)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-09 12:03:31 +08:00
Duansgandtomsun28 99b5460dd1 [feature] Support Huawei Cloud Cloud Eye alert source (#3443)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-09 00:22:31 +08:00
d681420c11 [feature] add support for darwin (#3431)
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-09 00:04:02 +08:00
Yang Chen 8618145e44 [feat] add support for ollama and update docs (#3441) 2025-06-07 18:48:41 +08:00
Duansgandkangli dd0045c6a4 [bugfix] Fix the issue where Prometheus RealTime Threshold is not eff… (#3434)
Co-authored-by: kangli <likang@apache.org>
2025-06-06 13:00:55 +08:00
Yang Chen 84e0a604e7 [improvement] disable default OpenTelemetry exporters to prevent connection errors (#3437) 2025-06-06 11:29:51 +08:00
Calvinandtomsun28 ce253f7069 [doc] japanese emqx (#3433)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-06 00:52:26 +08:00
Jast 767b70f70b [Improve] add mvnw (#3430) 2025-06-06 00:49:50 +08:00
Duansgandtomsun28 4aad08ca17 [feature]Support Alibaba Cloud 'Simple Log Service(SLS)' alert source (#3422)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-04 21:05:57 +08:00
c176bb2988 [doc] japanese elasticsearch (#3423)
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-04 09:31:22 +08:00
3cb4fc71f9 [doc] japanese dynamic_tp (#3419)
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-04 09:23:57 +08:00
c3f32132ca [feat] support sql("...") and promql("...") expressions including SQL condition parsing (#3410)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-06-04 09:07:18 +08:00
e1affb169f [feature]Make GreptimeDB as a grafana data source (#3403)
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-03 23:37:31 +08:00
shown db27159947 [infra]: Update issue tmpl (#3421)
Signed-off-by: shown <yuluo08290126@gmail.com>
2025-06-02 18:52:51 +08:00
63c187488f [doc] japanese doris_fe (#3416)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-31 09:14:37 +08:00
f3f66ada0d [doc] Update anonymous user auth configuration of the grafana dashboard document (#3407)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-05-31 09:12:33 +08:00
tomsun28 d5c934dc01 [doc] update home doc (#3418) 2025-05-30 21:26:14 +08:00
Jast 518eebb6b1 [Doc] Update 1.7.1 (#3411) 2025-05-29 16:12:24 +08:00
Calvin 210454f969 [doc] japanese doris_be (#3409) 2025-05-29 08:03:06 +08:00
Calvin a8db5e31a0 [doc] japanese docker (#3408) 2025-05-28 07:16:33 +08:00
Calvin c01653aff3 [doc] japanese dns sd (#3405) 2025-05-27 21:27:58 +08:00
Calvin 480bd0e449 [doc] japanese dns (#3404) 2025-05-27 08:47:33 +08:00
97faca6753 [docs] update alert threshold doc (#3399)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-26 16:12:50 +08:00
Calvinandtomsun28 e271bd7417 [doc] japanese deepseek & dm (#3400)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-26 08:53:40 +08:00
7de4fd1cbf [improve] http sd collector adds authentication (#3388)
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-05-26 08:42:21 +08:00
Calvin c2a7d0db35 [doc] japanese Debian (#3398) 2025-05-25 15:42:54 +08:00
Calvin 93b2630bf3 [doc] japanese dahua (#3396) 2025-05-25 13:09:06 +08:00
Calvinandliutianyou 23749e7fac [doc] japanese coreos (#3393)
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-24 09:35:46 +08:00
kangli d5eae40754 [doc]add new committer blog (#3395) 2025-05-24 08:23:17 +08:00
Calvin 7ef79de1de [doc] japanese consul sd (#3392) 2025-05-23 13:07:11 +08:00
tomsun28 8496771c8b [doc] new contributor and committer, update doc (#3391)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-05-23 12:15:09 +08:00
Calvin f31c835181 [doc] japanese clickhouse (#3390) 2025-05-23 08:42:33 +08:00
Logicandtomsun28 552554b96d [fix](warehouse): improve GreptimeDB data storage and querying (#3387)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-22 23:31:06 +08:00
zhangyaxiandtomsun28 4adcfd2a4e [test] Add unit tests for HttpSdCollectImpl (#3386)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-22 23:29:23 +08:00
Calvin e630beecb4 [doc] japanese cisco switch (#3389) 2025-05-22 22:46:53 +08:00
Duansgandtomsun28 6c2b38e180 [doc] modify supplement related documentation (#3383)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-21 23:17:50 +08:00
84986dd1bf [feature] support auto nacos service discovery (#3324)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: zhangyaxi <yaxi.zhang950408@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-05-21 22:01:09 +08:00
af868e083a [Task] Batch import metrics data in victoria-metrics (#3337)
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-05-20 23:56:56 +08:00
be3e2f3558 [doc] modify chinese words (#3380)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 21:42:37 +08:00
d32818e8ef [feat] Support Zookeeper Service Discovery (#3377)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 21:30:48 +08:00
10b283522c [improve] add path validation for pluginservice (#3375)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-20 12:32:24 +08:00
31ab2da41e [doc] Add blog (#3379)
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:16:06 +08:00
LL-LINandtomsun28 a9436049e3 [doc] fix incorrect time template syntax usercase in doc (#3378)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:12:20 +08:00
Calvinandtomsun28 41c010511c [doc] japanese centos (#3376)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:08:39 +08:00
aias00andCalvin b5281c1274 update maven dep (#3359)
Co-authored-by: Calvin <naruse_shinji@163.com>
2025-05-20 00:06:59 +08:00
921 changed files with 49721 additions and 6498 deletions
+120 -4
View File
@@ -6,6 +6,7 @@
"files": [
"README.md",
"README_CN.md",
"README_JP.md"
"home/src/pages/team/index.jsx"
],
"imageSize": 100,
@@ -1269,10 +1270,10 @@
]
},
{
"login": "a-little-fool",
"name": "小笨蛋",
"login": "lynx009",
"name": "lynx009",
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
"profile": "https://github.com/a-little-fool",
"profile": "https://github.com/lynx009",
"contributions": [
"code",
"doc",
@@ -1441,7 +1442,7 @@
},
{
"login": "Calvin979",
"name": "Naruse",
"name": "Calvin",
"avatar_url": "https://avatars.githubusercontent.com/u/131688897?v=4",
"profile": "https://github.com/Calvin979",
"contributions": [
@@ -2485,6 +2486,121 @@
"contributions": [
"code"
]
},
{
"login": "Duansg",
"name": "Duansg",
"avatar_url": "https://avatars.githubusercontent.com/u/112607719?v=4",
"profile": "https://github.com/Duansg",
"contributions": [
"doc"
]
},
{
"login": "xiaomizhou2",
"name": "zhangyaxi",
"avatar_url": "https://avatars.githubusercontent.com/u/47807926?v=4",
"profile": "https://github.com/xiaomizhou2",
"contributions": [
"code",
"test"
]
},
{
"login": "RainBondsongyg",
"name": "songyg",
"avatar_url": "https://avatars.githubusercontent.com/u/94501396?v=4",
"profile": "https://github.com/RainBondsongyg",
"contributions": [
"doc"
]
},
{
"login": "lx1229",
"name": "Liuxin",
"avatar_url": "https://avatars.githubusercontent.com/u/44620005?v=4",
"profile": "https://github.com/lx1229",
"contributions": [
"code"
]
},
{
"login": "yy549159265",
"name": "yy549159265",
"avatar_url": "https://avatars.githubusercontent.com/u/40821310?v=4",
"profile": "https://github.com/yy549159265",
"contributions": [
"code",
"test",
"design"
]
},
{
"login": "cto-huhang",
"name": "cto-huhang",
"avatar_url": "https://avatars.githubusercontent.com/u/53338629?v=4",
"profile": "https://github.com/cto-huhang",
"contributions": [
"doc"
]
},
{
"login": "Saramanda9988",
"name": "LunaRain_079",
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
"profile": "https://github.com/Saramanda9988",
"contributions": [
"doc"
]
},
{
"login": "delei",
"name": "DeleiGuo",
"avatar_url": "https://avatars.githubusercontent.com/u/17263766?v=4",
"profile": "https://github.com/delei",
"contributions": [
"doc",
"code",
"test",
"bug"
]
},
{
"login": "chingjustwe",
"name": "Rocky, Chi",
"avatar_url": "https://avatars.githubusercontent.com/u/13643747?v=4",
"profile": "https://github.com/chingjustwe",
"contributions": [
"code"
]
},
{
"login": "rowankid",
"name": "Wenqi Luo",
"avatar_url": "https://avatars.githubusercontent.com/u/18652781?v=4",
"profile": "https://github.com/rowankid",
"contributions": [
"bug"
]
},
{
"login": "tuzuy",
"name": "tuzuy",
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
"profile": "https://github.com/tuzuy",
"contributions": [
"code",
"design"
]
},
{
"login": "carlpinto25",
"name": "carl pinto",
"avatar_url": "https://avatars.githubusercontent.com/u/117299909?v=4",
"profile": "https://github.com/carlpinto25",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+4 -1
View File
@@ -16,7 +16,7 @@
#
github:
description: Apache HertzBeat(incubating) is a real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
description: Real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
homepage: https://hertzbeat.apache.org/
labels:
- monitoring
@@ -52,6 +52,9 @@ github:
required_pull_request_reviews:
dismiss_stale_reviews: true
required_approving_review_count: 1
# enable GitHub Dependabot to create PRs for security alerts but not for every dependency update
dependabot_alerts: true
dependabot_updates: false
notifications:
commits: notifications@hertzbeat.apache.org
issues: notifications@hertzbeat.apache.org
+2 -1
View File
@@ -53,4 +53,5 @@ body:
validations:
required: false
- type: markdown
value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR"
attributes:
value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR"
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
- uses: ./script/ci/github-actions/setup-deps
- name: Build with Maven
run: mvn clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
run: mvnd clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
+3 -5
View File
@@ -49,11 +49,9 @@ jobs:
- name: Dead Link Check
run: |
sudo npm install -g markdown-link-check@3.8.7
for file in $(find ./home -name "*.md"); do
if ! grep -Fxq "$file" ./script/ci/exclude_files.txt; then
markdown-link-check -c ./script/ci/link_check.json -q "$file"
fi
done
find ./home -name "*.md" > all_md_files.txt
grep -vFf ./script/ci/exclude_files.txt all_md_files.txt > to_check.txt
xargs -P 8 -a to_check.txt -I{} markdown-link-check -c ./script/ci/link_check.json -q "{}"
- name: NPM INSTALL
working-directory: home
+1
View File
@@ -67,6 +67,7 @@ header:
- 'karma.conf.js'
- 'proxy.conf.js'
- '.helmignore'
- 'script/helm/**'
- 'web-app/src/app/core/**'
- 'web-app/src/app/layout/**'
- 'web-app/src/app/routes/exception/**'
-10
View File
@@ -1,10 +0,0 @@
Apache HertzBeat (incubating) is an effort undergoing incubation at the Apache
Software Foundation (ASF), sponsored by the Apache Incubator PMC.
Incubation is required of all newly accepted projects until a further review
indicates that the infrastructure, communications, and decision making process
have stabilized in a manner consistent with other successful ASF projects.
While incubation status is not necessarily a reflection of the completeness
or stability of the code, it does indicate that the project has yet to be
fully endorsed by the ASF.
+1 -1
View File
@@ -1,4 +1,4 @@
Apache HertzBeat (incubating)
Apache HertzBeat
Copyright 2024-2025 The Apache Software Foundation
This product includes software developed at
+10 -4
View File
@@ -9,7 +9,7 @@
<b><a href="README.md">English</a></b> | <a href="README_CN.md">中文</a> | <a href="README_JP.md">日本語</a>
</p>
> A real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
> A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
@@ -28,7 +28,7 @@
## 🎡 <font color="green">Introduction</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating) is an easy-to-use, open source, real-time monitoring system with agentless, high performance cluster, prometheus-compatible, offers powerful custom monitoring and status page building capabilities.
[Apache HertzBeat](https://github.com/apache/hertzbeat) is an easy-to-use, open source, real-time observability system with agentless, high performance cluster, prometheus-compatible, offers powerful custom monitoring and status page building capabilities.
### Features
@@ -523,6 +523,12 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -566,8 +572,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
<p align="left">
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300">&nbsp;&nbsp;<img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
<br /><br />
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
CNCF Observability And Analysis - Monitoring Landscape.</a>
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
CNCF Observability And Analysis - Observability Landscape.</a>
</p>
## 🛡️ License
+17 -11
View File
@@ -9,7 +9,7 @@
<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a>
</p>
> 实时监控系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
> 实时观测系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
@@ -28,16 +28,16 @@
## 🎡 <font color="green">介绍</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) incubating是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
[Apache HertzBeat](https://github.com/apache/hertzbeat) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
### 特点
-**监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位。
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,零上手学习成本。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需配置下就能立刻适配一款 `K8s``Docker` 等新的监控类型吗?
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,无需学习成本。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的。
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
- 自由的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
- 灵活的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
@@ -59,8 +59,8 @@
## ⛄ 已支持
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模,用户可以导入这些模来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模,用户可以导入这些模来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
@@ -98,7 +98,7 @@
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
- 和更多自定义监控模
- 和更多自定义监控模
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`
## 🐕 快速开始
@@ -126,7 +126,7 @@
- `-e IDENTITY=custom-collector-name` : 配置此采集器的唯一性标识符名称,多个采集器名称不能相同,建议自定义英文名称。
- `-e MODE=public` : 配置运行模式(public or private), 公共集群模式或私有云边模式。
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertaBeat 服务的对外 IP。
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertzBeat 服务的对外 IP。
- `-e MANAGER_PORT=1158` : 配置连接主 HertzBeat 服务的对外端口,默认1158。
@@ -521,6 +521,12 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -566,8 +572,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
<p align="left">
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300">&nbsp;&nbsp;<img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
<br /><br />
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
CNCF Observability And Analysis - Monitoring Landscape.</a>
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
CNCF Observability And Analysis - Observability Landscape.</a>
</p>
## 🛡️ License
+8 -2
View File
@@ -525,6 +525,12 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
</tr>
</tbody>
</table>
@@ -570,8 +576,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
<p align="left">
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300">&nbsp;&nbsp;<img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
<br /><br />
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
CNCF Observability And Analysis - Monitoring Landscape.</a>
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
CNCF Observability And Analysis - Observability Landscape.</a>
</p>
## 🛡️ License
+3 -3
View File
@@ -1,6 +1,6 @@
# Security Policy
Apache HertzBeat follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
Apache HertzBeat follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
## Supported Versions
@@ -17,6 +17,6 @@ To report a new vulnerability you have discovered please follow the [ASF vulnera
## Extra Information
- [Apache HertzBeat Security Model](https://hertzbeat.apache.org/docs/help/security_model)
- [Apache HertzBeat Security Model](https://hertzbeat.apache.org/docs/help/security_model)
- [ASF Security](https://www.apache.org/security/)
- [Apache HertzBeat Document](https://hertzbeat.apache.org/docs/)
- [Apache HertzBeat Document](https://hertzbeat.apache.org/docs/)
+93
View File
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>hertzbeat-ai-agent</artifactId>
<version>${hertzbeat.version}</version>
<properties>
<spring-ai.version>1.0.1</spring-ai.version>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-common</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.data.domain.Page;
import java.util.List;
/**
* Interface that provides access to alert definition information by retrieving data
* through the underlying alert define service.
*/
public interface AlertDefineServiceAdapter {
/**
* Add a new alert rule definition
* @param alertDefine Alert definition to add
* @return Created alert definition with ID
*/
AlertDefine addAlertDefine(AlertDefine alertDefine);
/**
* Get alert definitions with filtering and pagination
* @param search Search term
* @param app Monitor type filter
* @param enabled Enabled status filter
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of alert definitions
*/
Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize);
/**
* Get alert definition by ID
* @param id Alert definition ID
* @return Alert definition if found
*/
AlertDefine getAlertDefine(Long id);
/**
* Enable or disable alert definition
* @param id Alert definition ID
* @param enabled Whether to enable
*/
void toggleAlertDefineStatus(Long id, boolean enabled);
/**
* Modify/update an existing alert definition
* @param alertDefine Alert definition to update
* @return Updated alert definition
*/
AlertDefine modifyAlertDefine(AlertDefine alertDefine);
/**
* Get specific app hierarchy structure
* @param app App type
* @param lang Language for localization
* @return List of hierarchy objects for specific app
*/
List<Hierarchy> getAppHierarchy(String app, String lang);
}
@@ -0,0 +1,60 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.data.domain.Page;
/**
* Interface that provides access to alert information by retrieving alert data
* through the underlying alert service.
*/
public interface AlertServiceAdapter {
/**
* Get single alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of single alerts
*/
Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get group alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of group alerts
*/
Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get alerts summary statistics
* @return Alert summary information
*/
AlertSummary getAlertsSummary();
}
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
/**
* Interface that provides access to metrics information by retrieving metrics data
* through the underlying metrics service.
*/
public interface MetricsServiceAdapter {
/**
* Check warehouse storage server status
* @return true if warehouse is available, false otherwise
*/
Boolean getWarehouseStorageServerStatus();
/**
* Query real-time metrics data
* @param monitorId Monitor ID
* @param metrics Metrics name
* @return Real-time metrics data
*/
MetricsData getMetricsData(Long monitorId, String metrics);
/**
* Query historical metrics data
* @param monitorId Monitor ID
* @param app Monitor type
* @param metrics Metrics name
* @param metric Metric field name
* @param label Label filter
* @param history Query historical time period
* @param interval Whether to aggregate data
* @return Historical metrics data
*/
MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
}
@@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.List;
import java.util.Map;
/**
* Interface that provides access to monitor information by retrieving monitor data
* through the underlying monitor service.
*/
public interface MonitorServiceAdapter {
Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels
);
/**
* Add a new monitor
*
* @param monitor The monitor entity to create
* @param params List of parameters for the monitor
* @param collector Optional collector assignment
* @return The created monitor ID
*/
Long addMonitor(Monitor monitor, List<Param> params, String collector);
/**
* Get all available monitor types with their display names
*
* @param language Language code (e.g., "en-US", "zh-CN")
* @return Map of monitor type key to display name
*/
Map<String, String> getAvailableMonitorTypes(String language);
/**
* Get parameter definitions for a specific monitor type
*
* @param app Monitor type/application name (e.g., "linux", "mysql", "redis")
* @return List of parameter definitions for the monitor type
*/
List<ParamDefine> getMonitorParamDefines(String app);
}
@@ -0,0 +1,257 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
/**
* Implementation of the AlertDefineServiceAdapter interface that provides access to alert definition information
* through reflection by invoking the underlying alert define service implementation.
*/
@Slf4j
@Component
public class AlertDefineServiceAdapterImpl implements AlertDefineServiceAdapter {
@Override
public AlertDefine addAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("addAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully added alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke addAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke addAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod(
"getAlertDefines",
List.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<AlertDefine> result = (Page<AlertDefine>) method.invoke(
alertDefineService, null, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} alert defines", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefines via adapter", e);
throw new RuntimeException("Failed to invoke getAlertDefines via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine getAlertDefine(Long id) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine result = (AlertDefine) method.invoke(alertDefineService, id);
log.debug("Successfully retrieved alert define with ID: {}", id);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefine via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke getAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public void toggleAlertDefineStatus(Long id, boolean enabled) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
// First get the existing AlertDefine
Method getMethod = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine alertDefine = (AlertDefine) getMethod.invoke(alertDefineService, id);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + id + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
Method modifyMethod = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
modifyMethod.invoke(alertDefineService, alertDefine);
log.debug("Successfully toggled alert define status for ID: {} to enabled: {}", id, enabled);
} catch (Exception e) {
log.error("Failed to invoke toggleAlertDefineStatus via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke toggleAlertDefineStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine modifyAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully modified alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: modifyAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke modifyAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke modifyAlertDefine via adapter: " + e.getMessage(), e);
}
}
/**
* Retrieves the application hierarchy for a given app and language.
* Uses reflection to call the underlying app service method.
*
* @param app The application name
* @param lang The language code (optional, defaults to "en-US")
* @return List of Hierarchy objects representing the app hierarchy
*/
@Override
public List<Hierarchy> getAppHierarchy(String app, String lang) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAppHierarchy: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (lang == null || lang.trim().isEmpty()) {
lang = "en-US";
}
// Call getAppHierarchy method: getAppHierarchy(String app, String lang)
Method method = appService.getClass().getMethod("getAppHierarchy", String.class, String.class);
List<?> managerHierarchies = (List<?>) method.invoke(appService, app, lang);
// Convert manager DTOs to ai-agent DTOs
List<Hierarchy> result = UtilityClass.convertToAgentHierarchies(managerHierarchies);
log.debug("Successfully retrieved and converted {} hierarchies for app '{}'", result.size(), app);
return result;
} catch (Exception e) {
log.error("Failed to get app hierarchy for app '{}': {}", app, e.getMessage(), e);
throw new RuntimeException("Failed to get app hierarchy for " + app, e);
}
}
}
@@ -0,0 +1,141 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the AlertServiceAdapter interface that provides access to alert information
* through reflection by invoking the underlying alert service implementation.
*/
@Slf4j
@Component
public class AlertServiceAdapterImpl implements AlertServiceAdapter {
@Override
public Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getSingleAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getSingleAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<SingleAlert> result = (Page<SingleAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} single alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getSingleAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getSingleAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getSingleAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getGroupAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getGroupAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<GroupAlert> result = (Page<GroupAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} group alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getGroupAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getGroupAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getGroupAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertSummary getAlertsSummary() {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertsSummary: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod("getAlertsSummary");
AlertSummary result = (AlertSummary) method.invoke(alertService);
log.debug("Successfully retrieved alerts summary");
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertsSummary", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertsSummary via adapter", e);
throw new RuntimeException("Failed to invoke getAlertsSummary via adapter: " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,136 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the MetricsServiceAdapter interface that provides access to metrics information
* through reflection by invoking the underlying metrics service implementation.
*/
@Slf4j
@Component
public class MetricsServiceAdapterImpl implements MetricsServiceAdapter {
@Override
public Boolean getWarehouseStorageServerStatus() {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod("getWarehouseStorageServerStatus");
Boolean result = (Boolean) method.invoke(metricsDataService);
log.debug("Successfully retrieved warehouse storage server status: {}", result);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getWarehouseStorageServerStatus", e);
} catch (Exception e) {
log.error("Failed to invoke getWarehouseStorageServerStatus via adapter", e);
throw new RuntimeException("Failed to invoke getWarehouseStorageServerStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsData getMetricsData(Long monitorId, String metrics) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricsData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricsData",
Long.class, String.class);
MetricsData result = (MetricsData) method.invoke(metricsDataService, monitorId, metrics);
log.debug("Successfully retrieved metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricsData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricsData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricsData via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricHistoryData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricHistoryData",
Long.class, String.class, String.class, String.class, String.class, String.class, Boolean.class);
MetricsHistoryData result = (MetricsHistoryData) method.invoke(
metricsDataService, monitorId, app, metrics, metric, label, history, interval);
log.debug("Successfully retrieved historical metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricHistoryData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricHistoryData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricHistoryData via adapter: " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,214 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/**
* Implementation of the MonitorServiceAdapter interface that provides access to monitor information
* through reflection by invoking the underlying monitor service implementation.
*/
@Slf4j
@Component
public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
@Override
public Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels) {
try {
// Provide default values for all nullable parameters
if (sort == null || sort.trim().isEmpty()) {
sort = "gmtCreate";
}
if (order == null || order.trim().isEmpty()) {
order = "desc";
}
if (pageIndex == null) {
pageIndex = 0;
}
if (pageSize == null) {
pageSize = 8;
}
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found: {}", monitorService.getClass().getSimpleName());
Method method = monitorService.getClass().getMethod(
"getMonitors",
List.class, String.class, String.class, Byte.class,
String.class, String.class, int.class, int.class, String.class);
@SuppressWarnings("unchecked")
Page<Monitor> result = (Page<Monitor>) method.invoke(
monitorService,
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result.getContent());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMonitors", e);
} catch (Exception e) {
log.debug("Failed to invoke getMonitors via adapter", e);
throw new RuntimeException("Failed to invoke getMonitors via adapter", e);
}
}
@Override
public Long addMonitor(Monitor monitor, List<Param> params, String collector) {
try {
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found for addMonitor: {}", monitorService.getClass().getSimpleName());
// Call addMonitor method: addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard)
Method method = monitorService.getClass().getMethod(
"addMonitor",
Monitor.class, List.class, String.class,
Class.forName("org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard"));
// Call the method with null dashboard
method.invoke(monitorService, monitor, params, collector, null);
log.debug("Successfully added monitor: {} with ID: {}", monitor.getName(), monitor.getId());
return monitor.getId();
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addMonitor", e);
} catch (Exception e) {
log.error("Failed to invoke addMonitor via adapter", e);
throw new RuntimeException("Failed to invoke addMonitor via adapter: " + e.getMessage(), e);
}
}
@Override
public Map<String, String> getAvailableMonitorTypes(String language) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAvailableMonitorTypes: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAvailableMonitorTypes: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Call getI18nApps method: getI18nApps(String lang)
Method method = appService.getClass().getMethod("getI18nApps", String.class);
@SuppressWarnings("unchecked")
Map<String, String> result = (Map<String, String>) method.invoke(appService, language);
log.debug("Successfully retrieved {} monitor types", result.size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getI18nApps", e);
} catch (Exception e) {
log.error("Failed to invoke getI18nApps via adapter", e);
throw new RuntimeException("Failed to invoke getI18nApps via adapter: " + e.getMessage(), e);
}
}
@Override
public List<ParamDefine> getMonitorParamDefines(String app) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getMonitorParamDefines: {}", appService.getClass().getSimpleName());
// Validate app parameter
if (app == null || app.trim().isEmpty()) {
throw new IllegalArgumentException("Monitor type/app parameter is required");
}
// Call getAppParamDefines method: getAppParamDefines(String app)
Method method = appService.getClass().getMethod("getAppParamDefines", String.class);
@SuppressWarnings("unchecked")
List<ParamDefine> result = (List<ParamDefine>) method.invoke(appService, app.toLowerCase().trim());
log.debug("Successfully retrieved {} parameter definitions for monitor type: {}", result.size(), app);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAppParamDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAppParamDefines via adapter for app: {}", app, e);
throw new RuntimeException("Failed to invoke getAppParamDefines via adapter: " + e.getMessage(), e);
}
}
}
@@ -0,0 +1,246 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.usthe.sureness.mgt.SurenessSecurityManager;
import com.usthe.sureness.subject.SubjectSum;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Custom Server-Sent Events transport provider for Model Context Protocol.
*/
@Slf4j
public class CustomSseServerTransport implements McpServerTransportProvider {
private final ObjectMapper objectMapper;
private final String messageEndpoint;
private final String sseEndpoint;
private final String baseUrl;
@Getter
private final RouterFunction<ServerResponse> routerFunction;
@Setter
private McpServerSession.Factory sessionFactory;
private final Map<String, Object> sessionRequest = new HashMap<>();
private final ConcurrentHashMap<String, McpServerSession> sessions;
private volatile boolean isClosing;
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
this(objectMapper, messageEndpoint, "/sse");
}
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
this(objectMapper, "", messageEndpoint, sseEndpoint);
}
public CustomSseServerTransport(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) {
this.sessions = new ConcurrentHashMap();
this.isClosing = false;
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.notNull(baseUrl, "Message base URL must not be null");
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
this.objectMapper = objectMapper;
this.baseUrl = baseUrl;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
this.routerFunction = RouterFunctions.route().GET(this.sseEndpoint, this::handleSseConnection).POST(this.messageEndpoint, this::handleMessage).build();
}
public Mono<Void> notifyClients(String method, Object params) {
if (this.sessions.isEmpty()) {
log.debug("No active sessions to broadcast message to");
return Mono.empty();
} else {
log.debug("Attempting to broadcast message to {} active sessions", this.sessions.size());
return Flux.fromIterable(this.sessions.values())
.flatMap((session) -> session.sendNotification(method, params)
.doOnError((e) -> log.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
.onErrorComplete())
.then();
}
}
public Mono<Void> closeGracefully() {
return Flux.fromIterable(this.sessions.values()).doFirst(() -> {
this.isClosing = true;
log.debug("Initiating graceful shutdown with {} active sessions", this.sessions.size());
}).flatMap(McpServerSession::closeGracefully).then().doOnSuccess((v) -> log.debug("Graceful shutdown completed"));
}
private ServerResponse handleSseConnection(ServerRequest request) {
log.debug("Handling SSE connection for request: {}", request);
HttpServletRequest servletRequest = request.servletRequest();
try {
log.debug("Processing SSE connection for servlet request: {}", servletRequest);
log.debug("Authorization header: {}", servletRequest.getHeader("Authorization"));
} catch (Exception e) {
log.error("Authentication failed for SSE connection: {}", e.getMessage());
return ServerResponse.status(HttpStatus.UNAUTHORIZED).body("Unauthorized: " + e.getMessage());
}
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
} else {
String sessionId = UUID.randomUUID().toString();
log.debug("Generated session ID for SSE connection: {}", sessionId);
log.debug("Creating new SSE connection for session: {}", sessionId);
return ServerResponse.sse((sseBuilder) -> {
sseBuilder.onComplete(() -> {
log.debug("SSE connection completed for session: {}", sessionId);
this.sessions.remove(sessionId);
});
sseBuilder.onTimeout(() -> {
log.debug("SSE connection timed out for session: {}", sessionId);
this.sessions.remove(sessionId);
});
CustomSseServerTransport.WebMvcMcpSessionTransport sessionTransport = new CustomSseServerTransport.WebMvcMcpSessionTransport(sessionId, sseBuilder);
McpServerSession session = this.sessionFactory.create(sessionTransport);
this.sessionRequest.put(sessionId, request.servletRequest());
this.sessions.put(sessionId, session);
try {
sseBuilder.id(sessionId).event("endpoint").data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
} catch (Exception e) {
log.error("Failed to send initial endpoint event: {}", e.getMessage());
sseBuilder.error(e);
}
}, Duration.ZERO);
}
}
private ServerResponse handleMessage(ServerRequest request) {
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
} else if (request.param("sessionId").isEmpty()) {
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
} else {
String sessionId = (String) request.param("sessionId").get();
McpServerSession session = (McpServerSession) this.sessions.get(sessionId);
log.debug("Authorization header for message request: {}", request.servletRequest().getHeader("Authorization"));
SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(sessionRequest.get(sessionId));
McpContextHolder.setSubject(subject);
if (session == null) {
return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));
} else {
try {
String body = request.body(String.class);
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, body);
session.handle(message).block();
return ServerResponse.ok().build();
} catch (IOException | IllegalArgumentException e) {
log.error("Failed to deserialize message: {}", ((Exception) e).getMessage());
return ServerResponse.badRequest().body(new McpError("Invalid message format"));
} catch (Exception e) {
log.error("Error handling message: {}", e.getMessage());
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
}
}
}
}
private class WebMvcMcpSessionTransport implements McpServerTransport {
private final String sessionId;
private final ServerResponse.SseBuilder sseBuilder;
WebMvcMcpSessionTransport(String sessionId, ServerResponse.SseBuilder sseBuilder) {
this.sessionId = sessionId;
this.sseBuilder = sseBuilder;
log.debug("Session transport {} initialized with SSE builder", sessionId);
}
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
try {
String jsonText = CustomSseServerTransport.this.objectMapper.writeValueAsString(message);
this.sseBuilder.id(this.sessionId).event("message").data(jsonText);
log.debug("Message sent to session {}", this.sessionId);
} catch (Exception e) {
log.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
this.sseBuilder.error(e);
}
});
}
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return (T) CustomSseServerTransport.this.objectMapper.convertValue(data, typeRef);
}
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
log.debug("Closing session transport: {}", this.sessionId);
try {
this.sseBuilder.complete();
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
} catch (Exception e) {
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
}
});
}
public void close() {
try {
this.sseBuilder.complete();
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
} catch (Exception e) {
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
}
}
}
}
@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.ai.model.ApiKey;
import org.springframework.stereotype.Component;
/**
* Dynamic OpenAI API Key implementation that retrieves the API key
* from our configuration service (database first, then YAML fallback)
*/
@Slf4j
@Component
public class DynamicOpenAiApiKey implements ApiKey {
private final OpenAiConfigService openAiConfigService;
public DynamicOpenAiApiKey(OpenAiConfigService openAiConfigService) {
this.openAiConfigService = openAiConfigService;
}
@Override
public String getValue() {
try {
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null) {
log.debug("Retrieved OpenAI API key from configuration service");
return effectiveConfig.getApiKey();
} else {
log.warn("No valid OpenAI API key found in configuration");
return null;
}
} catch (Exception e) {
log.error("Error retrieving OpenAI API key from configuration", e);
return null;
}
}
}
@@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration class for Large Language Model (LLM) settings.
*/
@Configuration
public class LlmConfig {
@Value("${spring.ai.openai.chat.options.model}")
private String model;
/**
* Create OpenAI API instance with dynamic API key
*/
@Bean
public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
return OpenAiApi.builder()
.apiKey(dynamicApiKey)
.build();
}
/**
* Create OpenAI Chat Options with custom settings
*/
@Bean
public OpenAiChatOptions openAiChatOptions() {
return OpenAiChatOptions.builder()
.model(model)
.temperature(0.3)
.build();
}
/**
* Create OpenAI Chat Model with custom API configuration
*/
@Bean
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions openAiChatOptions) {
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(openAiChatOptions)
.build();
}
@Bean
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
return ChatClient.create(openAiChatModel);
}
}
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import com.usthe.sureness.subject.SubjectSum;
import org.springframework.core.NamedInheritableThreadLocal;
/**
* Context holder for AI agent security context.
*/
public final class McpContextHolder {
private static final ThreadLocal<SubjectSum> subjectHolder =
new NamedInheritableThreadLocal<>("MCP Security and User Identification Context");
private McpContextHolder() {}
/**
* Attaches the user's context to the current thread.
*/
public static void setSubject(SubjectSum subject) {
subjectHolder.set(subject);
}
/**
* Retrieves the context from the current thread.
*/
public static SubjectSum getSubject() {
return subjectHolder.get();
}
/**
* Clears the context from the thread to prevent memory leaks.
*/
public static void clear() {
subjectHolder.remove();
}
}
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* OpenAI YAML Configuration - reads from spring.ai.openai.api-key
*/
@Data
@Component
@ConfigurationProperties(prefix = "spring.ai.openai")
public class OpenAiYamlConfig {
/**
* OpenAI API key from spring.ai.openai.api-key
*/
private String apiKey;
/**
* Check if OpenAI is enabled (has API key)
*/
public boolean isEnable() {
return apiKey != null && !apiKey.trim().isEmpty();
}
}
@@ -0,0 +1,182 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import org.springframework.stereotype.Component;
/**
* Provider for system prompts used in the AI agent
*/
@Component
public class PromptProvider {
/**
* Static version of the HertzBeat monitoring prompt
*/
public static final String HERTZBEAT_SYSTEM_PROMPT = """
You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat.
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues.
*******
VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system.
If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters.
********
## Available HertzBeat Tools:
### Monitor Management Tools:
- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels)
- **add_monitor**: Add a new monitor with dynamic app-specific parameter support
- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.)
- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types
### Alert Rule Management Tools:
- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding
- **list_alert_rules**: List existing alert rules with filtering by type, status, etc.
- **toggle_alert_rule**: Enable or disable alert rules
- **get_alert_rule_details**: Get detailed information about specific alert rules
- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts)
- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting
### Alert & Alarm Analysis Tools:
- **query_alerts**: Query fired alerts with comprehensive filtering and pagination
- **get_alerts_summary**: Get alert statistics and status distribution
### Metrics Data Analysis Tools:
- **query_realtime_metrics**: Get current real-time metrics data for monitors
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
- **get_warehouse_status**: Check metrics storage system status
## Natural Language Examples:
### Monitor Management:
- "Add a MySQL monitor for database server at 192.168.1.10 with user admin"
- "Monitor website https://example.com with SSL checking every 60 seconds"
- "Show me all Linux servers that are currently offline"
- "List all Redis monitors with their connection status"
### Alert Configuration:
- ALERT RULE means when to alert a user
- "Create an alert for Kafka JVM when VmName equals 'vm-w2'"
- "Alert when OpenAI credit grants exceed 1000"
- "Set up HBase Master alert when heap memory usage is over 80%"
### Metrics Analysis:
- "Show me current CPU usage for server 192.168.1.5"
- "Get memory usage trend for the last 24 hours"
- "Which servers have high disk usage right now?"
- "Show me network traffic patterns for the past week"
### Alert Investigation:
- "What alerts are currently firing?"
- "Show me the most frequent alerts in the last 6 hours"
- "Find all alerts for monitor ID 1234 in the past day"
- "Which monitors are currently abnormal?"
## Workflow Guidelines:
1. **Adding Monitors**:
- ALWAYS use get_monitor_additional_params first to check required parameters
- Use list_monitor_types to show available types
- Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor
- Example: "To monitor MySQL, I need host, port, username, password, and database name"
2. **Creating Alert Rules or Alerts**:
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
It is important to first understand the hierarchy of apps, metrics, and field conditions
Each app has its own metrics and each metric has its own field conditions.
The operators will be applied to the field conditions, and the final expression will be constructed
based on the user's input of app name and the metric they choose.
Read the create_alert_rule tool description for even more details
*******
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
4. Ask the user to choose a metric from the available metrics
5. Based on the metric chosen, present the available field conditions
6. You will construct the proper expression with field conditions
VERY VERY IMPORTANT:
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
*********
- Field Condition Expression format: [field_conditions]
- Give all the available fieldConditions to the user, so they can choose the one they want to use
- Field conditions can be simple (equals, greater than) or complex (logical expressions)
- Use parentheses for complex conditions to ensure correct evaluation order
- Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically
EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ):
- Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, \"my-vm\")"
→ equals(VmName, "my-vm")
- Complex OpenAI: app="openai", metrics="credit_grants",
fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)"
→ total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
- Priority levels: 0=critical, 1=warning, 2=info
3. **Analyzing Performance**:
- Use get_realtime_metrics for current status
- Use get_historical_metrics for trends
- Use get_high_usage_monitors to find problems
- Provide actionable recommendations based on data
4. **Troubleshooting Alerts**:
- Use query_alerts to find current issues
- Use get_monitor_alerts for specific monitor problems
- Use get_frequent_alerts to identify recurring issues
- Suggest root cause analysis steps
## Parameter Guidelines:
- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all
- **Time Ranges**: 1h, 6h, 24h, 7d, 30d
- **Alert Priorities**: critical, warning, info
- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes
- **Metric Types**: cpu, memory, disk, network, custom
- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases)
## Best Practices:
- Never create alert rules without exact user input on app, metrics, and field conditions
- Always validate monitor types and parameters before adding monitors
- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields
- Construct field conditions based on metric's children
- Use exact app and metric names from hierarchy (case-sensitive)
- Set appropriate alert thresholds based on baseline performance
- Use time-series data to identify trends and predict issues
- Correlate alerts with metrics data for root cause analysis
- Recommend monitoring intervals based on service criticality
- Provide clear explanations of monitoring data and actionable insights
## Avoid these common errors:
- Using Label name instead of the value from the heirarchy JSON while creating alert rules.
- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators
- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value.
So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<',
because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
""";
}
@@ -0,0 +1,197 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.controller;
import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.util.SurenessContextHolder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import javax.validation.Valid;
import java.util.List;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
/**
* Controller class for handling AI chat requests and conversation management.
*/
@Slf4j
@Tag(name = "AI Chat API")
@RestController
@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE})
public class ChatController {
private final ConversationService conversationService;
@Autowired
public ChatController(ConversationService conversationService) {
this.conversationService = conversationService;
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ConversationDto>> createConversation() {
try {
ConversationDto conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error creating conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to create conversation"));
}
}
/**
* Send a message and get a streaming response with conversation tracking
*
* @param context The chat request context containing message and optional conversationId
* @return Flux of ServerSentEvent for streaming response
*/
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(@Valid @RequestBody ChatRequestContext context) {
try {
// Validate message is not empty
SubjectSum subject = SurenessContextHolder.getBindSubject();
log.info(subject.toString());
McpContextHolder.setSubject(subject);
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(context.getConversationId())
.response("Error: Message cannot be empty")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
return conversationService.streamChat(context.getMessage(), context.getConversationId());
} catch (Exception e) {
log.error("Error in stream chat endpoint: ", e);
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(context.getConversationId())
.response("An error occurred: " + e.getMessage())
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
}
/**
* Get all conversations
*
* @return List of all conversations
*/
@GetMapping(path = "/conversations")
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
public ResponseEntity<Message<List<ConversationDto>>> listConversations() {
try {
List<ConversationDto> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
} catch (Exception e) {
log.error("Error listing conversations: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversations"));
}
}
/**
* Get conversation history
*
* @param conversationId The conversation ID
* @return Conversation details with message history
*/
@GetMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
public ResponseEntity<Message<ConversationDto>> getConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
ConversationDto conversation = conversationService.getConversation(conversationId);
if (conversation == null) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error getting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversation"));
}
}
/**
* Delete a conversation
*
* @param conversationId The conversation ID to delete
* @return Success or error message
*/
@DeleteMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
public ResponseEntity<Message<Void>> deleteConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
boolean deleted = conversationService.deleteConversation(conversationId);
if (!deleted) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success("Conversation deleted successfully"));
} catch (Exception e) {
log.error("Error deleting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to delete conversation"));
}
}
}
@@ -0,0 +1,160 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.http.ResponseEntity;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* OpenAI Configuration API
*/
@RestController
@RequestMapping(value = "/api/ai-agent/config", produces = {APPLICATION_JSON_VALUE})
@Tag(name = "OpenAI Configuration API")
@Slf4j
public class OpenAiConfigController {
private final OpenAiConfigService openAiConfigService;
public OpenAiConfigController(OpenAiConfigService openAiConfigService) {
this.openAiConfigService = openAiConfigService;
}
@PostMapping("/openai")
@Operation(summary = "Save OpenAI configuration", description = "Save or update OpenAI configuration")
public ResponseEntity<Map<String, Object>> saveOpenAiConfig(@Valid @RequestBody OpenAiConfigDto config) {
try {
Map<String, Object> response = new HashMap<>();
// Validate API key if enabled
if (config.isEnable() && config.getApiKey() != null && !config.getApiKey().trim().isEmpty()) {
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(config.getApiKey());
if (!validationResult.isValid()) {
log.warn("API key validation failed during save: {}", validationResult.getMessage());
response.put("code", 1);
response.put("msg", "API key validation failed: " + validationResult.getMessage());
return ResponseEntity.ok(response);
}
log.info("API key validation successful during save");
}
// Save the configuration
openAiConfigService.saveConfig(config);
response.put("code", 0);
response.put("msg", "OpenAI configuration saved successfully");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to save OpenAI configuration", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to save configuration: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
@GetMapping("/openai")
@Operation(summary = "Get OpenAI configuration", description = "Get current OpenAI configuration")
public ResponseEntity<Map<String, Object>> getOpenAiConfig() {
try {
OpenAiConfigDto config = openAiConfigService.getConfig();
Map<String, Object> response = new HashMap<>();
response.put("code", 0);
response.put("data", config);
response.put("msg", "Success");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to get OpenAI configuration", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to get configuration: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
@GetMapping("/openai/status")
@Operation(summary = "Check OpenAI configuration status", description = "Check if OpenAI is properly configured")
public ResponseEntity<Map<String, Object>> getOpenAiConfigStatus() {
try {
boolean configured = openAiConfigService.isConfigured();
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
boolean hasDbConfig = openAiConfigService.getConfig() != null;
boolean hasYamlConfig = effectiveConfig != null && !hasDbConfig;
// Validate the effective configuration
boolean validationPassed = false;
String validationMessage = "No configuration found";
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null && !effectiveConfig.getApiKey().trim().isEmpty()) {
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(effectiveConfig.getApiKey());
validationPassed = validationResult.isValid();
validationMessage = validationResult.getMessage();
if (!validationPassed) {
log.warn("OpenAI API key validation failed during status check: {}", validationMessage);
}
}
Map<String, Object> response = new HashMap<>();
response.put("code", 0);
response.put("data", Map.of(
"configured", configured && validationPassed,
"hasDbConfig", hasDbConfig,
"hasYamlConfig", hasYamlConfig,
"validationPassed", validationPassed,
"validationMessage", validationMessage
));
response.put("msg", "Success");
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Failed to get OpenAI configuration status", e);
Map<String, Object> response = new HashMap<>();
response.put("code", 1);
response.put("msg", "Failed to get status: " + e.getMessage());
return ResponseEntity.ok(response);
}
}
}
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.dao;
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* OpenAI Agent Configuration Dao
*/
@Repository
public interface OpenAiConfigDao extends JpaRepository<OpenAiConfig, String>, JpaSpecificationExecutor<OpenAiConfig> {
/**
* Query by type
* @param type type
* @return Return the queried configuration information
*/
OpenAiConfig findByType(String type);
}
@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.entity;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
/**
* OpenAI Agent Config Entity
*/
@Entity
@Table(name = "hzb_ai_agent_config")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "OpenAI Agent config entity")
@EntityListeners(AuditingEntityListener.class)
public class OpenAiConfig {
@Id
@Schema(title = "Config type: openai, primary key", description = "Config type: openai, primary key",
accessMode = READ_WRITE)
@NotBlank(message = "type can not null")
private String type;
@Schema(title = "Config content", description = "Config contentformat json", accessMode = READ_WRITE)
@Column(length = 8192)
private String content;
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
@CreatedBy
private String creator;
@Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY)
@LastModifiedBy
private String modifier;
@Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY)
@CreatedDate
private LocalDateTime gmtCreate;
@Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY)
@LastModifiedDate
private LocalDateTime gmtUpdate;
}
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.event;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
/**
* OpenAI configuration change event
*/
public class OpenAiConfigChangeEvent extends ApplicationEvent {
public OpenAiConfigChangeEvent(ApplicationContext source) {
super(source);
}
}
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Chat request context for AI chat endpoint.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatRequestContext {
/**
* The user's message (required)
*/
private String message;
/**
* Optional conversation ID for context
*/
private String conversationId;
/**
* Conversation history messages for context
*/
private List<MessageDto> conversationHistory;
}
@@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Chat response DTO for AI responses.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat response")
public class ChatResponseDto {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "AI response message", example = "Here are your monitors...")
private String response;
@Schema(description = "User message ID", example = "msg-user-123")
private String userMessageId;
@Schema(description = "Assistant message ID", example = "msg-assistant-123")
private String assistantMessageId;
}
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* Conversation DTO for AI chat conversations.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Conversation information")
public class ConversationDto {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Creation time")
private LocalDateTime createdAt;
@Schema(description = "Last updated time")
private LocalDateTime updatedAt;
@Schema(description = "Messages in this conversation")
private List<MessageDto> messages;
@Schema(description = "Message count")
private Integer messageCount;
}
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
/**
* Hierarchical structure
* eg: Monitoring Type metrics Information Hierarchy Relationship
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Schema(description = "Monitor Hierarchy")
public class Hierarchy {
/**
* Category value
*/
@Schema(description = "Category Value", example = "os", accessMode = READ_WRITE)
String category;
/**
* Attribute value
*/
@Schema(description = "Attribute value", example = "linux", accessMode = READ_WRITE)
String value;
/**
* Attribute internationalization tag
*/
@Schema(description = "Attribute internationalization tag", example = "Linux system", accessMode = READ_WRITE)
String label;
/**
* Is it a leaf node
*/
@Schema(description = "Is it a leaf node", example = "true", accessMode = READ_WRITE)
Boolean isLeaf = false;
/**
* Is hide this app type in main menus layout
*/
@Schema(description = "Is hide this app in main menus layout, only for app type, default true.", example = "true")
Boolean hide = true;
/**
* For leaf metric
* metric type 0-number: number 1-string: string
*/
@Schema(description = "metric type 0-number: number 1-string: string")
private Byte type;
/**
* metric unit
*/
@Schema(description = "metric unit")
private String unit;
/**
* Next level of association
*/
@Schema(description = "Next Hierarchy", accessMode = READ_WRITE)
private List<Hierarchy> children;
}
@@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Message DTO for chat messages.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat message")
public class MessageDto {
@Schema(description = "Message ID", example = "msg-123")
private String messageId;
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Message content", example = "List all monitors")
private String content;
@Schema(description = "Message role", example = "user", allowableValues = {"user", "assistant"})
private String role;
@Schema(description = "Message timestamp")
private LocalDateTime timestamp;
}
@@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* OpenAI Configuration DTO - simplified to handle only API key
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "OpenAI configuration")
public class OpenAiConfigDto {
/**
* Whether to enable OpenAI, default is false
*/
@Schema(title = "Enable OpenAI", description = "Whether OpenAI is enabled", example = "true")
private boolean enable = false;
/**
* OpenAI API key
*/
@Schema(title = "API Key", description = "OpenAI API key", example = "sk-...")
@NotBlank(message = "API Key cannot be empty when enabled")
private String apiKey;
}
@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import reactor.core.publisher.Flux;
/**
* Service for interacting with LLM providers (like OpenAI, Anthropic, etc.)
*/
public interface ChatClientProviderService {
/**
* Stream chat response from the LLM
*
* @param context Chat request context containing message and conversation history
* @return Flux of string chunks from the LLM response
*/
Flux<String> streamChat(ChatRequestContext context);
}
@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
import java.util.List;
/**
* Service for managing chat conversations and interactions with LLM providers.
*/
public interface ConversationService {
/**
* Create a new conversation
*
* @return Created conversation data
*/
ConversationDto createConversation();
/**
* Send a message and receive a streaming response
*
* @param message The user's message
* @param conversationId Optional conversation ID for continuing a chat
* @return Flux of ServerSentEvent for streaming the response
*/
Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId);
/**
* Get conversation history for a specific conversation
*
* @param conversationId Conversation ID
* @return Conversation data including messages
*/
ConversationDto getConversation(String conversationId);
/**
* Get all conversations for the current user
*
* @return List of conversations
*/
List<ConversationDto> getAllConversations();
/**
* Delete a conversation
*
* @param conversationId Conversation ID to delete
* @return true if deleted, false if conversation not found
*/
boolean deleteConversation(String conversationId);
/**
* Check if a conversation exists
*
* @param conversationId Conversation ID to check
* @return true if conversation exists, false otherwise
*/
boolean conversationExists(String conversationId);
}
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
/**
* Service interface for MCP server operations.
*/
public interface McpServerService {
/**
* Provides the HertzBeat tools for the MCP server
* @return ToolCallbackProvider with all HertzBeat monitoring tools
*/
ToolCallbackProvider hertzbeatTools();
/**
* Provides a custom SSE server transport for the MCP server
* @param objectMapper the ObjectMapper instance for JSON serialization
* @param serverProperties the properties for the MCP server configuration
* @return a CustomSseServerTransport instance configured with the provided properties
*/
CustomSseServerTransport webMvcSseServerTransportProvider(
ObjectMapper objectMapper,
McpServerProperties serverProperties
);
/**
* Provides the MCP server router function for web MVC
* @param transport Custom SSE server transport
* @return RouterFunction for handling MCP server requests
*/
RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport);
}
@@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service;
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
/**
* OpenAI Configuration Service
* Consolidated service for OpenAI configuration, validation, and client factory management
*/
public interface OpenAiConfigService {
/**
* Save OpenAI configuration
* @param config OpenAI configuration
*/
void saveConfig(OpenAiConfigDto config);
/**
* Get OpenAI configuration
* @return OpenAI configuration
*/
OpenAiConfigDto getConfig();
/**
* Check if OpenAI is properly configured
* @return true if configured and enabled
*/
boolean isConfigured();
/**
* Get effective OpenAI configuration (DB first, then YAML fallback)
* @return effective configuration or null if not configured
*/
OpenAiConfigDto getEffectiveConfig();
/**
* Validate OpenAI API key by calling the OpenAI API
* @param apiKey the API key to validate
* @return validation result with success status and message
*/
ValidationResult validateApiKey(String apiKey);
/**
* Force reload of OpenAI configuration cache
* This method is typically called when configuration changes
*/
void reloadConfig();
/**
* Handle OpenAI configuration change events
* @param event OpenAI configuration change event
*/
void onOpenAiConfigChange(OpenAiConfigChangeEvent event);
/**
* Validation result class
*/
class ValidationResult {
private final boolean valid;
private final String message;
private ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public static ValidationResult success(String message) {
return new ValidationResult(true, message);
}
public static ValidationResult failure(String message) {
return new ValidationResult(false, message);
}
public boolean isValid() {
return valid;
}
public String getMessage() {
return message;
}
}
}
@@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.PromptProvider;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/**
* Implementation of the {@link ChatClientProviderService}.
* Provides functionality to interact with the ChatClient for handling chat
* messages.
*/
@Slf4j
@Service
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
private final ChatClient chatClient;
@Qualifier("hertzbeatTools")
@Autowired
private ToolCallbackProvider toolCallbackProvider;
@Autowired
public ChatClientProviderServiceImpl(ChatClient openAiChatClient) {
this.chatClient = openAiChatClient;
}
public String complete(String message) {
return this.chatClient.prompt()
.user(message)
.call()
.content();
}
@Override
public Flux<String> streamChat(ChatRequestContext context) {
try {
List<Message> messages = new ArrayList<>();
// Add conversation history if available
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
for (MessageDto historyMessage : context.getConversationHistory()) {
if ("user".equals(historyMessage.getRole())) {
messages.add(new UserMessage(historyMessage.getContent()));
} else if ("assistant".equals(historyMessage.getRole())) {
messages.add(new AssistantMessage(historyMessage.getContent()));
}
}
}
messages.add(new UserMessage(context.getMessage()));
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
return this.chatClient.prompt()
.messages(messages)
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
.toolCallbacks(toolCallbackProvider)
.stream()
.content()
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
} catch (Exception e) {
log.error("Error setting up streaming chat: {}", e.getMessage(), e);
return Flux.error(e);
}
}
}
@@ -0,0 +1,276 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
private final Map<String, Map<String, Object>> conversations = new ConcurrentHashMap<>();
private final Map<String, List<Map<String, Object>>> conversationMessages = new ConcurrentHashMap<>();
@Autowired
private ChatClientProviderService chatClientProviderService;
@Autowired
private OpenAiConfigService openAiConfigService;
@Override
public ConversationDto createConversation() {
String conversationId = createNewConversation();
return getConversation(conversationId);
}
@Override
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId) {
// Validate conversation exists
if (!conversationExists(conversationId)) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("Error: Conversation not found: " + conversationId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
// Check if OpenAI is properly configured
if (!openAiConfigService.isConfigured()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("OpenAI is not configured. Please configure your OpenAI API key in the settings or application.yml file.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
String userMessageId = addMessageToConversation(conversationId, message, "user");
// Get conversation history for context
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> conversationHistory = new ArrayList<>();
if (messagesList != null && messagesList.size() > 1) {
// Get all messages except the last one (which is the current user message we just added)
for (int i = 0; i < messagesList.size() - 1; i++) {
Map<String, Object> msgMap = messagesList.get(i);
conversationHistory.add(mapToMessageDto(msgMap));
}
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(conversationHistory)
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseDto responseDto = ChatResponseDto.builder()
.conversationId(conversationId)
.response(chunk)
.userMessageId(userMessageId)
.build();
return ServerSentEvent.builder(responseDto)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
String assistantMessageId = addMessageToConversation(conversationId, fullResponse.toString(), "assistant");
ChatResponseDto finalResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("")
.userMessageId(userMessageId)
.assistantMessageId(assistantMessageId)
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
.event("complete")
.build());
}))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(userMessageId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
});
}
@Override
public ConversationDto getConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return null;
}
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation == null) {
return null;
}
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> messages = messagesList != null
? messagesList.stream().map(this::mapToMessageDto).collect(Collectors.toList()) :
new ArrayList<>();
return ConversationDto.builder()
.conversationId((String) conversation.get("conversationId"))
.createdAt((LocalDateTime) conversation.get("createdAt"))
.updatedAt((LocalDateTime) conversation.get("updatedAt"))
.messages(messages)
.build();
}
@Override
public List<ConversationDto> getAllConversations() {
List<ConversationDto> result = new ArrayList<>();
for (Map.Entry<String, Map<String, Object>> entry : conversations.entrySet()) {
Map<String, Object> conv = entry.getValue();
List<Map<String, Object>> messages = conversationMessages.get(entry.getKey());
ConversationDto dto = ConversationDto.builder()
.conversationId((String) conv.get("conversationId"))
.createdAt((LocalDateTime) conv.get("createdAt"))
.updatedAt((LocalDateTime) conv.get("updatedAt"))
.messages(new ArrayList<>()) // Don't include messages in list view for performance
.build();
result.add(dto);
}
result.sort((a, b) -> b.getUpdatedAt().compareTo(a.getUpdatedAt()));
return result;
}
@Override
public boolean deleteConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return false;
}
boolean existed = conversations.containsKey(conversationId);
if (existed) {
conversations.remove(conversationId);
conversationMessages.remove(conversationId);
log.info("Deleted conversation: {}", conversationId);
}
return existed;
}
@Override
public boolean conversationExists(String conversationId) {
return conversationId != null && !conversationId.isEmpty() && conversations.containsKey(conversationId);
}
private String createNewConversation() {
String conversationId = "conv-" + UUID.randomUUID().toString().substring(0, 8);
LocalDateTime now = LocalDateTime.now();
Map<String, Object> conversation = new HashMap<>();
conversation.put("conversationId", conversationId);
conversation.put("createdAt", now);
conversation.put("updatedAt", now);
conversations.put(conversationId, conversation);
conversationMessages.put(conversationId, new ArrayList<>());
log.info("Created new conversation: {}", conversationId);
return conversationId;
}
private MessageDto mapToMessageDto(Map<String, Object> messageMap) {
return MessageDto.builder()
.messageId((String) messageMap.get("messageId"))
.conversationId((String) messageMap.get("conversationId"))
.content((String) messageMap.get("content"))
.role((String) messageMap.get("role"))
.timestamp((LocalDateTime) messageMap.get("timestamp"))
.build();
}
private String addMessageToConversation(String conversationId, String content, String role) {
List<Map<String, Object>> messages = conversationMessages.computeIfAbsent(conversationId, k -> new ArrayList<>());
String messageId = "msg-" + UUID.randomUUID().toString().substring(0, 8);
Map<String, Object> message = new HashMap<>();
message.put("messageId", messageId);
message.put("conversationId", conversationId);
message.put("content", content);
message.put("role", role);
message.put("timestamp", LocalDateTime.now());
messages.add(message);
// Update conversation timestamp
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation != null) {
conversation.put("updatedAt", LocalDateTime.now());
// Auto-generate title from first user message
if ("user".equals(role) && messages.stream().filter(m -> "user".equals(m.get("role"))).count() == 1) {
String title = content.length() > 30 ? content.substring(0, 27) + "..." : content;
conversation.put("title", title);
}
}
return messageId;
}
}
@@ -0,0 +1,91 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.agent.service.McpServerService;
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
/**
* Implementation of the McpServerService interface.
* This service provides functionality for handling MCP server operations.
*/
@Service
@Configuration
public class McpServerServiceImpl implements McpServerService {
@Autowired
private MonitorTools monitorTools;
@Autowired
private AlertTools alertTools;
@Autowired
private MetricsTools metricsTools;
@Autowired
private AlertDefineTools alertDefineTools;
@Bean
public ToolCallbackProvider hertzbeatTools() {
return MethodToolCallbackProvider.builder().toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools).build();
}
/**
* Provides a custom SSE server transport for the MCP server.
*
* @param objectMapper the ObjectMapper instance for JSON serialization
* @param serverProperties the properties for the MCP server configuration
* @return a CustomSseServerTransport instance configured with the provided properties
*/
@Bean
public CustomSseServerTransport webMvcSseServerTransportProvider(
ObjectMapper objectMapper,
McpServerProperties serverProperties
) {
return new CustomSseServerTransport(
objectMapper,
serverProperties.getBaseUrl(),
serverProperties.getSseMessageEndpoint(),
serverProperties.getSseEndpoint()
);
}
/**
* Provides the MCP server transport bean.
*
* @param transport the custom SSE server transport
* @return the MCP server transport instance
*/
@Primary
@Bean
public RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport) {
return transport.getRouterFunction();
}
}
@@ -0,0 +1,200 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.OpenAiYamlConfig;
import org.apache.hertzbeat.ai.agent.dao.OpenAiConfigDao;
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* OpenAI Configuration Service Implementation
* Consolidated service for OpenAI configuration, validation, and client factory management
*/
@Slf4j
@Service
public class OpenAiConfigServiceImpl implements OpenAiConfigService {
private static final String CONFIG_TYPE = "openai";
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
private final OpenAiConfigDao openAiConfigDao;
private final ObjectMapper objectMapper;
private final ApplicationContext applicationContext;
private final OpenAiYamlConfig yamlConfig;
private final RestTemplate restTemplate;
// Client factory cache
private volatile OpenAiConfigDto currentConfig;
public OpenAiConfigServiceImpl(OpenAiConfigDao openAiConfigDao,
ObjectMapper objectMapper,
ApplicationContext applicationContext,
OpenAiYamlConfig yamlConfig) {
this.openAiConfigDao = openAiConfigDao;
this.objectMapper = objectMapper;
this.applicationContext = applicationContext;
this.yamlConfig = yamlConfig;
this.restTemplate = new RestTemplate();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveConfig(OpenAiConfigDto config) {
try {
String contentJson = objectMapper.writeValueAsString(config);
OpenAiConfig openAiConfig = OpenAiConfig.builder()
.type(CONFIG_TYPE)
.content(contentJson)
.build();
openAiConfigDao.save(openAiConfig);
log.info("OpenAI configuration saved successfully");
applicationContext.publishEvent(new OpenAiConfigChangeEvent(applicationContext));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to save OpenAI configuration: " + e.getMessage());
}
}
@Override
public OpenAiConfigDto getConfig() {
OpenAiConfig config = openAiConfigDao.findByType(CONFIG_TYPE);
if (config == null || !StringUtils.hasText(config.getContent())) {
return null;
}
try {
return objectMapper.readValue(config.getContent(), OpenAiConfigDto.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Failed to parse OpenAI configuration: " + e.getMessage());
}
}
@Override
public boolean isConfigured() {
OpenAiConfigDto effective = getEffectiveConfig();
return effective != null && effective.isEnable() && StringUtils.hasText(effective.getApiKey());
}
@Override
public OpenAiConfigDto getEffectiveConfig() {
OpenAiConfigDto dbConfig = getConfig();
if (dbConfig != null && dbConfig.isEnable() && StringUtils.hasText(dbConfig.getApiKey())) {
log.debug("Using database OpenAI configuration");
return dbConfig;
}
if (yamlConfig != null && yamlConfig.isEnable() && StringUtils.hasText(yamlConfig.getApiKey())) {
log.debug("Using YAML OpenAI configuration from spring.ai.openai.api-key");
OpenAiConfigDto yamlDto = new OpenAiConfigDto();
yamlDto.setEnable(true);
yamlDto.setApiKey(yamlConfig.getApiKey());
return yamlDto;
}
log.debug("No valid OpenAI configuration found");
return null;
}
@Override
public ValidationResult validateApiKey(String apiKey) {
if (!StringUtils.hasText(apiKey)) {
return ValidationResult.failure("API key cannot be empty");
}
if (!apiKey.startsWith("sk-")) {
return ValidationResult.failure("Invalid API key format. OpenAI API keys should start with 'sk-'");
}
try {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
headers.set("Content-Type", "application/json");
HttpEntity<String> entity = new HttpEntity<>(headers);
log.debug("Validating OpenAI API key by calling models endpoint");
ResponseEntity<String> response = restTemplate.exchange(
OPENAI_MODELS_ENDPOINT,
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode() == HttpStatus.OK) {
log.info("OpenAI API key validation successful");
return ValidationResult.success("API key is valid");
} else {
log.warn("OpenAI API key validation failed with status: {}", response.getStatusCode());
return ValidationResult.failure("API key validation failed: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Error validating OpenAI API key", e);
String errorMessage = e.getMessage();
// Parse common error messages
if (errorMessage.contains("401")) {
return ValidationResult.failure("Invalid API key - authentication failed");
} else if (errorMessage.contains("403")) {
return ValidationResult.failure("API key does not have permission to access models");
} else if (errorMessage.contains("429")) {
return ValidationResult.failure("Rate limit exceeded - please try again later");
} else if (errorMessage.contains("timeout") || errorMessage.contains("connect")) {
return ValidationResult.failure("Network error - unable to connect to OpenAI API");
} else {
return ValidationResult.failure("API key validation failed: " + errorMessage);
}
}
}
@Override
public void reloadConfig() {
synchronized (this) {
currentConfig = null; // Force reload
}
}
/**
* OpenAI configuration change event listener
*/
@EventListener(OpenAiConfigChangeEvent.class)
public void onOpenAiConfigChange(OpenAiConfigChangeEvent event) {
log.info("[OpenAiConfigService] OpenAI configuration change event received");
reloadConfig();
}
}
@@ -0,0 +1,91 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools;
/**
* Tools for alert definition and threshold configuration operations
*/
public interface AlertDefineTools {
/**
* Create a new alert rule with HertzBeat's expression format based on app hierarchy
*
* @param name Alert rule name (required, must be unique)
* @param app App name from hierarchy (must match exact hierarchy app value)
* @param metrics Metrics name from hierarchy (must match exact hierarchy metrics value)
* @param fieldConditions Field-specific conditions from metric's children (e.g., "VmName = 'arora'", "total_granted > 1000",
* "total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)")
* @param type Alert rule type: 'realtime' (default) or 'periodic'
* @param period Execution period in seconds (only for periodic rules, default: 300)
* @param times Number of consecutive violations before triggering (default: 3)
* @param priority Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)
* @param description Alert rule description (optional)
* @param template Alert message template with variables (optional)
* @param datasource Data source type: 'promql' (default)
* @param labels Labels as key:value pairs separated by commas
* @param annotations Annotations as key:value pairs separated by commas
* @param enable Whether to enable the rule immediately (default: true)
* @return Result message with rule ID if successful
*/
String createAlertRule(String name, String app, String metrics, String fieldConditions,
String type, Integer period, Integer times, Integer priority, String description,
String template, String datasource, String labels, String annotations, Boolean enable);
/**
* List existing alert rules with filtering
* @param search Search term for rule name or description
* @param monitorType Filter by monitor type
* @param enabled Filter by enabled status
* @param pageIndex Page index
* @param pageSize Page size
* @return Formatted list of alert rules
*/
String listAlertRules(String search, String monitorType, Boolean enabled, Integer pageIndex, Integer pageSize);
/**
* Enable or disable an alert rule
* @param ruleId Alert rule ID
* @param enabled Whether to enable the rule
* @return Result message
*/
String toggleAlertRule(Long ruleId, Boolean enabled);
/**
* Get detailed information about an alert rule
* @param ruleId Alert rule ID
* @return Detailed rule information
*/
String getAlertRuleDetails(Long ruleId);
/**
* Get the hierarchical structure of available apps and metrics for alert rule creation
* @param app App type to get hierarchy for (optional, gets all if not specified)
* @return Hierarchical structure showing apps and their available metrics
*/
String getAppsMetricsHierarchy(String app);
/**
* Bind monitors to an alert rule by modifying the alert expression
* @param ruleId Alert rule ID to bind monitors to
* @param monitorIds Comma-separated list of monitor IDs to bind
* @return Result message indicating success or failure
*/
String bindMonitorsToAlertRule(Long ruleId, String monitorIds);
}
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools;
/**
* Tools for alert operations and alarm data queries
*/
public interface AlertTools {
/**
* Query alerts with comprehensive filtering and pagination
* @param alertType Alert type (single, group, both)
* @param status Alert status (firing, resolved, all)
* @param search Search term for alert content or labels
* @param sort Sort field (startAt, triggerTimes, status)
* @param order Sort order (asc, desc)
* @param pageIndex Page index
* @param pageSize Page size
* @return Formatted string with alert information
*/
String queryAlerts(String alertType, String status, String search, String sort, String order, Integer pageIndex, Integer pageSize);
/**
* Get alerts summary statistics
* @return Alert summary information including counts by status
*/
String getAlertsSummary();
}
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools;
/**
* Tools for metrics data operations and queries
*/
public interface MetricsTools {
/**
* Get real-time metrics data for a monitor
* @param monitorId Monitor ID
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @return Formatted real-time metrics data
*/
String getRealtimeMetrics(Long monitorId, String metrics);
/**
* Get historical metrics data for a monitor
* @param monitorId Monitor ID
* @param app Monitor type (e.g., "linux", "mysql", "http")
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @param metric Specific metric field (e.g., "usage", "used", "available")
* @param label Label filter for specific instances
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
* @param interval Whether to aggregate data with intervals
* @return Historical metrics data formatted for display
*/
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
/**
* Check warehouse storage server status
* @return Status of the metrics storage system
*/
String getWarehouseStatus();
}
@@ -0,0 +1,97 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools;
import java.util.List;
/**
* Interface for Monitoring Tools
*/
public interface MonitorTools {
/**
* Add a new monitor with comprehensive configuration
*
* @param name Monitor name
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
* @param host Target host (IP address or domain name)
* @param port Target port (optional, depends on monitor type)
* @param intervals Collection interval in seconds (default: 600)
* @param username Username for authentication (optional)
* @param password Password for authentication (optional)
* @param database Database name (for database monitors)
* @param additionalParams Additional app-specific parameters as JSON string (optional)
* @param description Monitor description (optional)
* @return Result message with monitor ID if successful
*/
String addMonitor(
String name,
String app,
String host,
Integer port,
Integer intervals,
String username,
String password,
String database,
String additionalParams,
String description
);
/**
* List all available monitor types that can be added
*
* @param language Language code for localized names (e.g., 'en-US', 'zh-CN')
* @return Formatted string list of available monitor types with descriptions
*/
String listMonitorTypes(String language);
/**
* Comprehensive monitor querying with flexible filtering, pagination, and specialized views
* @param ids Specific monitor IDs to retrieve (optional)
* @param app Monitor type filter (linux, mysql, http, etc.)
* @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all)
* @param search Search in monitor names or hosts (partial matching)
* @param labels Label filters, format: 'key1:value1,key2:value2'
* @param sort Sort field (name, gmtCreate, gmtUpdate, status, app)
* @param order Sort order (asc, desc)
* @param pageIndex Page number starting from 0
* @param pageSize Items per page (1-100 recommended)
* @param includeStats Include status statistics summary
* @return Comprehensive monitor information with optional statistics
*/
String queryMonitors(
List<Long> ids,
String app,
Byte status,
String search,
String labels,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
Boolean includeStats);
/**
* Get parameter definitions required for a specific monitor type
*
* @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis')
* @return Formatted string with parameter definitions including field names, types, and requirements
*/
String getMonitorAdditionalParams(String app);
}
@@ -0,0 +1,665 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Implementation of Alert Define Tools functionality
*/
@Slf4j
@Service
public class AlertDefineToolsImpl implements AlertDefineTools {
@Autowired
private AlertDefineServiceAdapter alertDefineServiceAdapter;
@Override
@Tool(name = "create_alert_rule", description = """
ALERT RULE means when to alert a user
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
Create a HertzBeat alert rule based on app hierarchy structure and user requirements.
It is important to first understand the hierarchy of apps, metrics, and field conditions
Each app has its own metrics and each metric has its own field conditions.
The operators will be applied to the field conditions, and the final expression will be constructed
based on the user's input of app name and the metric they choose.
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
4. Ask the user to choose a metric from the available metrics
5. Based on the metric chosen, present the available field conditions/params
6. You will construct the proper expression with field conditions
7. once this tool successfully executes, ask the user if they want to bind any existing monitors to this alert rule,
get the monitors list for a particular app using the query_monitors tool.
8. based on the user's output, conditionally call the bind_monitors_to_alert_rule tool to bind monitors to the alert rule
VERY VERY IMPORTANT:
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
EXAMPLES FOR FIELD CONDITION EXPRESSION( Do not copy these examples, they are just for reference ):
These are all just examples, you can take inspiration from them and create a rule based on hierarchy, always ask the user for all params, do not assume them, even for these examples:
1. Kafka JVM Alert:
- App: "kafka", Metric: "jvm_basic"
- Field condition: equals(VmName, "myVM")
- Field condition expression: equals(VmName, "myVM")
2. LLM Credits Alert:
- App: "openai", Metric: "credit_grants"
- Field condition: total_granted > some_value
- Field condition expression: total_granted > 1000
3. HBase Master Alert:
- App: "hbase_master", Metric: "server"
- Field condition: heap_memory_used > 80 or some_factor<100
- Field condition expression: heap_memory_used > 80 or some_factor<100
4. Complex OpenAI Credits Alert:
- App: "openai", Metric: "credit_grants"
- Field condition: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
- Field condition expression: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
FIELD CONDITIONS GUIDANCE:
- Field names come from metric's children in hierarchy (leaf nodes)
- Use the "value" field from the metric's children, not the label when creating conditions
- Supported operators: >, <, >=, <=, ==, !=, exists(), !exists() for numeric fields
- equals(), contains(), matches(),exists(), !equals(), !contains(), !matches(), !exists() for string fields
- Supported logical operators: and, or to connect different field parameter rules or rulesets
- ONLY USE THESE OPERATORS, when creating conditions, do not use any other operators
- Support grouping with parentheses: (condition1 and condition2) or condition3
- String values should be quoted: equals(VmName, "my-vm")
- Simple conditions: heap_memory_used > 80, total_granted <= 1000
- Complex conditions: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
PRIORITY LEVELS:
- 0: Critical (immediate action required)
- 1: Warning (attention needed, default)
- 2: Info (informational only)
""")
public String createAlertRule(
@ToolParam(description = "Alert rule name (required, must be unique)", required = true) String name,
@ToolParam(description = "App name from hierarchy (must match exact hierarchy app value)", required = true) String app,
@ToolParam(description = "Metrics name from hierarchy (must match exact hierarchy metrics value)", required = true) String metrics,
@ToolParam(description = "Field conditions expression)", required = true) String fieldConditions,
@ToolParam(description = "Alert rule type: 'realtime' (default) or 'periodic'", required = false) String type,
@ToolParam(description = "Execution period in seconds (only for periodic rules, default: 300)", required = false) Integer period,
@ToolParam(description = "Number of consecutive violations before triggering (default: 3)", required = false) Integer times,
@ToolParam(description = "Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)", required = false) Integer priority,
@ToolParam(description = "Alert rule description (optional)", required = false) String description,
@ToolParam(description = "Alert message template with variables (optional)", required = false) String template,
@ToolParam(description = "Data source type: 'promql' (default)", required = false) String datasource,
@ToolParam(description = "Labels as key:value pairs separated by commas (e.g., 'env:prod,severity:critical')", required = false) String labels,
@ToolParam(description = "Annotations as key:value pairs separated by commas (e.g., 'summary:High CPU')", required = false) String annotations,
@ToolParam(description = "Whether to enable the rule immediately (default: true)", required = false) Boolean enable) {
try {
log.info("Creating HertzBeat alert rule: name={}, app={}, metrics={}, fieldConditions={}", name, app, metrics, fieldConditions);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in create_alert_rule tool: {}", subjectSum);
// Validate required parameters
if (name == null || name.trim().isEmpty()) {
return "Error: Alert rule name is required";
}
if (app == null || app.trim().isEmpty()) {
return "Error: App name is required (use get_apps_metrics_hierarchy to find exact names)";
}
if (metrics == null || metrics.trim().isEmpty()) {
return "Error: Metrics name is required (use get_apps_metrics_hierarchy to find exact names)";
}
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
return "Error: Field conditions are required (e.g., 'equals(VmName, \"arora\")', 'total_granted > 1000')";
}
// Set defaults
if (type == null || type.trim().isEmpty()) {
type = "realtime";
}
if (times == null || times <= 0) {
times = 3;
}
if (priority == null) {
priority = 1; // Default to warning
}
if (enable == null) {
enable = true;
}
if (datasource == null || datasource.trim().isEmpty()) {
datasource = "promql";
}
// Validate alert type
if (!type.equals("realtime") && !type.equals("periodic")) {
return "Error: Alert type must be 'realtime' or 'periodic'";
}
// Validate priority
if (priority < 0 || priority > 2) {
return "Error: Priority must be 0 (critical), 1 (warning), or 2 (info)";
}
// For periodic rules, validate period parameter
if (type.equals("periodic")) {
if (period == null || period <= 0) {
period = 300; // Default 5 minutes
}
}
// CRITICAL VALIDATION: Verify app-metric-field relationships using hierarchy
String validationResult = validateHierarchyRelationships(app.trim(), metrics.trim(), fieldConditions.trim());
if (!validationResult.equals("VALID")) {
return validationResult; // Return validation error message
}
// EXPRESSION VALIDATION: Verify field conditions syntax and operators
String expressionValidation = UtilityClass.validateExpressionSyntax(fieldConditions.trim());
if (!expressionValidation.equals("VALID")) {
return expressionValidation; // Return expression validation error message
}
String expr = String.format("equals(__app__,\"%s\") && equals(__metrics__,\"%s\") && %s",
app.trim(), metrics.trim(), fieldConditions.trim());
// Parse labels if provided
Map<String, String> labelsMap = new HashMap<>();
if (labels != null && !labels.trim().isEmpty()) {
labelsMap.putAll(UtilityClass.parseKeyValuePairs(labels));
}
// Add severity based on priority
String severityLabel = priority == 0 ? "critical" : (priority == 1 ? "warning" : "info");
labelsMap.put("severity", severityLabel);
// Parse annotations if provided
Map<String, String> annotationsMap = new HashMap<>();
if (annotations != null && !annotations.trim().isEmpty()) {
annotationsMap.putAll(UtilityClass.parseKeyValuePairs(annotations));
}
// Add default annotations if not provided
if (!annotationsMap.containsKey("summary")) {
annotationsMap.put("summary", description != null ? description :
String.format("Alert for %s %s when %s", app, metrics, fieldConditions));
}
if (!annotationsMap.containsKey("description")) {
annotationsMap.put("description", String.format("Monitor %s metrics %s with conditions: %s", app, metrics, fieldConditions));
}
// Generate default template if not provided
if (template == null || template.trim().isEmpty()) {
template = String.format("Alert: %s %s - %s", app, metrics, fieldConditions);
}
// Create comprehensive alert definition
AlertDefine alertDefine = AlertDefine.builder()
.name(name.trim())
.type(type)
.expr(expr)
.period(period)
.times(times)
.labels(labelsMap)
.annotations(annotationsMap)
.template(template)
.datasource(datasource)
.enable(enable)
.build();
AlertDefine createdAlertDefine = alertDefineServiceAdapter.addAlertDefine(alertDefine);
// Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool
String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)");
log.info("Successfully created alert rule '{}' with ID: {}", name, createdAlertDefine.getId());
StringBuilder response = new StringBuilder();
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
type, name, createdAlertDefine.getId()));
response.append(String.format("Expression: %s\n", expr));
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
response.append(String.format("Trigger after: %d consecutive violations\n", times));
if (type.equals("periodic")) {
response.append(String.format("Execution period: %d seconds\n", period));
}
response.append(String.format("Data source: %s\n", datasource));
response.append(String.format("Enabled: %s\n", enable));
if (!labelsMap.isEmpty()) {
response.append(String.format("Labels: %s\n", labelsMap));
}
response.append(bindingNote);
return response.toString();
} catch (Exception e) {
log.error("Failed to create alert rule '{}': {}", name, e.getMessage(), e);
return "Error creating alert rule '" + name + "': " + e.getMessage();
}
}
// ... other existing methods would go here ...
@Override
@Tool(name = "list_alert_rules", description = """
List existing alert rules with filtering options.
Shows configured thresholds and alert definitions.
""")
public String listAlertRules(
@ToolParam(description = "Search term for rule name or description", required = false) String search,
@ToolParam(description = "Filter by monitor type", required = false) String monitorType,
@ToolParam(description = "Filter by enabled status", required = false) Boolean enabled,
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
@ToolParam(description = "Page size (default: 10)", required = false) Integer pageSize) {
try {
log.info("Listing alert rules: search={}, monitorType={}, enabled={}", search, monitorType, enabled);
if (pageIndex == null || pageIndex < 0) {
pageIndex = 0;
}
if (pageSize == null || pageSize <= 0) {
pageSize = 10;
}
Page<AlertDefine> result = alertDefineServiceAdapter.getAlertDefines(
search, monitorType, enabled, "gmtCreate", "desc", pageIndex, pageSize);
StringBuilder response = new StringBuilder();
response.append("Found ").append(result.getContent().size())
.append(" alert rules (Total: ").append(result.getTotalElements()).append("):\n\n");
for (AlertDefine alertDefine : result.getContent()) {
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
response.append("Name: ").append(alertDefine.getName()).append("\n");
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
response.append("Type: ").append(alertDefine.getType()).append("\n");
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
}
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
response.append("Summary: ").append(alertDefine.getAnnotations().get("summary")).append("\n");
}
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
response.append("\n");
}
if (result.getContent().isEmpty()) {
response.append("No alert rules found matching the specified criteria.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to list alert rules: {}", e.getMessage(), e);
return "Error retrieving alert rules: " + e.getMessage();
}
}
@Override
@Tool(name = "toggle_alert_rule", description = """
Enable or disable an alert rule.
Allows activating or deactivating threshold monitoring.
""")
public String toggleAlertRule(
@ToolParam(description = "Alert rule ID", required = true) Long ruleId,
@ToolParam(description = "Whether to enable the rule", required = true) Boolean enabled) {
try {
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
alertDefineServiceAdapter.toggleAlertDefineStatus(ruleId, enabled);
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
return String.format("Successfully %s alert rule ID: %d",
enabled ? "enabled" : "disabled", ruleId);
} catch (Exception e) {
log.error("Failed to toggle alert rule ID {}: {}", ruleId, e.getMessage(), e);
return "Error toggling alert rule: " + e.getMessage();
}
}
@Override
@Tool(name = "get_alert_rule_details", description = """
Get detailed information about a specific alert rule.
Shows complete threshold configuration and rule settings.
""")
public String getAlertRuleDetails(
@ToolParam(description = "Alert rule ID", required = true) Long ruleId) {
try {
log.info("Getting alert rule details for ID: {}", ruleId);
AlertDefine alertDefine = alertDefineServiceAdapter.getAlertDefine(ruleId);
if (alertDefine == null) {
return "Alert rule with ID " + ruleId + " not found";
}
StringBuilder response = new StringBuilder();
response.append("ALERT RULE DETAILS\n");
response.append("==================\n\n");
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
response.append("Name: ").append(alertDefine.getName()).append("\n");
response.append("Type: ").append(alertDefine.getType()).append("\n");
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
if (alertDefine.getPeriod() != null) {
response.append("Period: ").append(alertDefine.getPeriod()).append(" seconds\n");
}
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
}
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
response.append("Annotations: ").append(alertDefine.getAnnotations()).append("\n");
}
if (alertDefine.getTemplate() != null) {
response.append("Template: ").append(alertDefine.getTemplate()).append("\n");
}
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
response.append("Modified: ").append(alertDefine.getGmtUpdate()).append("\n");
response.append("Creator: ").append(alertDefine.getCreator()).append("\n");
response.append("Modifier: ").append(alertDefine.getModifier()).append("\n");
return response.toString();
} catch (Exception e) {
log.error("Failed to get alert rule details for ID {}: {}", ruleId, e.getMessage(), e);
return "Error retrieving alert rule details: " + e.getMessage();
}
}
@Override
@Tool(name = "get_apps_metrics_hierarchy", description = """
Get the hierarchical structure of all available apps and their metrics for alert rule creation.
This tool provides the exact app name, metric name and corresponding param names according to each metric.
Returns structured JSON data showing the complete hierarchy with field parameters for alert expressions.
JSON Structure:
- app: The application name
- description: Tool description
- hierarchy: Array of hierarchical data
- Each node has: value, label, type, description
- Leaf nodes have: dataType (numeric/string), unit (if applicable)
- Non-leaf nodes have: children array
VERY IMPORTANT:
- ALWAYS USE the value field from the field parameters when creating alert expressions.
This structured data is needed to create proper alert expressions.
""")
public String getAppsMetricsHierarchy(
@ToolParam(description = "App/Monitor type to get hierarchy for (e.g., 'linux', 'mysql', 'website')", required = true) String app) {
try {
log.info("Getting apps metrics hierarchy for app: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
List<Hierarchy> hierarchies;
hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.trim().toLowerCase(), "en-US");
ObjectMapper mapper = new ObjectMapper();
ObjectNode result = mapper.createObjectNode();
result.put("app", app.toUpperCase());
if (hierarchies != null && !hierarchies.isEmpty()) {
ArrayNode hierarchyArray = mapper.createArrayNode();
for (Hierarchy hierarchy : hierarchies) {
hierarchyArray.add(UtilityClass.formatHierarchyAsJson(mapper, hierarchy));
}
result.set("hierarchy", hierarchyArray);
} else {
result.put("message", "No hierarchy data available");
}
String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
log.info("Hierarchy JSON: {}", jsonResult);
return jsonResult;
} catch (Exception e) {
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
return "Error retrieving apps metrics hierarchy: " + e.getMessage();
}
}
@Override
@Tool(name = "bind_monitors_to_alert_rule", description = """
Bind monitors to an alert rule.
Call this tool if users want to bind specific monitors to their alert rule.
Get the right monitor ids for a particular app using the query_monitors tool.
Get the alert rule ID from the create_alert_rule tool output OR use the list_alert_rules tool with app_name search filter, if the output of create_alert_rule is not applicable.
If monitors are already bound, this will add the new ones to the existing bindings.
""")
public String bindMonitorsToAlertRule(
@ToolParam(description = "Alert rule ID to bind monitors to", required = true) Long ruleId,
@ToolParam(description = "Comma-separated list of monitor IDs to bind", required = true) String monitorIds) {
try {
log.info("Binding monitors to alert rule ID: {}, monitors: {}", ruleId, monitorIds);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in bind_monitors_to_alert_rule tool: {}", subjectSum);
if (ruleId == null || ruleId <= 0) {
return "Error: Valid alert rule ID is required";
}
if (monitorIds == null) {
return "Error: Monitor IDs are required";
}
// Get the existing alert rule
AlertDefine existingRule = alertDefineServiceAdapter.getAlertDefine(ruleId);
if (existingRule == null) {
return String.format("Error: Alert rule with ID %d not found", ruleId);
}
// Parse monitor IDs from comma-separated string
String[] monitorIdArray = monitorIds.split(",");
List<String> validMonitorIds = new ArrayList<>();
for (String monitorId : monitorIdArray) {
String trimmedId = monitorId.trim();
if (!trimmedId.isEmpty()) {
try {
Long.parseLong(trimmedId); // Validate it's a number
validMonitorIds.add(trimmedId);
} catch (NumberFormatException e) {
return String.format("Error: Invalid monitor ID '%s'. Monitor IDs must be numeric.", trimmedId);
}
}
}
if (validMonitorIds.isEmpty()) {
return "Error: No valid monitor IDs provided";
}
// Build the monitor instance condition
String monitorCondition;
if (validMonitorIds.size() == 1) {
monitorCondition = String.format("equals(__instance__, \"%s\")", validMonitorIds.get(0));
} else {
StringBuilder conditionBuilder = new StringBuilder("(");
for (int i = 0; i < validMonitorIds.size(); i++) {
if (i > 0) {
conditionBuilder.append(" or ");
}
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", validMonitorIds.get(i)));
}
conditionBuilder.append(")");
monitorCondition = conditionBuilder.toString();
}
// Get the current expression and modify it
String currentExpr = existingRule.getExpr();
String newExpr;
// Check if the expression already has __instance__ conditions
if (currentExpr.contains("__instance__")) {
// Extract existing monitor IDs and merge with new ones
List<String> existingMonitorIds = UtilityClass.extractExistingMonitorIds(currentExpr);
// Add new monitor IDs that aren't already present
for (String newId : validMonitorIds) {
if (!existingMonitorIds.contains(newId)) {
existingMonitorIds.add(newId);
}
}
String updatedMonitorCondition;
if (existingMonitorIds.size() == 1) {
updatedMonitorCondition = String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(0));
} else {
StringBuilder conditionBuilder = new StringBuilder("(");
for (int i = 0; i < existingMonitorIds.size(); i++) {
if (i > 0) {
conditionBuilder.append(" or ");
}
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(i)));
}
conditionBuilder.append(")");
updatedMonitorCondition = conditionBuilder.toString();
}
// Replace existing __instance__ conditions with updated ones
newExpr = UtilityClass.replaceInstanceConditions(currentExpr, updatedMonitorCondition);
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
validMonitorIds.size(), ruleId, String.join(", ", existingMonitorIds), newExpr);
}
// Insert the monitor condition after the metrics condition
// Pattern: equals(__app__,"app") && equals(__metrics__,"metric") && [existing_conditions]
// Result: equals(__app__,"app") && equals(__metrics__,"metric") && [monitor_condition] && [existing_conditions]
if (currentExpr.matches(".*equals\\(__app__,\"[^\"]+\"\\)\\s*&&\\s*equals\\(__metrics__,\"[^\"]+\"\\)\\s*&&\\s*.*")) {
// Find the position after the metrics condition
String metricsPattern = "equals\\(__metrics__,\"[^\"]+\"\\)";
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(metricsPattern);
java.util.regex.Matcher matcher = regex.matcher(currentExpr);
if (matcher.find()) {
int metricsEnd = matcher.end();
// Find the " && " after the metrics condition
int andPosition = currentExpr.indexOf(" && ", metricsEnd);
if (andPosition != -1) {
String beforeAndPosition = currentExpr.substring(0, andPosition + 4); // Include " && "
String afterAndPosition = currentExpr.substring(andPosition + 4); // Everything after " && "
newExpr = beforeAndPosition + monitorCondition + " && " + afterAndPosition;
} else {
return String.format("Error: Unable to find field conditions after metrics in expression: %s", currentExpr);
}
} else {
return String.format("Error: Unable to parse metrics condition in expression: %s", currentExpr);
}
} else {
return String.format("Error: Expression format not supported for monitor binding: %s", currentExpr);
}
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
validMonitorIds.size(), ruleId, String.join(", ", validMonitorIds), newExpr);
} catch (Exception e) {
log.error("Failed to bind monitors to alert rule ID {}: {}", ruleId, e.getMessage(), e);
return String.format("Error binding monitors to alert rule: %s", e.getMessage());
}
}
/**
* Validates that the app, metric, and field conditions are valid according to hierarchy
* @param app App name to validate
* @param metrics Metric name to validate for the app
* @param fieldConditions Field conditions to validate for the metric
* @return "VALID" if all relationships are correct, error message otherwise
*/
private String validateHierarchyRelationships(String app, String metrics, String fieldConditions) {
try {
log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions);
// Get hierarchy for the specified app
List<Hierarchy> hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.toLowerCase(), "en-US");
if (hierarchies == null || hierarchies.isEmpty()) {
return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app);
}
// Find the metric in the app's hierarchy
Hierarchy metricHierarchy = UtilityClass.findMetricInHierarchy(hierarchies, metrics);
if (metricHierarchy == null) {
return String.format("Error: Metric '%s' not found for app '%s'. Please use get_apps_metrics_hierarchy to get valid metrics for this app.", metrics, app);
}
// Extract field names from field conditions and validate them
List<String> fieldNames = UtilityClass.extractFieldNamesFromConditions(fieldConditions);
for (String fieldName : fieldNames) {
if (!UtilityClass.isFieldValidForMetric(metricHierarchy, fieldName)) {
return String.format("Error: Field '%s' not found for metric '%s' in app '%s'. Please use get_apps_metrics_hierarchy to get valid field parameters.", fieldName, metrics, app);
}
}
log.debug("Hierarchy validation passed for app={}, metrics={}", app, metrics);
return "VALID";
} catch (Exception e) {
log.error("Error during hierarchy validation: {}", e.getMessage(), e);
return String.format("Error: Unable to validate hierarchy relationships: %s", e.getMessage());
}
}
}
@@ -0,0 +1,245 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
/**
* Implementation of Alert Tools functionality for alarm data queries and management
*/
@Slf4j
@Service
public class AlertToolsImpl implements AlertTools {
@Autowired
private AlertServiceAdapter alertServiceAdapter;
@Override
@Tool(name = "query_alerts", description = """
Query alerts with comprehensive filtering and pagination options.
ALERT TYPES:
- Pass alertType='single' for individual alert instances
- Pass alertType='group' for grouped/aggregated alerts
- Pass alertType='both' to get both types (separate sections)
STATUS FILTERING:
- 'firing': Currently active alerts requiring attention
- 'resolved': Previously active alerts that have been cleared
- 'all': Both firing and resolved alerts (default)
SEARCH & FILTERING:
- search: Search in alert content, labels, or descriptions
- sort: Order by 'startAt' (trigger time), 'triggerTimes' (frequency), 'status'
- order: 'asc' (oldest first) or 'desc' (newest first, default)
PAGINATION:
- pageIndex: Page number starting from 0
- pageSize: Number of alerts per page (default: 10, max recommended: 50)
EXAMPLE AND COMMON USE CASES:
- Recent active alerts: alertType='single', status='firing', sort='startAt', order='desc'
- Historical analysis: alertType='single', status='resolved', pageSize=50
- Alert grouping overview: alertType='group', status='all'
- Search specific issues: search='cpu', alertType='single', status='firing'
- Find abnormal monitors: status='firing' to get active alerts indicating monitor issues
- Monitor-specific alerts: use search parameter with monitor ID or name to find related alerts
- Frequent alerts analysis: sort='triggerTimes', order='desc' to find most frequently triggered alerts
- Recent recurring issues: status='all', sort='triggerTimes', order='desc', pageSize=20
""")
public String queryAlerts(
@ToolParam(description = "Alert type: 'single' (individual alerts), 'group' (grouped alerts), 'both' (default: single)", required = false) String alertType,
@ToolParam(description = "Alert status: 'firing' (active), 'resolved' (cleared), 'all' (default: all)", required = false) String status,
@ToolParam(description = "Search term for alert content or labels", required = false) String search,
@ToolParam(description = "Sort field: 'startAt', 'triggerTimes', 'status' (default: startAt)", required = false) String sort,
@ToolParam(description = "Sort order: 'asc' or 'desc' (default: desc)", required = false) String order,
@ToolParam(description = "Page index starting from 0 (default: 0)", required = false) Integer pageIndex,
@ToolParam(description = "Page size, 1-50 recommended (default: 10)", required = false) Integer pageSize) {
try {
log.info("Querying alerts: alertType={}, status={}, search={}, sort={}, order={}", alertType, status, search, sort, order);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in query_alerts tool: {}", subjectSum);
// Set defaults
if (alertType == null || alertType.trim().isEmpty()) {
alertType = "single";
}
if (status == null || status.trim().isEmpty()) {
status = "all";
}
if (sort == null || sort.trim().isEmpty()) {
sort = "startAt";
}
if (order == null || order.trim().isEmpty()) {
order = "desc";
}
if (pageIndex == null) {
pageIndex = 0;
}
if (pageSize == null) {
pageSize = 10;
}
StringBuilder response = new StringBuilder();
response.append("ALERT QUERY RESULTS\n");
response.append("===================\n\n");
// Handle different alert types
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
Page<SingleAlert> singleResult = alertServiceAdapter.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("SINGLE ALERTS:\n");
response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
for (SingleAlert alert : singleResult.getContent()) {
response.append("Alert ID: ").append(alert.getId()).append("\n");
response.append("Status: ").append(alert.getStatus()).append("\n");
response.append("Content: ").append(alert.getContent() != null ? alert.getContent() : "No content").append("\n");
response.append("Trigger Times: ").append(alert.getTriggerTimes()).append("\n");
if (alert.getStartAt() != null) {
response.append("Started At: ").append(UtilityClass.formatTimestamp(alert.getStartAt())).append("\n");
}
if (alert.getActiveAt() != null) {
response.append("Active At: ").append(UtilityClass.formatTimestamp(alert.getActiveAt())).append("\n");
}
if (alert.getEndAt() != null) {
response.append("Ended At: ").append(UtilityClass.formatTimestamp(alert.getEndAt())).append("\n");
}
if (alert.getLabels() != null && !alert.getLabels().isEmpty()) {
response.append("Labels: ").append(alert.getLabels()).append("\n");
}
response.append("\n");
}
if (singleResult.getContent().isEmpty()) {
response.append("No single alerts found matching the specified criteria.\n");
}
}
// Handle group alerts
if ("group".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
if ("both".equalsIgnoreCase(alertType)) {
response.append("\n");
}
Page<GroupAlert> groupResult = alertServiceAdapter.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("GROUP ALERTS:\n");
response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
for (GroupAlert alert : groupResult.getContent()) {
response.append("Group Alert ID: ").append(alert.getId()).append("\n");
response.append("Status: ").append(alert.getStatus()).append("\n");
response.append("Group Key: ").append(alert.getGroupKey() != null ? alert.getGroupKey() : "No group key").append("\n");
if (alert.getGmtCreate() != null) {
response.append("Created At: ").append(alert.getGmtCreate()).append("\n");
}
if (alert.getGmtUpdate() != null) {
response.append("Updated At: ").append(alert.getGmtUpdate()).append("\n");
}
if (alert.getCommonLabels() != null && !alert.getCommonLabels().isEmpty()) {
response.append("Common Labels: ").append(alert.getCommonLabels()).append("\n");
}
if (alert.getCommonAnnotations() != null && !alert.getCommonAnnotations().isEmpty()) {
response.append("Annotations: ").append(alert.getCommonAnnotations()).append("\n");
}
response.append("\n");
}
if (groupResult.getContent().isEmpty()) {
response.append("No group alerts found matching the specified criteria.\n");
}
}
return response.toString();
} catch (Exception e) {
log.error("Failed to query alerts: {}", e.getMessage(), e);
return "Error retrieving alerts: " + e.getMessage();
}
}
@Override
@Tool(name = "get_alerts_summary", description = """
Get alerts summary statistics including total counts, status distribution, and recent trends.
Returns comprehensive overview of the current alerting status across all monitors.
""")
public String getAlertsSummary() {
try {
log.info("Getting alerts summary");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
AlertSummary summary = alertServiceAdapter.getAlertsSummary();
StringBuilder response = new StringBuilder();
response.append("ALERTS SUMMARY\n");
response.append("==============\n\n");
if (summary != null) {
response.append("Total Alerts: ").append(summary.getTotal()).append("\n");
response.append("Handled Alerts: ").append(summary.getDealNum()).append("\n");
response.append("Handling Rate: ").append(String.format("%.1f", summary.getRate())).append("%\n\n");
response.append("Priority Breakdown (Unhandled):\n");
response.append("- Critical: ").append(summary.getPriorityCriticalNum()).append("\n");
response.append("- Emergency: ").append(summary.getPriorityEmergencyNum()).append("\n");
response.append("- Warning: ").append(summary.getPriorityWarningNum()).append("\n\n");
long totalUnhandled = summary.getPriorityCriticalNum() + summary.getPriorityEmergencyNum() + summary.getPriorityWarningNum();
response.append("Total Unhandled Alerts: ").append(totalUnhandled).append("\n");
if (totalUnhandled > 0) {
response.append("\nUnhandled Alert Distribution:\n");
response.append("- Critical: ").append(String.format("%.1f", (summary.getPriorityCriticalNum() * 100.0 / totalUnhandled))).append("%\n");
response.append("- Emergency: ").append(String.format("%.1f", (summary.getPriorityEmergencyNum() * 100.0 / totalUnhandled))).append("%\n");
response.append("- Warning: ").append(String.format("%.1f", (summary.getPriorityWarningNum() * 100.0 / totalUnhandled))).append("%\n");
}
} else {
response.append("No alert summary data available.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to get alerts summary: {}", e.getMessage(), e);
return "Error retrieving alerts summary: " + e.getMessage();
}
}
}
@@ -0,0 +1,263 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
import org.apache.hertzbeat.common.entity.dto.Field;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.common.entity.dto.Value;
import org.apache.hertzbeat.common.entity.dto.ValueRow;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* Implementation of Metrics Tools functionality for metrics data queries and analysis
*/
@Slf4j
@Service
public class MetricsToolsImpl implements MetricsTools {
@Autowired
private MetricsServiceAdapter metricsServiceAdapter;
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
@Override
@Tool(name = "query_realtime_metrics", description = """
Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
Use the query_monitors tool to find monitor IDs in case the user does not tell the id explicitly. You might have to use this multiple times based on the user's query
Get real-time metrics data for a specific monitor.
Returns current metrics values including CPU, memory, disk usage, etc.
Based on the monitor type/name, use the get_apps_metrics_hierarchy tool to get the metrics hierarchy. i.e., metrics and the field parameter (sub-metric).
Each metric has its submetrics as well for example: cpu has field parameters or sub-metrics like 'usage', 'load', 'core'. These value might be numeric or string
User might ask about specific metrics like 'cpu usage', 'memory used', 'disk available', etc.
So use this tool to get the real-time metrics data for the monitor- user asked the specific metrics for, along with any logical or numeric conditions if the user mentions
In case of multiple monitors matching the user's description, you might have to call query_realtime_metrics tool mutliple times with different parameters.
EXAMPLE WORKFLOW
If the user asks for 'cpu usage' for a particular monitor or multiple matching monitors like 'web server', 'database server', etc.
Get the closest matching monitor name from list_monitor_types tool.
Call query_monitors tool to get the monitor ID/IDs with the obtained monitor type
Call get_apps_metrics_hierarchy tool to get all the metrics (type=metric) for the obtained monitor type
Call query_realtime_metrics tool with each of the IDs and the closest matching metrics name (e.g., 'cpu', 'memory', etc.)
From the result, do whatever operation user wants you to do with the data. In this example case display the cpu usage for each matching monitor.
""")
public String getRealtimeMetrics(
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
@ToolParam(description = "Metrics name (e.g., 'system', 'cpu', 'memory') obtained from get_apps_metrics_hierarchy result", required = true) String metrics) {
try {
log.info("Getting real-time metrics for monitor {} and metrics {}", monitorId, metrics);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
MetricsData metricsData = metricsServiceAdapter.getMetricsData(monitorId, metrics);
if (metricsData == null) {
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
}
StringBuilder response = new StringBuilder();
response.append("REAL-TIME METRICS DATA\n");
response.append("=".repeat(50)).append("\n");
response.append("Monitor ID: ").append(monitorId).append("\n");
response.append("Metrics: ").append(metrics).append("\n");
response.append("=".repeat(50)).append("\n\n");
if (metricsData.getValueRows() != null && !metricsData.getValueRows().isEmpty()) {
List<Field> fields = metricsData.getFields();
response.append("Available Field Parameters (Sub-metrics):\n");
response.append("-".repeat(40)).append("\n");
for (ValueRow valueRow : metricsData.getValueRows()) {
// Show labels if available
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
response.append("Instance Labels: ").append(valueRow.getLabels()).append("\n");
response.append("-".repeat(20)).append("\n");
}
List<Value> values = valueRow.getValues();
for (int i = 0; i < values.size() && i < fields.size(); i++) {
Field field = fields.get(i);
Value value = values.get(i);
// Enhanced field information display
response.append("• Field Parameter: ").append(field.getName());
if (field.getUnit() != null && !field.getUnit().isEmpty()) {
response.append(" (").append(field.getUnit()).append(")");
}
response.append("\n");
response.append(" Current Value: ").append(value.getOrigin());
// Add data type indication
try {
Double.parseDouble(value.getOrigin());
response.append(" [Numeric]");
} catch (NumberFormatException e) {
response.append(" [String]");
}
response.append("\n\n");
}
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
response.append("-".repeat(20)).append("\n");
}
}
} else {
response.append("No field parameter data available for metrics '").append(metrics).append("'.\n");
response.append("Use get_apps_metrics_hierarchy tool to check available metrics for this monitor type.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to get real-time metrics: {}", e.getMessage(), e);
return "Error retrieving real-time metrics: " + e.getMessage();
}
}
@Override
@Tool(name = "get_historical_metrics", description = """
Get historical metrics data for analysis and trending.
Returns time-series data for specified metrics over a time range.
Use the query_monitors tool to find the correct monitor IDs/ name or type for the monitor(s) user asked the metrics for
Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field paramater
DO NOT USE THE LABEL FIELD ALWAYS USE THE VALUE FIELD FROM THE HIERARCHY JSON
Ask user to provide the filters for labels, history and interval aggregation
""")
public String getHistoricalMetrics(
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
@ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app,
@ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics,
@ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter,
@ToolParam(description = "Label filter for specific instances", required = false) String label,
@ToolParam(description = "Time range (e.g., '1h', '6h', '24h', '7d')", required = false) String history,
@ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) {
try {
log.info("Getting historical metrics for monitor {} and metrics {}", monitorId, metrics);
if (history == null || history.trim().isEmpty()) {
history = "24h";
}
if (interval == null) {
interval = true;
}
MetricsHistoryData historyData = metricsServiceAdapter.getMetricHistoryData(
monitorId, app, metrics, fieldParameter, label, history, interval);
if (historyData == null) {
return String.format("No historical metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
}
StringBuilder response = new StringBuilder();
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(monitorId).append(")\n");
response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n");
response.append("=".repeat(60)).append("\n\n");
if (historyData.getValues() != null && !historyData.getValues().isEmpty()) {
response.append("Field: ").append(historyData.getField() != null ? historyData.getField().getName() : "Unknown").append("\n");
// Calculate total data points
int totalPoints = historyData.getValues().values().stream()
.mapToInt(List::size)
.sum();
response.append("Data Points: ").append(totalPoints).append("\n");
// Show sample data points (first 10) from all value lists
int count = 0;
response.append("\nSample Data Points:\n");
for (Map.Entry<String, List<Value>> entry : historyData.getValues().entrySet()) {
String labelKey = entry.getKey();
List<Value> values = entry.getValue();
for (Value value : values) {
if (count >= 10) break;
response.append("Label: ").append(labelKey)
.append(" | Time: ").append(value.getTime())
.append(" | Value: ").append(value.getOrigin()).append("\n");
count++;
}
if (count >= 10) break;
}
if (totalPoints > 10) {
response.append("... and ").append(totalPoints - 10).append(" more data points\n");
}
} else {
response.append("No historical data points available.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to get historical metrics: {}", e.getMessage(), e);
return "Error retrieving historical metrics: " + e.getMessage();
}
}
@Override
@Tool(name = "get_warehouse_status", description = """
Check the status of the metrics storage warehouse system.
Returns whether the metrics storage is operational and accessible.
""")
public String getWarehouseStatus() {
try {
log.info("Checking warehouse storage status");
Boolean status = metricsServiceAdapter.getWarehouseStorageServerStatus();
StringBuilder response = new StringBuilder();
response.append("METRICS WAREHOUSE STATUS\n");
response.append("========================\n\n");
if (status != null && status) {
response.append("Status: ONLINE ✓\n");
response.append("The metrics storage warehouse is operational and accessible.\n");
response.append("Historical metrics data queries are available.");
} else {
response.append("Status: OFFLINE ✗\n");
response.append("The metrics storage warehouse is not accessible.\n");
response.append("Only real-time metrics may be available.");
}
return response.toString();
} catch (Exception e) {
log.error("Failed to get warehouse status: {}", e.getMessage(), e);
return "Error checking warehouse status: " + e.getMessage();
}
}
}
@@ -0,0 +1,491 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Implementation of Monitoring Tools functionality
*/
@Slf4j
@Service
public class MonitorToolsImpl implements MonitorTools {
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
/**
* Tool to query monitor information with flexible filtering and pagination.
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
* pagination.
* Returns detailed monitor information including ID, name, type, host, and status.
*/
@Override
@Tool(name = "query_monitors", description = """
Query Existing/configured monitors in HertzBeat.
This tool retrieves monitors based on various filters and parameters.
Comprehensive monitor querying with flexible filtering, pagination, and specialized views.
MONITOR STATUSES:
- status=1: Online/Active monitors (healthy, responding normally)
- status=2: Offline monitors (not responding, connection failed)
- status=3: Unreachable monitors (network/connectivity issues)
- status=0: Paused monitors (manually disabled/suspended)
- status=9 or null: All monitors regardless of status (default)
COMMON USE CASES & PARAMETER COMBINATIONS:
1. BASIC MONITOR LISTING:
- Default: No parameters (shows all monitors, 8 per page)
- By type: app='linux' (show only Linux monitors)
- Search: search='web' (find monitors with 'web' in name/host)
2. STATUS-BASED QUERIES:
- Healthy monitors: status=1, pageSize=50
- Problem monitors: status=2 or status=3, pageSize=50
- Offline monitors only: status=2
- Unreachable monitors only: status=3
- Paused monitors: status=0
3. MONITORING HEALTH OVERVIEW:
- All statuses with statistics: status=9, includeStats=true, pageSize=100
- Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls)
4. ADVANCED FILTERING:
- Specific monitor types: app='mysql', status=1 (healthy MySQL monitors)
- Label-based: labels='env:prod,critical:true'
- Host search: search='192.168' (find by IP pattern)
- Monitor IDs: ids=[1,2,3] (specific monitors by ID)
5. SORTING & PAGINATION:
- Recently updated: sort='gmtUpdate', order='desc'
- Alphabetical: sort='name', order='asc'
- By creation: sort='gmtCreate', order='desc' (newest first)
- Large datasets: pageSize=50-100 for bulk operations
RESPONSE FORMAT:
- includeStats=true: Adds status distribution summary at top
- Default: Simple list with ID, name, type, host, status
- Shows total count and pagination info
""")
public String queryMonitors(
@ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List<Long> ids,
@ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app,
@ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status,
@ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search,
@ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels,
@ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort,
@ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order,
@ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex,
@ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize,
@ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) {
try {
// Set defaults
if (pageSize == null || pageSize <= 0) {
pageSize = 20;
}
if (pageIndex == null) {
pageIndex = 0;
}
if (includeStats == null) {
includeStats = false;
}
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
StringBuilder response = new StringBuilder();
response.append("MONITOR QUERY RESULTS\n");
response.append("====================\n\n");
// Include statistics if requested
if (includeStats) {
// Get status distribution by calling with different status values
long onlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
response.append("STATUS OVERVIEW:\n");
response.append("- Online: ").append(onlineCount).append("\n");
response.append("- Offline: ").append(offlineCount).append("\n");
response.append("- Unreachable: ").append(unreachableCount).append("\n");
response.append("- Paused: ").append(pausedCount).append("\n");
long total = onlineCount + offlineCount + unreachableCount + pausedCount;
if (total > 0) {
double healthPercentage = (onlineCount * 100.0) / total;
response.append("- Health Rate: ").append(String.format("%.1f", healthPercentage)).append("%\n");
}
response.append("\n");
}
response.append("Query Results: ").append(result.getContent().size())
.append(" monitors (Total: ").append(result.getTotalElements()).append(")\n");
if (result.getTotalPages() > 1) {
response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n");
}
response.append("\n");
for (Monitor monitor : result.getContent()) {
response.append("ID: ").append(monitor.getId())
.append(" | Name: ").append(monitor.getName())
.append(" | Type: ").append(monitor.getApp())
.append(" | Host: ").append(monitor.getHost())
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
// Add creation date for better context
if (monitor.getGmtCreate() != null) {
response.append(" | Created: ").append(monitor.getGmtCreate().toString(), 0, 10);
}
response.append("\n");
}
if (result.getContent().isEmpty()) {
response.append("No monitors found matching the specified criteria.\n");
response.append("Try adjusting your filters or search terms.");
}
return response.toString();
} catch (Exception e) {
return "Error retrieving monitors: " + e.getMessage();
}
}
@Override
@Tool(name = "add_monitor", description = """
Add a new monitoring target to HertzBeat with comprehensive configuration.
This tool dynamically handles different parameter requirements for each monitor type.
This tool creates monitors with proper app-specific parameters.
*********
VERY IMPORTANT:
ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it.
Use list_monitor_types tool to see available monitor type names to use here in the app parameter.
Use the information obtained from this to query user for parameters.
If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided.
**********
Examples of natural language requests this tool handles:
- "Monitor website example.com with HTTPS on port 443"
- "Add MySQL monitoring for database server at 192.168.1.10 with user admin"
- "Monitor Linux server health on host server.company.com via SSH"
- "Set up Redis monitoring on localhost port 6379 with password"
PARAMETER MAPPING: The tool intelligently maps common parameters:
- host: Target server/domain
- port: Service port (auto-detected if not specified)
- username: Authentication username
- password: Authentication password
- database: Database name (for DB monitors)
- additionalParams: JSON string for app-specific parameters (to be obtained from get_monitor_param_defines)
ADDITIONAL PARAMETERS EXAMPLES:
- Website: {"uri":"/api/health", "ssl":"true", "method":"POST"}
- Linux: {"privateKey":"ssh-key-content", "script":"custom-script"}
- Database: {"url":"jdbc:mysql://custom", "timeout":"10000"}
""")
public String addMonitor(
@ToolParam(description = "Monitor name (required)", required = true) String name,
@ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app,
@ToolParam(description = "Target host: IP address or domain name", required = true) String host,
@ToolParam(description = "Target port (optional, auto-detected if not specified)", required = false) Integer port,
@ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
@ToolParam(description = "Username for authentication (optional)", required = false) String username,
@ToolParam(description = "Password for authentication (optional)", required = false) String password,
@ToolParam(description = "Database name (for database monitors)", required = false) String database,
@ToolParam(description = "Additional app-specific parameters as JSON: {\"uri\":\"/api\", \"ssl\":\"true\", \"method\":\"POST\"}", required = false) String additionalParams,
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
try {
log.info("Adding monitor: name={}, app={}, host={}", name, app, host);
// Validate required parameters
if (name == null || name.trim().isEmpty()) {
return "Error: Monitor name is required";
}
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application is required";
}
if (host == null || host.trim().isEmpty()) {
return "Error: Host is required";
}
// Set defaults
if (intervals == null || intervals < 10) {
intervals = 600;
}
// Create Monitor entity
Monitor monitor = Monitor.builder()
.name(name.trim())
.app(app.toLowerCase().trim())
.host(host.trim())
.intervals(intervals)
.status((byte) 1)
.type((byte) 0)
.description(description != null ? description.trim() : "")
.build();
List<Param> params = createBasicParams(host, port, username, password, database, additionalParams);
// Validate that all required parameters for this monitor type are provided
try {
List<ParamDefine> requiredParams = monitorServiceAdapter.getMonitorParamDefines(app);
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
List<String> missingParams = new ArrayList<>();
for (ParamDefine paramDefine : requiredParams) {
if (paramDefine.isRequired()) {
String fieldName = paramDefine.getField();
boolean hasParam = params.stream()
.anyMatch(param -> fieldName.equals(param.getField()));
if (!hasParam) {
missingParams.add(fieldName);
}
}
}
if (!missingParams.isEmpty()) {
return String.format("Error: Missing required parameters for monitor type '%s': %s. "
+ "Use get_monitor_additional_params tool to see all required parameters.",
app, String.join(", ", missingParams));
}
} catch (Exception e) {
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
}
// Call adapter - it handles all the complexity (validation, defaults, app-specific logic)
Long monitorId = monitorServiceAdapter.addMonitor(monitor, params, null);
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
app.toUpperCase(), name, monitorId, host, intervals);
} catch (Exception e) {
log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e);
return "Error adding monitor '" + name + "': " + e.getMessage();
}
}
/**
* Create basic parameter list from user inputs
*/
private List<Param> createBasicParams(String host, Integer port, String username,
String password, String database, String additionalParams) {
List<Param> params = new ArrayList<>();
// Add host (always required)
params.add(Param.builder().field("host").paramValue(host.trim()).type((byte) 1).build());
// Add optional common parameters
if (port != null) {
params.add(Param.builder().field("port").paramValue(port.toString()).type((byte) 0).build());
}
if (username != null && !username.trim().isEmpty()) {
params.add(Param.builder().field("username").paramValue(username.trim()).type((byte) 1).build());
}
if (password != null && !password.trim().isEmpty()) {
params.add(Param.builder().field("password").paramValue(password.trim()).type((byte) 2).build());
}
if (database != null && !database.trim().isEmpty()) {
params.add(Param.builder().field("database").paramValue(database.trim()).type((byte) 1).build());
}
// Parse additional parameters if provided
if (additionalParams != null && !additionalParams.trim().isEmpty()) {
try {
String cleaned = additionalParams.trim().replaceAll("[{}]", "");
String[] pairs = cleaned.split(",");
for (String pair : pairs) {
String[] keyValue = pair.split(":");
if (keyValue.length == 2) {
String key = keyValue[0].trim().replaceAll("\"", "");
String value = keyValue[1].trim().replaceAll("\"", "");
params.add(Param.builder().field(key).paramValue(value).type((byte) 1).build());
}
}
} catch (Exception e) {
log.warn("Failed to parse additionalParams: {}", e.getMessage());
}
}
return params;
}
@Override
@Tool(name = "list_monitor_types", description = """
List all available monitor types that can be added to HertzBeat.
This tool shows all supported monitor types with their display names.
Use this to see what types of monitors you can create with the add_monitor tool.
""")
public String listMonitorTypes(
@ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) {
try {
log.info("Listing available monitor types for language: {}", language);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
// Set default language if not provided
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Get available monitor types from adapter
Map<String, String> monitorTypes = monitorServiceAdapter.getAvailableMonitorTypes(language);
if (monitorTypes == null || monitorTypes.isEmpty()) {
return "No monitor types are currently available.";
}
// Format the response as a nice list
StringBuilder response = new StringBuilder();
response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n");
// Sort monitor types alphabetically by key
List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.toList();
for (Map.Entry<String, String> entry : sortedTypes) {
String typeKey = entry.getKey();
String displayName = entry.getValue();
response.append("").append(typeKey)
.append(" - ").append(displayName)
.append("\n");
}
response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter.");
log.info("Successfully listed {} monitor types", monitorTypes);
return response.toString();
} catch (Exception e) {
log.error("Failed to list monitor types: {}", e.getMessage(), e);
return "Error retrieving monitor types: " + e.getMessage();
}
}
@Override
@Tool(name = "get_monitor_additional_params", description = """
Get the parameter definitions required for a specific monitor type.
This tool shows what parameters are needed when adding a monitor of the specified type,
ALWAYS use this before adding a monitor to understand what parameters the user needs to provide.
Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool.
""")
public String getMonitorAdditionalParams(
@ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) {
try {
log.info("Getting parameter definitions for monitor type: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
// Validate required parameter
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application parameter is required";
}
// Get parameter definitions from adapter
List<ParamDefine> paramDefines = monitorServiceAdapter.getMonitorParamDefines(app);
if (paramDefines == null || paramDefines.isEmpty()) {
return String.format("No parameter definitions found for monitor type '%s'. "
+ "This monitor type may not exist or may not require additional parameters.", app);
}
// Format the response
StringBuilder response = new StringBuilder();
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
app, paramDefines.size()));
for (ParamDefine paramDefine : paramDefines) {
response.append("• Field: ").append(paramDefine.getField()).append("\n");
// Add display name if available
if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) {
response.append(" Name: ").append(paramDefine.getName()).append("\n");
}
// Add type
if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) {
response.append(" Type: ").append(paramDefine.getType()).append("\n");
}
// Add required status
response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n");
// Add default value if present
if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) {
response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n");
}
// Add validation range if present
if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) {
response.append(" Range: ").append(paramDefine.getRange()).append("\n");
}
// Add limit if present
if (paramDefine.getLimit() != null) {
response.append(" Limit: ").append(paramDefine.getLimit()).append("\n");
}
// Add placeholder text if present
if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) {
response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n");
}
response.append("\n");
}
response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n");
response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app));
log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app);
return response.toString();
} catch (Exception e) {
log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e);
return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage();
}
}
}
@@ -0,0 +1,691 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utility class providing helper methods for field expression validation, parsing,
* hierarchy management, timestamp formatting and metric/status conversions.
* This class contains stateless utility methods used across the application for
* common operations and validations.
*/
@Slf4j
@lombok.experimental.UtilityClass
public class UtilityClass {
/**
* Validates the syntax of field conditions expression
* @param fieldConditions Field conditions string to validate
* @return "VALID" if syntax is correct, error message otherwise
*/
public String validateExpressionSyntax(String fieldConditions) {
try {
log.debug("Validating expression syntax: {}", fieldConditions);
// Check for basic syntax requirements
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
return "Error: Field conditions cannot be empty";
}
// Check for balanced parentheses
if (!hasBalancedParentheses(fieldConditions)) {
return "Error: Unbalanced parentheses in field conditions. Please check your expression syntax.";
}
// Validate operators used in the expression
String operatorValidation = validateOperators(fieldConditions);
if (!operatorValidation.equals("VALID")) {
return operatorValidation;
}
// Validate logical connectors
String logicalValidation = validateLogicalConnectors(fieldConditions);
if (!logicalValidation.equals("VALID")) {
return logicalValidation;
}
// Validate function syntax (equals, contains, etc.)
String functionValidation = validateFunctions(fieldConditions);
if (!functionValidation.equals("VALID")) {
return functionValidation;
}
log.debug("Expression syntax validation passed for: {}", fieldConditions);
return "VALID";
} catch (Exception e) {
log.error("Error during expression syntax validation: {}", e.getMessage(), e);
return String.format("Error: Unable to validate expression syntax: %s", e.getMessage());
}
}
/**
* Checks if parentheses are balanced in the expression
*/
public boolean hasBalancedParentheses(String expression) {
int count = 0;
for (char c : expression.toCharArray()) {
if (c == '(') {
count++;
} else if (c == ')') {
count--;
if (count < 0) {
return false; // More closing than opening
}
}
}
return count == 0; // Should be perfectly balanced
}
/**
* Validates that only supported operators are used
*/
public String validateOperators(String fieldConditions) {
// Define supported operators for different field types
String[] numericOperators = {">", "<", ">=", "<=", "==", "!=", "exists()", "!exists()"};
String[] stringOperators = {"equals(", "contains(", "matches(", "exists()", "!equals(", "!contains(", "!matches(", "!exists()"};
String[] logicalOperators = {" and ", " or "};
// Remove quotes and function calls temporarily for operator checking
String tempExpression = fieldConditions
.replaceAll("\"[^\"]*\"", "VALUE") // Remove quoted strings
.replaceAll("'[^']*'", "VALUE") // Remove single quoted strings
.replaceAll("\\w+\\([^)]*\\)", "FUNCTION"); // Remove function calls
// Check for invalid operators (common mistakes)
String[] invalidOperators = {"&&", "||", "AND", "OR", "=", "!="};
for (String invalidOp : invalidOperators) {
if (tempExpression.contains(invalidOp)) {
if (invalidOp.equals("&&") || invalidOp.equals("||")) {
return String.format("Error: Use 'and'/'or' instead of '%s' for logical operations", invalidOp);
}
if (invalidOp.equals("AND") || invalidOp.equals("OR")) {
return String.format("Error: Use lowercase '%s' for logical operations", invalidOp.toLowerCase());
}
if (invalidOp.equals("=")) {
return "Error: Use '==' for equality comparison, not '='";
}
}
}
// Check for unsupported special characters that might indicate syntax errors
if (tempExpression.matches(".*[#$%^&*+\\[\\]{}|\\\\;:'\"`~].*")) {
return "Error: Expression contains unsupported special characters. Use only supported operators and functions.";
}
return "VALID";
}
/**
* Validates logical connectors syntax
*/
public String validateLogicalConnectors(String fieldConditions) {
// Check for proper spacing around logical operators
if (fieldConditions.matches(".*(\\S(and|or)\\S).*")) {
return "Error: Logical operators 'and'/'or' must be surrounded by spaces";
}
// Check for consecutive logical operators
if (fieldConditions.matches(".*(and\\s+and|or\\s+or|and\\s+or\\s+and|or\\s+and\\s+or).*")) {
return "Error: Consecutive logical operators found. Use parentheses to group conditions properly.";
}
// Check for logical operators at the beginning or end
String trimmed = fieldConditions.trim();
if (trimmed.startsWith("and ") || trimmed.startsWith("or ")
|| trimmed.endsWith(" and") || trimmed.endsWith(" or")) {
return "Error: Expression cannot start or end with logical operators 'and'/'or'";
}
return "VALID";
}
/**
* Validates function syntax (equals, contains, matches, etc.)
*/
public String validateFunctions(String fieldConditions) {
// Check for properly formed function calls
String[] supportedFunctions = {"equals", "contains", "matches", "exists", "!equals", "!contains", "!matches", "!exists"};
// Find all function-like patterns
java.util.regex.Pattern functionPattern = java.util.regex.Pattern.compile("(!?\\w+)\\s*\\(([^)]*)\\)");
java.util.regex.Matcher matcher = functionPattern.matcher(fieldConditions);
while (matcher.find()) {
String functionName = matcher.group(1);
String functionArgs = matcher.group(2);
// Check if function is supported
boolean isSupported = false;
for (String supportedFunc : supportedFunctions) {
if (functionName.equals(supportedFunc)) {
isSupported = true;
break;
}
}
if (!isSupported) {
return String.format("Error: Unsupported function '%s'. Supported functions: %s",
functionName, String.join(", ", supportedFunctions));
}
// Validate function arguments
if (functionName.equals("exists") || functionName.equals("!exists")) {
// exists() should have one parameter or no parameters
String[] args = functionArgs.trim().isEmpty() ? new String[0] : functionArgs.split(",");
if (args.length > 1) {
return String.format("Error: Function '%s' should have at most one parameter", functionName);
}
} else {
// Other functions should have exactly 2 parameters
String[] args = functionArgs.split(",");
if (args.length != 2) {
return String.format("Error: Function '%s' requires exactly 2 parameters (field, value)", functionName);
}
// Check that parameters are not empty
for (String arg : args) {
if (arg.trim().isEmpty()) {
return String.format("Error: Function '%s' has empty parameter", functionName);
}
}
}
}
return "VALID";
}
/**
* Helper method to validate operator
*/
public boolean isValidOperator(String operator) {
return operator != null && (operator.equals(">") || operator.equals("<")
|| operator.equals(">=") || operator.equals("<=")
|| operator.equals("==") || operator.equals("!="));
}
/**
* Helper method to validate priority
*/
public boolean isValidPriority(String priority) {
return priority != null && (priority.equalsIgnoreCase("critical")
|| priority.equalsIgnoreCase("warning") || priority.equalsIgnoreCase("info"));
}
/**
* Helper method to build expression
*/
public String buildExpression(String metric, String operator, String threshold) {
return String.format("%s %s %s", metric, operator, threshold);
}
/**
* Helper method to parse existing expression into components
*/
public String[] parseExpression(String expression) {
if (expression == null || expression.trim().isEmpty()) {
return null;
}
// Simple parsing for basic expressions like "metric > value"
String[] operators = {">", "<", ">=", "<=", "==", "!="};
for (String op : operators) {
if (expression.contains(" " + op + " ")) {
String[] parts = expression.split(" " + op + " ");
if (parts.length == 2) {
return new String[]{parts[0].trim(), op, parts[1].trim()};
}
}
}
return null;
}
/**
* Helper method to parse key-value pairs from a string
* Format: "key1:value1, key2:value2, ..."
*/
public Map<String, String> parseKeyValuePairs(String input) {
Map<String, String> result = new HashMap<>();
if (input == null || input.trim().isEmpty()) {
return result;
}
String[] pairs = input.split(",");
for (String pair : pairs) {
String[] keyValue = pair.split(":");
if (keyValue.length == 2) {
result.put(keyValue[0].trim(), keyValue[1].trim());
}
}
return result;
}
/**
* Recursively searches for a metric in the hierarchy
*/
public Hierarchy findMetricInHierarchy(List<Hierarchy> hierarchies, String metricName) {
for (Hierarchy hierarchy : hierarchies) {
// Check if this is the metric we're looking for
if (metricName.equals(hierarchy.getValue())) {
// Verify it has field children (leaf nodes)
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
boolean hasLeafChildren = hierarchy.getChildren().stream()
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
if (hasLeafChildren) {
return hierarchy;
}
}
}
// Recursively search in children
if (hierarchy.getChildren() != null) {
Hierarchy found = findMetricInHierarchy(hierarchy.getChildren(), metricName);
if (found != null) {
return found;
}
}
}
return null;
}
/**
* Checks if a field is valid for the given metric
*/
public boolean isFieldValidForMetric(Hierarchy metricHierarchy, String fieldName) {
if (metricHierarchy.getChildren() == null) {
return false;
}
for (Hierarchy child : metricHierarchy.getChildren()) {
if (child.getIsLeaf() != null && child.getIsLeaf() && fieldName.equals(child.getValue())) {
return true;
}
// Also check nested children
if (child.getChildren() != null && isFieldValidForMetric(child, fieldName)) {
return true;
}
}
return false;
}
/**
* Extracts field names from field conditions string
* Handles simple cases like "field > 80", "equals(field, 'value')", complex expressions
*/
public List<String> extractFieldNamesFromConditions(String fieldConditions) {
List<String> fieldNames = new ArrayList<>();
// Split by logical operators (and, or) and parentheses, but preserve the field names
// This is a simple implementation - could be enhanced with a proper parser
String[] parts = fieldConditions.split("\\s+(and|or|&&|\\|\\|)\\s+|[()]+");
for (String part : parts) {
part = part.trim();
if (part.isEmpty()) {
continue;
}
// Handle equals() function: equals(fieldName, "value")
if (part.contains("equals(")) {
String fieldName = extractFieldFromEquals(part);
if (fieldName != null && !fieldNames.contains(fieldName)) {
fieldNames.add(fieldName);
}
} else {
// Handle simple comparisons: fieldName > value, fieldName <= value
String fieldName = extractFieldFromComparison(part);
if (fieldName != null && !fieldNames.contains(fieldName)) {
fieldNames.add(fieldName);
}
}
}
return fieldNames;
}
/**
* Extracts field name from equals() function
*/
public String extractFieldFromEquals(String condition) {
// Pattern: equals(fieldName, "value") or equals(fieldName, value)
int startParen = condition.indexOf('(');
int comma = condition.indexOf(',');
if (startParen != -1 && comma != -1 && comma > startParen) {
String fieldName = condition.substring(startParen + 1, comma).trim();
// Remove quotes if present
if (fieldName.startsWith("\"") && fieldName.endsWith("\"")) {
fieldName = fieldName.substring(1, fieldName.length() - 1);
}
return fieldName;
}
return null;
}
/**
* Extracts field name from comparison operation
*/
public String extractFieldFromComparison(String condition) {
// Pattern: fieldName operator value
// Updated to include all supported operators
String[] operators = {" >= ", " <= ", " > ", " < ", " == ", " != "};
for (String operator : operators) {
if (condition.contains(operator)) {
String fieldName = condition.split(operator)[0].trim();
// Basic validation - field names shouldn't contain quotes or special chars
if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) {
return fieldName;
}
}
}
return null;
}
/**
* Helper method to format hierarchy structure as JSON recursively
*/
public ObjectNode formatHierarchyAsJson(ObjectMapper mapper, Hierarchy hierarchy) {
ObjectNode node = mapper.createObjectNode();
node.put("value", hierarchy.getValue());
node.put("label", hierarchy.getLabel());
if (hierarchy.getIsLeaf() != null && hierarchy.getIsLeaf()) {
// Leaf node - actual metric field parameter
node.put("type", "field_parameter");
if (hierarchy.getType() != null) {
node.put("dataType", hierarchy.getType() == 0 ? "numeric" : "string");
}
if (hierarchy.getUnit() != null && !hierarchy.getUnit().trim().isEmpty()) {
node.put("unit", hierarchy.getUnit());
}
node.put("description", "Available field parameter for alert conditions");
} else {
// Category, app, or metric node
// Determine node type based on children
boolean hasLeafChildren = hierarchy.getChildren().stream()
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
if (hasLeafChildren) {
node.put("type", "metric");
node.put("description", "Metric with available field parameters");
} else {
node.put("type", "app");
node.put("description", "Application with available metrics");
}
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
ArrayNode childrenArray = mapper.createArrayNode();
for (Hierarchy child : hierarchy.getChildren()) {
childrenArray.add(formatHierarchyAsJson(mapper, child));
}
node.set("children", childrenArray);
}
}
return node;
}
/**
* Format timestamp to readable format
*/
public String formatTimestamp(Long timestamp) {
if (timestamp == null) {
return "N/A";
}
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* Parse time range string to milliseconds
*/
public long parseTimeRangeToMillis(String timeRange) {
return switch (timeRange.toLowerCase()) {
case "1h" -> 60 * 60 * 1000L;
case "6h" -> 6 * 60 * 60 * 1000L;
case "24h" -> 24 * 60 * 60 * 1000L;
case "7d" -> 7 * 24 * 60 * 60 * 1000L;
default -> 24 * 60 * 60 * 1000L; // default to 24h
};
}
/**
* Helper method to convert monitor status byte to readable text
* @param status The status byte from monitor
* @return Human-readable status text
*/
public String getStatusText(Byte status) {
if (status == null) {
return "Unknown";
}
return switch (status) {
case 0 -> "Paused";
case 1 -> "Online";
case 2 -> "Offline";
case 3 -> "Unreachable";
default -> "Unknown (" + status + ")";
};
}
/**
* Helper method to get metrics name for a metric type
*/
public String getMetricsNameForType(String metricType) {
return switch (metricType.toLowerCase()) {
case "cpu" -> "cpu";
case "memory" -> "memory";
case "disk" -> "disk";
case "network" -> "network";
default -> "system";
};
}
/**
* Helper method to check if a field represents usage for a metric type
*/
public boolean isUsageField(String field, String metricType) {
if (field == null) return false;
String fieldLower = field.toLowerCase();
String typeLower = metricType.toLowerCase();
return fieldLower.contains("usage")
|| fieldLower.contains("percent")
|| fieldLower.contains("util")
|| (typeLower.equals("cpu") && (fieldLower.contains("cpu") || fieldLower.contains("idle")))
|| (typeLower.equals("memory") && fieldLower.contains("memory"))
|| (typeLower.equals("disk") && fieldLower.contains("disk"));
}
/**
* Convert manager module Hierarchy objects to ai-agent module Hierarchy objects
* This handles the cross-module DTO conversion to avoid ClassCastException
*/
public List<Hierarchy> convertToAgentHierarchies(List<?> managerHierarchies) {
List<Hierarchy> agentHierarchies = new ArrayList<>();
for (Object managerHierarchy : managerHierarchies) {
Hierarchy agentHierarchy = convertToAgentHierarchy(managerHierarchy);
agentHierarchies.add(agentHierarchy);
}
return agentHierarchies;
}
/**
* Convert a single manager Hierarchy object to ai-agent Hierarchy object using reflection
*/
public Hierarchy convertToAgentHierarchy(Object managerHierarchy) {
try {
Hierarchy agentHierarchy = new Hierarchy();
// Use reflection to copy properties from manager DTO to agent DTO
Class<?> managerClass = managerHierarchy.getClass();
// Copy basic properties
agentHierarchy.setCategory(getStringField(managerHierarchy, managerClass, "category"));
agentHierarchy.setValue(getStringField(managerHierarchy, managerClass, "value"));
agentHierarchy.setLabel(getStringField(managerHierarchy, managerClass, "label"));
agentHierarchy.setIsLeaf(getBooleanField(managerHierarchy, managerClass, "isLeaf"));
agentHierarchy.setHide(getBooleanField(managerHierarchy, managerClass, "hide"));
agentHierarchy.setType(getByteField(managerHierarchy, managerClass, "type"));
agentHierarchy.setUnit(getStringField(managerHierarchy, managerClass, "unit"));
// Handle children recursively
List<?> managerChildren = getListField(managerHierarchy, managerClass, "children");
if (managerChildren != null && !managerChildren.isEmpty()) {
List<Hierarchy> agentChildren = convertToAgentHierarchies(managerChildren);
agentHierarchy.setChildren(agentChildren);
}
return agentHierarchy;
} catch (Exception e) {
log.error("Failed to convert manager hierarchy to agent hierarchy: {}", e.getMessage(), e);
throw new RuntimeException("Failed to convert hierarchy", e);
}
}
public String getStringField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value != null ? value.toString() : null;
} catch (Exception e) {
log.debug("Could not get string field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public Boolean getBooleanField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e) {
try {
// Try alternative getter pattern for boolean fields
Method isGetter = clazz.getMethod("is" + capitalize(fieldName));
Object value = isGetter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e2) {
log.debug("Could not get boolean field '{}': {}", fieldName, e.getMessage());
return null;
}
}
}
public Byte getByteField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Byte ? (Byte) value : null;
} catch (Exception e) {
log.debug("Could not get byte field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public List<?> getListField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof List ? (List<?>) value : null;
} catch (Exception e) {
log.debug("Could not get list field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
/**
* Extract existing monitor IDs from the alert expression
* @param expression The alert expression containing __instance__ conditions
* @return List of existing monitor IDs
*/
public List<String> extractExistingMonitorIds(String expression) {
List<String> monitorIds = new ArrayList<>();
String pattern = "equals\\(__instance__,\\s*\"([^\"]+)\"\\)";
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher matcher = regex.matcher(expression);
while (matcher.find()) {
monitorIds.add(matcher.group(1));
}
return monitorIds;
}
/**
* Replace existing __instance__ conditions in the expression with new ones
* @param expression The current expression
* @param newMonitorCondition The new monitor condition to replace with
* @return Updated expression
*/
public String replaceInstanceConditions(String expression, String newMonitorCondition) {
// More precise pattern to match complete __instance__ condition blocks without cutting field parameters
// Pattern matches either:
// 1. Single: equals(__instance__, "id")
// 2. Multiple: (equals(__instance__, "id1") or equals(__instance__, "id2") or ...)
// First try to match grouped conditions: (equals(__instance__, "id1") or equals(__instance__, "id2"))
String groupedPattern = "\\(\\s*equals\\(__instance__,\\s*\"[^\"]+\"\\)(?:\\s+or\\s+equals\\(__instance__,\\s*\"[^\"]+\"\\))*\\s*\\)";
if (expression.matches(".*" + groupedPattern + ".*")) {
return expression.replaceFirst(groupedPattern, newMonitorCondition);
}
// Then try single condition: equals(__instance__, "id")
String singlePattern = "equals\\(__instance__,\\s*\"[^\"]+\"\\)";
if (expression.matches(".*" + singlePattern + ".*")) {
return expression.replaceFirst(singlePattern, newMonitorCondition);
}
// If no match found, return original expression
return expression;
}
}
@@ -17,6 +17,9 @@
package org.apache.hertzbeat.alert.calculate;
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -24,7 +27,6 @@ import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -33,49 +35,83 @@ import java.util.concurrent.ConcurrentHashMap;
@Component
public class AlarmCacheManager {
private static final String CUSTOM_FIRING_ROW_KEY = "CUSTOM_FIRING_";
/**
* The alarm in the process is triggered
* key - labels fingerprint
* rowKey - define id
* columnKey - labels fingerprint
*/
private final Map<String, SingleAlert> pendingAlertMap;
private final Table<String, String, SingleAlert> pendingAlertMap;
/**
* The not recover alert
* key - labels fingerprint
* rowKey - define id
* columnKey - labels fingerprint
*/
private final Map<String, SingleAlert> firingAlertMap;
private final Table<String, String, SingleAlert> firingAlertMap;
public AlarmCacheManager(SingleAlertDao singleAlertDao) {
this.pendingAlertMap = new ConcurrentHashMap<>(8);
this.firingAlertMap = new ConcurrentHashMap<>(8);
this.pendingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
this.firingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
List<SingleAlert> singleAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING);
for (SingleAlert singleAlert : singleAlerts) {
String fingerprint = AlertUtil.calculateFingerprint(singleAlert.getLabels());
String defineId = singleAlert.getLabels().get(CommonConstants.LABEL_DEFINE_ID);
if (StringUtils.isBlank(defineId)) {
defineId = getCustomKey(fingerprint);
}
singleAlert.setId(null);
this.firingAlertMap.put(fingerprint, singleAlert);
this.firingAlertMap.put(defineId, fingerprint, singleAlert);
}
}
public void putPending(String fingerPrint, SingleAlert alert) {
this.pendingAlertMap.put(fingerPrint, alert);
public void putPending(Long defineId, String fingerPrint, SingleAlert alert) {
this.pendingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
}
public SingleAlert getPending(String fingerPrint) {
return this.pendingAlertMap.get(fingerPrint);
public SingleAlert getPending(Long defineId, String fingerPrint) {
return this.pendingAlertMap.get(String.valueOf(defineId), fingerPrint);
}
public SingleAlert removePending(String fingerPrint) {
return this.pendingAlertMap.remove(fingerPrint);
public void removePending(Long defineId, String fingerPrint) {
this.pendingAlertMap.remove(String.valueOf(defineId), fingerPrint);
}
public void putFiring(Long defineId, String fingerPrint, SingleAlert alert) {
this.firingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
}
public void putFiring(String fingerPrint, SingleAlert alert) {
this.firingAlertMap.put(fingerPrint, alert);
this.firingAlertMap.put(getCustomKey(fingerPrint), fingerPrint, alert);
}
public SingleAlert getFiring(Long defineId, String fingerPrint) {
SingleAlert singleAlert = this.firingAlertMap.get(String.valueOf(defineId), fingerPrint);
if (null != singleAlert) {
return singleAlert;
}
return getFiring(fingerPrint);
}
public SingleAlert removeFiring(Long defineId, String fingerPrint) {
SingleAlert singleAlert = this.firingAlertMap.remove(String.valueOf(defineId), fingerPrint);
if (null == singleAlert) {
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
}
return singleAlert;
}
public SingleAlert getFiring(String fingerPrint) {
return this.firingAlertMap.get(fingerPrint);
return this.firingAlertMap.get(getCustomKey(fingerPrint), fingerPrint);
}
private String getCustomKey(String fingerPrint) {
return CUSTOM_FIRING_ROW_KEY + fingerPrint;
}
public SingleAlert removeFiring(String fingerPrint) {
return this.firingAlertMap.remove(fingerPrint);
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
}
}
@@ -76,11 +76,12 @@ public class CollectorAlertHandler {
fingerPrints.put(KEY_COLLECTOR_VERSION, collector.getVersion());
fingerPrints.put(KEY_COLLECTOR_HOST, collector.getIp());
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert firingAlert = alarmCacheManager.getFiring(fingerprint);
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
if (firingAlert != null) {
firingAlert.setTriggerTimes(1);
firingAlert.setEndAt(System.currentTimeMillis());
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
firingAlert.setContent(this.bundle.getString("alerter.availability.collector.recover"));
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
}
@@ -17,8 +17,9 @@
package org.apache.hertzbeat.alert.calculate;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
@@ -26,11 +27,11 @@ import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
/**
* Periodic Alert Calculator
@@ -54,9 +55,9 @@ public class PeriodicAlertCalculator {
this.alarmCacheManager = alarmCacheManager;
}
public void calculate(AlertDefine rule) {
if (!rule.isEnable() || StringUtils.isEmpty(rule.getExpr())) {
log.error("Periodic rule {} is disabled or expression is empty", rule.getName());
public void calculate(AlertDefine define) {
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
log.error("Periodic define {} is disabled or expression is empty", define.getName());
return;
}
long currentTimeMilli = System.currentTimeMillis();
@@ -66,8 +67,8 @@ public class PeriodicAlertCalculator {
// the return result should be matched with threshold
try {
List<Map<String, Object>> results = dataSourceService.calculate(
rule.getDatasource(),
rule.getExpr()
define.getDatasource(),
define.getExpr()
);
// if no match the expr threshold, the results item map {'value': null} should be null and others field keep
// if results has multi list, should trigger multi alert
@@ -77,8 +78,9 @@ public class PeriodicAlertCalculator {
for (Map<String, Object> result : results) {
Map<String, String> fingerPrints = new HashMap<>(8);
// here use the alert name as finger, not care the alert name may be changed
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
fingerPrints.putAll(rule.getLabels());
fingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
fingerPrints.putAll(define.getLabels());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
&& !TIMESTAMP.equals(entry.getKey())) {
@@ -87,32 +89,33 @@ public class PeriodicAlertCalculator {
}
if (result.get(VALUE) == null) {
// recovery the alert
handleRecoveredAlert(fingerPrints);
handleRecoveredAlert(define.getId(), fingerPrints);
continue;
}
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.putAll(rule.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
fieldValueMap.putAll(define.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue());
}
}
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, rule);
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
}
} catch (Exception ignored) {
// ignore the query exception eg: no result, timeout, etc
return;
}
} catch (Exception e) {
log.error("Calculate periodic rule {} failed: {}", rule.getName(), e.getMessage());
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
}
}
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define) {
Long defineId = define.getId();
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getPending(fingerprint);
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
Map<String, String> labels = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
labels.putAll(fingerPrints);
@@ -133,11 +136,11 @@ public class PeriodicAlertCalculator {
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(fingerprint, newAlert);
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
} else {
// Otherwise put into pending queue first
alarmCacheManager.putPending(fingerprint, newAlert);
alarmCacheManager.putPending(defineId, fingerprint, newAlert);
}
} else {
// Update existing alert
@@ -147,17 +150,17 @@ public class PeriodicAlertCalculator {
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
alarmCacheManager.removePending(fingerprint);
alarmCacheManager.removePending(defineId, fingerprint);
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(fingerprint, existingAlert);
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
}
}
}
private void handleRecoveredAlert(Map<String, String> fingerprints) {
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
if (firingAlert != null) {
// todo consider multi times to tig for resolved alert
firingAlert.setTriggerTimes(1);
@@ -165,7 +168,7 @@ public class PeriodicAlertCalculator {
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
alarmCacheManager.removePending(fingerprint);
alarmCacheManager.removePending(defineId, fingerprint);
}
}
@@ -139,6 +139,9 @@ public class RealTimeAlertCalculator {
String instanceHost = metricsData.getInstanceHost();
String app = metricsData.getApp();
String metrics = metricsData.getMetrics();
if ((CommonConstants.PROMETHEUS_APP_PREFIX + instanceName).equals(metricsData.getApp())) {
app = CommonConstants.PROMETHEUS;
}
int priority = metricsData.getPriority();
int code = metricsData.getCode().getNumber();
Map<String, String> labels = metricsData.getLabels();
@@ -180,9 +183,11 @@ public class RealTimeAlertCalculator {
if (StringUtils.isBlank(expr)) {
continue;
}
Long defineId = define.getId();
Map<String, String> commonFingerPrints = new HashMap<>(8);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
// here use the alert name as finger, not care the alert name may be changed
commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
@@ -197,9 +202,9 @@ public class RealTimeAlertCalculator {
try {
if (match) {
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
afterThresholdRuleMatch(currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
afterThresholdRuleMatch(defineId, currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
} else {
handleRecoveredAlert(commonFingerPrints);
handleRecoveredAlert(defineId, commonFingerPrints);
}
// if this threshold pre compile success, ignore blew
continue;
@@ -251,9 +256,9 @@ public class RealTimeAlertCalculator {
boolean match = execAlertExpression(fieldValueMap, expr, false);
try {
if (match) {
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
afterThresholdRuleMatch(defineId, currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
} else {
handleRecoveredAlert(fingerPrints);
handleRecoveredAlert(defineId, fingerPrints);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -331,9 +336,9 @@ public class RealTimeAlertCalculator {
.collect(Collectors.toList());
}
private void handleRecoveredAlert(Map<String, String> fingerprints) {
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
if (firingAlert != null) {
// todo consider multi times to tig for resolved alert
firingAlert.setTriggerTimes(1);
@@ -341,13 +346,14 @@ public class RealTimeAlertCalculator {
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
alarmCacheManager.removePending(fingerprint);
alarmCacheManager.removePending(defineId, fingerprint);
}
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define, Map<String, String> annotations) {
private void afterThresholdRuleMatch(long defineId, long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define,
Map<String, String> annotations) {
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
SingleAlert existingAlert = alarmCacheManager.getPending(fingerprint);
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
fieldValueMap.putAll(define.getLabels());
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
if (existingAlert == null) {
@@ -379,11 +385,11 @@ public class RealTimeAlertCalculator {
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(fingerprint, newAlert);
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
} else {
// Otherwise put into pending queue first
alarmCacheManager.putPending(fingerprint, newAlert);
alarmCacheManager.putPending(define.getId(), fingerprint, newAlert);
}
} else {
// Update existing alert
@@ -393,9 +399,9 @@ public class RealTimeAlertCalculator {
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
alarmCacheManager.removePending(fingerprint);
alarmCacheManager.removePending(defineId, fingerprint);
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
alarmCacheManager.putFiring(fingerprint, existingAlert);
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
}
}
@@ -22,10 +22,12 @@ package org.apache.hertzbeat.alert.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -54,16 +56,24 @@ public class AlertSseManager {
.name("ALERT_EVENT")
.data(data));
} catch (IOException | IllegalStateException e) {
emitter.complete();
removeEmitter(clientId);
tryCompleteAndClean(clientId, emitter);
} catch (Exception exception) {
log.error("Failed to broadcast alert data to client: {}", exception.getMessage());
emitter.complete();
removeEmitter(clientId);
tryCompleteAndClean(clientId, emitter);
}
});
}
private void tryCompleteAndClean(Long clientId, SseEmitter emitter) {
try {
Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete);
} catch (Throwable e) {
log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage());
}
// execute clear
removeEmitter(clientId);
}
private void removeEmitter(Long clientId) {
emitters.remove(clientId);
}
@@ -17,17 +17,16 @@
package org.apache.hertzbeat.alert.controller;
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.Objects;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -36,8 +35,17 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* Alarm definition management API
*/
@@ -90,4 +98,18 @@ public class AlertDefineController {
return ResponseEntity.ok(Message.success("Delete success"));
}
@GetMapping(path = "/preview/{datasource}")
@Operation(summary = "Alarm definition expression preview",
description = "If the expression is formal, then the result of the query will be returned, otherwise it will respond with an error")
public ResponseEntity<Message<List<Map<String, Object>>>> getDefinePreview(
@Parameter(description = "Data Source Type", example = "promql") @PathVariable("datasource") String datasource,
@Parameter(description = "alert threshold type:realtime,periodic") @RequestParam String type,
@Parameter(description = "alert threshold expression") @RequestParam String expr) {
try {
return ResponseEntity.ok(Message.successWithData(alertDefineService.getDefinePreview(datasource, type, expr)));
} catch (AlertExpressionException ae) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Message.fail(FAIL_CODE, ae.getMessage()));
}
}
}
@@ -0,0 +1,184 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Alibaba Cloud 'Simple Log Service(SLS)' alert content entity.
*
* @see <a href="https://help.aliyun.com/zh/sls/user-guide/variables-in-new-alert-templates"/>
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AlibabaCloudSlsExternAlert {
/**
* The id of the instance on which the alarm was triggered
*/
@JsonProperty("alert_instance_id")
private String alertInstanceId;
/**
* Alarm rule id, unique within project
*/
@JsonProperty("alert_id")
private String alertId;
/**
* Alarm rule name
*/
@JsonProperty("alert_name")
private String alertName;
/**
* Region
*/
private String region;
/**
* Alarm rule belongs to Project
*/
private String project;
/**
* Time of this evaluation
*/
@JsonProperty("alert_time")
private int alertTime;
/**
* First trigger time
*/
@JsonProperty("fire_time")
private int fireTime;
/**
* Alarm recovery time
* If the alarm status is firing, the value is 0.
* If the alarm state is resolved, the value is the specific recovery time.
*/
@JsonProperty("resolve_time")
private int resolveTime;
/**
* Alarm status.
* firing: Triggers an alarm.
* resolved: Notification of resumption.
*/
private String status;
/**
* The total number of entries in the data that triggered the alert,
* which may be more than 100, for example after a Cartesian product operation.
*/
@JsonProperty("fire_results_count")
private int fireResultsCount;
/**
* Tag list
* Example: {"env":"test"}
*/
private Map<String, String> labels;
/**
* Labeled lists
* Example: { "title": "Alarm title","desc": "Alarm desc" }
*/
private Map<String, String> annotations;
/**
* Alarm severity.
*
* 10: Critical
* 8: High
* 6: Medium
* 4: Low
* 2: Report only
*/
private int severity;
/**
*
*/
@JsonProperty("signin_url")
private String signinUrl;
public String getAnnotation(String key) {
if (null == this.annotations || this.annotations.isEmpty()) {
return "N/A";
}
return this.annotations.get(key);
}
/**
* Severity
*/
public enum Severity {
CRITICAL(10, "Critical"),
HIGH(8, "High"),
MEDIUM(6, "Medium"),
LOW(4, "Low"),
REPORT_ONLY(2, "Report only");
private static final Map<Integer, Severity> STATUS_MAP;
static {
STATUS_MAP = Arrays.stream(Severity.values()).collect(Collectors.toMap(Severity::getStatus, t -> t, (oldVal, newVal) -> newVal));
}
private final int status;
private final String alias;
Severity(int status, String alias) {
this.status = status;
this.alias = alias;
}
public static Optional<Severity> convert(int severity) {
return Optional.ofNullable(STATUS_MAP.get(severity));
}
public int getStatus() {
return status;
}
public String getAlias() {
return alias;
}
}
}
@@ -0,0 +1,237 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
/**
* Huawei Cloud (CES) alert content entity.
*
* @see <a href="https://support.huaweicloud.com/usermanual-ces/ces_01_0218.html"/>
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HuaweiCloudExternAlert {
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_MESSAGE_ID = "message_id";
public static final String FIELD_TIMESTAMP = "timestamp";
public static final String FIELD_TOPIC_URN = "topic_urn";
public static final String FIELD_TYPE = "type";
public static final String FIELD_SUBJECT = "subject";
public static final String FIELD_SUBSCRIBE_URL = "subscribe_url";
/**
* Signature information.
*/
private String signature;
/**
* Subject
*/
private String subject;
/**
* The unique identifier of a topic, indicating the topic to which the message belongs.
*/
@JsonProperty("topic_urn")
private String topicUrn;
/**
* Message unique identifier.
*/
@JsonProperty("message_id")
private String messageId;
/**
* Message
*/
private String message;
/**
* message types, the message types are respectively:
* SubscriptionConfirmation、Notification、UnsubscribeConfirmation
*/
private String type;
/**
* Subscription confirms the URL that needs to be accessed
*/
@JsonProperty("subscribe_url")
private String subscribeUrl;
/**
* The certificate URL used for message signing, which does not require authentication and can be accessed directly.
*/
@JsonProperty("signing_cert_url")
private String signingCertUrl;
/**
* The timestamp of when the message was first sent.
*/
private String timestamp;
/**
* Alert message
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class AlertMessage {
private String version;
private AlertData data;
}
/**
* Alert data
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class AlertData {
/**
* Whether an alarm occurs.
* Note: Empty and false are both recovery notifications.
* Note: There are no recovery notifications for event types
*/
@JsonProperty("IsAlarm")
private Boolean alarm;
/**
* Alarm time
*/
@JsonProperty("AlarmTime")
private String alarmTime;
/**
* Resource ID
*/
@JsonProperty("ResourceId")
private String resourceId;
/**
* The name of the metric
*/
@JsonProperty("MetricName")
private String metricName;
/**
* Specifies the alarm severity, which can be Critical, Major, Minor, or Informational.
*/
@JsonProperty("AlarmLevel")
private String alarmLevel;
/**
* Namespace
*/
@JsonProperty("Namespace")
private String namespace;
/**
* Region
*/
@JsonProperty("Region")
private String region;
/**
* Dimension name
*/
@JsonProperty("DimensionName")
private String dimensionName;
/**
* Resource name
*/
@JsonProperty("ResourceName")
private String resourceName;
/**
* Alarm record ID
*/
@JsonProperty("AlarmRecordID")
private String alarmRecordId;
/**
* Current data
*/
@JsonProperty("CurrentData")
private String currentData;
/**
* The comparison conditions for the alarm thresholds can be >, =, <, >=, <=.
*/
@JsonProperty("ComparisonOperator")
private String comparisonOperator;
/**
* Alarm value
*/
@JsonProperty("Value")
private String value;
/**
* Number of consecutive occurrences of triggered alarms
*/
@JsonProperty("Count")
private int count;
}
/**
* Huawei cloud alert type
*/
public enum AlertType {
SUBSCRIPTION("SubscriptionConfirmation"),
UNSUBSCRIBE("UnsubscribeConfirmation"),
NOTIFICATION("Notification");
private final String type;
AlertType(String type) {
this.type = type;
}
public static boolean valid(String type) {
if (null == type || type.isEmpty()) {
return false;
}
return Arrays.stream(AlertType.values()).anyMatch(alertType -> alertType.getType().equals(type));
}
public String getType() {
return type;
}
}
}
@@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* volcengine event alert entity class
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VolcEngineExternEventAlert {
@JsonProperty("Type")
private String type;
@JsonProperty("AccountId")
private String accountId;
@JsonProperty("Source")
private String source;
@JsonProperty("ProjectName")
private String projectName;
@JsonProperty("Id")
private String id;
@JsonProperty("EventType")
private String eventType;
@JsonProperty("DescriptionCN")
private String descriptionCn;
@JsonProperty("HappenedAt")
private Long happenedAt;
@JsonProperty("Region")
private String region;
@JsonProperty("Details")
private EventDetails details;
@JsonProperty("Rules")
private List<EventRule> rules;
/**
* event details
*/
@Data
public static class EventDetails {
@JsonProperty("specversion")
private String specVersion;
@JsonProperty("id")
private String id;
@JsonProperty("source")
private String source;
@JsonProperty("type")
private String type;
@JsonProperty("volcpublishtime")
private String volcPublishTime;
@JsonProperty("volcregion")
private String volcRegion;
@JsonProperty("volcaccountid")
private String volcAccountId;
@JsonProperty("volceventbusname")
private String volcEventBusName;
@JsonProperty("volcresourcename")
private String volcResourceName;
@JsonProperty("subject")
private String subject;
@JsonProperty("time")
private String time;
}
/**
* event rule
*/
@Data
public static class EventRule {
@JsonProperty("RuleId")
private String ruleId;
@JsonProperty("RuleName")
private String ruleName;
@JsonProperty("Level")
private String level;
}
}
@@ -0,0 +1,203 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Volcengine alarm entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VolcEngineExternMetricAlert {
@JsonProperty("Type")
private String type;
@JsonProperty("AccountId")
private String accountId;
@JsonProperty("RuleName")
private String ruleName;
@JsonProperty("RuleId")
private String ruleId;
@JsonProperty("Namespace")
private String namespace;
@JsonProperty("SubNamespace")
private String subNamespace;
@JsonProperty("Level")
private String level;
@JsonProperty("HappenedAt")
private String happenedAt;
@JsonProperty("RuleCondition")
private String ruleCondition;
@JsonProperty("Resources")
private List<Resource> resources;
@JsonProperty("RecoveredResources")
private List<Resource> recoveredResources;
@JsonProperty("NoDataResources")
private List<NoDataResource> noDataResources;
@JsonProperty("NoDataRecoveredResources")
private List<NoDataRecoveredResource> noDataRecoveredResources;
/**
* alert resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Resource {
@JsonProperty("Id")
private String id;
@JsonProperty("Name")
private String name;
@JsonProperty("Region")
private String region;
@JsonProperty("FirstAlertTime")
private Long firstAlertTime;
@JsonProperty("LastAlertTime")
private Long lastAlertTime;
@JsonProperty("Metrics")
private List<Metric> metrics;
@JsonProperty("AlertGroupId")
private String alertGroupId;
@JsonProperty("Dimensions")
private List<Dimension> dimensions;
}
/**
* alert no data resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class NoDataResource extends Resource {
@JsonProperty("NoDataMetrics")
private List<Metric> noDataMetrics;
}
/**
* alert no data recovered resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class NoDataRecoveredResource extends Resource {
@JsonProperty("DroppedMetrics")
private List<DroppedMetric> droppedMetrics;
}
/**
* alert metric entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Metric {
@JsonProperty("Name")
private String name;
@JsonProperty("Unit")
private String unit;
@JsonProperty("Threshold")
private Double threshold;
@JsonProperty("CurrentValue")
private Object currentValue;
@JsonProperty("Description")
private String description;
@JsonProperty("Warning")
private Boolean warning;
}
/**
* alert dropped metric entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class DroppedMetric extends Metric {
@JsonProperty("Reason")
private String reason;
}
/**
* resource dimension entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Dimension {
@JsonProperty("Name")
private String name;
@JsonProperty("NameCN")
private String nameCn;
@JsonProperty("Value")
private String value;
@JsonProperty("Description")
private String description;
}
public static final String ALERT_TYPE_METRIC = "Metric";
public static final String ALERT_TYPE_EVENT = "Event";
public static final String ALERT_TYPE_METRIC_RECOVERED = "MetricRecovered";
public static final String ALERT_TYPE_METRICS_NODATA = "MetricsNoData";
public static final String ALERT_TYPE_NO_DATA_RECOVERED = "NoDataRecovered";
}
@@ -27,7 +27,7 @@ import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor;
* of the available methods.
*
* @param <T> The return type of the visit operation. Use {@link Void} for
* operations with no return type.
* operations with no return type.
*/
@SuppressWarnings("CheckReturnValue")
public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements AlertExpressionVisitor<T> {
@@ -37,7 +37,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitExpression(AlertExpressionParser.ExpressionContext ctx) { return visitChildren(ctx); }
@Override
public T visitExpression(AlertExpressionParser.ExpressionContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -45,15 +48,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitQueryExpr(AlertExpressionParser.QueryExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -61,7 +59,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -69,7 +70,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -77,7 +81,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -85,15 +92,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -101,7 +103,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitIdentifier(AlertExpressionParser.IdentifierContext ctx) { return visitChildren(ctx); }
@Override
public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -109,5 +114,349 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitNumber(AlertExpressionParser.NumberContext ctx) { return visitChildren(ctx); }
@Override
public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitParameterList(AlertExpressionParser.ParameterListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitParameter(AlertExpressionParser.ParameterContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitNumber(AlertExpressionParser.NumberContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitString(AlertExpressionParser.StringContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitDuration(AlertExpressionParser.DurationContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitSelectField(AlertExpressionParser.SelectFieldContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitGroupByList(AlertExpressionParser.GroupByListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitOrderByList(AlertExpressionParser.OrderByListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitRelList(AlertExpressionParser.RelListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitRelation(AlertExpressionParser.RelationContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitConditionList(AlertExpressionParser.ConditionListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitCompOp(AlertExpressionParser.CompOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitCondition(AlertExpressionParser.ConditionContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitPromql(AlertExpressionParser.PromqlContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitLabelList(AlertExpressionParser.LabelListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override
public T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) {
return visitChildren(ctx);
}
}
@@ -17,26 +17,34 @@
package org.apache.hertzbeat.alert.expr;
import org.antlr.v4.runtime.CommonTokenStream;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.hertzbeat.common.support.exception.ExpressionVisitorException;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Alert expression visitor implement
*/
public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<Map<String, Object>>> {
private static final String THRESHOLD = "__threshold__";
private static final String SCALAR = "__scalar__";
private static final String NAME = "__name__";
private static final String VALUE = "__value__";
private static final String TIMESTAMP = "__timestamp__";
private final QueryExecutor executor;
private final CommonTokenStream tokens;
public AlertExpressionEvalVisitor(QueryExecutor executor) {
public AlertExpressionEvalVisitor(QueryExecutor executor, CommonTokenStream tokens) {
this.executor = executor;
this.tokens = tokens;
}
@Override
@@ -53,69 +61,126 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
public List<Map<String, Object>> visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
List<Map<String, Object>> leftResult = visit(ctx.left);
List<Map<String, Object>> rightResult = visit(ctx.right);
if (rightResult.size() == 1 && rightResult.get(0).containsKey(THRESHOLD)) {
double threshold = (double) rightResult.get(0).get(THRESHOLD);
String operator = ctx.op.getText();
int type = ctx.op.getType();
boolean boolModifier = ctx.BOOL() != null;
boolean leftIsScalar = isScalar(leftResult);
boolean rightIsScalar = isScalar(rightResult);
List<Map<String, Object>> results = new ArrayList<>();
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> item : leftResult) {
Object queryValues = item.get(VALUE);
if (queryValues == null) {
// ignore the query result data is empty
// scalar and scalar
if (leftIsScalar && rightIsScalar) {
if (!boolModifier) {
// Between two scalars,
// the bool modifier must be provided and these operators result in another scalar that is either 0 (false) or 1 (true), depending on the comparison result.
return results;
}
Object leftVal = leftResult.get(0).get(SCALAR);
Object rightVal = rightResult.get(0).get(SCALAR);
Boolean match = compareOp(leftVal, type, rightVal);
// returns a result only if the comparison condition is met.
Map<String, Object> result = new HashMap<>();
result.put(VALUE, match ? 1 : 0);
results.add(result);
return results;
}
// scalar and vector
if (leftIsScalar) {
Object leftVal = leftResult.get(0).get(SCALAR);
for (Map<String, Object> rightItem : rightResult) {
Object rightVal = rightItem.getOrDefault(VALUE, null);
if (isValidValue(rightVal)) {
continue;
}
// queryValues may be a list of values, or a single value
Object matchValue = evaluateCondition(queryValues, operator, threshold);
item.put(VALUE, matchValue);
// if matchValue is null, mean not match the threshold
// if not null, mean match the threshold
result.add(new HashMap<>(item));
Boolean match = compareOp(leftVal, type, rightVal);
Map<String, Object> result = new HashMap<>(rightItem);
if (boolModifier) {
result.put(VALUE, match ? 1 : 0);
results.add(result);
} else {
result.put(VALUE, match ? rightVal : null);
results.add(result);
}
}
return result;
return results;
}
return new LinkedList<>();
// vector and scalar
if (rightIsScalar) {
Object rightVal = rightResult.get(0).get(SCALAR);
for (Map<String, Object> leftItem : leftResult) {
Object leftVal = leftItem.getOrDefault(VALUE, null);
if (isValidValue(leftVal)) {
continue;
}
Boolean match = compareOp(leftVal, type, rightVal);
Map<String, Object> result = new HashMap<>(leftItem);
if (boolModifier) {
result.put(VALUE, match ? 1 : 0);
results.add(result);
} else {
result.put(VALUE, match ? leftVal : null);
results.add(result);
}
}
return results;
}
// vector and vector
Map<String, Map<String, Object>> rightMap = rightResult.stream()
.filter(item -> item.get(VALUE) != null)
.collect(Collectors.toMap(this::labelKey, item -> item, (existing, replacement) -> existing));
for (Map<String, Object> leftItem : leftResult) {
Object leftVal = leftItem.getOrDefault(VALUE, null);
if (isValidValue(leftVal)) {
continue;
}
Map<String, Object> rightItem = rightMap.get(labelKey(leftItem));
if (rightItem == null) {
continue;
}
Object rightVal = rightItem.get(VALUE);
if (isValidValue(rightVal)) {
continue;
}
Boolean match = compareOp(leftVal, type, rightVal);
Map<String, Object> result = new HashMap<>(leftItem);
if (boolModifier) {
result.put(VALUE, match ? 1 : 0);
results.add(result);
} else {
result.put(VALUE, match ? leftVal : null);
results.add(result);
}
}
return results;
}
@Override
public List<Map<String, Object>> visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
List<Map<String, Object>> results = new ArrayList<>();
Map<String, Object> leftMap = null;
boolean leftMatch = false;
Map<String, Object> rightMap = null;
boolean rightMatch = false;
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
// build a hash set of the right-side tag collection
Set<String> rightLabelsSet = rightOperand.stream()
.filter(item -> item.get(VALUE) != null)
.map(this::labelKey)
.collect(Collectors.toSet());
// iterate over the left side, O(1) match
for (Map<String, Object> leftItem : leftOperand) {
Object leftVal = leftItem.get(VALUE);
if (leftVal == null) {
continue;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
String labelKey = labelKey(leftItem);
if (rightLabelsSet.contains(labelKey)) {
results.add(new HashMap<>(leftItem));
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && rightMatch) {
rightMap.putAll(leftMap);
return new LinkedList<>(List.of(rightMap));
} else if (leftMap != null) {
leftMap.put(VALUE, null);
return new LinkedList<>(List.of(leftMap));
} else if (rightMap != null) {
rightMap.put(VALUE, null);
return new LinkedList<>(List.of(rightMap));
}
return new LinkedList<>();
return results;
}
@Override
@@ -123,98 +188,51 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
Map<String, Object> leftMap = null;
boolean leftMatch = false;
Map<String, Object> rightMap = null;
boolean rightMatch = false;
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
// build a hashMap of the left-hand label collection
Map<String, Map<String, Object>> leftLabelMap = leftOperand.stream()
.filter(item -> item.get(VALUE) != null)
.collect(Collectors.toMap(this::labelKey, HashMap::new, (k1, k2) -> k1));
// first add all the non-empty items on the left side
List<Map<String, Object>> results = new ArrayList<>(leftLabelMap.values());
// add the term that has a value on the right side and not on the left side
for (Map<String, Object> rightItem : rightOperand) {
Object rightVal = rightItem.get(VALUE);
if (rightVal == null) {
continue;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
String key = labelKey(rightItem);
if (!leftLabelMap.containsKey(key)) {
results.add(new HashMap<>(rightItem));
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && rightMatch) {
rightMap.putAll(leftMap);
return new LinkedList<>(List.of(rightMap));
} else if (leftMatch) {
return new LinkedList<>(List.of(leftMap));
} else if (rightMatch) {
return new LinkedList<>(List.of(rightMap));
} else {
if (leftMap != null && rightMap != null) {
rightMap.putAll(leftMap);
return new LinkedList<>(List.of(rightMap));
} else if (leftMap != null) {
return new LinkedList<>(List.of(leftMap));
} else if (rightMap != null) {
return new LinkedList<>(List.of(rightMap));
}
}
return new LinkedList<>();
return results;
}
@Override
public List<Map<String, Object>> visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
Map<String, Object> leftMap = null;
boolean leftMatch = false;
Map<String, Object> rightMap = null;
boolean rightMatch = false;
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && !rightMatch) {
return new LinkedList<>(List.of(leftMap));
} else {
if (leftMap != null) {
leftMap.put(VALUE, null);
return new LinkedList<>(List.of(leftMap));
} else {
if (rightMap != null) {
rightMap.put(VALUE, null);
return new LinkedList<>(List.of(rightMap));
}
}
}
return new LinkedList<>();
}
List<Map<String, Object>> results = new ArrayList<>();
@Override
public List<Map<String, Object>> visitQueryExpr(AlertExpressionParser.QueryExprContext ctx) {
String query = ctx.identifier().getText();
return executor.execute(query);
// build a hash set of the right-side tag collection
Set<String> rightLabelSet = rightOperand.stream()
.filter(item -> item.get(VALUE) != null)
.map(this::labelKey)
.collect(Collectors.toSet());
// iterate over the left side, O(1) match
for (Map<String, Object> leftItem : leftOperand) {
Object leftVal = leftItem.get(VALUE);
if (leftVal == null) {
continue;
}
if (!rightLabelSet.contains(labelKey(leftItem))) {
results.add(new HashMap<>(leftItem));
}
}
return results;
}
@Override
@@ -222,84 +240,83 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
double value = Double.parseDouble(ctx.number().getText());
List<Map<String, Object>> numAsList = new ArrayList<>();
Map<String, Object> valueMap = new HashMap<>();
valueMap.put(THRESHOLD, value);
valueMap.put(SCALAR, value);
numAsList.add(valueMap);
return numAsList;
}
private Object evaluateCondition(Object value, String operator, Double threshold) {
// value may be a list of values, or a single value
switch (operator) {
case ">":
// if value is list, return the max value
if (value instanceof List<?> values) {
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).max(Double::compareTo).orElse(null);
if (doubleValue != null) {
return doubleValue > threshold ? doubleValue : null;
} else {
return null;
}
} else {
return Double.parseDouble(value.toString()) > threshold ? value : null;
}
case ">=":
if (value instanceof List<?> values) {
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).max(Double::compareTo).orElse(null);
if (doubleValue != null) {
return doubleValue >= threshold ? doubleValue : null;
} else {
return null;
}
} else {
return Double.parseDouble(value.toString()) >= threshold ? value : null;
}
case "<":
if (value instanceof List<?> values) {
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).min(Double::compareTo).orElse(null);
if (doubleValue != null) {
return doubleValue < threshold ? doubleValue : null;
} else {
return null;
}
} else {
return Double.parseDouble(value.toString()) < threshold ? value : null;
}
case "<=":
if (value instanceof List<?> values) {
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).min(Double::compareTo).orElse(null);
if (doubleValue != null) {
return doubleValue <= threshold ? doubleValue : null;
} else {
return null;
}
} else {
return Double.parseDouble(value.toString()) <= threshold ? value : null;
}
case "==":
if (value instanceof List<?> values) {
for (Object v : values) {
if (v.equals(threshold)) {
return v;
}
}
return null;
} else {
return value.equals(threshold) ? value : null;
}
case "!=":
if (value instanceof List<?> values) {
for (Object v : values) {
if (v.equals(threshold)) {
return null;
}
}
return value;
} else {
return value.equals(threshold) ? null : value;
}
default:
// unsupported operator todo add more operator
return null;
@Override
public List<Map<String, Object>> visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
String rawPromql = tokens.getText(ctx.promql());
return executor.execute(rawPromql);
}
@Override
public List<Map<String, Object>> visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
String rawSql = tokens.getText(ctx.selectSql());
return executor.execute(rawSql);
}
@Override
public List<Map<String, Object>> visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
return callSqlOrPromql(tokens.getText(ctx.string()));
}
@Override
public List<Map<String, Object>> visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
return callSqlOrPromql(tokens.getText(ctx.string()));
}
private List<Map<String, Object>> callSqlOrPromql(String text) {
String script = text.substring(1, text.length() - 1);
return executor.execute(script);
}
/**
* Generate tag key (excluding `__name__` and `__value__` and `__timestamp__`)
*/
private String labelKey(Map<String, Object> labelsMap) {
if (null == labelsMap || labelsMap.isEmpty()) {
return "-";
}
String key = labelsMap.entrySet().stream()
.filter(e -> !e.getKey().equals(VALUE) && !e.getKey().equals(NAME) && !e.getKey().equals(TIMESTAMP))
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + (e.getValue() == null ? "" : e.getValue()))
.collect(Collectors.joining(","));
return key.isEmpty() ? "-" : key;
}
private boolean isScalar(List<Map<String, Object>> context) {
return CollectionUtils.isNotEmpty(context)
&& context.size() == 1
&& context.get(0).containsKey(SCALAR)
&& null != context.get(0).get(SCALAR);
}
private double parseStrToDouble(String text) {
try {
return Double.parseDouble(text);
} catch (NumberFormatException e) {
throw new ExpressionVisitorException("number format exception", e);
}
}
}
private boolean isValidValue(Object val) {
return val == null || val instanceof List<?>;
}
private Boolean compareOp(Object leftVal, int opType, Object rightVal) {
double left = parseStrToDouble(leftVal.toString());
double right = parseStrToDouble(rightVal.toString());
return switch (opType) {
case AlertExpressionParser.GT -> left > right;
case AlertExpressionParser.GE -> left >= right;
case AlertExpressionParser.LT -> left < right;
case AlertExpressionParser.LE -> left <= right;
case AlertExpressionParser.EQ -> left == right;
case AlertExpressionParser.NE -> left != right;
default -> false;
};
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -26,12 +26,13 @@ import org.antlr.v4.runtime.tree.ParseTreeVisitor;
* by {@link AlertExpressionParser}.
*
* @param <T> The return type of the visit operation. Use {@link Void} for
* operations with no return type.
* operations with no return type.
*/
public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#expression}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -39,23 +40,17 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code AndExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitAndExpr(AlertExpressionParser.AndExprContext ctx);
/**
* Visit a parse tree produced by the {@code QueryExpr}
* labeled alternative in AlertExpressionParser#expr
* @param ctx the parse tree
* @return the visitor result
*/
T visitQueryExpr(AlertExpressionParser.QueryExprContext ctx);
/**
* Visit a parse tree produced by the {@code ComparisonExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -63,15 +58,35 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code UnlessExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx);
/**
* Visit a parse tree produced by the {@code SqlExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx);
/**
* Visit a parse tree produced by the {@code SqlCallExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx);
/**
* Visit a parse tree produced by the {@code LiteralExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -79,31 +94,261 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code ParenExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitParenExpr(AlertExpressionParser.ParenExprContext ctx);
/**
* Visit a parse tree produced by the {@code PromqlCallExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx);
/**
* Visit a parse tree produced by the {@code PromqlExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx);
/**
* Visit a parse tree produced by the {@code OrExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrExpr(AlertExpressionParser.OrExprContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#identifier}.
* Visit a parse tree produced by {@link AlertExpressionParser#functionCall}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitIdentifier(AlertExpressionParser.IdentifierContext ctx);
T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#parameterList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitParameterList(AlertExpressionParser.ParameterListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#parameter}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitParameter(AlertExpressionParser.ParameterContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#number}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitNumber(AlertExpressionParser.NumberContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#string}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitString(AlertExpressionParser.StringContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#duration}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitDuration(AlertExpressionParser.DurationContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#functionName}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectSql}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectFieldList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectField}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectField(AlertExpressionParser.SelectFieldContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#groupByList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitGroupByList(AlertExpressionParser.GroupByListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#orderByList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrderByList(AlertExpressionParser.OrderByListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#orderByField}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#limitClause}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#relList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitRelList(AlertExpressionParser.RelListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#relation}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitRelation(AlertExpressionParser.RelationContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#conditionList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitConditionList(AlertExpressionParser.ConditionListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#compOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitCompOp(AlertExpressionParser.CompOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#condition}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitCondition(AlertExpressionParser.ConditionContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#conditionUnit}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#promql}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromql(AlertExpressionParser.PromqlContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#metricSelector}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherItem}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelList(AlertExpressionParser.LabelListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#instantVectorOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#aggregationOperator}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx);
}
@@ -17,8 +17,6 @@
package org.apache.hertzbeat.alert.notice.impl;
import java.util.Arrays;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -29,11 +27,14 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* Send alert information through FeiShu
*/
@@ -47,19 +48,18 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
*/
private static final String[] TITLE_COLOR = {"red", "yellow", "orange"};
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
try {
String notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert));
// todo priority custom the color
// todo priority custom the color
String cardMessage = createLarkMessage(receiver.getUserId(), notificationContent, (byte) 1);
String webHookUrl = alerterProperties.getFlyBookWebhookUrl() + receiver.getAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> flyEntity = new HttpEntity<>(cardMessage, headers);
ResponseEntity<CommonRobotNotifyResp> entity = restTemplate.postForEntity(webHookUrl,
flyEntity, CommonRobotNotifyResp.class);
flyEntity, CommonRobotNotifyResp.class);
if (entity.getStatusCode() == HttpStatus.OK) {
assert entity.getBody() != null;
if (entity.getBody().getCode() == null || entity.getBody().getCode() == 0) {
@@ -87,115 +87,120 @@ final class FlyBookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
*/
private String createLarkMessage(String userId, String notificationContent, byte priority) {
String larkCardMessage = """
{
"msg_type": "interactive",
"card": {
"config": {
"update_multi": true
},
"i18n_elements": {
"zh_cn": [
{
"tag": "column_set",
"flex_mode": "none",
"horizontal_spacing": "default",
"background_style": "default",
"columns": [
{
"tag": "column",
"elements": [
{
"tag": "div",
"text": {
"tag": "plain_text",
"content": "",
"text_size": "normal",
"text_align": "left",
"text_color": "default"
}
}
],
"width": "weighted",
"weight": 1
}
]
},
{
"tag": "column_set",
"flex_mode": "none",
"horizontal_spacing": "default",
"background_style": "default",
"columns": [
{
"tag": "column",
"elements": [
{
"tag": "div",
"text": {
"tag": "plain_text",
"content": %s,
"text_size": "normal",
"text_align": "left",
"text_color": "default"
}
}
],
"width": "weighted",
"weight": 1
}
]
},
%s
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "登入控制台"
},
"type": "default",
"complex_interaction": true,
"width": "default",
"size": "medium",
"multi_url": {
"url": "%s"
}
}
]
}
]
},
"i18n_header": {
"zh_cn": {
"title": {
"tag": "plain_text",
"content": "HertzBeat 告警"
},
"template": "%s"
}
}
{
"msg_type": "interactive",
"card": {
"schema": "2.0",
"config": {
"update_multi": true,
"locales": [
"en_us",
"zh_cn"
],
"style": {
"text_size": {
"normal_v2": {
"default": "normal",
"pc": "normal",
"mobile": "heading"
}
}
}
},
"body": {
"direction": "vertical",
"padding": "12px 12px 12px 12px",
"elements": [
{
"tag": "markdown",
"content": "%s",
"i18n_content": {
"en_us": ""
},
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
},
{
"tag": "hr",
"margin": "0px 0px 0px 0px"
},
{
"tag": "column_set",
"horizontal_align": "left",
"columns": [
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "登入控制台",
"i18n_content": {
"en_us": "Login In"
}
},
"type": "default",
"width": "default",
"size": "medium",
"behaviors": [
{
"type": "open_url",
"default_url": "%s",
"pc_url": "",
"ios_url": "",
"android_url": ""
}
]
}
],
"direction": "horizontal",
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
}
]
},
"header": {
"title": {
"tag": "plain_text",
"content": "HertzBeat 告警",
"i18n_content": {
"en_us": "HertzBeat Alarm"
}
},
"subtitle": {
"tag": "plain_text",
"content": ""
},
"template": "%s",
"padding": "12px 12px 12px 12px"
}
}
}
}
""";
""";
String atUserElement = "";
if (StringUtils.isNotBlank(userId)) {
String atUserId = Arrays.stream(userId.split(","))
.map(id -> "<at id=" + id + "></at>")
.collect(Collectors.joining(" "));
atUserElement = String.format("""
{
"tag": "div",
"text": {
"content": "%s",
"tag": "lark_md"
}
},
""", atUserId);
atUserElement = "\\n" + Arrays.stream(userId.split(","))
.map(id -> "<at id=" + id + "></at>")
.collect(Collectors.joining(" "));
}
return String.format(larkCardMessage, notificationContent, atUserElement, alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]);
if (notificationContent.startsWith("\"") && notificationContent.endsWith("\"")) {
notificationContent = StringUtils.removeStart(notificationContent, "\"");
notificationContent = StringUtils.removeEnd(notificationContent, "\"");
}
return String.format(larkCardMessage,
notificationContent.replace("\"", "\\\"") + atUserElement,
alerterProperties.getConsoleUrl(), TITLE_COLOR[priority]);
}
@Override
@@ -60,7 +60,7 @@ public class ServerChanAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), sanitizedToken);
// Validate the constructed URL against a whitelist
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com");
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com", "https://sctapi.ftqq.com");
boolean isValidUrl = allowedBaseUrls.stream().anyMatch(webHookUrl::startsWith);
if (!isValidUrl) {
throw new AlertNoticeException("Invalid webhook URL: " + webHookUrl);
@@ -18,12 +18,15 @@
package org.apache.hertzbeat.alert.service;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Set;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.springframework.data.domain.Page;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Alarm define manager service
*/
@@ -107,5 +110,11 @@ public interface AlertDefineService {
* @return Real-time alarm definition list
*/
List<AlertDefine> getRealTimeAlertDefines();
/**
* Get define preview
* @return Data queried based on expressions
* @throws AlertExpressionException expression error
*/
List<Map<String, Object>> getDefinePreview(String datasource, String type, String expr);
}
@@ -28,6 +28,9 @@ import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO;
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.util.LogUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.util.CollectionUtils;
@@ -41,12 +44,15 @@ public abstract class AlertDefineAbstractImExportServiceImpl implements AlertDef
@Lazy
private AlertDefineService alertDefineService;
private static final Logger logger = LoggerFactory.getLogger(AlertDefineAbstractImExportServiceImpl.class);
@Override
public void importConfig(InputStream is) {
var formList = parseImport(is)
.stream()
.map(this::convert)
.toList();
LogUtil.info(logger, "Importing alert defines from {0}", formList);
if (!CollectionUtils.isEmpty(formList)) {
formList.forEach(alertDefine -> {
alertDefineService.validate(alertDefine, false);
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.alert.service.impl;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -29,7 +28,9 @@ import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.cache.CacheFactory;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.ExportFileConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
@@ -72,18 +73,21 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Autowired
private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
private final DataSourceService dataSourceService;
private final Map<String, AlertDefineImExportService> alertDefineImExportServiceMap = new HashMap<>();
private static final String CONTENT_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + "charset=" + StandardCharsets.UTF_8;
public AlertDefineServiceImpl(List<AlertDefineImExportService> alertDefineImExportServiceList) {
public AlertDefineServiceImpl(List<AlertDefineImExportService> alertDefineImExportServiceList, DataSourceService dataSourceService) {
alertDefineImExportServiceList.forEach(it -> alertDefineImExportServiceMap.put(it.type(), it));
this.dataSourceService = dataSourceService;
}
@Override
public void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException {
if (StringUtils.hasText(alertDefine.getExpr())) {
if (ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
if (CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
try {
JexlExpressionRunner.compile(alertDefine.getExpr());
} catch (Exception e) {
@@ -213,9 +217,23 @@ public class AlertDefineServiceImpl implements AlertDefineService {
public List<AlertDefine> getRealTimeAlertDefines() {
List<AlertDefine> alertDefines = CacheFactory.getAlertDefineCache();
if (alertDefines == null) {
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_REALTIME);
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME);
CacheFactory.setAlertDefineCache(alertDefines);
}
return alertDefines;
}
@Override
public List<Map<String, Object>> getDefinePreview(String datasource, String type, String expr) {
if (!StringUtils.hasText(expr) || !StringUtils.hasText(datasource) || !StringUtils.hasText(type)) {
return Collections.emptyList();
}
switch (type) {
case CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC:
return dataSourceService.calculate(datasource, expr);
default:
log.error("Get define preview unsupported type: {}", type);
return Collections.emptyList();
}
}
}
@@ -0,0 +1,185 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.stereotype.Service;
import java.text.MessageFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* Alibaba Cloud 'Simple Log Service(SLS)' external alarm service impl
*/
@Slf4j
@Service
public class AlibabaCloudSlsExternAlertService implements ExternAlertService {
private final AlarmCommonReduce alarmCommonReduce;
public AlibabaCloudSlsExternAlertService(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
}
@Override
public void addExternAlert(String content) {
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
if (externAlert == null) {
log.warn("Failure to parse external alert content. content: {}", content);
return;
}
SingleAlert singleAlert = new AlibabaCloudSlsConverter().convert(externAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
@Override
public String supportSource() {
return "alibabacloud-sls";
}
/**
*
*/
public static class AlibabaCloudSlsConverter {
/**
* convert
*
* @param externAlert alert content entity
* @return Single alert
*/
public SingleAlert convert(AlibabaCloudSlsExternAlert externAlert) {
return SingleAlert.builder()
.triggerTimes(1)
.status(externAlert.getStatus())
.startAt(Instant.ofEpochSecond(externAlert.getFireTime()).toEpochMilli())
.activeAt(Instant.ofEpochSecond(externAlert.getAlertTime()).toEpochMilli())
.endAt(convertResolveTime(externAlert.getStatus(), externAlert.getResolveTime()))
.labels(buildLabels(externAlert))
.annotations(buildAnnotations(externAlert))
.content(formatContent(externAlert))
.build();
}
/**
* todo i18n
*
* @param externAlert alert content entity
* @return content
*/
private String formatContent(AlibabaCloudSlsExternAlert externAlert) {
// convet severity
Optional<AlibabaCloudSlsExternAlert.Severity> severity = AlibabaCloudSlsExternAlert.Severity.convert(externAlert.getSeverity());
// If the alarm state is resolved, the value is the specific recovery time.
Long resolveTimeMilli = convertResolveTime(externAlert.getStatus(), externAlert.getResolveTime());
return MessageFormat.format(
"AlibabaCloud-sls alert , {0} - [{1}], level: [{2}], desc: {3}, fire_time:{4}, resolve_time:{5}",
externAlert.getAnnotation("title"),
externAlert.getStatus(),
severity.isPresent() ? severity.get().getAlias() : "N/A",
externAlert.getAnnotation("desc"),
timeSecondToDate(Instant.ofEpochSecond(externAlert.getFireTime()).toEpochMilli()),
null != resolveTimeMilli ? timeSecondToDate(resolveTimeMilli) : "N/A"
);
}
/**
* Converts a timestamp (milliseconds) to a formatted date-time string.
*
* @param timestampMillis timestamp in milliseconds
* @return formatted date-time string in the pattern: yyyy-MM-dd HH:mm:ss
*/
private String timeSecondToDate(long timestampMillis) {
LocalDateTime dateTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(timestampMillis),
ZoneId.systemDefault()
);
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* Build basic annotations and fill annotations for alibaba cloud sls.
*
* @param externAlert alert content entity
* @return annotations
*/
private Map<String, String> buildAnnotations(AlibabaCloudSlsExternAlert externAlert) {
Map<String, String> annotations = new HashMap<>(8);
Optional<AlibabaCloudSlsExternAlert.Severity> severity = AlibabaCloudSlsExternAlert.Severity.convert(externAlert.getSeverity());
severity.ifPresent(value -> annotations.put("severity", value.getAlias()));
// Notification templates for sls need to be configured.
if (StringUtils.isNotBlank(externAlert.getSigninUrl()) && IpDomainUtil.isHasSchema(externAlert.getSigninUrl())) {
annotations.put("signinUrl", "<a target=\"_blank\" href=\"" + externAlert.getSigninUrl() + "\">View Details</a>");
}
// Filling the annotations with the alibaba cloud sls.
if (null != externAlert.getAnnotations() && !externAlert.getAnnotations().isEmpty()) {
annotations.putAll(externAlert.getAnnotations());
}
return annotations;
}
/**
* Build basic labels and fill labels for alibaba cloud sls.
*
* @param externAlert alert content entity
* @return labels
*/
private Map<String, String> buildLabels(AlibabaCloudSlsExternAlert externAlert) {
Map<String, String> labels = new HashMap<>(8);
labels.put("__source__", "alibabacloud-sls");
labels.put("alertname", externAlert.getAlertName());
labels.put("region", externAlert.getRegion());
// The project name is globally unique.
labels.put("project", externAlert.getProject());
// Filling the labels with the alibaba cloud sls.
if (null != externAlert.getLabels() && !externAlert.getLabels().isEmpty()){
labels.putAll(externAlert.getLabels());
}
return labels;
}
/**
* If the alarm status is firing, the value is 0.
* If the alarm state is resolved, the value is the specific recovery time.
*
* @param status alert status
* @param resolveTimeSecond recovery time
* @return milliseconds
*/
private Long convertResolveTime(String status, int resolveTimeSecond) {
return CommonConstants.ALERT_STATUS_RESOLVED.equals(status) ? Instant.ofEpochSecond(resolveTimeSecond).toEpochMilli() : null;
}
}
}
@@ -27,11 +27,14 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.common.util.LogUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
@@ -62,6 +65,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
private final String accessKeySecret;
private final String signName;
private final String templateCode;
private static final Logger logger = LoggerFactory.getLogger(AlibabaSmsClientImpl.class);
public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
if (config != null) {
@@ -173,7 +177,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
log.info("Successfully sent SMS to phone: {}", phoneNumber);
}
} catch (Exception e) {
log.warn("Failed to send SMS: {}", e.getMessage());
LogUtil.warn(logger, "Failed to send SMS: {0}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
@@ -192,6 +196,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
// Step 4: Build authorization header
return ALGORITHM + " Credential=" + accessKeyId + ",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + "x-acs-signature-nonce;x-acs-version,Signature=" + signature;
} catch (Exception e) {
LogUtil.warn(logger, "Failed to calculate authorization {0}", e.getMessage());
throw new RuntimeException("Failed to calculate authorization", e);
}
}
@@ -24,11 +24,14 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.hertzbeat.alert.expr.AlertExpressionEvalVisitor;
import org.apache.hertzbeat.alert.expr.AlertExpressionLexer;
import org.apache.hertzbeat.alert.expr.AlertExpressionParser;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -36,6 +39,7 @@ import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.TimeUnit;
/**
@@ -45,6 +49,8 @@ import java.util.concurrent.TimeUnit;
@Slf4j
public class DataSourceServiceImpl implements DataSourceService {
protected ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
@Setter
@Autowired(required = false)
private List<QueryExecutor> executors;
@@ -69,7 +75,7 @@ public class DataSourceServiceImpl implements DataSourceService {
throw new IllegalArgumentException("Empty expression");
}
if (executors == null || executors.isEmpty()) {
throw new IllegalArgumentException("No query executor found");
throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found"));
}
QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null);
@@ -80,6 +86,9 @@ public class DataSourceServiceImpl implements DataSourceService {
expr = expr.replaceAll("\\s+", " ");
try {
return evaluate(expr, executor);
} catch (AlertExpressionException ae) {
log.error("Calculate query parse error {}: {}", datasource, ae.getMessage());
throw ae;
} catch (Exception e) {
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
throw new RuntimeException("Query execution failed", e);
@@ -87,14 +96,14 @@ public class DataSourceServiceImpl implements DataSourceService {
}
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
ParseTree tree = expressionCache.get(expr, e -> {
CommonTokenStream tokens = tokenStreamCache.get(e, this::createTokenStream);
AlertExpressionParser parser = new AlertExpressionParser(tokens);
return parser.expr();
});
AlertExpressionEvalVisitor visitor = new AlertExpressionEvalVisitor(executor);
CommonTokenStream tokens = tokenStreamCache.get(expr, this::createTokenStream);
AlertExpressionParser parser = new AlertExpressionParser(tokens);
ParseTree tree = expressionCache.get(expr, e -> parser.expr());
if (null != tokens && tokens.LA(1) != Token.EOF) {
throw new AlertExpressionException(bundle.getString("alerter.calculate.parse.error"));
}
AlertExpressionEvalVisitor visitor = new AlertExpressionEvalVisitor(executor, tokens);
return visitor.visit(tree);
}
private CommonTokenStream createTokenStream(String expr) {
@@ -0,0 +1,343 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.alert.util.DateUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.exception.IgnoreException;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.NOTIFICATION;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.SUBSCRIPTION;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.UNSUBSCRIBE;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE_ID;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBJECT;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBSCRIBE_URL;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TIMESTAMP;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TOPIC_URN;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TYPE;
/**
* Huawei cloud external alarm service impl
*/
@Slf4j
@Service
public class HuaweiCloudExternAlertService implements ExternAlertService {
private static final String CERTIFICATE_TYPE = "X.509";
private static final String CHARSET_UTF8 = StandardCharsets.UTF_8.name();
private final AlarmCommonReduce alarmCommonReduce;
public HuaweiCloudExternAlertService(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
}
@Override
public void addExternAlert(String content) {
HuaweiCloudExternAlert externAlert = JsonUtil.fromJson(content, HuaweiCloudExternAlert.class);
if (externAlert == null || StringUtils.isBlank(externAlert.getMessage())) {
log.warn("Failure to parse external alert content. content: {}", content);
return;
}
if (!isMessageValid(externAlert)) {
log.warn("Huawei cloud alert verify failed. content: {}", content);
return;
}
process(externAlert);
}
/**
* Process according to different types
*
* @param externAlert alert content entity
*/
private void process(HuaweiCloudExternAlert externAlert) {
if (NOTIFICATION.getType().equals(externAlert.getType())) {
Optional.ofNullable(buildSendAlert(externAlert)).ifPresent(alarmCommonReduce::reduceAndSendAlarm);
} else if (SUBSCRIPTION.getType().equals(externAlert.getType())) {
autoSubscribeForUrl(externAlert.getSubscribeUrl());
} else if (UNSUBSCRIBE.getType().equals(externAlert.getType())) {
log.warn("Huawei cloud notifies the recipient of the notification to cancel the subscription.");
}
}
/**
* Build single alert.
*
* @param externAlert alert content entity
* @return single alert
*/
private SingleAlert buildSendAlert(HuaweiCloudExternAlert externAlert) {
HuaweiCloudExternAlert.AlertMessage message = JsonUtil.fromJson(externAlert.getMessage(), HuaweiCloudExternAlert.AlertMessage.class);
if (null == message || null == message.getData()) {
log.warn("Failure to parse external alert message. message: {}", externAlert.getMessage());
return null;
}
// Note: Empty and false are both recovery notifications.
// Note: There are no recovery notifications for event types
boolean isAlarm = null != message.getData().getAlarm() && message.getData().getAlarm();
Long alarmTime = DateUtil.getZonedTimeStampFromFormat(message.getData().getAlarmTime(), "yyyy/MM/dd HH:mm:ss 'GMT'XXX");
return SingleAlert.builder()
.triggerTimes(1)
.status(isAlarm ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED)
.startAt(alarmTime)
.activeAt(Instant.now().toEpochMilli())
.endAt(isAlarm ? null : alarmTime)
.labels(buildLabels(message.getData()))
.annotations(buildAnnotations(message.getData()))
.content(formatContent(externAlert.getSubject(), message.getData()))
.build();
}
/**
* Build basic annotations and fill annotations for huawei cloud.
*
* @param alertData alert content entity
* @return annotations
*/
private Map<String, String> buildAnnotations(HuaweiCloudExternAlert.AlertData alertData) {
Map<String, String> annotations = new HashMap<>(8);
if (null != alertData) {
putIfNotBlank(annotations, "region", alertData.getRegion());
putIfNotBlank(annotations, "dimensionName", alertData.getDimensionName());
putIfNotBlank(annotations, "resourceName", alertData.getResourceName());
putIfNotBlank(annotations, "alarmRecordId", alertData.getAlarmRecordId());
}
return annotations;
}
/**
* Build basic labels and fill labels for huawei cloud.
*
* @param alertData alert content entity
* @return labels
*/
private Map<String, String> buildLabels(HuaweiCloudExternAlert.AlertData alertData) {
Map<String, String> labels = new HashMap<>(8);
labels.put("__source__", "huaweicloud-ces");
if (null != alertData) {
putIfNotBlank(labels, "namespace", alertData.getNamespace());
putIfNotBlank(labels, "metricName", alertData.getMetricName());
putIfNotBlank(labels, "resourceId", alertData.getResourceId());
putIfNotBlank(labels, "level", alertData.getAlarmLevel());
}
return labels;
}
/**
* todo i18n
*
* @param subject alert subject
* @param alertData alert content entity
* @return content
*/
private String formatContent(String subject, HuaweiCloudExternAlert.AlertData alertData) {
if (null == alertData) {
return subject;
}
return MessageFormat.format(
"{0} threshold:{1}{2}, current{3}",
subject,
alertData.getComparisonOperator(),
alertData.getValue(),
alertData.getCurrentData()
);
}
/**
* Automatic subscription url.
*
* @param subscribeUrl subscribeUrl
*/
public void autoSubscribeForUrl(String subscribeUrl) {
if (StringUtils.isBlank(subscribeUrl)) {
return;
}
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(subscribeUrl);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
if (statusCode != 200) {
log.error("Subscribe url request failed with status code: " + statusCode + ", response: " + responseBody);
return;
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
if (jsonResponse == null) {
throw new IgnoreException("Subscribe url failed with status code: " + statusCode + ", response: " + responseBody);
}
JsonNode surnNode = jsonResponse.get("subscription_urn");
if (surnNode == null || StringUtils.isBlank(surnNode.asText())) {
throw new IgnoreException("Subscribe url failed with status code: " + statusCode + ", response: " + responseBody);
}
log.info("Successfully subscribed to Huawei Cloud(SMN) url.");
}
} catch (Exception e) {
log.error("Failed to subscribe url request: {}", e.getMessage());
}
}
/**
* Verifying the signature of huawei cloud alert message.
*
* @param externAlert alert content entity
* @return verification result
* @throws SecurityException thrown when validation fails
*/
private boolean isMessageValid(HuaweiCloudExternAlert externAlert) {
try {
String signMessage = buildSignMessage(externAlert);
if (StringUtils.isBlank(signMessage)) {
throw new SecurityException("Verify sign message is null");
}
X509Certificate cert = getCertificate(externAlert.getSigningCertUrl());
return verifySignature(signMessage, cert, externAlert.getSignature());
} catch (Exception e) {
log.error("Failed to verify message signature: ", e);
return false;
}
}
/**
* Build sign message.
*
* @param externAlert alert content entity
* @return sign message
*/
private String buildSignMessage(HuaweiCloudExternAlert externAlert) {
if (NOTIFICATION.getType().equals(externAlert.getType())) {
return buildNotificationMessage(externAlert);
} else if (SUBSCRIPTION.getType().equals(externAlert.getType()) || UNSUBSCRIBE.getType().equals(externAlert.getType())){
return buildSubscriptionMessage(externAlert);
}
return null;
}
/**
* Building sign message of 'Notification' type
*
* @param externAlert alert content entity
* @return sign message
*/
private String buildNotificationMessage(HuaweiCloudExternAlert externAlert) {
StringBuilder message = new StringBuilder();
appendField(message, FIELD_MESSAGE, externAlert.getMessage());
appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId());
if (StringUtils.isNotBlank(externAlert.getSubject())) {
appendField(message, FIELD_SUBJECT, externAlert.getSubject());
}
appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp());
appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn());
appendField(message, FIELD_TYPE, externAlert.getType());
return message.toString();
}
/**
* Building sign message of 'SubscriptionConfirmation' or 'UnsubscribeConfirmation' type
*
* @param externAlert alert content entity
* @return sign message
*/
private String buildSubscriptionMessage(HuaweiCloudExternAlert externAlert) {
StringBuilder message = new StringBuilder();
appendField(message, FIELD_MESSAGE, externAlert.getMessage());
appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId());
appendField(message, FIELD_SUBSCRIBE_URL, externAlert.getSubscribeUrl());
appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp());
appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn());
appendField(message, FIELD_TYPE, externAlert.getType());
return message.toString();
}
/**
* Obtain certificate
*
* @param signCertUrl sign cert url
* @return X509 certificate
* @throws Exception Thrown when certificate acquisition fails
*/
private X509Certificate getCertificate(String signCertUrl) throws Exception {
URL url = new URL(signCertUrl);
try (InputStream in = url.openStream()) {
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
return (X509Certificate) cf.generateCertificate(in);
}
}
/**
* Verify signature
*
* @param message sign message
* @param cert cert
* @param signature signature
* @return verification result
* @throws Exception thrown when an error occurs in the validation process
*/
private boolean verifySignature(String message, X509Certificate cert, String signature) throws Exception {
Signature sig = Signature.getInstance(cert.getSigAlgName());
sig.initVerify(cert.getPublicKey());
sig.update(message.getBytes(CHARSET_UTF8));
return sig.verify(Base64.getDecoder().decode(signature));
}
private void putIfNotBlank(Map<String, String> map, String key, String value) {
if (StringUtils.isNotBlank(value)){
map.put(key, value);
}
}
private void appendField(StringBuilder builder, String fieldName, String value) {
builder.append(fieldName).append("\n").append(value).append("\n");
}
@Override
public String supportSource() {
return "huaweicloud-ces";
}
}
@@ -0,0 +1,244 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.hertzbeat.alert.dto.VolcEngineExternEventAlert;
import org.apache.hertzbeat.alert.dto.VolcEngineExternMetricAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Volcengine alarm entity class
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VolcEngineExternAlertService implements ExternAlertService {
private final AlarmCommonReduce alarmCommonReduce;
private static final Map<String, Integer> severityOrder = Map.of(
CommonConstants.ALERT_SEVERITY_CRITICAL, 1,
CommonConstants.ALERT_SEVERITY_WARNING, 2,
CommonConstants.ALERT_SEVERITY_INFO, 3
);
@Override
@SneakyThrows
public void addExternAlert(String content) {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(content);
String type = root.get("Type").asText();
if (VolcEngineExternMetricAlert.ALERT_TYPE_EVENT.equals(type)) {
VolcEngineExternEventAlert eventAlert = JsonUtil.fromJson(content, VolcEngineExternEventAlert.class);
if (eventAlert == null) {
log.warn("parse extern event alert content failed! content: {}", content);
return;
}
SingleAlert singleAlert = new VolcEngineAlertConverter().convertEventToSingleAlert(eventAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
} else {
// deal with metric alert
VolcEngineExternMetricAlert report = JsonUtil.fromJson(content, VolcEngineExternMetricAlert.class);
if (report == null) {
log.warn("parse extern metrics alert content failed! content: {}", content);
return;
}
for (SingleAlert singleAlert : new VolcEngineAlertConverter().convertMetricAlertToSingeAlert(report)) {
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
}
}
@Override
public String supportSource() {
return "volcengine";
}
/**
* Converter: VolcEngine alert to SingleAlert
*/
public static class VolcEngineAlertConverter {
/**
* Convert VolcEngine metric alert to SingleAlert List
*/
public List<SingleAlert> convertMetricAlertToSingeAlert(VolcEngineExternMetricAlert alert) {
String status = convertStatus(alert);
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRIC)) {
return convertMetricAlert(alert, status, alert.getResources());
}
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRICS_NODATA)) {
return convertMetricAlert(alert, status, alert.getNoDataResources());
}
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_NO_DATA_RECOVERED)) {
return convertMetricAlert(alert, status, alert.getNoDataRecoveredResources());
}
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRIC_RECOVERED)) {
return convertMetricAlert(alert, status, alert.getRecoveredResources());
}
return List.of();
}
public SingleAlert convertEventToSingleAlert(VolcEngineExternEventAlert alert) {
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
.status(CommonConstants.ALERT_STATUS_FIRING)
.startAt(alert.getHappenedAt() * 1000)
.activeAt(alert.getHappenedAt() * 1000)
.labels(buildEventLabels(alert))
.content(alert.getDescriptionCn())
.annotations(new HashMap<>());
return builder.build();
}
/**
* build labels for volcengine event alert
*/
private Map<String, String> buildEventLabels(VolcEngineExternEventAlert event) {
Map<String, String> labels = new HashMap<>();
labels.put("severity", convertEventSeverity(event));
labels.put("__source__", "volcengine");
labels.put("resource_name", event.getDetails().getVolcResourceName());
labels.put("account_id", event.getAccountId());
labels.put("region", event.getRegion());
labels.put("event_type", event.getEventType());
labels.put("source", event.getSource());
return labels;
}
/**
* convert volcengine event alert status to heartbeat alert status
* use the most severe level in the rules
*
* @param alert volcengine event alert
* @return status
*/
private String convertEventSeverity(VolcEngineExternEventAlert alert) {
return alert.getRules().stream().map(VolcEngineExternEventAlert.EventRule::getLevel)
.min((o1, o2) -> severityOrder.get(convertCommonSeverity(o1)).compareTo(severityOrder.get(convertCommonSeverity(o2))))
.orElse(CommonConstants.ALERT_SEVERITY_INFO);
}
private String convertCommonSeverity(String level) {
return switch (level) {
case "critical" -> CommonConstants.ALERT_SEVERITY_CRITICAL;
case "warning" -> CommonConstants.ALERT_SEVERITY_WARNING;
default -> CommonConstants.ALERT_SEVERITY_INFO;
};
}
/**
* create SingleAlert for each resources in volcengine alert
*
* @param alert volcengine alert
* @param status status
* @param resources resources
* @return List of SingleAlert
*/
private List<SingleAlert> convertMetricAlert(VolcEngineExternMetricAlert alert, String status,
List<? extends VolcEngineExternMetricAlert.Resource> resources) {
if (CollectionUtils.isEmpty(resources)) {
return List.of();
}
List<SingleAlert> result = new ArrayList<>();
for (VolcEngineExternMetricAlert.Resource resource : resources) {
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
.status(status)
.startAt(resource.getFirstAlertTime() * 1000)
.endAt(resource.getLastAlertTime() * 1000)
.labels(buildLabels(alert, resource))
.activeAt(convertHappenAt(alert.getHappenedAt()))
.content(resource.getName() + alert.getRuleCondition())
.annotations(buildAnnotations(resource));
result.add(builder.build());
}
return result;
}
private Long convertHappenAt(String happenAt) {
String cleanedStr = happenAt.replace("UTC", "").replace("(", "").replace(")", "");
DateTimeFormatter altFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX");
OffsetDateTime odt = OffsetDateTime.parse(cleanedStr, altFormatter);
return odt.toEpochSecond() * 1000;
}
private Map<String, String> buildLabels(VolcEngineExternMetricAlert alert, VolcEngineExternMetricAlert.Resource resource) {
Map<String, String> labels = new HashMap<>();
labels.put("__source__", "volcengine");
labels.put("resource_name", resource.getName());
labels.put("resource_id", resource.getId());
labels.put("rule_id", alert.getRuleId());
return labels;
}
private Map<String, String> buildAnnotations(VolcEngineExternMetricAlert.Resource resource) {
Map<String, String> annotations = new HashMap<>();
if (resource instanceof VolcEngineExternMetricAlert.NoDataResource noDataResource) {
for (VolcEngineExternMetricAlert.Metric noDataMetric : noDataResource.getNoDataMetrics()) {
annotations.put(noDataMetric.getName(), "N/A");
}
}
for (VolcEngineExternMetricAlert.Metric metric : resource.getMetrics()) {
annotations.put(metric.getName(), metric.getCurrentValue() + metric.getUnit());
}
annotations.put("region", resource.getRegion());
List<VolcEngineExternMetricAlert.Dimension> dimensions = resource.getDimensions();
if (CollectionUtils.isNotEmpty(dimensions)) {
for (VolcEngineExternMetricAlert.Dimension dimension : dimensions) {
annotations.put(dimension.getNameCn(), dimension.getValue());
}
}
return annotations;
}
/**
* convert volcengine alert status to heartbeat alert status
*
* @param alert volcengine alert
* @return status
*/
private String convertStatus(VolcEngineExternMetricAlert alert) {
String type = alert.getType();
if (Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_METRIC)
|| Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_EVENT)
|| Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_METRICS_NODATA)) {
return CommonConstants.ALERT_STATUS_FIRING;
}
return CommonConstants.ALERT_STATUS_RESOLVED;
}
}
}
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.alert.util;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Optional;
@@ -80,4 +81,19 @@ public final class DateUtil {
return Optional.empty();
}
/**
* convert format data to timestamp
*/
public static Long getZonedTimeStampFromFormat(String dateStr, String format) {
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
// the parsed zoned date-time, not null
return ZonedDateTime.parse(dateStr, formatter).toInstant().toEpochMilli();
} catch (Exception e) {
log.error("Error parsing date '{}' with format '{}': {}",
dateStr, format, e.getMessage());
}
return null;
}
}
@@ -32,3 +32,5 @@ alerter.notify.console = Console Login
alerter.priority.0 = Emergency Alert
alerter.priority.1 = Critical Alert
alerter.priority.2 = Warning Alert
alerter.calculate.parse.error = Expression is not fully parsed, may have syntax errors or incomplete inputs
alerter.datasource.executor.not.found = No query executor found
@@ -32,3 +32,5 @@ alerter.notify.console = 登入控制台
alerter.priority.0 = 紧急告警
alerter.priority.1 = 严重告警
alerter.priority.2 = 警告告警
alerter.calculate.parse.error = 表达式未完全解析,可能存在语法错误或输入不完整
alerter.datasource.executor.not.found = 未找到查询执行器
@@ -32,3 +32,5 @@ alerter.notify.console = 控制台登錄
alerter.priority.0 = 緊急警報
alerter.priority.1 = 嚴重警報
alerter.priority.2 = 警告警報
alerter.calculate.parse.error = 表達式未完全解析,可能存在語法錯誤或輸入不完整
alerter.datasource.executor.not.found = 未找到查詢執行器
@@ -22,36 +22,268 @@ expression
;
expr
: '(' expr ')' # ParenExpr
| left=expr op=('>='|'<='|'>'|'<'|'=='|'!=') right=expr # ComparisonExpr
| left=expr 'and' right=expr # AndExpr
| left=expr 'unless' right=expr # UnlessExpr
| left=expr 'or' right=expr # OrExpr
| identifier # QueryExpr
| number # LiteralExpr
: LPAREN expr RPAREN # ParenExpr
| left=expr op=(GE|LE|GT|LT|EQ|NE) BOOL? right=expr # ComparisonExpr
| left=expr AND right=expr # AndExpr
| left=expr UNLESS right=expr # UnlessExpr
| left=expr OR right=expr # OrExpr
| promql # PromqlExpr
| selectSql # SqlExpr
| number # LiteralExpr
| SQL_FUNCTION LPAREN string RPAREN # SqlCallExpr
| PROMQL_FUNCTION LPAREN string RPAREN # PromqlCallExpr
;
// Lexer rules
AND : 'and' ;
OR : 'or' ;
UNLESS : 'unless' ;
GT : '>' ;
GE : '>=' ;
LT : '<' ;
LE : '<=' ;
EQ : '==' ;
NE : '!=' ;
LPAREN : '(' ;
RPAREN : ')' ;
functionCall
: functionName LPAREN parameterList RPAREN
;
identifier
: IDENTIFIER
parameterList
: parameter (COMMA parameter)*
;
parameter
: expr
| STAR
| string
| duration
;
number
: NUMBER
| FLOAT
| SCIENTIFIC_NUMBER
;
IDENTIFIER : [a-zA-Z_] [a-zA-Z0-9_={}[\]".]*;
NUMBER : [0-9]+ ('.' [0-9]+)? ;
WS : [ \t\r\n]+ -> skip ;
string
: STRING
;
duration
: DURATION
;
functionName
: COUNT
| AVG
| SUM
| MIN
| MAX
| RATE_FUNCTION
| INCREASE_FUNCTION
| HISTOGRAM_QUANTILE_FUNCTION
| BY_FUNCTION
| WITHOUT_FUNCTION
| GROUP_LEFT_FUNCTION
| GROUP_RIGHT_FUNCTION
| IGNORING_FUNCTION
| ON_FUNCTION
| IDENTIFIER
;
// SQL grammar for complex queries
selectSql
: SELECT selectFieldList FROM relList (WHERE conditionList)?
(GROUP BY groupByList)? (HAVING conditionList)?
(ORDER BY orderByList)? (LIMIT limitClause)?
;
selectFieldList
: selectField (COMMA selectField)*
;
selectField
: functionCall (AS? IDENTIFIER)?
| IDENTIFIER (AS? IDENTIFIER)?
| STAR (AS? IDENTIFIER)?
| IDENTIFIER DOT IDENTIFIER (AS? IDENTIFIER)?
;
groupByList
: IDENTIFIER (COMMA IDENTIFIER)*
;
orderByList
: orderByField (COMMA orderByField)*
;
orderByField
: IDENTIFIER (ASC | DESC)?
| functionCall (ASC | DESC)?
;
limitClause
: NUMBER
;
relList
: relation (COMMA relation)*
;
relation
: IDENTIFIER (AS? IDENTIFIER)?
| LPAREN selectSql RPAREN AS IDENTIFIER
;
conditionList
: conditionList AND conditionList
| conditionList OR conditionList
| condition
| LPAREN conditionList RPAREN
;
compOp
: EQ | LT | GT | LE | GE | NE | LIKE | NOT LIKE | IN | NOT IN | IS | IS NOT
;
condition
: conditionUnit compOp conditionUnit
| LPAREN condition RPAREN
| IDENTIFIER BETWEEN number AND number
;
conditionUnit
: number
| string
| IDENTIFIER
| IDENTIFIER DOT IDENTIFIER
| NULL
| LPAREN selectSql RPAREN
| functionCall
;
// PromQL query expressions
promql
: metricSelector instantVectorOp?
| aggregationOperator LPAREN promql (BY labelList)? RPAREN
| functionCall
| promql LBRACKET duration RBRACKET
| promql LBRACKET duration COLON duration RBRACKET
| promql OFFSET duration
| IDENTIFIER
;
metricSelector
: LBRACE labelMatcherList? RBRACE
;
labelMatcherList
: labelMatcherItem (COMMA labelMatcherItem)*
;
labelMatcherItem
: IDENTIFIER labelMatcherOp string
;
labelMatcherOp
: EQ | NE
;
labelList
: LPAREN IDENTIFIER (COMMA IDENTIFIER)* RPAREN
;
instantVectorOp
: LBRACKET duration RBRACKET
;
aggregationOperator
: SUM | AVG | COUNT | MIN | MAX | STDDEV | STDVAR | TOPK | BOTTOMK | QUANTILE
;
// Lexer rules
// Boolean operators
AND : [Aa][Nn][Dd] ;
OR : [Oo][Rr] ;
UNLESS : [Uu][Nn][Ll][Ee][Ss][Ss] ;
NOT : [Nn][Oo][Tt] ;
// SQL keywords
SELECT : [Ss][Ee][Ll][Ee][Cc][Tt] ;
FROM : [Ff][Rr][Oo][Mm] ;
WHERE : [Ww][Hh][Ee][Rr][Ee] ;
GROUP : [Gg][Rr][Oo][Uu][Pp] ;
BY : [Bb][Yy] ;
HAVING : [Hh][Aa][Vv][Ii][Nn][Gg] ;
ORDER : [Oo][Rr][Dd][Ee][Rr] ;
LIMIT : [Ll][Ii][Mm][Ii][Tt] ;
OFFSET : [Oo][Ff][Ff][Ss][Ee][Tt] ;
AS : [Aa][Ss] ;
ASC : [Aa][Ss][Cc] ;
DESC : [Dd][Ee][Ss][Cc] ;
IN : [Ii][Nn] ;
IS : [Ii][Ss] ;
NULL : [Nn][Uu][Ll][Ll] ;
LIKE : [Ll][Ii][Kk][Ee] ;
BETWEEN : [Bb][Ee][Tt][Ww][Ee][Ee][Nn] ;
STAR : '*' ;
// Aggregate functions
COUNT : [Cc][Oo][Uu][Nn][Tt] ;
SUM : [Ss][Uu][Mm] ;
AVG : [Aa][Vv][Gg] ;
MIN : [Mm][Ii][Nn] ;
MAX : [Mm][Aa][Xx] ;
STDDEV : [Ss][Tt][Dd][Dd][Ee][Vv] ;
STDVAR : [Ss][Tt][Dd][Vv][Aa][Rr] ;
VARIANCE: [Vv][Aa][Rr][Ii][Aa][Nn][Cc][Ee] ;
// PromQL specific functions
RATE_FUNCTION : [Rr][Aa][Tt][Ee] ;
INCREASE_FUNCTION: [Ii][Nn][Cc][Rr][Ee][Aa][Ss][Ee] ;
HISTOGRAM_QUANTILE_FUNCTION: [Hh][Ii][Ss][Tt][Oo][Gg][Rr][Aa][Mm] '_' [Qq][Uu][Aa][Nn][Tt][Ii][Ll][Ee] ;
TOPK : [Tt][Oo][Pp][Kk] ;
BOTTOMK : [Bb][Oo][Tt][Tt][Oo][Mm][Kk] ;
QUANTILE: [Qq][Uu][Aa][Nn][Tt][Ii][Ll][Ee] ;
BY_FUNCTION: [Bb][Yy] ;
WITHOUT_FUNCTION: [Ww][Ii][Tt][Hh][Oo][Uu][Tt] ;
GROUP_LEFT_FUNCTION: [Gg][Rr][Oo][Uu][Pp] '_' [Ll][Ee][Ff][Tt] ;
GROUP_RIGHT_FUNCTION: [Gg][Rr][Oo][Uu][Pp] '_' [Rr][Ii][Gg][Hh][Tt] ;
IGNORING_FUNCTION: [Ii][Gg][Nn][Oo][Rr][Ii][Nn][Gg] ;
ON_FUNCTION: [Oo][Nn] ;
// Other functions
SQL_FUNCTION: [Ss][Qq][Ll] ;
PROMQL_FUNCTION: [Pp][Rr][Oo][Mm][Qq][Ll] ;
// Comparison operators
GT : '>' ;
GE : '>=' ;
LT : '<' ;
LE : '<=' ;
EQ : '==' | '=' ;
NE : '!=' ;
BOOL : 'bool';
// Delimiters
LPAREN : '(' ;
RPAREN : ')' ;
LBRACE : '{' ;
RBRACE : '}' ;
LBRACKET: '[' ;
RBRACKET: ']' ;
COMMA : ',' ;
DOT : '.' ;
COLON : ':' ;
SEMICOLON: ';' ;
// number formats
SCIENTIFIC_NUMBER: [0-9]+ ('.' [0-9]+)? [eE] [+-]? [0-9]+ ;
FLOAT : [0-9]+ '.' [0-9]+ ;
NUMBER : [0-9]+ ;
// Duration literals for PromQL (e.g., 5m, 1h, 30s)
DURATION : [0-9]+ [a-zA-Z]+ ;
// String literals
STRING : '"' (~["\r\n\\] | '\\' .)* '"'
| '\'' (~['\r\n\\] | '\\' .)* '\'' ;
// Identifiers and metric names
IDENTIFIER : [a-zA-Z_] [a-zA-Z0-9_={}[\]".~-]* ;
// Whitespace and comments
WS : [ \t\r\n]+ -> channel(HIDDEN) ;
LINE_COMMENT : '//' ~[\r\n]* -> skip ;
BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
@@ -0,0 +1,135 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
/**
* alert cache manager test
*/
@ExtendWith(MockitoExtension.class)
public class AlarmCacheManagerTest {
@Mock
private SingleAlertDao singleAlertDao;
private AlarmCacheManager alarmCacheManager;
@BeforeEach
public void setUp() {
Map<String, String> labels = new HashMap<>();
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(1L));
SingleAlert alert = new SingleAlert();
alert.setContent("Alert cache manager test");
alert.setLabels(labels);
when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert));
alarmCacheManager = new AlarmCacheManager(singleAlertDao);
}
@Test
void testInit() {
Map<String, String> labels = new HashMap<>();
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(1L));
String fingerprint = AlertUtil.calculateFingerprint(labels);
SingleAlert firingSingleAlert = alarmCacheManager.getFiring(1L, fingerprint);
assertNotNull(firingSingleAlert);
assertEquals("Alert cache manager test", firingSingleAlert.getContent());
alarmCacheManager.removeFiring(1L, fingerprint);
firingSingleAlert = alarmCacheManager.getFiring(1L, fingerprint);
assertNull(firingSingleAlert);
}
@Test
void testPending() {
Map<String, String> labels = new HashMap<>();
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
labels.put(CommonConstants.ALERT_SEVERITY_INFO, CommonConstants.ALERT_STATUS_PENDING);
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(2L));
SingleAlert alert = new SingleAlert();
alert.setContent("Alert cache manager test");
alert.setLabels(labels);
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
alarmCacheManager.putPending(2L, fingerprint, alert);
SingleAlert pendingSingleAlert = alarmCacheManager.getPending(2L, fingerprint);
assertNotNull(pendingSingleAlert);
alarmCacheManager.removePending(2L, fingerprint);
pendingSingleAlert = alarmCacheManager.getPending(2L, fingerprint);
assertNull(pendingSingleAlert);
}
@Test
void testFiring() {
Map<String, String> labels = new HashMap<>();
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
labels.put(CommonConstants.ALERT_SEVERITY_INFO, CommonConstants.ALERT_STATUS_PENDING);
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(3L));
SingleAlert alert = new SingleAlert();
alert.setContent("Alert cache manager test");
alert.setLabels(labels);
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
alarmCacheManager.putFiring(3L, fingerprint, alert);
SingleAlert firingSingleAlert = alarmCacheManager.getFiring(3L, fingerprint);
assertNotNull(firingSingleAlert);
alarmCacheManager.removeFiring(3L, fingerprint);
firingSingleAlert = alarmCacheManager.getFiring(3L, fingerprint);
assertNull(firingSingleAlert);
}
@Test
void testHistorical() {
SingleAlert alert = new SingleAlert();
alert.setContent("Alert cache manager test");
alert.setLabels(Collections.singletonMap(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL));
when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert));
alarmCacheManager = new AlarmCacheManager(singleAlertDao);
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
SingleAlert historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint);
assertNotNull(historicalSingleAlert);
SingleAlert singleAlert = alarmCacheManager.removeFiring(4L, fingerprint);
assertNotNull(singleAlert);
historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint);
assertNull(historicalSingleAlert);
}
}
@@ -0,0 +1,147 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
import org.apache.hertzbeat.alert.dao.AlertCollectorDao;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.util.AlertUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.manager.Collector;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Collector alert handler test
*/
@ExtendWith(MockitoExtension.class)
public class CollectorAlertHandlerTest {
@Mock
private AlertCollectorDao alertCollectorDao;
@Mock
private SingleAlertDao singleAlertDao;
@Mock
private AlarmCommonReduce alarmCommonReduce;
private AlarmCacheManager alarmCacheManager;
private CollectorAlertHandler collectorAlertHandler;
@BeforeEach
void setUp() {
alarmCacheManager = Mockito.spy(new AlarmCacheManager(singleAlertDao));
collectorAlertHandler = new CollectorAlertHandler(alarmCommonReduce, alertCollectorDao, alarmCacheManager);
}
@Test
public void testOnline() {
String identity = "localhost-collector";
Collector collector = Collector.builder()
.name(identity)
.ip("127.0.0.1")
.version("1.0")
.build();
SingleAlert singleAlert = SingleAlert.builder().build();
when(alertCollectorDao.findCollectorByName(any(String.class))).thenReturn(collector);
when(alarmCacheManager.removeFiring(any(String.class))).thenReturn(singleAlert);
collectorAlertHandler.online(identity);
assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, singleAlert.getStatus());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
public void testOffline() {
String identity = "localhost-collector";
Collector collector = Collector.builder()
.name(identity)
.ip("127.0.0.1")
.version("1.0")
.build();
when(alertCollectorDao.findCollectorByName(any(String.class))).thenReturn(collector);
when(alarmCacheManager.getFiring(any(String.class))).thenReturn(null);
collectorAlertHandler.offline(identity);
verify(alarmCacheManager, times(1)).putFiring(any(String.class), any(SingleAlert.class));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testOfflineAndOnlineLifecycle() {
String identity = "localhost-collector";
Collector collector = Collector.builder()
.name(identity)
.ip("127.0.0.1")
.version("1.0")
.build();
Map<String, String> fingerPrints = new HashMap<>(8);
fingerPrints.put("collectorName", collector.getName());
fingerPrints.put("collectorVersion", collector.getVersion());
fingerPrints.put("collectorHost", collector.getIp());
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
when(alertCollectorDao.findCollectorByName(identity)).thenReturn(collector);
// step1. first time offline
collectorAlertHandler.offline(identity);
assertNotNull(alarmCacheManager.getFiring(fingerprint));
// step2. online
collectorAlertHandler.online(identity);
assertNull(alarmCacheManager.getFiring(fingerprint));
// step3. second time offline
collectorAlertHandler.offline(identity);
assertNotNull(alarmCacheManager.getFiring(fingerprint));
// Verify that the push status is correct.
ArgumentCaptor<SingleAlert> captor = ArgumentCaptor.forClass(SingleAlert.class);
verify(alarmCommonReduce, atLeast(3)).reduceAndSendAlarm(captor.capture());
List<SingleAlert> alerts = captor.getAllValues();
assertEquals(CommonConstants.ALERT_STATUS_FIRING, alerts.get(0).getStatus());
assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, alerts.get(1).getStatus());
assertEquals(CommonConstants.ALERT_STATUS_FIRING, alerts.get(2).getStatus());
}
}
@@ -42,6 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -92,12 +93,12 @@ class PeriodicAlertCalculatorTest {
result.put("__value__", 95.0); // Non-null, matched with threshold
result.put("__timestamp__", System.currentTimeMillis());
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
when(alarmCacheManager.getPending(anyString())).thenReturn(null);
when(alarmCacheManager.getPending(eq(rule.getId()), anyString())).thenReturn(null);
periodicAlertCalculator.calculate(rule);
// Verify that putFiring is called
ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
verify(alarmCacheManager).putFiring(idCaptor.capture(), alertCaptor.capture());
verify(alarmCacheManager).putFiring(eq(rule.getId()), idCaptor.capture(), alertCaptor.capture());
// Assertion alarm status and content
SingleAlert alert = alertCaptor.getValue();
assertAll(() -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus()),
@@ -112,7 +113,7 @@ class PeriodicAlertCalculatorTest {
result.put("__timestamp__", System.currentTimeMillis());
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
periodicAlertCalculator.calculate(rule);
verify(alarmCacheManager, times(0)).putFiring(any(), any());
verify(alarmCacheManager, times(0)).putFiring(any(), any(), any());
}
@Test
@@ -126,7 +127,7 @@ class PeriodicAlertCalculatorTest {
.triggerTimes(2).startAt(System.currentTimeMillis() - 60000)
.activeAt(System.currentTimeMillis() - 30000)
.build();
when(alarmCacheManager.removeFiring(anyString())).thenReturn(pendingAlert);
when(alarmCacheManager.removeFiring(eq(rule.getId()), anyString())).thenReturn(pendingAlert);
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
periodicAlertCalculator.calculate(rule);
ArgumentCaptor<SingleAlert> resolvedCaptor = ArgumentCaptor.forClass(SingleAlert.class);
@@ -0,0 +1,254 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.MetricDataConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.queue.impl.InMemoryCommonDataQueue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
*
*/
public class RealTimeAlertCalculatorMatchTest {
private final AlerterWorkerPool workerPool = new AlerterWorkerPool();
@Mock
private CommonDataQueue dataQueue = new InMemoryCommonDataQueue();
@Mock
private AlertDefineService alertDefineService;
@Mock
private SingleAlertDao singleAlertDao;
@Mock
private AlarmCommonReduce alarmCommonReduce;
@Mock
private AlarmCacheManager alarmCacheManager;
private RealTimeAlertCalculator realTimeAlertCalculator;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
when(singleAlertDao.querySingleAlertsByStatus(any())).thenReturn(new ArrayList<>());
realTimeAlertCalculator = new RealTimeAlertCalculator(
workerPool,
dataQueue,
alertDefineService,
singleAlertDao,
alarmCommonReduce,
alarmCacheManager,
false
);
}
@Test
void testFilterThresholdsByAppAndMetrics_withInstanceExpr_HasSpace() {
String app = "redis";
String instanceId = "501045327364864";
int priority = 0;
AlertDefine matchDefine = new AlertDefine();
matchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__, \"501045327364864\")");
AlertDefine unmatchDefine = new AlertDefine();
unmatchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__, \"999999999\")");
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
List<AlertDefine> filtered = realTimeAlertCalculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority);
// It should filter out 999999999.
assertEquals(1, filtered.size());
assertEquals("equals(__app__,\"redis\") && equals(__instance__, \"501045327364864\")",
filtered.get(0).getExpr());
}
@Test
void testPrometheusReplaceMultipleJobsApp() throws InterruptedException {
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
builder.setId(518789738974464L)
.setApp("_prometheus_Cool_Stingray_34Nj_copy")
.setMetrics("canal_instance")
.setPriority(0)
.setCode(CollectRep.Code.SUCCESS);
CollectRep.Field destination = CollectRep.Field.newBuilder().setName("destination").setType(CommonConstants.TYPE_STRING).setLabel(true).build();
CollectRep.Field mode = CollectRep.Field.newBuilder().setName("mode").setType(CommonConstants.TYPE_STRING).setLabel(true).build();
CollectRep.Field metricValue = CollectRep.Field.newBuilder().setName("metric_value").setType(CommonConstants.TYPE_NUMBER).setLabel(true).build();
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj_copy");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(destination, mode, metricValue));
builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("example").addColumn("spring").addColumn("1.0").build());
CollectRep.MetricsData metricsData = builder.build();
AlertDefine matchDefine = new AlertDefine();
matchDefine.setId(1L);
matchDefine.setName("test");
matchDefine.setExpr(
"equals(__app__,\"prometheus\") && "
+ "equals(__metrics__,\"canal_instance\") && "
+ "(equals(__instance__, \"515224274242816\") or equals(__instance__, \"518789738974464\")) && "
+ "metric_value > 0"
);
matchDefine.setTemplate("Canal instance val: ${value}%");
matchDefine.setTimes(1);
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@Test
void testPrometheusReplaceApp() throws InterruptedException {
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
builder.setId(1)
.setApp("_prometheus_Cool_Stingray_34Nj")
.setMetrics("canal_instance")
.setPriority(0)
.setCode(CollectRep.Code.SUCCESS);
CollectRep.Field destination = CollectRep.Field.newBuilder().setName("destination").setType(CommonConstants.TYPE_STRING).setLabel(true).build();
CollectRep.Field mode = CollectRep.Field.newBuilder().setName("mode").setType(CommonConstants.TYPE_STRING).setLabel(true).build();
CollectRep.Field metricValue = CollectRep.Field.newBuilder().setName("metric_value").setType(CommonConstants.TYPE_NUMBER).setLabel(true).build();
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(destination, mode, metricValue));
builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("example").addColumn("spring").addColumn("1.0").build());
CollectRep.MetricsData metricsData = builder.build();
AlertDefine matchDefine = new AlertDefine();
matchDefine.setId(1L);
matchDefine.setName("test");
matchDefine.setExpr("equals(__app__,\"prometheus\") && equals(__metrics__,\"canal_instance\") && metric_value > 0");
matchDefine.setTemplate("Canal instance val: ${value}%");
matchDefine.setTimes(1);
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@Test
void testCalculateWithNormalApp() throws InterruptedException {
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
builder.setId(1)
.setApp("springboot3")
.setMetrics("available")
.setPriority(0)
.setCode(CollectRep.Code.SUCCESS)
.setTenantId(0).setId(518679137103104L)
.setTime(1749110170834L)
.setPriority(0);
CollectRep.Field responseTime = CollectRep.Field.newBuilder()
.setName("responseTime")
.setType(CommonConstants.TYPE_STRING)
.setUnit("ms")
.setLabel(false)
.build();
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Vibrant_Gazelle_83vJ");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(responseTime));
builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("18").build());
CollectRep.MetricsData metricsData = builder.build();
AlertDefine matchDefine = new AlertDefine();
matchDefine.setId(1L);
matchDefine.setName("test");
matchDefine.setExpr("equals(__app__,\"springboot3\") && equals(__metrics__,\"available\") && equals(__instance__, \"518679137103104\") && responseTime > 0");
matchDefine.setTemplate("Canal instance val: ${value}%");
matchDefine.setTimes(1);
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
}
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
/**
* alert sse manager test
*/
public class AlertSseManagerTest {
private AlertSseManager alertSseManager;
@BeforeEach
void setUp() {
alertSseManager = new AlertSseManager();
}
@Test
void testCompleteThrowsException() throws Exception {
SseEmitter emitter = alertSseManager.createEmitter(1L);
assertNotNull(emitter);
Map<Long, SseEmitter> emitters = new HashMap<>();
SseEmitter spyEmitter = mock(SseEmitter.class);
doThrow(new IllegalStateException("Simulated output stream error")).when(spyEmitter).send(any(SseEmitter.SseEventBuilder.class));
doThrow(new RuntimeException("Complete failed")).when(spyEmitter).complete();
emitters.put(1L, spyEmitter);
Field emittersField = AlertSseManager.class.getDeclaredField("emitters");
emittersField.setAccessible(true);
emittersField.set(alertSseManager, emitters);
assertThrows(RuntimeException.class, () -> alertSseManager.broadcast("{\"id\":1,\"content\":\"Test alert\"}"));
Map<Long, SseEmitter> currentEmitters = (Map<Long, SseEmitter>) emittersField.get(alertSseManager);
assertFalse(currentEmitters.containsKey(1L), "Emitter should still exist because complete() threw exception");
}
}
@@ -17,15 +17,22 @@
package org.apache.hertzbeat.alert.controller;
import static org.mockito.ArgumentMatchers.anyString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -100,6 +107,37 @@ class AlertDefineControllerTest {
.andReturn();
}
@Test
void testGetDefinePreview() throws Exception {
List<Map<String, Object>> previewData = new ArrayList<>();
Map<String, Object> row = new HashMap<>();
row.put("__value__", 123);
row.put("job", "spring-boot");
previewData.add(row);
Mockito.when(alertDefineService.getDefinePreview(anyString(), anyString(), anyString()))
.thenReturn(previewData);
mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/define/preview/{datasource}", "promql")
.param("type", "periodic")
.param("expr", "up == 1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data[0].__value__").value(123))
.andExpect(jsonPath("$.data[0].job").value("spring-boot"));
Mockito.when(alertDefineService.getDefinePreview(anyString(), anyString(), anyString()))
.thenThrow(new AlertExpressionException("Expression error"));
mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/define/preview/{datasource}", "promql")
.param("type", "periodic")
.param("expr", "http_server_requests_seconds_count{!@~!!#$%^&}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").exists())
.andExpect(jsonPath("$.msg").value("Expression error"));
}
@Test
void modifyAlertDefine() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.put("/api/alert/define")
@@ -24,12 +24,14 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
/**
@@ -38,76 +40,130 @@ import static org.mockito.Mockito.when;
class AlertExpressionEvalVisitorTest {
private QueryExecutor mockExecutor;
private AlertExpressionEvalVisitor visitor;
@BeforeEach
void setUp() {
mockExecutor = Mockito.mock(QueryExecutor.class);
visitor = new AlertExpressionEvalVisitor(mockExecutor);
}
@Test
void testGreaterThan() {
when(mockExecutor.execute("cpu")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80.0))));
when(mockExecutor.execute("select cpu from cpu_table where id = 1")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 80.0))));
// promql
List<Map<String, Object>> result = evaluate("cpu > 70");
assertEquals(1, result.size());
assertEquals(80.0, result.get(0).get("__value__"));
//sql
result = evaluate("(select cpu from cpu_table where id = 1) > 70");
assertEquals(1, result.size());
assertEquals(80.0, result.get(0).get("__value__"));
}
@Test
void testGreaterThanWithInteger() {
when(mockExecutor.execute("cpu")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80))));
when(mockExecutor.execute("select cpu_usage from system_metrics where host = 'server1'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 80))));
// promql
List<Map<String, Object>> result = evaluate("cpu > 70");
assertEquals(1, result.size());
assertEquals(80, result.get(0).get("__value__"));
// sql
result = evaluate("(select cpu_usage from system_metrics where host = 'server1') > 70");
assertEquals(1, result.size());
assertEquals(80, result.get(0).get("__value__"));
}
@Test
void testLessThan() {
when(mockExecutor.execute("memory")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 65.0))));
when(mockExecutor.execute("select memory_usage from memory_table where instance = 'web1'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 65.0))));
// promql
List<Map<String, Object>> result = evaluate("memory < 70");
assertEquals(1, result.size());
assertEquals(65.0, result.get(0).get("__value__"));
// sql
result = evaluate("(select memory_usage from memory_table where instance = 'web1') < 70");
assertEquals(1, result.size());
assertEquals(65.0, result.get(0).get("__value__"));
}
@Test
void testEqualWithTolerance() {
when(mockExecutor.execute("disk")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 99.999))));
when(mockExecutor.execute("select disk_usage from storage_metrics where partition = '/'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 99.999))));
// promql
List<Map<String, Object>> result = evaluate("disk == 100");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
// sql
result = evaluate("(select disk_usage from storage_metrics where partition = '/') == 100");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
}
@Test
void testNotEqual() {
when(mockExecutor.execute("network")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 50.0))));
when(mockExecutor.execute("select bandwidth from network_stats where interface = 'eth0'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 50.0))));
// promql
List<Map<String, Object>> result = evaluate("network != 60");
assertEquals(1, result.size());
assertEquals(50.0, result.get(0).get("__value__"));
// sql
result = evaluate("(select bandwidth from network_stats where interface = 'eth0') != 60");
assertEquals(1, result.size());
assertEquals(50.0, result.get(0).get("__value__"));
}
@Test
void testExactlyEqual() {
when(mockExecutor.execute("threshold")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 100.0))));
when(mockExecutor.execute("select alert_threshold from alert_config where rule_id = 'cpu_high'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 100.0))));
// promql
List<Map<String, Object>> result = evaluate("threshold == 100");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
// sql
result = evaluate("(select alert_threshold from alert_config where rule_id = 'cpu_high') == 100");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
void testMaxValueBoundary() {
when(mockExecutor.execute("max_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", Double.MAX_VALUE))));
when(mockExecutor.execute("select max_value from boundary_test where test_case = 'extreme'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", Double.MAX_VALUE))));
// promql
List<Map<String, Object>> result = evaluate("max_val > 100");
assertEquals(1, result.size());
assertEquals(Double.MAX_VALUE, result.get(0).get("__value__"));
// sql
result = evaluate("(select max_value from boundary_test where test_case = 'extreme') > 100");
assertEquals(1, result.size());
assertEquals(Double.MAX_VALUE, result.get(0).get("__value__"));
}
@Test
void testMinValueBoundary() {
when(mockExecutor.execute("min_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", Double.MIN_VALUE))));
when(mockExecutor.execute("select min_value from boundary_test where test_case = 'minimal'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", Double.MIN_VALUE))));
// promql
List<Map<String, Object>> result = evaluate("min_val > 0");
assertEquals(1, result.size());
assertEquals(Double.MIN_VALUE, result.get(0).get("__value__"));
// sql
result = evaluate("(select min_value from boundary_test where test_case = 'minimal') > 0");
assertEquals(1, result.size());
assertEquals(Double.MIN_VALUE, result.get(0).get("__value__"));
}
@Test
@@ -121,25 +177,40 @@ class AlertExpressionEvalVisitorTest {
@Test
void testListValueWithMax() {
when(mockExecutor.execute("multi_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
when(mockExecutor.execute("select values from multi_metrics where group_id = 'test_group'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
// promql
List<Map<String, Object>> result = evaluate("multi_val > 25");
assertEquals(1, result.size());
assertEquals(30.0, result.get(0).get("__value__"));
assertEquals(0, result.size());
// sql
result = evaluate("(select values from multi_metrics where group_id = 'test_group') > 25");
assertEquals(0, result.size());
}
@Test
void testListValueWithMin() {
when(mockExecutor.execute("multi_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
when(mockExecutor.execute("select response_times from performance_data where service = 'api'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
// promql
List<Map<String, Object>> result = evaluate("multi_val < 15");
assertEquals(1, result.size());
assertEquals(10.0, result.get(0).get("__value__"));
assertEquals(0, result.size());
// sql
result = evaluate("(select response_times from performance_data where service = 'api') < 15");
assertEquals(0, result.size());
}
@Test
void testEmptyListValue() {
when(mockExecutor.execute("empty_list")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of()))));
when(mockExecutor.execute("select error_codes from error_log where date = '2024-01-01'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", List.of()))));
// promql
List<Map<String, Object>> result = evaluate("empty_list > 50");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(0, result.size());
// sql
result = evaluate("(select error_codes from error_log where date = '2024-01-01') > 50");
assertEquals(0, result.size());
}
@Test
@@ -147,19 +218,219 @@ class AlertExpressionEvalVisitorTest {
when(mockExecutor.execute("a")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 10.0))));
when(mockExecutor.execute("b")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 20.0))));
when(mockExecutor.execute("c")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 30.0))));
when(mockExecutor.execute("select cpu from server_a where region = 'us-east'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 10.0))));
when(mockExecutor.execute("select memory from server_b where region = 'us-west'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 20.0))));
when(mockExecutor.execute("select disk from server_c where region = 'eu-central'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 30.0))));
// promql
List<Map<String, Object>> result = evaluate("(a > 5) and (b > 15 or c < 25)");
assertEquals(1, result.size());
assertEquals(10.0, result.get(0).get("__value__"));
// sql
result = evaluate("((select cpu from server_a where region = 'us-east') > 5)"
+ " and ((select memory from server_b where region = 'us-west') > 15"
+ " or (select disk from server_c where region = 'eu-central') < 25)");
assertEquals(1, result.size());
assertEquals(10.0, result.get(0).get("__value__"));
}
@Test
void testMultipleUnlessConditions() {
when(mockExecutor.execute("metric1")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 40.0))));
when(mockExecutor.execute("metric2")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 50.0))));
when(mockExecutor.execute("metric3")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 60.0))));
List<Map<String, Object>> result = evaluate("metric1 > 30 unless metric2 > 45 unless metric3 < 70");
void testUnlessOpPromql() {
String promql = "http_server_requests_seconds_count > 10 unless http_server_requests_seconds_max > 0";
Map<String, Object> countValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> maxValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 10.007799125);
put("method", "GET");
put("__name__", "http_server_requests_seconds_max");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/health");
put("outcome", "SUCCESS");
put("status", "200");
}
};
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1));
when(mockExecutor.execute("http_server_requests_seconds_max")).thenReturn(List.of(maxValue1));
List<Map<String, Object>> result = evaluate(promql);
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(1307, result.get(0).get("__value__"));
maxValue1.put("uri", "/actuator/prometheus");
result = evaluate(promql);
assertEquals(0, result.size());
Map<String, Object> sumValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 20.018);
put("method", "GET");
put("__name__", "http_server_requests_seconds_sum");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/**");
put("outcome", "SUCCESS");
put("status", "200");
}
};
maxValue1.put("uri", "/actuator/health");
when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(sumValue1));
promql = "(http_server_requests_seconds_count > 10 unless http_server_requests_seconds_max > 0) unless http_server_requests_seconds_sum > 10";
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(1307, result.get(0).get("__value__"));
}
@Test
void testOrOpPromql() {
String promql = "http_server_requests_seconds_count > 10 or jvm_threads_states_threads > 0";
Map<String, Object> countValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> countValue2 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 16);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/**");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> countValue3 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 7);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/health");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> threadsValue1 = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 10.007799125);
put("__name__", "jvm_threads_states_threads");
put("__timestamp__", "1.750320922467E9");
put("job", "spring-boot-app");
put("state=", "runnable");
}
};
Map<String, Object> threadsValue2 = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 1);
put("__name__", "jvm_threads_states_threads");
put("__timestamp__", "1.750320922467E9");
put("job", "spring-boot-app");
put("state=", "timed-waiting");
}
};
Map<String, Object> threadsValue3 = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 19.02);
put("__name__", "jvm_threads_states_threads");
put("__timestamp__", "1.750320922467E9");
put("job", "spring-boot-app");
put("state=", "waiting");
}
};
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1, countValue2, countValue3));
when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(List.of(threadsValue1, threadsValue2, threadsValue3));
List<Map<String, Object>> result = evaluate(promql);
assertEquals(5, result.size());
assertTrue(result.stream().allMatch(t -> null != t.get("__value__")));
when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(new ArrayList<>());
result = evaluate(promql);
assertEquals(2, result.size());
assertTrue(result.stream().allMatch(t -> null != t.get("__value__")));
assertEquals(1307, result.get(0).get("__value__"));
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(new ArrayList<>());
when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(List.of(threadsValue1, threadsValue2, threadsValue3));
result = evaluate(promql);
assertEquals(3, result.size());
assertTrue(result.stream().allMatch(t -> null != t.get("__value__")));
assertEquals(10.007799125, result.get(0).get("__value__"));
}
@Test
void testMultipleUnlessConditions() {
when(mockExecutor.execute("metric1")).thenReturn(List.of(new HashMap<>(Map.of("job", "api", "__value__", 40.0))));
when(mockExecutor.execute("metric2")).thenReturn(List.of(new HashMap<>(Map.of("job", "web", "__value__", 50.0))));
when(mockExecutor.execute("metric3")).thenReturn(List.of(new HashMap<>(Map.of("job", "api", "__value__", 60.0))));
when(mockExecutor.execute("select cpu_usage from metrics where service = 'web'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 40.0))));
when(mockExecutor.execute("select memory_usage from metrics where service = 'db'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 50.0))));
when(mockExecutor.execute("select disk_usage from metrics where service = 'cache'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 60.0))));
// promql
List<Map<String, Object>> result = evaluate("(metric1 > 30 unless metric2 > 45) unless metric3 > 50");
assertEquals(0, result.size());
// sql
result = evaluate("((select cpu_usage from metrics where service = 'web') > 30"
+ " unless (select memory_usage from metrics where service = 'db') > 55)"
+ " unless (select disk_usage from metrics where service = 'cache') > 50");
assertEquals(0, result.size());
}
@Test
@@ -167,15 +438,436 @@ class AlertExpressionEvalVisitorTest {
when(mockExecutor.execute("cpu_temp")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 75.0))));
when(mockExecutor.execute("gpu_temp")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 85.0))));
when(mockExecutor.execute("fan_speed")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 2000.0))));
when(mockExecutor.execute("select cpu_temperature from hardware_metrics where component = 'cpu'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 75.0))));
when(mockExecutor.execute("select gpu_temperature from hardware_metrics where component = 'gpu'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 85.0))));
when(mockExecutor.execute("select fan_rpm from hardware_metrics where component = 'fan'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 2000.0))));
// promql
List<Map<String, Object>> result = evaluate("(cpu_temp > 70 and gpu_temp < 90) or fan_speed > 1500");
assertEquals(1, result.size());
assertEquals(75.0, result.get(0).get("__value__"));
// sql
result = evaluate("((select cpu_temperature from hardware_metrics where component = 'cpu') > 70"
+ " and (select gpu_temperature from hardware_metrics where component = 'gpu') < 90)"
+ " or (select fan_rpm from hardware_metrics where component = 'fan') > 1500");
assertEquals(1, result.size());
assertEquals(75.0, result.get(0).get("__value__"));
}
@Test
void testSqlAggregateCount() {
when(mockExecutor.execute("select count(*) from cpu_metrics where host = 'server1'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 150))));
List<Map<String, Object>> result = evaluate("(select count(*) from cpu_metrics where host = 'server1') > 100");
assertEquals(1, result.size());
assertEquals(150, result.get(0).get("__value__"));
}
@Test
void testSqlAggregateAvg() {
when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where region = 'us-east'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 75.5))));
List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where region = 'us-east') > 70");
assertEquals(1, result.size());
assertEquals(75.5, result.get(0).get("__value__"));
}
@Test
void testSqlAggregateSum() {
when(mockExecutor.execute("select sum(memory_used) from memory_stats where service = 'web'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 2048.0))));
List<Map<String, Object>> result = evaluate("(select sum(memory_used) from memory_stats where service = 'web') < 3000");
assertEquals(1, result.size());
assertEquals(2048.0, result.get(0).get("__value__"));
}
@Test
void testSqlAggregateMaxMin() {
when(mockExecutor.execute("select max(response_time) from api_metrics where endpoint = '/api/users'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 250.0))));
when(mockExecutor.execute("select min(response_time) from api_metrics where endpoint = '/api/users'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 50.0))));
List<Map<String, Object>> result = evaluate("(select max(response_time) from api_metrics where endpoint = '/api/users') > 200");
assertEquals(1, result.size());
assertEquals(250.0, result.get(0).get("__value__"));
result = evaluate("(select min(response_time) from api_metrics where endpoint = '/api/users') < 100");
assertEquals(1, result.size());
assertEquals(50.0, result.get(0).get("__value__"));
}
@Test
void testSqlGroupBy() {
when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where timestamp > '2024-01-01' group by host")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 80.5))));
List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where timestamp > '2024-01-01' group by host) > 75");
assertEquals(1, result.size());
assertEquals(80.5, result.get(0).get("__value__"));
}
@Test
void testSqlGroupByHaving() {
when(mockExecutor.execute("select count(*) from error_logs where level = 'ERROR' group by service having count(*) > 10")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 25))));
List<Map<String, Object>> result = evaluate("(select count(*) from error_logs where level = 'ERROR' group by service having count(*) > 10) > 20");
assertEquals(1, result.size());
assertEquals(25, result.get(0).get("__value__"));
}
@Test
void testSqlOrderByLimit() {
when(mockExecutor.execute("select cpu_usage from system_metrics where host like 'web%' order by cpu_usage desc limit 1")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 95.2))));
List<Map<String, Object>> result = evaluate("(select cpu_usage from system_metrics where host like 'web%' order by cpu_usage desc limit 1) > 90");
assertEquals(1, result.size());
assertEquals(95.2, result.get(0).get("__value__"));
}
@Test
void testSqlMultipleColumns() {
when(mockExecutor.execute("select avg(cpu_usage), max(memory_usage) from system_metrics where region = 'us-west'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 65.8))));
List<Map<String, Object>> result = evaluate("(select avg(cpu_usage), max(memory_usage) from system_metrics where region = 'us-west') < 70");
assertEquals(1, result.size());
assertEquals(65.8, result.get(0).get("__value__"));
}
@Test
void testSqlSubquery() {
when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where host in (select host from active_servers where status = 'running')")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 72.3))));
List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where host in (select host from active_servers where status = 'running')) > 70");
assertEquals(1, result.size());
assertEquals(72.3, result.get(0).get("__value__"));
}
@Test
void testSqlComplexSubquery() {
when(mockExecutor.execute("select count(*) from alerts where severity = 'HIGH' and service_id in (select id from services where category = 'critical')")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 8))));
List<Map<String, Object>> result = evaluate("(select count(*) from alerts where severity = 'HIGH' and service_id in (select id from services where category = 'critical')) >= 5");
assertEquals(1, result.size());
assertEquals(8, result.get(0).get("__value__"));
}
@Test
void testSqlWithJoins() {
when(mockExecutor.execute("select avg(m.cpu_usage) from metrics m, servers s where m.server_id = s.id and s.environment = 'production'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 78.9))));
List<Map<String, Object>> result = evaluate("(select avg(m.cpu_usage) from metrics m, servers s where m.server_id = s.id and s.environment = 'production') < 80");
assertEquals(1, result.size());
assertEquals(78.9, result.get(0).get("__value__"));
}
@Test
void testSqlBetweenCondition() {
when(mockExecutor.execute("select count(*) from performance_logs where response_time between 100 and 500")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 1200))));
List<Map<String, Object>> result = evaluate("(select count(*) from performance_logs where response_time between 100 and 500) > 1000");
assertEquals(1, result.size());
assertEquals(1200, result.get(0).get("__value__"));
}
@Test
void testSqlComplexGroupByOrderBy() {
when(mockExecutor.execute("select service, avg(response_time) from api_metrics where timestamp > '2024-01-01'"
+ " group by service order by avg(response_time) desc limit 5")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 180.5))));
List<Map<String, Object>> result = evaluate("(select service, avg(response_time) from api_metrics where timestamp > '2024-01-01'"
+ " group by service order by avg(response_time) desc limit 5) > 150");
assertEquals(1, result.size());
assertEquals(180.5, result.get(0).get("__value__"));
}
@Test
void testSqlVarianceStddev() {
when(mockExecutor.execute("select stddev(cpu_usage) from system_metrics where host = 'db-server' and timestamp > '2024-01-01'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 12.5))));
List<Map<String, Object>> result = evaluate("(select stddev(cpu_usage) from system_metrics where host = 'db-server' and timestamp > '2024-01-01') < 15");
assertEquals(1, result.size());
assertEquals(12.5, result.get(0).get("__value__"));
}
@Test
void testSqlNestedAggregation() {
when(mockExecutor.execute("select max(daily_avg) from (select date, avg(cpu_usage) as daily_avg from system_metrics group by date) as daily_stats")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 88.7))));
List<Map<String, Object>> result = evaluate("(select max(daily_avg) from (select date, avg(cpu_usage) as daily_avg from system_metrics group by date) as daily_stats) > 85");
assertEquals(1, result.size());
assertEquals(88.7, result.get(0).get("__value__"));
}
@Test
void testSqlComplexWhereConditions() {
when(mockExecutor.execute("select count(*) from alerts where (severity = 'HIGH' or severity = 'CRITICAL') and status = 'ACTIVE' and created_at > '2024-01-01'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 42))));
List<Map<String, Object>> result = evaluate("(select count(*) from alerts where (severity = 'HIGH' or severity = 'CRITICAL') and status = 'ACTIVE' and created_at > '2024-01-01') != 50");
assertEquals(1, result.size());
assertEquals(42, result.get(0).get("__value__"));
}
@Test
void testSqlWithNullChecks() {
when(mockExecutor.execute("select count(*) from system_metrics where cpu_usage is not null and memory_usage is not null")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 9876))));
List<Map<String, Object>> result = evaluate("(select count(*) from system_metrics where cpu_usage is not null and memory_usage is not null) > 9000");
assertEquals(1, result.size());
assertEquals(9876, result.get(0).get("__value__"));
}
@Test
void testSqlMultipleAggregatesWithAlias() {
when(mockExecutor.execute("select max(cpu_usage) as max_cpu, min(cpu_usage) as min_cpu, avg(cpu_usage) as avg_cpu from system_metrics where region = 'asia'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 92.1))));
List<Map<String, Object>> result = evaluate("(select max(cpu_usage) as max_cpu, min(cpu_usage) as min_cpu, avg(cpu_usage) as avg_cpu from system_metrics where region = 'asia') > 90");
assertEquals(1, result.size());
assertEquals(92.1, result.get(0).get("__value__"));
}
@Test
void testSqlAndPromqlCallExpr() {
when(mockExecutor.execute("sum(rate(http_requests_total{job='your-service'}[1m]))")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80))));
when(mockExecutor.execute("select cpu_usage from system_metrics where host = 'server1'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 80))));
// promql
List<Map<String, Object>> result = evaluate("promql(\"sum(rate(http_requests_total{job='your-service'}[1m]))\") > 70");
assertEquals(1, result.size());
assertEquals(80, result.get(0).get("__value__"));
// sql
result = evaluate("sql(\"select cpu_usage from system_metrics where host = 'server1'\") > 70");
assertEquals(1, result.size());
assertEquals(80, result.get(0).get("__value__"));
}
@Test
void testAndOpPromql() {
String promql = "http_server_requests_seconds_count > 10 and http_server_requests_seconds_max > 5";
Map<String, Object> countValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> countValue2 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 16);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/**");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> countValue3 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 7);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/health");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> maxValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 10.007799125);
put("method", "GET");
put("__name__", "http_server_requests_seconds_max");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> maxValue2 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 10);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/**");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> maxValue3 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 0);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/health");
put("outcome", "SUCCESS");
put("status", "200");
}
};
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1, countValue2, countValue3));
when(mockExecutor.execute("http_server_requests_seconds_max")).thenReturn(List.of(maxValue1, maxValue2, maxValue3));
List<Map<String, Object>> result = evaluate(promql);
assertEquals(2, result.size());
assertEquals(1307, result.get(0).get("__value__"));
assertEquals(16, result.get(1).get("__value__"));
}
@Test
void testComparisonExpr() {
String promql = "3 > bool 2";
// scalar and scalar
List<Map<String, Object>> result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(1, result.get(0).get("__value__"));
// scalar and vector
promql = "0 < http_server_requests_seconds_count";
Map<String, Object> vector = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("uri", "/actuator/prometheus");
put("status", "200");
}
};
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(vector));
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(1307, result.get(0).get("__value__"));
// scalar and vector bool
promql = "0 < bool http_server_requests_seconds_count";
// true
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(1, result.get(0).get("__value__"));
// false
vector.put("__value__", -1);
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(0, result.get(0).get("__value__"));
// vector and scalar
promql = "http_server_requests_seconds_sum > 0";
vector = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 13);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("uri", "/actuator/prometheus");
put("status", "200");
}
};
when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(vector));
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(13, result.get(0).get("__value__"));
// vector and scalar bool
promql = "http_server_requests_seconds_sum > bool 0";
// true
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(1, result.get(0).get("__value__"));
// false
vector.put("__value__", -1);
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(0, result.get(0).get("__value__"));
// vector and vector
promql = "http_server_requests_seconds_count > http_server_requests_seconds_sum";
Map<String, Object> vectorCount = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 13);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("uri", "/actuator/prometheus");
put("status", "200");
}
};
Map<String, Object> vectorSum = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 10);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("uri", "/actuator/prometheus");
put("status", "200");
}
};
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(vectorCount));
when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(vectorSum));
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(13, result.get(0).get("__value__"));
// vector and vector bool
promql = "http_server_requests_seconds_count > bool http_server_requests_seconds_sum";
// true
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(1, result.get(0).get("__value__"));
// false
vectorCount.put("__value__", 1);
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(0, result.get(0).get("__value__"));
}
private List<Map<String, Object>> evaluate(String expression) {
AlertExpressionLexer lexer = new AlertExpressionLexer(CharStreams.fromString(expression));
AlertExpressionParser parser = new AlertExpressionParser(new CommonTokenStream(lexer));
return visitor.visit(parser.expression());
CommonTokenStream tokens = new CommonTokenStream(lexer);
AlertExpressionParser parser = new AlertExpressionParser(tokens);
return new AlertExpressionEvalVisitor(mockExecutor, tokens).visit(parser.expression());
}
}
@@ -17,19 +17,7 @@
package org.apache.hertzbeat.alert.service;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anySet;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl;
@@ -45,6 +33,27 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anySet;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test case for {@link AlertDefineService}
*/
@@ -62,6 +71,9 @@ class AlertDefineServiceTest {
@Mock
private List<AlertDefineImExportService> alertDefineImExportServiceList;
@Mock
private DataSourceService dataSourceService;
@InjectMocks
private AlertDefineServiceImpl alertDefineService;
@@ -131,4 +143,36 @@ class AlertDefineServiceTest {
assertNotNull(alertDefineService.getAlertDefines(null, null, "id", "desc", 1, 10));
verify(alertDefineDao, times(1)).findAll(any(Specification.class), any(PageRequest.class));
}
@Test
void getDefinePreview() {
String expr = "http_server_requests_seconds_count > 10";
Map<String, Object> countValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
when(dataSourceService.calculate(eq("promql"), eq(expr))).thenReturn(Lists.newArrayList(countValue1));
List<Map<String, Object>> result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_PERIODIC, expr);
assertNotNull(result);
assertEquals(1307, result.get(0).get("__value__"));
result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_PERIODIC, null);
assertEquals(0, result.size());
result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_REALTIME, null);
assertEquals(0, result.size());
}
}
@@ -0,0 +1,150 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.impl.AlibabaCloudSlsExternAlertService;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* unit test for {@link AlibabaCloudSlsExternAlertServiceTest }
*/
@ExtendWith(MockitoExtension.class)
public class AlibabaCloudSlsExternAlertServiceTest {
@Mock
private AlarmCommonReduce alarmCommonReduce;
@InjectMocks
private AlibabaCloudSlsExternAlertService externAlertService;
@Test
void testAddExternAlertWithInvalidContent() {
String invalidContent = "invalid json";
externAlertService.addExternAlert(invalidContent);
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlert() {
String source = externAlertService.supportSource();
assertEquals("alibabacloud-sls", source);
AlibabaCloudSlsExternAlert externAlert = new AlibabaCloudSlsExternAlert();
externAlert.setAlertName("Test SLS alert");
externAlert.setFireTime((int) Instant.now().getEpochSecond());
externAlert.setAlertTime((int) Instant.now().getEpochSecond());
externAlert.setRegion("cn-hangzhou");
externAlert.setProject("project");
externAlert.setStatus("firing");
externAlert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus());
Map<String, String> labels = new HashMap<>();
labels.put("labels-k", "labels-v");
externAlert.setLabels(labels);
Map<String, String> annotations = new HashMap<>();
annotations.put("annotations-k", "annotations-v");
externAlert.setAnnotations(annotations);
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlertWithSigninUrl() {
AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert();
alert.setAlertName("Test Alert");
alert.setFireTime((int) Instant.now().getEpochSecond());
alert.setAlertTime((int) Instant.now().getEpochSecond());
alert.setRegion("cn-hangzhou");
alert.setProject("test-project");
alert.setStatus("firing");
alert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus());
alert.setSigninUrl("https://example.com");
externAlertService.addExternAlert(JsonUtil.toJson(alert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlertWithDifferentSeverityLevels() {
for (AlibabaCloudSlsExternAlert.Severity severity : AlibabaCloudSlsExternAlert.Severity.values()) {
AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert();
alert.setAlertName("Test Alert");
alert.setFireTime((int) Instant.now().getEpochSecond());
alert.setAlertTime((int) Instant.now().getEpochSecond());
alert.setRegion("cn-hangzhou");
alert.setProject("test-project");
alert.setStatus("firing");
alert.setSeverity(severity.getStatus());
externAlertService.addExternAlert(JsonUtil.toJson(alert));
}
verify(alarmCommonReduce, times(AlibabaCloudSlsExternAlert.Severity.values().length))
.reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlertWithEmptyLabelsAndAnnotations() {
AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert();
alert.setAlertName("Test Alert");
alert.setFireTime((int) Instant.now().getEpochSecond());
alert.setAlertTime((int) Instant.now().getEpochSecond());
alert.setRegion("cn-hangzhou");
alert.setProject("test-project");
alert.setStatus("firing");
alert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus());
alert.setLabels(new HashMap<>());
alert.setAnnotations(new HashMap<>());
externAlertService.addExternAlert(JsonUtil.toJson(alert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlertWithInvalidSeverity() {
AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert();
alert.setAlertName("Test Alert");
alert.setFireTime((int) Instant.now().getEpochSecond());
alert.setAlertTime((int) Instant.now().getEpochSecond());
alert.setRegion("cn-hangzhou");
alert.setProject("test-project");
alert.setStatus("firing");
alert.setSeverity(-99);
externAlertService.addExternAlert(JsonUtil.toJson(alert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
}
@@ -17,29 +17,35 @@
package org.apache.hertzbeat.alert.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.HashMap;
import com.github.benmanes.caffeine.cache.Cache;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.hertzbeat.alert.service.impl.DataSourceServiceImpl;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* test case for {@link DataSourceService}
*/
class DataSourceServiceTest {
private DataSourceServiceImpl dataSourceService;
@BeforeEach
void setUp() {
dataSourceService = new DataSourceServiceImpl();
@@ -51,12 +57,12 @@ class DataSourceServiceTest {
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 150");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
@@ -296,45 +302,36 @@ class DataSourceServiceTest {
@Test
void calculate15() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
new HashMap<>(Map.of("__value__", 1))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"user\"} > 250)")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"idle\"} < 220 )")).thenReturn(new ArrayList<>());
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 and node_cpu_seconds_total{mode=\"idle\"} < 220");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "count(node_cpu_seconds_total{mode=\"user\"} > 250) > 0 and count(node_cpu_seconds_total{mode=\"idle\"} < 220 ) > 0");
assertEquals(0, result.size());
}
@Test
void calculate16() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
new HashMap<>(Map.of("__value__", 1))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
new HashMap<>(Map.of("__value__", 1))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"user\"} > 250)")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"idle\"} < 220 )")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 and node_cpu_seconds_total{mode=\"idle\"} < 20");
List<Map<String, Object>> result = dataSourceService.calculate("promql", "count(node_cpu_seconds_total{mode=\"user\"} > 250) > 0 and count(node_cpu_seconds_total{mode=\"idle\"} < 220 ) > 0");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertNotNull(result.get(0).get("__value__"));
}
@Test
@@ -399,8 +396,7 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 or node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(0, result.size());
}
@Test
@@ -421,8 +417,7 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 or node_cpu_seconds_total{mode=\"idle\"} < 320");
assertEquals(1, result.size());
assertNotNull(result.get(0).get("__value__"));
assertEquals(4, result.size());
}
@Test
@@ -443,8 +438,8 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 unless node_cpu_seconds_total{mode=\"idle\"} < 320");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
@@ -465,8 +460,8 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 unless node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNotNull(result.get(0).get("__value__"));
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
@@ -487,8 +482,7 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 unless node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(0, result.size());
}
@Test
@@ -565,7 +559,6 @@ class DataSourceServiceTest {
tokenStreamCache.invalidateAll();
long beforeHits = tokenStreamCache.stats().hitCount();
dataSourceService.calculate("promql", expr);
expressionCache.invalidateAll();
dataSourceService.calculate("promql", expr);
long actualHits = tokenStreamCache.stats().hitCount() - beforeHits;
assertEquals(1, actualHits, "expression cache should hit but miss");
@@ -616,4 +609,34 @@ class DataSourceServiceTest {
long actualHits = tokenStreamCache.stats().hitCount() - beforeHits;
assertEquals(0, actualHits, "expression cache should miss but hit");
}
@Test
void testAlertExpressionException() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
});
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support(eq("promql"))).thenReturn(true);
when(mockExecutor.execute(eq("http_server_requests_seconds_count"))).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "http_server_requests_seconds_count > 10");
assertNotNull(result);
assertEquals(1307, result.get(0).get("__value__"));
assertThrows(AlertExpressionException.class, () -> dataSourceService.calculate("promql", "http_server_requests_seconds_count{!@~!!#$%^&}"));
}
}
@@ -0,0 +1,152 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.impl.HuaweiCloudExternAlertService;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* unit test for {@link AlibabaCloudSlsExternAlertServiceTest }
*/
@ExtendWith(MockitoExtension.class)
public class HuaweiCloudExternAlertServiceTest {
@Mock
private AlarmCommonReduce alarmCommonReduce;
@InjectMocks
private HuaweiCloudExternAlertService externAlertService;
@BeforeEach
void setUp() {
assertEquals("huaweicloud-ces", externAlertService.supportSource());
}
@Test
void testAddExternAlertWithInvalidContent() {
String invalidContent = "invalid json";
externAlertService.addExternAlert(invalidContent);
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testMessageValidFailed() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
externAlert.setType("failedType");
externAlert.setMessage("failedMessage");
externAlert.setTimestamp("2025-06-07T15:12:09Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testCertFailed() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLb"
+ "vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX"
+ "w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E"
+ "0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
externAlert.setType("Notification");
externAlert.setMessage("{}");
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/failedUrl");
externAlert.setTimestamp("2025-06-07T15:12:09Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlert() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSignature("Igs0bBhzw0JGmlgBH+9ejw2xWfPTXjAatAEsKDkkWcC5bZ/jveckdRZdgp/S0JER9eiJfMF427YDABufIN0sv/vBRXaRQKfRBLTJYbSTl+AQpEbIW5yUfJSRLEG3HNEhUDjASolbrW7zdPCoGkkqjifE23FCvw"
+ "+4tewMzqmHnfJHcFBq3W89CJzdPBjwO1UcY9C39moUZgqZk+qDVLpxb4bHSrEYAwPOSrOPR7TZpETJ30UOgFYajJydQk692edfs0NeVutHoQiOJ5/YC83ULHft0aXhichjtfZE4KF69nROAKez0ubk3l"
+ "Ey/mBIM9Ylbxn5b84OIrzzZQrIWe8Syw==");
externAlert.setSubject("[华为云][紧急告警]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已触发告警。");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("1565df032a19494590d61e05f7b0dc0e");
externAlert.setType("Notification");
externAlert.setMessage("{\"version\":\"v1\",\"data\":{\"AccountName\":\"hid_hk6tij5o1v-95zn\",\"Namespace\":\"分布式缓存服务\",\"DimensionName\":\"DCS Redis实例\",\"ResourceName\""
+ ":\"dcs-h4tv\",\"MetricName\":\"每秒并发操作数\",\"IsAlarm\":true,\"AlarmLevel\":\"紧急\",\"Region\":\"华东-上海一\",\"RegionId\":\"cn-east-3\",\"ResourceId\":\"3dc7b9ea"
+ "-70b4-4c38-942d-e2636e6d844c\",\"PrivateIp\":\"192.168.0.54\",\"CurrentData\":\"6.00 count\",\"AlarmTime\":\"2025/06/02 22:56:15 GMT+08:00\","
+ "\"AlarmRecordID\":\"ah1748876175242njvndyzMZ\","
+ "\"AlarmRuleName\":\"alarm-c5jj\",\"IsOriginalValue\":true,\"Filter\":\"原始值\",\"ComparisonOperator\":\"\\u003e\",\"Value\":\"5 count\",\"Unit\":\"count\",\"Count\":2}}");
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
externAlert.setTimestamp("2025-06-02T14:56:17Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testSubscriptionUrl() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSubscribeUrl("https://console.huaweicloud.com/smn/subscription/confirm?token=477a784601d744e4ab9ab83986502d31c4b938"
+ "0ec0b64392b134e517c3aa17eb7b3a12dc9f3b4ab495e61c4dee654b435d7223ea934345bf8ae8901cef912b1d&topic_urn=urn:smn:cn-north-4"
+ ":477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX&region=cn-north-4");
externAlert.setSignature("ottf37C/2RdDgqimRQMIBU6i7XjUfPPMU760jJn71wwP3825YPoIT22uw2A9399rkm9Jrt1qUEFrDLuA5yHFLd5n/XoM4FghIgyFn7VIfgpuVM31a+co78s"
+ "YBiZ1egOCE/AwFm2oygRhfIceUj9Kw9vmc06el9TXY6RtE5tAEF6qEmICtTh45KwtCO/WRs3DY72dQi5hm0w7/tktS4WFZ1iP4LHt5eCwFvnH0u29Y96cJNI0fLUQxI5MkhgjK"
+ "77JkFK7UT6ZYJZhzgSp/B7OQGStOQx+3Duvx4T4CzccZQM3sca81Z0B0GFGWeVXuEHyCPLsayY/Iz+5Tco51elT8w==");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("242fac183d3a4936b5ead6c725a32ed0");
externAlert.setType("SubscriptionConfirmation");
externAlert.setMessage("You are invited to subscribe to topic: urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:"
+ "CES_notification_group_bngJ2aMpX. To confirm this subscription, please visit the subscribe_url included in this message. The subscribe_url is valid only within 48 hours.");
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
externAlert.setTimestamp("2025-06-07T15:07:14Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testUnsubscribe() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLbvaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np"
+ "1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwXw1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9l"
+ "YyeL7vVI0e4sfRbuQruz3E0+ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
externAlert.setType("UnsubscribeConfirmation");
externAlert.setMessage("{}");
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
externAlert.setTimestamp("2025-06-07T15:12:09Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
}
@@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
/**
* unit test for {@link SmsClientFactory }
*/
@ExtendWith(MockitoExtension.class)
public class SmsClientFactoryTest {
@Mock
private GeneralConfigDao generalConfigDao;
@Mock
private ObjectMapper objectMapper;
@Mock
private SmsConfig yamlSmsConfig;
@InjectMocks
private SmsClientFactory smsClientFactory;
@Test
void testloadDbConfig() throws JsonProcessingException {
GeneralConfig generalConfig = new GeneralConfig();
SmsConfig smsConfig = new SmsConfig();
smsConfig.setType("smslocal");
smsConfig.setEnable(true);
smsConfig.setSmslocal(new SmslocalSmsProperties("11"));
generalConfig.setContent(JsonUtil.toJson(smsConfig));
when(objectMapper.readValue(generalConfig.getContent(), SmsConfig.class)).thenReturn(smsConfig);
when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(generalConfig);
assertNotNull(smsClientFactory.getSmsClient());
}
@Test
void testloadYamlConfig() {
when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(null);
when(yamlSmsConfig.getType()).thenReturn("smslocal");
when(yamlSmsConfig.isEnable()).thenReturn(true);
when(yamlSmsConfig.getSmslocal()).thenReturn(new SmslocalSmsProperties("11"));
assertNotNull(smsClientFactory.getSmsClient());
}
@Test
void testNull() {
when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(null);
when(yamlSmsConfig.getType()).thenReturn("");
assertNull(smsClientFactory.getSmsClient());
}
}
@@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
/**
* Test case for {@link SmsLocalSmsClientImpl}
*/
@ExtendWith(MockitoExtension.class)
public class SmsLocalSmsClientImplTest {
@Mock
private SmslocalSmsProperties smslocalSmsProperties;
private SmsLocalSmsClientImpl smsLocalSmsClient;
@BeforeEach
void setUp() {
smsLocalSmsClient = new SmsLocalSmsClientImpl(smslocalSmsProperties);
when(smslocalSmsProperties.getApiKey()).thenReturn("2");
}
@Test
void testSendMessage() {
assertEquals("smslocal", smsLocalSmsClient.getType());
assertTrue(smsLocalSmsClient.checkConfig());
//
NoticeReceiver noticeReceiver = new NoticeReceiver();
noticeReceiver.setPhone("13888888888");
SingleAlert singleAlert = new SingleAlert();
singleAlert.setContent("test");
GroupAlert groupAlert = new GroupAlert();
groupAlert.setAlerts(Lists.newArrayList(singleAlert));
assertThrows(SendMessageException.class,
() -> smsLocalSmsClient.sendMessage(noticeReceiver, null, groupAlert));
}
}
@@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
/**
* Test case for {@link UniSmsClientImpl}
*/
@ExtendWith(MockitoExtension.class)
public class UniSmsClientImplTest {
@Mock
private UniSmsProperties uniSmsProperties;
private UniSmsClientImpl uniSmsClient;
@BeforeEach
void setUp() {
uniSmsClient = new UniSmsClientImpl(uniSmsProperties);
when(uniSmsProperties.getSignature()).thenReturn("2");
when(uniSmsProperties.getTemplateId()).thenReturn("any(String.class)");
when(uniSmsProperties.getAuthMode()).thenReturn("hmac");
when(uniSmsProperties.getAccessKeyId()).thenReturn("hmac");
when(uniSmsProperties.getAccessKeySecret()).thenReturn("hmac");
}
@Test
void testSendMessage() {
assertEquals("unisms", uniSmsClient.getType());
assertTrue(uniSmsClient.checkConfig());
//
NoticeReceiver noticeReceiver = new NoticeReceiver();
noticeReceiver.setPhone("13888888888");
Map<String, String> commonLabels = new HashMap<>();
commonLabels.put("instance", "");
commonLabels.put("priority", "unknown");
Map<String, String> commonAnnotations = new HashMap<>();
commonAnnotations.put("test", "test");
GroupAlert groupAlert = new GroupAlert();
groupAlert.setCommonLabels(commonLabels);
groupAlert.setCommonAnnotations(commonAnnotations);
assertThrows(SendMessageException.class,
() -> uniSmsClient.sendMessage(noticeReceiver, null, groupAlert));
}
}
@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Test case for {@link CryptoUtils}
*/
public class CryptoUtilsTest {
@Test
void testSha256Hex() {
String sign = CryptoUtils.sha256Hex("Hello world.");
assertEquals("aa3ec16e6acc809d8b2818662276256abfd2f1b441cb51574933f3d4bd115d11", sign);
}
@Test
void testHmacSha256Base64Debug() {
String signature = CryptoUtils.hmacSha256Base64("your-real-key", "your-real-data");
assertEquals("8JrfX0v5Tt3s8PfI85o6jcf5XM3C+vLlMwvFp45LupU=", signature);
}
@Test
void testHmacSha256Hex() {
String signature = CryptoUtils.hmacSha256Hex("your-real-key", "your-real-data");;
assertEquals("41878ccd7ecd795a2dd7ec39be7f33fed4be3ec75f5307689e39dd6f41fdbaac", signature);
}
}

Some files were not shown because too many files have changed in this diff Show More