Compare commits

..
Author SHA1 Message Date
Logic 9090884588 Merge branch 'master' into update-docs 2025-08-16 15:58:20 +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
tomsun28 4050e91a13 [doc] update contribution doc 2025-08-16 15:25:06 +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
373 changed files with 15716 additions and 6294 deletions
+19
View File
@@ -6,6 +6,7 @@
"files": [
"README.md",
"README_CN.md",
"README_JP.md"
"home/src/pages/team/index.jsx"
],
"imageSize": 100,
@@ -2504,6 +2505,24 @@
"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"
]
}
],
"contributorsPerLine": 7,
+3
View File
@@ -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
+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
+2
View File
@@ -527,6 +527,8 @@ Thanks to these wonderful people, welcome to join us:
</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>
+6 -4
View File
@@ -33,11 +33,11 @@
### 特点
-**监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,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酱` 等方式消息及时送达。
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
@@ -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。
@@ -525,6 +525,8 @@ Thanks these wonderful people, welcome to join us:
</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>
+6
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>
@@ -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);
}
}
@@ -183,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);
@@ -200,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;
@@ -254,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);
@@ -334,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);
@@ -344,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) {
@@ -382,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
@@ -396,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());
}
}
@@ -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()));
}
}
}
@@ -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,7 +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); }
@Override
public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -53,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}
@@ -61,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}
@@ -69,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 visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -77,7 +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 visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -85,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 visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -93,7 +114,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); }
@Override
public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -101,7 +125,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 visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -109,7 +136,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 visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -117,7 +147,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 visitOrExpr(AlertExpressionParser.OrExprContext ctx) { return visitChildren(ctx); }
@Override
public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -125,7 +158,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 visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) { return visitChildren(ctx); }
@Override
public T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -133,7 +169,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 visitParameterList(AlertExpressionParser.ParameterListContext ctx) { return visitChildren(ctx); }
@Override
public T visitParameterList(AlertExpressionParser.ParameterListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -141,7 +180,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 visitParameter(AlertExpressionParser.ParameterContext ctx) { return visitChildren(ctx); }
@Override
public T visitParameter(AlertExpressionParser.ParameterContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -149,7 +191,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 visitNumber(AlertExpressionParser.NumberContext ctx) { return visitChildren(ctx); }
@Override
public T visitNumber(AlertExpressionParser.NumberContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -157,7 +202,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 visitString(AlertExpressionParser.StringContext ctx) { return visitChildren(ctx); }
@Override
public T visitString(AlertExpressionParser.StringContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -165,7 +213,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 visitDuration(AlertExpressionParser.DurationContext ctx) { return visitChildren(ctx); }
@Override
public T visitDuration(AlertExpressionParser.DurationContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -173,7 +224,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 visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) { return visitChildren(ctx); }
@Override
public T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -181,7 +235,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 visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) { return visitChildren(ctx); }
@Override
public T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -189,7 +246,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 visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) { return visitChildren(ctx); }
@Override
public T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -197,7 +257,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 visitSelectField(AlertExpressionParser.SelectFieldContext ctx) { return visitChildren(ctx); }
@Override
public T visitSelectField(AlertExpressionParser.SelectFieldContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -205,7 +268,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 visitGroupByList(AlertExpressionParser.GroupByListContext ctx) { return visitChildren(ctx); }
@Override
public T visitGroupByList(AlertExpressionParser.GroupByListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -213,7 +279,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 visitOrderByList(AlertExpressionParser.OrderByListContext ctx) { return visitChildren(ctx); }
@Override
public T visitOrderByList(AlertExpressionParser.OrderByListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -221,7 +290,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 visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) { return visitChildren(ctx); }
@Override
public T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -229,7 +301,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 visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) { return visitChildren(ctx); }
@Override
public T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -237,7 +312,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 visitRelList(AlertExpressionParser.RelListContext ctx) { return visitChildren(ctx); }
@Override
public T visitRelList(AlertExpressionParser.RelListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -245,7 +323,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 visitRelation(AlertExpressionParser.RelationContext ctx) { return visitChildren(ctx); }
@Override
public T visitRelation(AlertExpressionParser.RelationContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -253,7 +334,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 visitConditionList(AlertExpressionParser.ConditionListContext ctx) { return visitChildren(ctx); }
@Override
public T visitConditionList(AlertExpressionParser.ConditionListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -261,7 +345,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 visitCompOp(AlertExpressionParser.CompOpContext ctx) { return visitChildren(ctx); }
@Override
public T visitCompOp(AlertExpressionParser.CompOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -269,7 +356,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 visitCondition(AlertExpressionParser.ConditionContext ctx) { return visitChildren(ctx); }
@Override
public T visitCondition(AlertExpressionParser.ConditionContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -277,7 +367,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 visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) { return visitChildren(ctx); }
@Override
public T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -285,7 +378,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 visitPromql(AlertExpressionParser.PromqlContext ctx) { return visitChildren(ctx); }
@Override
public T visitPromql(AlertExpressionParser.PromqlContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -293,7 +389,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 visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) { return visitChildren(ctx); }
@Override
public T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -301,7 +400,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 visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) { return visitChildren(ctx); }
@Override
public T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -309,7 +411,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 visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) { return visitChildren(ctx); }
@Override
public T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -317,7 +422,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 visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) { return visitChildren(ctx); }
@Override
public T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -325,7 +433,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 visitLabelList(AlertExpressionParser.LabelListContext ctx) { return visitChildren(ctx); }
@Override
public T visitLabelList(AlertExpressionParser.LabelListContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -333,7 +444,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 visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) { return visitChildren(ctx); }
@Override
public T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) {
return visitChildren(ctx);
}
/**
* {@inheritDoc}
@@ -341,13 +455,8 @@ 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 visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitBinaryOperator(AlertExpressionParser.BinaryOperatorContext ctx) { return visitChildren(ctx); }
@Override
public T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) {
return visitChildren(ctx);
}
}
@@ -18,21 +18,26 @@
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;
@@ -56,70 +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);
Map<String, Object> resultMap = new HashMap(item);
resultMap.put(VALUE, matchValue);
// if matchValue is null, mean not match the threshold
// if not null, mean match the threshold
result.add(resultMap);
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
@@ -127,92 +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;
List<Map<String, Object>> results = new ArrayList<>();
// 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 (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
if (!rightLabelSet.contains(labelKey(leftItem))) {
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) {
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<>();
return results;
}
@Override
@@ -220,7 +240,7 @@ 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;
}
@@ -247,84 +267,56 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
return callSqlOrPromql(tokens.getText(ctx.string()));
}
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;
}
}
private List<Map<String, Object>> callSqlOrPromql(String text){
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
*/
@@ -40,6 +41,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code AndExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -48,6 +50,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code ComparisonExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -56,6 +59,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code UnlessExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -64,6 +68,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code SqlExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -72,6 +77,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code SqlCallExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -80,6 +86,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code LiteralExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -88,6 +95,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code ParenExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -96,6 +104,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code PromqlCallExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -104,6 +113,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code PromqlExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -112,6 +122,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code OrExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -119,6 +130,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#functionCall}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -126,6 +138,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#parameterList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -133,6 +146,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#parameter}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -140,6 +154,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#number}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -147,6 +162,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#string}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -154,6 +170,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#duration}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -161,6 +178,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#functionName}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -168,6 +186,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectSql}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -175,6 +194,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectFieldList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -182,6 +202,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectField}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -189,6 +210,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#groupByList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -196,6 +218,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#orderByList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -203,6 +226,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#orderByField}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -210,6 +234,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#limitClause}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -217,6 +242,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#relList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -224,6 +250,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#relation}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -231,6 +258,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#conditionList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -238,6 +266,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#compOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -245,6 +274,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#condition}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -252,6 +282,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#conditionUnit}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -259,6 +290,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#promql}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -266,6 +298,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#metricSelector}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -273,6 +306,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -280,6 +314,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherItem}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -287,6 +322,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -294,6 +330,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelList}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -301,6 +338,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#instantVectorOp}.
*
* @param ctx the parse tree
* @return the visitor result
*/
@@ -308,15 +346,9 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by {@link AlertExpressionParser#aggregationOperator}.
*
* @param ctx the parse tree
* @return the visitor result
*/
T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#binaryOperator}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitBinaryOperator(AlertExpressionParser.BinaryOperatorContext 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
@@ -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();
}
}
}
@@ -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);
@@ -90,9 +99,11 @@ public class DataSourceServiceImpl implements DataSourceService {
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) {
@@ -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 = 未找到查詢執行器
@@ -23,7 +23,7 @@ expression
expr
: LPAREN expr RPAREN # ParenExpr
| left=expr op=(GE|LE|GT|LT|EQ|NE) right=expr # ComparisonExpr
| 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
@@ -156,7 +156,6 @@ conditionUnit
promql
: metricSelector instantVectorOp?
| aggregationOperator LPAREN promql (BY labelList)? RPAREN
| promql binaryOperator promql
| functionCall
| promql LBRACKET duration RBRACKET
| promql LBRACKET duration COLON duration RBRACKET
@@ -192,11 +191,6 @@ aggregationOperator
: SUM | AVG | COUNT | MIN | MAX | STDDEV | STDVAR | TOPK | BOTTOMK | QUANTILE
;
binaryOperator
: EQ | NE | GT | LT | GE | LE
| AND | OR | UNLESS
;
// Lexer rules
// Boolean operators
@@ -260,6 +254,7 @@ LT : '<' ;
LE : '<=' ;
EQ : '==' | '=' ;
NE : '!=' ;
BOOL : 'bool';
// Delimiters
LPAREN : '(' ;
@@ -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);
@@ -132,6 +132,7 @@ public class RealTimeAlertCalculatorMatchTest {
AlertDefine matchDefine = new AlertDefine();
matchDefine.setId(1L);
matchDefine.setName("test");
matchDefine.setExpr(
"equals(__app__,\"prometheus\") && "
@@ -151,8 +152,8 @@ public class RealTimeAlertCalculatorMatchTest {
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(any(), any());
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@@ -180,6 +181,7 @@ public class RealTimeAlertCalculatorMatchTest {
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}%");
@@ -194,8 +196,8 @@ public class RealTimeAlertCalculatorMatchTest {
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(any(), any());
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@@ -229,6 +231,7 @@ public class RealTimeAlertCalculatorMatchTest {
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}%");
@@ -243,8 +246,8 @@ public class RealTimeAlertCalculatorMatchTest {
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(any(), any());
verify(alarmCacheManager, times(1)).getPending(any(), any());
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@@ -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;
/**
@@ -179,12 +181,10 @@ class AlertExpressionEvalVisitorTest {
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(1, result.size());
assertEquals(30.0, result.get(0).get("__value__"));
assertEquals(0, result.size());
}
@Test
@@ -194,12 +194,10 @@ class AlertExpressionEvalVisitorTest {
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(1, result.size());
assertEquals(10.0, result.get(0).get("__value__"));
assertEquals(0, result.size());
}
@Test
@@ -209,12 +207,10 @@ class AlertExpressionEvalVisitorTest {
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(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(0, result.size());
}
@Test
@@ -240,11 +236,187 @@ class AlertExpressionEvalVisitorTest {
assertEquals(10.0, result.get(0).get("__value__"));
}
@Test
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());
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("__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))));
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(
@@ -252,15 +424,13 @@ class AlertExpressionEvalVisitorTest {
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 < 70");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
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') > 45"
+ " unless (select disk_usage from metrics where service = 'cache') < 70");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
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
@@ -320,11 +490,11 @@ class AlertExpressionEvalVisitorTest {
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__"));
@@ -473,6 +643,227 @@ class AlertExpressionEvalVisitorTest {
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));
CommonTokenStream tokens = new CommonTokenStream(lexer);
@@ -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());
}
}
@@ -23,7 +23,6 @@ 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.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -43,7 +42,6 @@ import static org.mockito.Mockito.verify;
/**
* unit test for {@link AlibabaCloudSlsExternAlertServiceTest }
*/
@Disabled
@ExtendWith(MockitoExtension.class)
public class AlibabaCloudSlsExternAlertServiceTest {
@@ -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{!@~!!#$%^&}"));
}
}
@@ -23,7 +23,6 @@ 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.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -39,7 +38,6 @@ import static org.mockito.Mockito.verify;
/**
* unit test for {@link AlibabaCloudSlsExternAlertServiceTest }
*/
@Disabled
@ExtendWith(MockitoExtension.class)
public class HuaweiCloudExternAlertServiceTest {
@@ -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);
}
}
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.alert.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
@@ -58,4 +59,13 @@ class DateUtilTest {
actualTimestamp = DateUtil.getTimeStampFromFormat(date, format);
assertFalse(actualTimestamp.isPresent());
}
@Test
void getZonedTimeStampFromFormat() {
String dataStr = "2025/06/02 22:56:15 GMT+08:00";
Long time = DateUtil.getZonedTimeStampFromFormat(dataStr, "yyyy/MM/dd HH:mm:ss 'GMT'XXX");
assertNotNull(time);
assertEquals(1748876175000L, time);
}
}
@@ -33,7 +33,7 @@
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mqtt.version>1.3.3</mqtt.version>
<mqtt.version>1.2.5</mqtt.version>
</properties>
<dependencies>
@@ -140,10 +140,19 @@
</dependency>
<!-- mqtt -->
<dependency>
<groupId>com.hivemq</groupId>
<artifactId>hivemq-mqtt-client</artifactId>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>${mqtt.version}</version>
</dependency>
<!--Bouncy Castle-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.68</version>
</dependency>
<!--plc-->
<dependency>
<groupId>org.apache.plc4x</groupId>
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.collector.collect.database;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
@@ -74,6 +75,7 @@ public class JdbcCommonCollect extends AbstractCollect {
// code execution related - may result in remote code execution
"init=", "javaobjectserializer=", "runscript", "serverstatusdiffinterceptor",
"queryinterceptors=", "statementinterceptors=", "exceptioninterceptors=",
"xp_cmdshell", "create function", "dbms_java", "sp_sysexecute", "load_file",
// multiple statement execution - may lead to SQL injection
"allowmultiqueries",
@@ -82,6 +84,96 @@ public class JdbcCommonCollect extends AbstractCollect {
"autodeserialize", "detectcustomcollations",
};
// universal bypass detection mode - applicable to all databases for dangerous command bypass detection
private static final String[] UNIVERSAL_BYPASS_PATTERNS = {
".*create\\s*([/\\\\]|\\\\n|/n|\\n)\\s*trigger.*",
".*create\\s*([/\\\\]|\\\\n|/n|\\n)\\s*function.*",
".*drop\\s*([/\\\\]|\\\\n|/n|\\n)\\s*table.*",
".*drop\\s*([/\\\\]|\\\\n|/n|\\n)\\s*database.*",
".*run\\s*([/\\\\]|\\\\n|/n|\\n)\\s*script.*",
".*alter\\s*([/\\\\]|\\\\n|/n|\\n)\\s*system.*",
".*grant\\s*([/\\\\]|\\\\n|/n|\\n)\\s*all.*",
".*revoke\\s*([/\\\\]|\\\\n|/n|\\n)\\s*all.*",
".*xp\\s*([/\\\\]|\\\\n|/n|\\n)\\s*cmdshell.*",
".*load\\s*([/\\\\]|\\\\n|/n|\\n)\\s*file.*"
};
// database platform specific bypass detection mode
private static final HashMap<String, String[]> PLATFORM_BYPASS_PATTERNS = new HashMap<>();
static {
// H2 database special character bypass mode
PLATFORM_BYPASS_PATTERNS.put("h2", new String[]{
".*(\\\\\\\\|/|\\\\|\\\\n|/n|\\n)\\s*init\\s*=.*",
".*in\\s*([/\\\\]|\\\\n|/n|\\n)\\s*it\\s*=.*",
".*(\\\\\\\\|/|\\\\|\\\\n|/n|\\n)\\s*runscript\\s+from.*",
".*ru\\s*([/\\\\]|\\\\n|/n|\\n)\\s*script\\s+from.*"
});
// MySQL/MariaDB bypass mode
String[] mysqlPatterns = {
".*allow\\s*([/\\\\]|\\\\n|/n|\\n)\\s*load\\s*([/\\\\]|\\\\n|/n|\\n)\\s*local\\s*([/\\\\]|\\\\n|/n|\\n)\\s*infile.*",
".*allow\\s*([/\\\\]|\\\\n|/n|\\n)\\s*multi\\s*([/\\\\]|\\\\n|/n|\\n)\\s*queries.*",
".*query\\s*([/\\\\]|\\\\n|/n|\\n)\\s*interceptors.*",
".*statement\\s*([/\\\\]|\\\\n|/n|\\n)\\s*interceptors.*",
".*exception\\s*([/\\\\]|\\\\n|/n|\\n)\\s*interceptors.*",
".*auto\\s*([/\\\\]|\\\\n|/n|\\n)\\s*deserialize.*"
};
PLATFORM_BYPASS_PATTERNS.put("mysql", mysqlPatterns);
PLATFORM_BYPASS_PATTERNS.put("mariadb", mysqlPatterns);
// PostgreSQL bypass mode
PLATFORM_BYPASS_PATTERNS.put("postgresql", new String[]{
".*socket\\s*([/\\\\]|\\\\n|/n|\\n)\\s*factory.*",
".*logger\\s*([/\\\\]|\\\\n|/n|\\n)\\s*file.*",
".*ssl\\s*([/\\\\]|\\\\n|/n|\\n)\\s*mode.*",
".*logger\\s*([/\\\\]|\\\\n|/n|\\n)\\s*level.*"
});
// SQL Server bypass mode
PLATFORM_BYPASS_PATTERNS.put("sqlserver", new String[]{
".*integrated\\s*([/\\\\]|\\\\n|/n|\\n)\\s*security.*",
".*authentication\\s*([/\\\\]|\\\\n|/n|\\n)\\s*scheme.*",
".*select\\s*([/\\\\]|\\\\n|/n|\\n)\\s*method.*",
".*send\\s*([/\\\\]|\\\\n|/n|\\n)\\s*string\\s*([/\\\\]|\\\\n|/n|\\n)\\s*parameters\\s*([/\\\\]|\\\\n|/n|\\n)\\s*as\\s*([/\\\\]|\\\\n|/n|\\n)\\s*unicode.*",
".*x\\s*([/\\\\]|\\\\n|/n|\\n)\\s*open\\s*([/\\\\]|\\\\n|/n|\\n)\\s*state.*",
".*application\\s*([/\\\\]|\\\\n|/n|\\n)\\s*intent.*"
});
// ClickHouse bypass mode
PLATFORM_BYPASS_PATTERNS.put("clickhouse", new String[]{
".*custom\\s*([/\\\\]|\\\\n|/n|\\n)\\s*http\\s*([/\\\\]|\\\\n|/n|\\n)\\s*params.*",
".*http\\s*([/\\\\]|\\\\n|/n|\\n)\\s*connection\\s*([/\\\\]|\\\\n|/n|\\n)\\s*provider.*",
".*check\\s*([/\\\\]|\\\\n|/n|\\n)\\s*all\\s*([/\\\\]|\\\\n|/n|\\n)\\s*nodes.*",
".*fail\\s*([/\\\\]|\\\\n|/n|\\n)\\s*over.*",
".*use\\s*([/\\\\]|\\\\n|/n|\\n)\\s*objects\\s*([/\\\\]|\\\\n|/n|\\n)\\s*in\\s*([/\\\\]|\\\\n|/n|\\n)\\s*arrays.*"
});
// Oracle bypass mode
PLATFORM_BYPASS_PATTERNS.put("oracle", new String[]{
".*oracle\\s*([/\\\\]|\\\\n|/n|\\n)\\s*jdbc.*",
".*oracle\\s*([/\\\\]|\\\\n|/n|\\n)\\s*net.*",
".*oracle\\.jdbc\\.timezoneinfotable\\s*=.*",
".*oracle\\.net\\.wallet_location\\s*=.*",
".*oracle\\.net\\.ssl_server_dn_match\\s*=\\s*false.*",
".*oracle\\.jdbc\\.enablesqlinjectionattack\\s*=\\s*true.*",
".*oracle\\.jdbc\\.implicitstatementcachesize\\s*=\\s*0.*",
".*oracle\\.jdbc\\.timezoneinfotable\\s*=.*",
".*oracle\\.net\\.wallet_location\\s*=.*",
".*oracle\\.net\\.ssl_server_dn_match\\s*=\\s*false.*",
".*oracle\\.jdbc\\.enablesqlinjectionattack\\s*=\\s*true.*",
".*oracle\\.jdbc\\.implicitstatementcachesize\\s*=\\s*0.*"
});
// DM bypass mode
PLATFORM_BYPASS_PATTERNS.put("dm", new String[]{
".*login\\s*([/\\\\]|\\\\n|/n|\\n)\\s*mode.*",
".*compatible\\s*([/\\\\]|\\\\n|/n|\\n)\\s*mode.*",
".*en\\s*([/\\\\]|\\\\n|/n|\\n)\\s*crypt.*",
".*ci\\s*([/\\\\]|\\\\n|/n|\\n)\\s*pher.*"
});
}
private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance();
@@ -91,8 +183,9 @@ public class JdbcCommonCollect extends AbstractCollect {
throw new IllegalArgumentException("Database collect must has jdbc params");
}
if (StringUtils.hasText(metrics.getJdbc().getUrl())) {
String url = metrics.getJdbc().getUrl().toLowerCase();
for (String keyword : VULNERABLE_KEYWORDS) {
if (metrics.getJdbc().getUrl().contains(keyword)) {
if (url.contains(keyword.toLowerCase())) {
throw new IllegalArgumentException("Jdbc url prohibit contains vulnerable param " + keyword);
}
}
@@ -340,6 +433,27 @@ public class JdbcCommonCollect extends AbstractCollect {
}
}
/**
* Recursively decode the URL to prevent multiple encoding bypasses.
*
* @param url jdbc url
* @return decoded decoded jdbc url
*/
private String recursiveDecode(String url) {
String prev;
String decoded = url;
int max = 5; // Decode it at most 5 times to prevent infinite loops.
do {
prev = decoded;
try {
decoded = java.net.URLDecoder.decode(prev, StandardCharsets.UTF_8);
} catch (Exception e) {
break;
}
} while (!prev.equals(decoded) && --max > 0);
return decoded;
}
/**
* construct jdbc url due the jdbc protocol
*
@@ -355,20 +469,54 @@ public class JdbcCommonCollect extends AbstractCollect {
throw new IllegalArgumentException("JDBC URL length exceeds maximum limit of 2048 characters");
}
// remove special characters
String cleanedUrl = jdbcProtocol.getUrl().replaceAll("[\\x00-\\x1F\\x7F]", "");
String url = cleanedUrl.toLowerCase();
String cleanedUrl = jdbcProtocol.getUrl().replaceAll("[\\x00-\\x1F\\x7F\\xA0]", "");
String url = recursiveDecode(cleanedUrl);
String urlLowerCase = url.toLowerCase();
// url format check
if (!urlLowerCase.matches("^jdbc:[a-zA-Z0-9]+:([^\\s;]+)(;[^\\s;]+)*$")) {
throw new IllegalArgumentException("Invalid JDBC URL format");
}
// backlist check
for (String keyword : BLACK_LIST) {
if (url.contains(keyword)) {
if (urlLowerCase.contains(keyword.toLowerCase())) {
throw new IllegalArgumentException("Invalid JDBC URL: contains potentially malicious parameter: " + keyword);
}
}
// url format check
if (!url.matches("^jdbc:[a-zA-Z0-9]+://[^\\s]+$")) {
throw new IllegalArgumentException("Invalid JDBC URL format");
// universal detection
String normalizedUrl = urlLowerCase.replaceAll("[\\x00-\\x1F\\x7F\\xA0]", " ");
// universal detection of JDBC injection and deserialization attacks
if (normalizedUrl.matches(".*jndi\\s*[:=].*")
|| normalizedUrl.matches(".*ldap\\s*[:=].*")
|| normalizedUrl.matches(".*rmi\\s*[:=].*")
|| normalizedUrl.matches(".*java\\s*[:=].*")
|| normalizedUrl.matches(".*serialization\\s*[:=].*")
|| normalizedUrl.matches(".*deserializ.*\\s*[:=].*")
|| normalizedUrl.matches(".*objectinputstream\\s*[:=].*")
|| normalizedUrl.matches(".*readobject\\s*[:=].*")) {
throw new IllegalArgumentException("Invalid JDBC URL: contains potentially malicious JNDI or deserialization parameter");
}
return cleanedUrl;
// universal detection of bypass
for (String pattern : UNIVERSAL_BYPASS_PATTERNS) {
if (normalizedUrl.matches(pattern)) {
throw new IllegalArgumentException("Invalid JDBC URL: contains potentially malicious bypass pattern");
}
}
// database platform specific bypass detection
if (jdbcProtocol.getPlatform() != null) {
String platform = jdbcProtocol.getPlatform().toLowerCase();
// check for specific bypass modes on the platform
String[] platformPatterns = PLATFORM_BYPASS_PATTERNS.get(platform);
if (platformPatterns != null) {
for (String pattern : platformPatterns) {
if (normalizedUrl.matches(pattern)) {
throw new IllegalArgumentException("Invalid " + platform.toUpperCase() + " JDBC URL: contains potentially malicious bypass pattern");
}
}
}
}
return url;
}
assert jdbcProtocol.getPlatform() != null;
return switch (jdbcProtocol.getPlatform()) {
case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase())
@@ -68,6 +68,7 @@ import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
@@ -144,37 +145,46 @@ public class HttpCollectImpl extends AbstractCollect {
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return;
}
/*
this could create large objects, potentially impacting JVM memory space significantly.
Option 1: Parse using InputStream, but this requires significant code changes;
Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs.
*/
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
if (!StringUtils.hasText(resp)) {
log.info("http response entity is empty, status: {}.", statusCode);
}
Long responseTime = System.currentTimeMillis() - startTime;
long responseTime = System.currentTimeMillis() - startTime;
String parseType = metrics.getHttp().getParseType();
HttpEntity entity = response.getEntity();
try {
switch (parseType) {
case DispatchConstants.PARSE_JSON_PATH ->
parseResponseByJsonPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
case DispatchConstants.PARSE_PROM_QL ->
parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder);
case DispatchConstants.PARSE_PROMETHEUS ->
parseResponseByPrometheusExporter(response.getEntity().getContent(), metrics.getAliasFields(), builder);
case DispatchConstants.PARSE_XML_PATH ->
parseResponseByXmlPath(resp, metrics, builder, responseTime);
case DispatchConstants.PARSE_WEBSITE ->
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime);
case DispatchConstants.PARSE_SITE_MAP ->
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
case DispatchConstants.PARSE_HEADER ->
parseResponseByHeader(builder, metrics.getAliasFields(), response);
case DispatchConstants.PARSE_CONFIG ->
parseResponseByConfig(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
default ->
parseResponseByDefault(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
if (DispatchConstants.PARSE_PROMETHEUS.equals(parseType)) {
if (entity != null) {
parseResponseByPrometheusExporter(entity.getContent(), metrics.getAliasFields(), builder);
}
} else if (DispatchConstants.PARSE_HEADER.equals(parseType)) {
parseResponseByHeader(builder, metrics.getAliasFields(), response);
// Consume entity to release connection
EntityUtils.consumeQuietly(entity);
} else {
/*
this could create large objects, potentially impacting JVM memory space significantly.
Option 1: Parse using InputStream, but this requires significant code changes;
Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs.
*/
String resp = entity == null ? "" : EntityUtils.toString(entity, StandardCharsets.UTF_8);
if (!StringUtils.hasText(resp)) {
log.info("http response entity is empty, status: {}.", statusCode);
}
switch (parseType) {
case DispatchConstants.PARSE_JSON_PATH ->
parseResponseByJsonPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
case DispatchConstants.PARSE_PROM_QL ->
parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder);
case DispatchConstants.PARSE_XML_PATH ->
parseResponseByXmlPath(resp, metrics, builder, responseTime);
case DispatchConstants.PARSE_WEBSITE ->
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime, statusCode);
case DispatchConstants.PARSE_SITE_MAP ->
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
case DispatchConstants.PARSE_CONFIG ->
parseResponseByConfig(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
default ->
parseResponseByDefault(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
}
}
} catch (Exception e) {
log.info("parse error: {}.", e.getMessage(), e);
@@ -237,11 +247,15 @@ public class HttpCollectImpl extends AbstractCollect {
}
private void parseResponseByWebsite(String resp, Metrics metrics, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime) {
CollectRep.MetricsData.Builder builder, Long responseTime, int statusCode) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
for (String alias : metrics.getAliasFields()) {
addColumnForSummary(responseTime, valueRowBuilder, keywordNum, alias);
if (CollectorConstants.STATUS_CODE.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(statusCode));
} else {
addColumnForSummary(responseTime, valueRowBuilder, keywordNum, alias);
}
}
builder.addValueRow(valueRowBuilder.build());
}
@@ -590,6 +604,18 @@ public class HttpCollectImpl extends AbstractCollect {
}
}
builder.addValueRow(valueRowBuilder.build());
} else if (objectValue instanceof Number numberValue) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
valueRowBuilder.addColumn(numberValue.toString());
}
}
builder.addValueRow(valueRowBuilder.build());
}
}
}
@@ -852,4 +878,4 @@ public class HttpCollectImpl extends AbstractCollect {
}
return successCodeSet.contains(statusCode);
}
}
}
@@ -42,7 +42,6 @@ import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.naming.Context;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
@@ -54,13 +53,15 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.JmxProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.LogUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* jmx protocol acquisition implementation
*/
@Slf4j
public class JmxCollectImpl extends AbstractCollect {
private static final String JMX_URL_PREFIX = "service:jmx:rmi:///jndi/rmi://";
@@ -75,6 +76,8 @@ public class JmxCollectImpl extends AbstractCollect {
private final ClassLoader jmxClassLoader;
private static final Logger logger = LoggerFactory.getLogger(JmxCollectImpl.class);
public JmxCollectImpl() {
jmxClassLoader = new JmxClassLoader(ClassLoader.getSystemClassLoader());
}
@@ -195,12 +198,12 @@ public class JmxCollectImpl extends AbstractCollect {
}
} catch (IOException exception) {
String errorMsg = CommonUtil.getMessageFromThrowable(exception);
log.error("JMX IOException :{}", errorMsg);
LogUtil.error(logger, "JMX IOException: {0}", errorMsg);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("JMX Error :{}", errorMsg);
LogUtil.error(logger, "JMX Error: {0}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
@@ -221,7 +224,7 @@ public class JmxCollectImpl extends AbstractCollect {
for (Attribute attribute : attributeList.asList()) {
Object value = attribute.getValue();
if (value == null) {
log.info("attribute {} value is null.", attribute.getName());
LogUtil.info(logger, "attribute {0} value is null.", attribute.getName());
continue;
}
if (value instanceof Number || value instanceof String || value instanceof ObjectName
@@ -245,7 +248,7 @@ public class JmxCollectImpl extends AbstractCollect {
}
attributeValueMap.put(attribute.getName(), builder.toString());
} else {
log.warn("attribute value type {} not support.", value.getClass().getName());
LogUtil.warn(logger, "attribute value type {0} not support.", value.getClass().getName());
}
}
return attributeValueMap;
@@ -319,7 +322,7 @@ public class JmxCollectImpl extends AbstractCollect {
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
} catch (Exception e) {
log.error("Failed to connect to JMX server: {}", e.getMessage());
LogUtil.error(logger, "Failed to connect to JMX connection: {0}", e.getMessage());
throw new IOException("Failed to connect to JMX server: " + e.getMessage(), e);
}
}
@@ -0,0 +1,195 @@
/*
* 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.collector.collect.mqtt;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Formats the private key and certificate, supporting concatenation of multiple certificates in PEM format.
*/
public class CertificateFormatter {
public static String formatCertificateChain(String input) {
if (input == null || input.trim().isEmpty()) {
return input;
}
String normalized = normalizeInput(input);
List<String> certificates = extractCertificates(normalized);
if (certificates.isEmpty()) {
return formatAsSingleCertificate(normalized);
}
StringBuilder formattedChain = new StringBuilder();
for (String cert : certificates) {
if (cert.trim().isEmpty()) continue;
String formatted = formatPemBlock(cert);
formattedChain.append(formatted).append("\n");
}
return formattedChain.toString().trim();
}
private static String normalizeInput(String input) {
return input
.replace("\r\n", "\n")
.replace("\r", "\n")
.replaceAll("\\s*\\\\n\\s*", "\n")
.replaceAll("(?m)^\\s+|\\s+$", "")
.trim();
}
private static List<String> extractCertificates(String input) {
List<String> certificates = new ArrayList<>();
String regex = "(-----BEGIN\\s+[\\w\\s]+?-----)[\\s\\S]*?(-----END\\s+[\\w\\s]+?-----)";
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(input);
int lastEnd = 0;
while (matcher.find()) {
if (matcher.start() > lastEnd) {
String gap = input.substring(lastEnd, matcher.start());
if (!gap.trim().isEmpty()) {
certificates.add(gap);
}
}
certificates.add(matcher.group());
lastEnd = matcher.end();
}
if (lastEnd < input.length()) {
certificates.add(input.substring(lastEnd));
}
return certificates;
}
private static String formatPemBlock(String block) {
try {
Pattern pattern = Pattern.compile(
"(-----BEGIN\\s+[\\w\\s]+?-----)(.*?)(-----END\\s+[\\w\\s]+?-----)",
Pattern.DOTALL | Pattern.CASE_INSENSITIVE
);
Matcher matcher = pattern.matcher(block);
if (matcher.find()) {
String header = matcher.group(1).trim();
String body = matcher.group(2);
String footer = matcher.group(3).trim();
if (body == null) body = "";
String cleanBody = body
.replaceAll("\\s", "")
.replaceAll("\"", "")
.trim();
if (cleanBody.isEmpty() && body != null && !body.trim().isEmpty()) {
cleanBody = body.replaceAll("[^a-zA-Z0-9+/=]", "").trim();
}
String formattedBody = formatBase64Body(cleanBody);
return header + "\n" + formattedBody + "\n" + footer;
} else {
return formatAsCertificate(block);
}
} catch (Exception e) {
return block;
}
}
private static String formatAsCertificate(String content) {
String cleanContent = content.replaceAll("[^a-zA-Z0-9+/=]", "").trim();
if (cleanContent.isEmpty()) {
return content;
}
String formattedBody = formatBase64Body(cleanContent);
if (cleanContent.toLowerCase().contains("private")) {
if (cleanContent.startsWith("MII") || cleanContent.length() > 1000) {
return "-----BEGIN PRIVATE KEY-----\n" + formattedBody + "\n-----END PRIVATE KEY-----";
} else {
return "-----BEGIN RSA PRIVATE KEY-----\n" + formattedBody + "\n-----END RSA PRIVATE KEY-----";
}
} else {
return "-----BEGIN CERTIFICATE-----\n" + formattedBody + "\n-----END CERTIFICATE-----";
}
}
private static String formatAsSingleCertificate(String input) {
String cleanContent = input.replaceAll("[^a-zA-Z0-9+/=]", "").trim();
return formatAsCertificate(cleanContent);
}
private static String formatBase64Body(String body) {
StringBuilder formatted = new StringBuilder();
int index = 0;
while (index < body.length()) {
int end = Math.min(index + 64, body.length());
formatted.append(body.substring(index, end));
if (end < body.length()) {
formatted.append("\n");
}
index = end;
}
return formatted.toString().trim();
}
public static String formatPrivateKey(String input) {
if (input == null || input.trim().isEmpty()) {
return input;
}
String normalized = normalizeInput(input);
if (isPemEncapsulated(normalized)) {
return formatPemBlock(normalized);
}
return formatAsCertificate(normalized);
}
private static boolean isPemEncapsulated(String block) {
return block.contains("-----BEGIN") && block.contains("-----END");
}
}
@@ -17,26 +17,7 @@
package org.apache.hertzbeat.collector.collect.mqtt;
import com.hivemq.client.mqtt.MqttVersion;
import com.hivemq.client.mqtt.datatypes.MqttQos;
import com.hivemq.client.mqtt.mqtt3.Mqtt3AsyncClient;
import com.hivemq.client.mqtt.mqtt3.Mqtt3Client;
import com.hivemq.client.mqtt.mqtt3.Mqtt3ClientBuilder;
import com.hivemq.client.mqtt.mqtt3.message.connect.connack.Mqtt3ConnAck;
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
import com.hivemq.client.mqtt.mqtt5.Mqtt5ClientBuilder;
import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAck;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
@@ -46,13 +27,27 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.message.CollectRep.MetricsData.Builder;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StopWatch;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* collect mqtt metrics
* collect mqtt metrics using Eclipse Paho
*/
public class MqttCollectImpl extends AbstractCollect {
@@ -61,138 +56,224 @@ public class MqttCollectImpl extends AbstractCollect {
private static final Logger logger = LoggerFactory.getLogger(MqttCollectImpl.class);
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
MqttProtocol mqttProtocol = metrics.getMqtt();
Assert.hasText(mqttProtocol.getHost(), "MQTT protocol host is required");
Assert.hasText(mqttProtocol.getPort(), "MQTT protocol port is required");
Assert.hasText(mqttProtocol.getProtocolVersion(), "MQTT protocol version is required");
}
@Override
public void collect(Builder builder, Metrics metrics) {
MqttProtocol mqtt = metrics.getMqtt();
String protocolVersion = mqtt.getProtocolVersion();
MqttVersion mqttVersion = MqttVersion.valueOf(protocolVersion);
if (mqttVersion == MqttVersion.MQTT_3_1_1) {
collectWithVersion3(metrics, builder);
} else if (mqttVersion == MqttVersion.MQTT_5_0) {
collectWithVersion5(metrics, builder);
}
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_MQTT;
}
/**
* collecting data of MQTT 5
*/
private void collectWithVersion5(Metrics metrics, Builder builder) {
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
MqttProtocol mqttProtocol = metrics.getMqtt();
Assert.hasText(mqttProtocol.getHost(), "MQTT protocol host is required");
Assert.hasText(mqttProtocol.getPort(), "MQTT protocol port is required");
if ("mqtts".equalsIgnoreCase(mqttProtocol.getProtocol())) {
if (Boolean.parseBoolean(mqttProtocol.getEnableMutualAuth())) {
Assert.hasText(mqttProtocol.getCaCert(), "CA certificate is required for mutual auth");
Assert.hasText(mqttProtocol.getClientCert(), "Client certificate is required for mutual auth");
Assert.hasText(mqttProtocol.getClientKey(), "Client private key is required for mutual auth");
}
}
}
@Override
public void collect(Builder builder, Metrics metrics) {
MqttProtocol mqttProtocol = metrics.getMqtt();
Map<Object, String> data = new HashMap<>();
Mqtt5AsyncClient client = buildMqtt5Client(mqttProtocol);
long responseTime = connectClient(client, mqtt5AsyncClient -> {
CompletableFuture<Mqtt5ConnAck> connectFuture = mqtt5AsyncClient.connect();
try {
connectFuture.get(Long.parseLong(mqttProtocol.getTimeout()), TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(getErrorMessage(e.getMessage()));
try {
MqttAsyncClient client = buildMqttClient(mqttProtocol);
long responseTime = connectClient(client, mqttProtocol);
testSubscribeAndPublish(client, mqttProtocol, data);
convertToMetricsData(builder, metrics, responseTime, data);
client.disconnect();
} catch (Exception e) {
logger.error("MQTT collection error: {}", e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Collection failed: " + e.getMessage());
}
}
private MqttAsyncClient buildMqttClient(MqttProtocol protocol) throws Exception {
String clientId = protocol.getClientId();
String serverUri = String.format("%s://%s:%s",
StringUtils.equals(protocol.getProtocol(), "MQTT") ? "tcp" : "ssl",
protocol.getHost(),
protocol.getPort());
MqttClientPersistence persistence = new MemoryPersistence();
return new MqttAsyncClient(serverUri, clientId, persistence);
}
private long connectClient(MqttAsyncClient client, MqttProtocol protocol) throws Exception {
MqttConnectOptions connOpts = new MqttConnectOptions();
if (protocol.hasAuth()) {
connOpts.setUserName(protocol.getUsername());
connOpts.setPassword(protocol.getPassword().toCharArray());
}
connOpts.setKeepAliveInterval(Integer.parseInt(protocol.getKeepalive()));
connOpts.setConnectionTimeout(Integer.parseInt(protocol.getTimeout()) / 1000);
connOpts.setCleanSession(true);
connOpts.setAutomaticReconnect(false);
if ("mqtts".equalsIgnoreCase(protocol.getProtocol())) {
boolean insecureSkipVerify = Boolean.parseBoolean(protocol.getInsecureSkipVerify());
if (insecureSkipVerify) {
connOpts.setHttpsHostnameVerificationEnabled(false);
}
if (Boolean.parseBoolean(protocol.getEnableMutualAuth())) {
connOpts.setSocketFactory(MqttSslFactory.getMslSocketFactory(protocol, insecureSkipVerify));
} else {
connOpts.setSocketFactory(MqttSslFactory.getSslSocketFactory(protocol, insecureSkipVerify));
}
}
StopWatch connectWatch = new StopWatch();
connectWatch.start();
client.connect(connOpts).waitForCompletion(Long.parseLong(protocol.getTimeout()));
connectWatch.stop();
return connectWatch.getTotalTimeMillis();
}
/**
* Test MQTT subscribe and publish capabilities
*/
private void testSubscribeAndPublish(MqttAsyncClient client, MqttProtocol protocol, Map<Object, String> data) {
// 1 test subscribe
if (StringUtils.isNotBlank(protocol.getTopic())) {
String subscribe = testSubscribe(client, protocol.getTopic());
if (StringUtils.isBlank(subscribe)) {
data.put("canSubscribe", "Subscription successful");
} else {
data.put("canSubscribe", String.format("Subscription failed: %s", subscribe));
}
} else {
data.put("canSubscribe", "No topic, subscription test skipped");
}
// 2 test publish
if (StringUtils.isNotBlank(protocol.getTestMessage())) {
String publish = testPublish(client, protocol.getTopic(), protocol.getTestMessage());
if (StringUtils.isBlank(publish)) {
data.put("canPublish", "Message published successfully");
// 3 test receive message
String receivedData = getReceivedData(client, protocol.getTopic());
data.put("canReceive", receivedData);
} else {
data.put("canPublish", String.format("Message publishing failed: %s", publish));
data.put("canReceive", "Message reception skipped due to failed publish");
}
} else {
data.put("canPublish", "No test message, publish test skipped");
data.put("canReceive", "No test message, receive test skipped");
}
// 4 test unsubscribe
if (StringUtils.isNotBlank(protocol.getTopic())) {
String subscribe = testUnSubscribe(client, protocol.getTopic());
if (StringUtils.isBlank(subscribe)) {
data.put("canUnSubscribe", "Unsubscription successful");
} else {
data.put("canUnSubscribe", String.format("Unsubscription failed: %s", subscribe));
}
} else {
data.put("canUnSubscribe", "No topic, unsubscription test skipped");
}
}
private String getReceivedData(MqttAsyncClient client, String topic) {
final CountDownLatch latch = new CountDownLatch(1);
final StringBuilder messageHolder = new StringBuilder();
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable cause) {
latch.countDown();
}
@Override
public void messageArrived(String arrivedTopic, MqttMessage message) {
if (topic.equals(arrivedTopic)) {
messageHolder.append(new String(message.getPayload()));
latch.countDown();
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
}
});
testDescribeAndPublish5(client, mqttProtocol, data);
convertToMetricsData(builder, metrics, responseTime, data);
client.disconnect();
try {
boolean received = latch.await(5, TimeUnit.SECONDS);
if (messageHolder.length() > 0) {
return messageHolder.toString();
} else if (!received) {
return "Message reception timed out after 5 seconds";
} else {
return "No valid message received";
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return e.getMessage();
} finally {
client.setCallback(null);
}
}
private String testSubscribe(MqttAsyncClient client, String topic) {
try {
IMqttToken subToken = client.subscribe(topic, 1);
subToken.waitForCompletion(5000);
return "";
} catch (MqttException e) {
logger.warn("MQTT subscribe test failed: {}", e.getMessage());
return e.getMessage();
}
}
private String testPublish(MqttAsyncClient client, String topic, String message) {
try {
MqttMessage mqttMessage = new MqttMessage(message.getBytes());
mqttMessage.setQos(1);
IMqttToken pubToken = client.publish(topic, mqttMessage);
pubToken.waitForCompletion(5000);
return "";
} catch (MqttException e) {
logger.warn("MQTT publish test failed: {}", e.getMessage());
return e.getMessage();
}
}
private String testUnSubscribe(MqttAsyncClient client, String topic) {
try {
IMqttToken unsubToken = client.unsubscribe(topic);
unsubToken.waitForCompletion(5000);
return "";
} catch (MqttException e) {
logger.warn("MQTT unsubscribe test failed: {}", e.getMessage());
return e.getMessage();
}
}
/**
* collecting data of MQTT 3.1.1
* Convert collected data to MetricsData
*/
private void collectWithVersion3(Metrics metrics, Builder builder) {
MqttProtocol mqttProtocol = metrics.getMqtt();
Map<Object, String> data = new HashMap<>();
Mqtt3AsyncClient client = buildMqtt3Client(mqttProtocol);
long responseTime = connectClient(client, mqtt3AsyncClient -> {
CompletableFuture<Mqtt3ConnAck> connectFuture = mqtt3AsyncClient.connect();
try {
connectFuture.get(Long.parseLong(mqttProtocol.getTimeout()), TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(getErrorMessage(e.getMessage()));
}
});
testDescribeAndPublish3(client, mqttProtocol, data);
convertToMetricsData(builder, metrics, responseTime, data);
client.disconnect();
}
private void testDescribeAndPublish3(Mqtt3AsyncClient client, MqttProtocol mqttProtocol, Map<Object, String> data) {
data.put("canDescribe", test(() -> {
client.subscribeWith().topicFilter(mqttProtocol.getTopic()).qos(MqttQos.AT_LEAST_ONCE).send();
client.unsubscribeWith().topicFilter(mqttProtocol.getTopic()).send();
}, "subscribe").toString());
data.put("canPublish", !mqttProtocol.testPublish() ? Boolean.FALSE.toString() : test(() -> {
client.publishWith().topic(mqttProtocol.getTopic())
.payload(mqttProtocol.getTestMessage().getBytes(StandardCharsets.UTF_8))
.qos(MqttQos.AT_LEAST_ONCE).send();
data.put("canPublish", Boolean.TRUE.toString());
}, "publish").toString());
}
private void testDescribeAndPublish5(Mqtt5AsyncClient client, MqttProtocol mqttProtocol, Map<Object, String> data) {
data.put("canDescribe", test(() -> {
client.subscribeWith().topicFilter(mqttProtocol.getTopic()).qos(MqttQos.AT_LEAST_ONCE).send();
client.unsubscribeWith().topicFilter(mqttProtocol.getTopic()).send();
}, "subscribe").toString());
data.put("canPublish", !mqttProtocol.testPublish() ? Boolean.FALSE.toString() : test(() -> {
client.publishWith().topic(mqttProtocol.getTopic())
.payload(mqttProtocol.getTestMessage().getBytes(StandardCharsets.UTF_8))
.qos(MqttQos.AT_LEAST_ONCE).send();
data.put("canPublish", Boolean.TRUE.toString());
}, "publish").toString());
}
private Mqtt5AsyncClient buildMqtt5Client(MqttProtocol mqttProtocol) {
Mqtt5ClientBuilder mqtt5ClientBuilder = Mqtt5Client.builder()
.serverHost(mqttProtocol.getHost())
.identifier(mqttProtocol.getClientId())
.serverPort(Integer.parseInt(mqttProtocol.getPort()));
if (mqttProtocol.hasAuth()) {
mqtt5ClientBuilder.simpleAuth().username(mqttProtocol.getUsername())
.password(mqttProtocol.getPassword().getBytes(StandardCharsets.UTF_8))
.applySimpleAuth();
}
return mqtt5ClientBuilder.buildAsync();
}
private Mqtt3AsyncClient buildMqtt3Client(MqttProtocol mqttProtocol) {
Mqtt3ClientBuilder mqtt3ClientBuilder = Mqtt3Client.builder()
.serverHost(mqttProtocol.getHost())
.identifier(mqttProtocol.getClientId())
.serverPort(Integer.parseInt(mqttProtocol.getPort()));
if (mqttProtocol.hasAuth()) {
mqtt3ClientBuilder.simpleAuth().username(mqttProtocol.getUsername())
.password(mqttProtocol.getPassword().getBytes(StandardCharsets.UTF_8))
.applySimpleAuth();
}
return mqtt3ClientBuilder.buildAsync();
}
public <T> long connectClient(T client, Consumer<T> connect) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
connect.accept(client);
stopWatch.stop();
return stopWatch.getTotalTimeMillis();
}
private void convertToMetricsData(Builder builder, Metrics metrics, long responseTime, Map<Object, String> data) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String column : metrics.getAliasFields()) {
@@ -207,25 +288,4 @@ public class MqttCollectImpl extends AbstractCollect {
builder.addValueRow(valueRowBuilder.build());
}
private Boolean test(Runnable runnable, String operationName) {
try {
runnable.run();
return true;
} catch (Exception e) {
logger.error("{} fail", operationName, e);
}
return false;
}
private String getErrorMessage(String errorMessage) {
if (StringUtils.isBlank(errorMessage)) {
return "connect failed";
}
String[] split = errorMessage.split(":");
if (split.length > 1) {
return Arrays.stream(split).skip(1).collect(Collectors.joining(":"));
}
return errorMessage;
}
}
@@ -0,0 +1,186 @@
/*
* 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.collector.collect.mqtt;
import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collection;
/**
* Support MQTT SSL Factory
*/
public class MqttSslFactory {
/**
* Get MSL Socket Factory
*/
public static SSLSocketFactory getMslSocketFactory(MqttProtocol mqttProtocol, boolean insecureSkipVerify) {
try {
Security.addProvider(new BouncyCastleProvider());
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
Certificate[] chain = null;
if (mqttProtocol.getClientCert() != null && !mqttProtocol.getClientCert().isEmpty()) {
String formatClientCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getClientCert());
try (InputStream certIn = new ByteArrayInputStream(formatClientCert.getBytes())) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certs = cf.generateCertificates(certIn);
chain = certs.toArray(new Certificate[0]);
}
}
PrivateKey privateKey;
if (mqttProtocol.getClientKey() != null && !mqttProtocol.getClientKey().isEmpty()) {
String formatClientKey = CertificateFormatter.formatPrivateKey(mqttProtocol.getClientKey());
try (PEMParser pemParser = new PEMParser(new StringReader(formatClientKey))) {
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
Object object = pemParser.readObject();
if (object instanceof PEMKeyPair) {
privateKey = converter.getPrivateKey(((PEMKeyPair) object).getPrivateKeyInfo());
} else if (object instanceof PrivateKeyInfo) {
privateKey = converter.getPrivateKey((PrivateKeyInfo) object);
} else {
throw new IllegalArgumentException("Unsupported private key type");
}
ks.setKeyEntry("private-key", privateKey, "".toCharArray(), chain);
}
}
TrustManager[] trustManagers;
if (insecureSkipVerify) {
trustManagers = createInsecureTrustManager();
} else {
String formatCaCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getCaCert());
KeyStore trustStore = createMergedTrustStore(formatCaCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
trustManagers = tmf.getTrustManagers();
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "".toCharArray());
SSLContext context = SSLContext.getInstance(mqttProtocol.getTlsVersion());
context.init(kmf.getKeyManagers(), trustManagers, null);
return context.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException("Fails to SSL initialize: " + e.getMessage(), e);
}
}
/**
* Get SSL Socket Factory
*/
public static SSLSocketFactory getSslSocketFactory(MqttProtocol mqttProtocol, boolean insecureSkipVerify) {
try {
Security.addProvider(new BouncyCastleProvider());
TrustManager[] trustManagers;
if (insecureSkipVerify) {
trustManagers = createInsecureTrustManager();
} else {
String formatCaCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getCaCert());
KeyStore trustStore = createMergedTrustStore(formatCaCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
trustManagers = tmf.getTrustManagers();
}
SSLContext sslContext = SSLContext.getInstance(mqttProtocol.getTlsVersion());
sslContext.init(null, trustManagers, null);
return sslContext.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException("Fails to SSL initialize: " + e.getMessage(), e);
}
}
private static TrustManager[] createInsecureTrustManager() {
return new TrustManager[]{
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};
}
private static KeyStore createMergedTrustStore(String caCertPem) throws Exception {
KeyStore mergedKs = KeyStore.getInstance(KeyStore.getDefaultType());
mergedKs.load(null, null);
TrustManagerFactory systemTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
systemTmf.init((KeyStore) null);
X509TrustManager systemTm = (X509TrustManager) systemTmf.getTrustManagers()[0];
int systemIndex = 1;
for (X509Certificate cert : systemTm.getAcceptedIssuers()) {
mergedKs.setCertificateEntry("system-ca-" + systemIndex++, cert);
}
if (caCertPem != null && !caCertPem.isEmpty()) {
try (InputStream caIn = new ByteArrayInputStream(caCertPem.getBytes())) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> customCerts = cf.generateCertificates(caIn);
int customIndex = 1;
for (Certificate cert : customCerts) {
mergedKs.setCertificateEntry("custom-ca-" + customIndex++, cert);
}
}
}
return mergedKs;
}
}
@@ -17,11 +17,6 @@
package org.apache.hertzbeat.collector.collect.database;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol;
@@ -29,6 +24,14 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Test case for {@link JdbcCommonCollect}
*/
@@ -128,4 +131,81 @@ class JdbcCommonCollectTest {
String protocol = jdbcCommonCollect.supportProtocol();
assertEquals(DispatchConstants.PROTOCOL_JDBC, protocol);
}
@Test
void testUrlPassThrough() {
String[] testUrls = {
"jdbc:mysql://localhost:3306/test?allowPublicKeyRetrieval=true&useSSL=false",
"jdbc:mysql://localhost:3306/test?usessl=false&verifyServerCertificate=true",
"jdbc:mysql://localhost:3306/test?serverTimezone=UTC&autoReconnect=false"
};
JdbcCommonCollect jdbcCollect = new JdbcCommonCollect();
for (String originalUrl : testUrls) {
try {
JdbcProtocol jdbcProtocol = JdbcProtocol.builder()
.host("localhost")
.port("3306")
.platform("mysql")
.username("root")
.password("root")
.database("test")
.url(originalUrl)
.build();
// Use reflection to call constructDatabaseUrl method
Method constructMethod = JdbcCommonCollect.class.getDeclaredMethod("constructDatabaseUrl", JdbcProtocol.class, String.class, String.class);
constructMethod.setAccessible(true);
String processedUrl = (String) constructMethod.invoke(jdbcCollect, jdbcProtocol, "localhost", "3306");
// Verify that the processed URL is the same as the original URL
assertEquals(originalUrl, processedUrl,
"URL should be passed through without modification: " + originalUrl);
} catch (Exception e) {
System.out.println("URL rejected by security validation: " + originalUrl + ", reason: " + e.getMessage());
}
}
}
@Test
void testConstructDatabaseUrlSecurityInterception() {
JdbcCommonCollect jdbcCollect = new JdbcCommonCollect();
String[] maliciousUrls = {
// URL length limit test
"jdbc:mysql://localhost:3306/test?" + "a".repeat(2050) + "=value",
// url format check
"jdbca:mysql://localhost:3306/test?allowLoadLocalInfile=true",
// backlist check
"jdbc:mysql://localhost:3306/test?allowLoadLocalInfile=true",
// universal detection of JDBC injection and deserialization attacks
"jdbc:mysql://localhost:3306/test?jndi:ldap://duansg.com/exploit",
// universal detection of bypass
"jdbc:mysql://localhost:3306/test?param=create\\trigger",
// database platform specific bypass detection
"jdbc:mysql://localhost:3306/test?allow\\nload\\nlocal\\ninfile=true"
};
// Test malicious URLs - should throw exceptions
for (String maliciousUrl : maliciousUrls) {
JdbcProtocol jdbcProtocol = JdbcProtocol.builder()
.host("localhost")
.port("3306")
.platform("mysql")
.username("root")
.password("root")
.database("test")
.url(maliciousUrl)
.build();
assertThrows(Exception.class, () -> {
try {
Method constructMethod = JdbcCommonCollect.class.getDeclaredMethod("constructDatabaseUrl", JdbcProtocol.class, String.class, String.class);
constructMethod.setAccessible(true);
constructMethod.invoke(jdbcCollect, jdbcProtocol, "localhost", "3306");
} catch (InvocationTargetException e) {
throw e.getCause();
}
}, "Malicious URL should be blocked: " + maliciousUrl);
}
}
}
@@ -17,14 +17,7 @@
package org.apache.hertzbeat.collector.collect.http;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
@@ -32,6 +25,15 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Test case for {@link HttpCollectImpl}
*/
@@ -73,6 +75,30 @@ class HttpCollectImplTest {
assert "http".equals(protocol);
}
@Test
void parseResponseByWebsite() {
HttpProtocol http = HttpProtocol.builder().build();
http.setMethod("GET");
http.setHost("http://127.0.0.1");
http.setUrl("/");
http.setPort("8428");
http.setParseType("website");
http.setEnableUrlEncoding("true");
Metrics metrics = Metrics.builder()
.http(http)
.aliasFields(Lists.newArrayList("responseTime", "keyword", "statusCode"))
.build();
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
httpCollectImpl.collect(builder, metrics);
assertNotNull(builder.getValuesList());
for (CollectRep.ValueRow row : builder.getValuesList()) {
assertNotNull(row.getColumns(0));
assertEquals(row.getColumns(1), "0");
assertEquals(row.getColumns(2), "200");
}
}
@Test
void parseResponseByXmlPath() throws Exception {
// Create a sample XML response
@@ -160,4 +186,107 @@ class HttpCollectImplTest {
assertEquals("0.0", secondRow.getColumns(2), "Second server CPU should be 0.0");
assertEquals("0", secondRow.getColumns(3), "Second server memory should be 0");
}
@Test
void parseResponseByJsonPath() throws Exception {
String jsonResponse = "{"
+ " \"name\": \"jvm.memory.used\","
+ " \"description\": \"The amount of used memory\","
+ " \"baseUnit\": \"bytes\","
+ " \"measurements\": ["
+ " {"
+ " \"statistic\": \"VALUE\","
+ " \"value\": 90282296"
+ " }"
+ " ],"
+ " \"availableTags\": ["
+ " {"
+ " \"tag\": \"area\","
+ " \"values\": ["
+ " \"heap\","
+ " \"nonheap\""
+ " ]"
+ " },"
+ " {"
+ " \"tag\": \"id\","
+ " \"values\": ["
+ " \"G1 Survivor Space\","
+ " \"G1 Eden Space\""
+ " ]"
+ " }"
+ " ]"
+ "}";
HttpProtocol http = HttpProtocol.builder()
.parseType(DispatchConstants.PARSE_JSON_PATH)
.parseScript("$.availableTags[?(@.tag == \"id\")].values[*]")
.build();
List<CollectRep.ValueRow> capturedRows = new ArrayList<>();
CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() {
@Override
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
capturedRows.add(valueRow);
return super.addValueRow(valueRow);
}
};
Method parseMethod = HttpCollectImpl.class.getDeclaredMethod(
"parseResponseByJsonPath",
String.class,
List.class,
HttpProtocol.class,
CollectRep.MetricsData.Builder.class,
Long.class);
parseMethod.setAccessible(true);
// Call the method
parseMethod.invoke(httpCollectImpl, jsonResponse, Lists.newArrayList("id"), http, builder, 100L);
// Verify the results
assertEquals(2, capturedRows.size());
CollectRep.ValueRow firstRow = capturedRows.get(0);
assertEquals("G1 Survivor Space", firstRow.getColumns(0));
CollectRep.ValueRow secondRow = capturedRows.get(1);
assertEquals("G1 Eden Space", secondRow.getColumns(0));
// number
String numberJson = "{"
+ " \"name\": \"system.cpu.usage\","
+ " \"description\": \"The \\\"recent cpu usage\\\" of the system the application is running in\","
+ " \"measurements\": ["
+ " {"
+ " \"statistic\": \"VALUE\","
+ " \"value\": 0.268751364291017"
+ " }"
+ " ],"
+ " \"availableTags\": []"
+ "}";
http = HttpProtocol.builder()
.parseType(DispatchConstants.PARSE_JSON_PATH)
.parseScript("$.measurements[?(@.statistic == \"VALUE\")].value")
.build();
capturedRows.clear();
builder = new CollectRep.MetricsData.Builder() {
@Override
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
capturedRows.add(valueRow);
return super.addValueRow(valueRow);
}
};
parseMethod = HttpCollectImpl.class.getDeclaredMethod(
"parseResponseByJsonPath",
String.class,
List.class,
HttpProtocol.class,
CollectRep.MetricsData.Builder.class,
Long.class);
parseMethod.setAccessible(true);
// Call the method
parseMethod.invoke(httpCollectImpl, numberJson, Lists.newArrayList("usage"), http, builder, 100L);
// Verify the results
assertEquals(1, capturedRows.size());
firstRow = capturedRows.get(0);
assertEquals("0.268751364291017", firstRow.getColumns(0));
}
}
@@ -17,108 +17,91 @@
package org.apache.hertzbeat.collector.collect.mqtt;
import com.hivemq.client.mqtt.MqttVersion;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Test case for {@link MqttCollectImpl}
*/
public class MqttCollectTest {
class MqttCollectTest {
private MqttCollectImpl mqttCollect;
private Metrics metrics;
private CollectRep.MetricsData.Builder builder;
private MqttProtocol.MqttProtocolBuilder mqttBuilder;
@BeforeEach
public void setup() {
void setup() {
mqttCollect = new MqttCollectImpl();
MqttProtocol mqtt = MqttProtocol.builder().build();
metrics = Metrics.builder()
.mqtt(mqtt)
.build();
builder = CollectRep.MetricsData.newBuilder();
metrics = new Metrics();
// Initialize base MQTT parameters for test cases
mqttBuilder = MqttProtocol.builder()
.host("example.com")
.port("1883")
.protocol("mqtt")
.timeout("5000")
.keepalive("60");
}
// Region: preCheck validation tests
@Test
// Verify preCheck throws exception when host is missing
void preCheckShouldThrowWhenHostMissing() {
metrics.setMqtt(mqttBuilder.host("").build());
assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics));
}
@Test
void preCheck() {
// host is empty
assertThrows(IllegalArgumentException.class, () -> {
mqttCollect.preCheck(metrics);
});
// port is empty
assertThrows(IllegalArgumentException.class, () -> {
MqttProtocol mqtt = MqttProtocol.builder().build();
mqtt.setHost("example.com");
metrics.setMqtt(mqtt);
mqttCollect.preCheck(metrics);
});
// protocol version is empty
assertThrows(IllegalArgumentException.class, () -> {
MqttProtocol mqtt = MqttProtocol.builder().build();
mqtt.setHost("example.com");
mqtt.setPort("1883");
metrics.setMqtt(mqtt);
mqttCollect.preCheck(metrics);
});
// everything is ok
assertDoesNotThrow(() -> {
MqttProtocol mqtt = MqttProtocol.builder().build();
mqtt.setHost("example.com");
mqtt.setPort("1883");
metrics.setMqtt(mqtt);
mqtt.setProtocolVersion("3.1.1");
mqttCollect.preCheck(metrics);
});
// Verify preCheck throws exception when port is missing
void preCheckShouldThrowWhenPortMissing() {
metrics.setMqtt(mqttBuilder.port("").build());
assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics));
}
@Test
void supportProtocol() {
Assertions.assertEquals(DispatchConstants.PROTOCOL_MQTT, mqttCollect.supportProtocol());
// Verify preCheck throws exception when MQTTS mutual auth is enabled but CA cert is missing
void preCheckShouldThrowWhenMqttsMutualAuthMissingCerts() {
metrics.setMqtt(mqttBuilder
.protocol("mqtts")
.enableMutualAuth("true")
.caCert("")
.clientCert("client.crt")
.clientKey("client.key")
.build());
assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics));
}
@Test
void collect() {
// with version 3.1.1
assertDoesNotThrow(() -> {
MqttProtocol mqtt = MqttProtocol.builder().build();
mqtt.setHost("example.com");
mqtt.setPort("1883");
mqtt.setClientId("clientid");
mqtt.setTimeout("1");
mqtt.setProtocolVersion(MqttVersion.MQTT_3_1_1.name());
// Verify preCheck succeeds with valid standard MQTT parameters
void preCheckShouldSucceedWithValidMqttParams() {
metrics.setMqtt(mqttBuilder.build());
assertDoesNotThrow(() -> mqttCollect.preCheck(metrics));
}
metrics.setMqtt(mqtt);
metrics.setAliasFields(new ArrayList<>());
@Test
// Verify preCheck succeeds with valid MQTTS parameters including mutual authentication
void preCheckShouldSucceedWithValidMqttsMutualAuth() {
metrics.setMqtt(mqttBuilder
.protocol("mqtts")
.enableMutualAuth("true")
.caCert("ca.pem")
.clientCert("client.crt")
.clientKey("client.key")
.build());
assertDoesNotThrow(() -> mqttCollect.preCheck(metrics));
}
// End region
mqttCollect.collect(builder, metrics);
});
assertDoesNotThrow(() -> {
MqttProtocol mqtt = MqttProtocol.builder().build();
mqtt.setHost("example.com");
mqtt.setPort("1883");
mqtt.setClientId("clientid");
mqtt.setTimeout("1");
mqtt.setProtocolVersion(MqttVersion.MQTT_5_0.name());
metrics.setMqtt(mqtt);
metrics.setAliasFields(new ArrayList<>());
mqttCollect.collect(builder, metrics);
});
@Test
// Verify supportProtocol method returns correct MQTT constant
void supportProtocolShouldReturnMqttConstant() {
assertEquals(DispatchConstants.PROTOCOL_MQTT, mqttCollect.supportProtocol());
}
}
@@ -70,14 +70,11 @@
<version>${hertzbeat.version}</version>
</dependency>
<!-- spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
@@ -87,7 +84,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- metrics -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
<build>
@@ -23,6 +23,7 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
import org.apache.hertzbeat.collector.metrics.HertzBeatMetricsCollector;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
@@ -33,6 +34,7 @@ import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
@@ -90,6 +92,9 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
private final String collectorIdentity;
@Autowired
private HertzBeatMetricsCollector metricsCollector;
public CommonDispatcher(MetricsCollectorQueue jobRequestQueue,
TimerDispatch timerDispatch,
CommonDataQueue commonDataQueue,
@@ -153,12 +158,23 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
for (Map.Entry<String, MetricsTime> entry : metricsTimeoutMonitorMap.entrySet()) {
MetricsTime metricsTime = entry.getValue();
if (metricsTime.getStartTime() < deadline) {
// Metrics collection timeout
// Metrics collection timeout
MetricsTime removedMetricsTime = metricsTimeoutMonitorMap.remove(entry.getKey());
if (removedMetricsTime == null) {
continue;
}
WheelTimerTask timerJob = (WheelTimerTask) metricsTime.getTimeout().task();
Job job = timerJob.getJob();
// timeout metrics
if (metricsCollector != null) {
long duration = System.currentTimeMillis() - removedMetricsTime.getStartTime();
metricsCollector.recordCollectMetrics(job, duration, "timeout");
}
CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder()
.setId(timerJob.getJob().getMonitorId())
.setTenantId(timerJob.getJob().getTenantId())
.setApp(timerJob.getJob().getApp())
.setId(job.getMonitorId())
.setTenantId(job.getTenantId())
.setApp(job.getApp())
.setMetrics(metricsTime.getMetrics().getName())
.setPriority(metricsTime.getMetrics().getPriority())
.setTime(System.currentTimeMillis())
@@ -167,7 +183,6 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
if (metricsData.getPriority() == 0) {
dispatchCollectData(metricsTime.timeout, metricsTime.getMetrics(), metricsData);
}
metricsTimeoutMonitorMap.remove(entry.getKey());
}
}
} catch (Exception e) {
@@ -177,7 +192,7 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
@Override
public void dispatchMetricsTask(Timeout timeout) {
// Divide the collection task of a single application into corresponding collection tasks of the metrics according to the metrics under it.
// Divide the collection task of a single application into corresponding collection tasks of the metrics under it.
// Put each collect task into the thread pool for scheduling
WheelTimerTask timerTask = (WheelTimerTask) timeout.task();
Job job = timerTask.getJob();
@@ -201,16 +216,27 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) {
WheelTimerTask timerJob = (WheelTimerTask) timeout.task();
Job job = timerJob.getJob();
String monitorKey;
if (metrics.isHasSubTask()) {
monitorKey = job.getId() + "-" + metrics.getName() + "-sub-" + metrics.getSubTaskId();
} else {
monitorKey = job.getId() + "-" + metrics.getName();
}
MetricsTime metricsTime = metricsTimeoutMonitorMap.remove(monitorKey);
// job completed metrics
if (metricsTime != null && metricsCollector != null) {
long duration = System.currentTimeMillis() - metricsTime.getStartTime();
String status = metricsData.getCode() == CollectRep.Code.SUCCESS ? "success" : "fail";
metricsCollector.recordCollectMetrics(job, duration, status);
}
if (metrics.isHasSubTask()) {
metricsTimeoutMonitorMap.remove(job.getId() + "-" + metrics.getName() + "-sub-" + metrics.getSubTaskId());
boolean isLastTask = metrics.consumeSubTaskResponse(metricsData);
if (isLastTask) {
metricsData = metrics.getSubTaskDataRef().get().build();
} else {
return;
}
} else {
metricsTimeoutMonitorMap.remove(job.getId() + "-" + metrics.getName());
}
Set<Metrics> metricsSet = job.getNextCollectMetrics(metrics, false);
if (job.isCyclic()) {
@@ -322,7 +348,13 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) {
WheelTimerTask timerJob = (WheelTimerTask) timeout.task();
Job job = timerJob.getJob();
metricsTimeoutMonitorMap.remove(String.valueOf(job.getId()));
MetricsTime metricsTime = metricsTimeoutMonitorMap.remove(String.valueOf(job.getId()));
if (metricsTime != null && metricsCollector != null) {
long duration = System.currentTimeMillis() - metricsTime.getStartTime();
// For a list, we consider it a success if at least one item is successful.
boolean isSuccess = metricsDataList.stream().anyMatch(item -> item.getCode() == CollectRep.Code.SUCCESS);
metricsCollector.recordCollectMetrics(job, duration, isSuccess ? "success" : "fail");
}
if (job.isCyclic()) {
// The collection and execution of all task of this job are completed.
// The periodic task pushes the task to the time wheel again.
@@ -340,7 +372,6 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
// and the result listener is notified of the combination of all metrics data
timerDispatch.responseSyncJobData(job.getId(), metricsDataList);
}
}
@@ -354,4 +385,4 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
private Metrics metrics;
private Timeout timeout;
}
}
}
@@ -422,6 +422,9 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
*/
private Object[] transformCal(String cal, Map<String, String> fieldAliasMap) {
int splitIndex = cal.indexOf("=");
if (splitIndex < 0) {
return null;
}
String field = cal.substring(0, splitIndex).trim();
String expressionStr = cal.substring(splitIndex + 1).trim().replace("\\#", "#");
JexlExpression expression;
@@ -0,0 +1,89 @@
/*
* 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.collector.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.job.Job;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Map;
/**
* Service for managing and recording Micrometer metrics.
* This service uses Micrometer which integrates natively with Spring Boot Actuator.
*/
@Service
@Slf4j
public class HertzBeatMetricsCollector {
private final MeterRegistry meterRegistry;
public HertzBeatMetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
log.info("MetricsService initialized with MeterRegistry: {}", meterRegistry.getClass().getSimpleName());
}
/**
* Records the metrics for a completed collection sub-task.
*
* @param job The parent job containing monitor info.
* @param durationMillis The duration of the collection task in milliseconds.
* @param status The final status of the collection ("success", "fail", "timeout").
*/
public void recordCollectMetrics(Job job, long durationMillis, String status) {
if (job == null) {
return;
}
Map<String, String> metadata = job.getMetadata();
String monitorName = metadata != null ? metadata.get("instancename") : "unknown";
String monitorTarget = metadata != null ? metadata.get("instancehost") : "unknown";
// Record collection count
Counter.builder("hertzbeat.collect.total")
.description("The total number of collection tasks executed")
.tag("status", status)
.tag("monitor_type", job.getApp())
.tag("monitor_id", String.valueOf(job.getMonitorId()))
.tag("monitor_name", monitorName)
.tag("monitor_target", monitorTarget)
.register(meterRegistry)
.increment();
// Record collection duration
Timer.builder("hertzbeat.collect.duration")
.description("The duration of collection task executions")
.tag("status", status)
.tag("monitor_type", job.getApp())
.tag("monitor_id", String.valueOf(job.getMonitorId()))
.tag("monitor_name", monitorName)
.tag("monitor_target", monitorTarget)
.register(meterRegistry)
.record(Duration.ofMillis(durationMillis));
if (log.isDebugEnabled()) {
log.debug("Recorded metrics for monitor [{}] ({}): status={}, duration={}ms",
monitorName, job.getMonitorId(), status, durationMillis);
}
}
}
@@ -30,7 +30,23 @@ spring:
# need to disable spring boot mongodb auto config, or default mongodb connection tried and failed...
autoconfigure:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
management:
endpoints:
web:
exposure:
include:
- 'prometheus'
endpoint:
prometheus:
access: read_only
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
prometheus:
metrics:
export:
enabled: true
---
spring:
config:
@@ -39,7 +55,7 @@ spring:
collector:
info:
version: ${COLLECTOR_VERSION:1.7.1}
version: ${COLLECTOR_VERSION:1.7.2}
ip: ${COLLECTOR_IP:}
dispatch:
entrance:
@@ -47,6 +47,6 @@ public interface CollectorConstants extends NetworkConstants {
String RESPONSE_TIME = "responseTime";
String STATUS_CODE = "StatusCode";
String STATUS_CODE = "statusCode";
}
@@ -22,8 +22,11 @@ import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.dto.ServerInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
/**
@@ -40,6 +43,16 @@ public class GoOnlineProcessor implements NettyRemotingProcessor {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
if (message.getMsg().isEmpty()) {
log.warn("The message that server response to collector is empty, please upgrade server");
} else {
ServerInfo serverInfo = JsonUtil.fromJson(message.getMsg().toStringUtf8(), ServerInfo.class);
if (serverInfo == null || serverInfo.getAesSecret() == null) {
log.warn("The message that server response to collector has not secret empty, please check");
} else {
AesUtil.setDefaultSecretKey(serverInfo.getAesSecret());
}
}
timerDispatch.goOnline();
log.info("receive online message and handle success");
return ClusterMsg.Message.newBuilder()
@@ -19,7 +19,6 @@ package org.apache.hertzbeat.common.config;
import org.apache.hertzbeat.common.constants.ConfigConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.util.AesUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
@@ -34,10 +33,4 @@ import org.springframework.context.annotation.ComponentScan;
+ ConfigConstants.FunctionModuleConstants.COMMON)
@EnableConfigurationProperties(CommonProperties.class)
public class CommonConfig {
public CommonConfig(CommonProperties commonProperties) {
if (commonProperties != null && commonProperties.getSecret() != null) {
AesUtil.setDefaultSecretKey(commonProperties.getSecret());
}
}
}
@@ -87,6 +87,11 @@ public interface CommonConstants {
*/
String LABEL_INSTANCE = "instance";
/**
* label key: defineid
*/
String LABEL_DEFINE_ID = "defineid";
/**
* label key: alert name
*/
@@ -367,4 +372,9 @@ public interface CommonConstants {
* status page incident state resolved
*/
byte MONITOR_TYPE_DISCOVERY_AUTO_CREATE = 2;
/**
* JEXL custom function `json`
*/
String JEXL_CUSTOM_JSON_FUNCTION = "json";
}
@@ -0,0 +1,62 @@
/*
* 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.common.constants;
import java.util.Arrays;
/**
* Jexl keywords enum
*/
public enum JexlKeywordsEnum {
SIZE("size"),
EMPTY("empty"),
NEW("new"),
VAR("var"),
RETURN("return"),
IF("if"),
ELSE("else"),
ELSEIF("elseif"),
WHILE("while"),
DO("do"),
FOR("for"),
CONTINUE("continue"),
BREAK("break"),
TRUE("true"),
FALSE("false"),
NULL("null"),
UNDEFINED("undefined");
private final String keyword;
JexlKeywordsEnum(String keyword) {
this.keyword = keyword;
}
public String getKeyword() {
return keyword;
}
public static boolean match(String word) {
if (word == null || word.trim().isEmpty()) {
return false;
}
return Arrays.stream(values()).anyMatch(t -> t.keyword.equals(word));
}
}
@@ -81,12 +81,12 @@ public class GroupAlert {
@Schema(title = "Common Annotations", example = "{\"summary\": \"High CPU usage detected\", \"description\": \"CPU usage is back to normal for server1\"}")
@Convert(converter = JsonMapAttributeConverter.class)
@Column(length = 4096)
@Column(columnDefinition = "TEXT")
private Map<String, String> commonAnnotations;
@Schema(title = "Alert Fingerprints", example = "[\"dxsdfdsf\"]")
@Convert(converter = JsonStringListAttributeConverter.class)
@Column(length = 8192)
@Column(columnDefinition = "TEXT")
private List<String> alertFingerprints;
@Schema(title = "The creator of this record", example = "tom")
@@ -0,0 +1,40 @@
/*
* 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.common.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* collector info
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "manager server info")
public class ServerInfo {
/**
* the aes secret key value for aes encode decode
*/
private String aesSecret;
}
@@ -33,49 +33,73 @@ import org.apache.commons.lang3.StringUtils;
public class MqttProtocol implements CommonRequestProtocol, Protocol {
/**
* ip address or domain name of the peer host
*/
private String host;
/**
* peer host port
*/
private String port;
/**
* username
*/
private String username;
/**
* password
*/
private String password;
/**
* time out period
*/
private String timeout;
/**
* client id
* mqtt client id
*/
private String clientId;
/**
* message used to test whether the mqtt connection can be pushed normally
* mqtt username
*/
private String testMessage;
private String username;
/**
* protocol version of mqtt
* mqtt password
*/
private String protocolVersion;
private String password;
/**
* monitor topic
* mqtt host
*/
private String host;
/**
* mqtt port
*/
private String port;
/**
* mqtt protocol version
* MQTT,MQTTS
*/
private String protocol;
/**
* mqtt connect timeout
* the maximum time to wait for a connection to be established
*/
private String timeout;
/**
* mqtt keepalive
* between ping requests to the broker to keep the connection alive
*/
private String keepalive;
/**
* mqtt topic name
*/
private String topic;
/**
* mqtt publish message
*/
private String testMessage;
/**
* mqtt tls version
* TLSv1.2, TLSv1.3
*/
private String tlsVersion;
/**
* mqtt tls insecure skip verify server certificate
*/
private String insecureSkipVerify;
/**
* mqtt tls ca cert
*/
private String caCert;
/**
* mqtt tls enable mutual auth
*/
private String enableMutualAuth;
/**
* mqtt tls client cert
*/
private String clientCert;
/**
* mqtt tls client key
*/
private String clientKey;
/**
* Determine whether authentication is required
@@ -85,11 +109,4 @@ public class MqttProtocol implements CommonRequestProtocol, Protocol {
return StringUtils.isNotBlank(this.username) && StringUtils.isNotBlank(this.password);
}
/**
* Determine whether you need to test whether messages can be pushed normally
* @return turn if it has test message
*/
public boolean testPublish(){
return StringUtils.isNotBlank(this.testMessage);
}
}
@@ -0,0 +1,28 @@
/*
* 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.common.support.exception;
/**
* Alert expression exception
*/
public class AlertExpressionException extends RuntimeException {
public AlertExpressionException(String message) {
super(message);
}
}
@@ -0,0 +1,29 @@
/*
* 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.common.support.exception;
/**
* Expression visitor exception
*/
public class ExpressionVisitorException extends RuntimeException {
public ExpressionVisitorException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -37,7 +37,7 @@ public final class AesUtil {
* Default encryption key The AES encryption key is 16 bits by default.
* If the AES encryption key is larger than or smaller than 16 bits, an error message is displayed
*/
private static final String ENCODE_RULES = "tomSun28HaHaHaHa";
public static final String DEFAULT_ENCODE_RULES = "tomSun28HaHaHaHa";
/**
* Default algorithm
@@ -50,13 +50,17 @@ public final class AesUtil {
* Encryption key The AES encryption key is 16 bits.
* If the AES encryption key is larger than 16 bits, an error message is displayed
*/
private static String secretKey = ENCODE_RULES;
private static String secretKey = DEFAULT_ENCODE_RULES;
private AesUtil() {}
public static void setDefaultSecretKey(String secretKeyNow) {
secretKey = secretKeyNow;
}
public static String getDefaultSecretKey() {
return secretKey;
}
public static String aesEncode(String content) {
return aesEncode(content, secretKey);
@@ -109,9 +113,9 @@ public final class AesUtil {
byte[] byteDecode = getBytes(content, decryptKey);
return new String(byteDecode, StandardCharsets.UTF_8);
} catch (BadPaddingException e) {
if (!ENCODE_RULES.equals(decryptKey)) {
if (!DEFAULT_ENCODE_RULES.equals(decryptKey)) {
log.warn("There has default encode secret encode content, try to decode with default secret key");
return aesDecode(content, ENCODE_RULES);
return aesDecode(content, DEFAULT_ENCODE_RULES);
}
log.error("aes decode content error: {}, please config right common secret key", e.getMessage());
return content;
@@ -19,15 +19,19 @@
package org.apache.hertzbeat.common.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Maps;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.JexlFeatures;
import org.apache.commons.jexl3.MapContext;
import org.apache.hertzbeat.common.constants.CommonConstants;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.function.Function;
/**
* jexl express runner
@@ -41,6 +45,7 @@ public class JexlExpressionRunner {
Map<String, Object> functions = Maps.newLinkedHashMap();
// set the root namespace function
functions.put(null, new JexlCommonFunction());
functions.put(CommonConstants.JEXL_CUSTOM_JSON_FUNCTION, jsonFunction());
ClassLoader classLoader = new ClassLoader() {
@Override
public String getName() {
@@ -78,5 +83,17 @@ public class JexlExpressionRunner {
public static JexlExpression compile(String expression) {
return jexlEngine.createExpression(expression);
}
/**
* custom function `json:apply(xx)`
*
* @return java.util.function.Function
*/
private static Function<String, Map<String, Object>> jsonFunction() {
return json -> {
TypeReference<Map<String, Object>> typeReference = new TypeReference<>() {
};
return JsonUtil.fromJson(json, typeReference);
};
}
}
@@ -17,6 +17,8 @@
package org.apache.hertzbeat.common.util;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -44,6 +46,7 @@ public final class JsonUtil {
OBJECT_MAPPER
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
.registerModule(new JavaTimeModule());
}
@@ -0,0 +1,167 @@
/*
* 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.common.util;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.slf4j.Logger;
import java.text.MessageFormat;
/**
* Log utility class that provides formatted logging methods with location information.
* This class enhances standard SLF4J logging by automatically adding caller location details.
*/
public class LogUtil {
private static final String TEMPLATE_REGEX = "\\{\\d}";
/**
* Print debug level formatted log
* Example: LogUtil.debug(logger, "hello,{0},here has a {1} exception", "other information");
*/
@SuppressWarnings("unused")
public static void debug(Logger logger, String msg, Object... params) {
if (logger.isDebugEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.debug(LogUtil.buildLocationInfo() + msg);
} else {
logger.debug(LogUtil.buildLocationInfo() + format(msg, params));
}
}
}
/**
* Print info level formatted log
* Example: LogUtil.info(logger, "hello,{0},{1} exception", "dear", "database operation");
*/
public static void info(Logger logger, String msg, Object... params) {
if (logger.isInfoEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.info(LogUtil.buildLocationInfo() + msg);
} else {
logger.info(LogUtil.buildLocationInfo() + format(msg, params));
}
}
}
/**
* Print warn level formatted log
*/
public static void warn(Logger logger, String msg, Object... params) {
if (logger.isWarnEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.warn(LogUtil.buildLocationInfo() + msg);
} else {
logger.warn(LogUtil.buildLocationInfo() + format(msg, params));
}
}
}
/**
* Print error level formatted log, use {0},{1},.. for parameter replacement
* Example: LogUtil.error(logger, "hello,{0}, a {1} exception occurred here", "dear", "database operation");
*/
public static void error(Logger logger, String msg, Object... params) {
if (logger.isErrorEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.error(LogUtil.buildLocationInfo() + msg);
} else {
logger.error(LogUtil.buildLocationInfo() + format(msg, params));
}
}
}
/**
* Print warn level formatted log with exception, use {0},{1},.. for parameter replacement
* Example: LogUtil.warn(logger, e, "hello,{0}, a {1} exception occurred here", "dear", "database operation");
*/
public static void warn(Logger logger, Throwable e, String msg, Object... params) {
if (logger.isWarnEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.warn(LogUtil.buildLocationInfo() + msg, e);
} else {
logger.warn(LogUtil.buildLocationInfo() + format(msg, params), e);
}
}
}
/**
* Print error level formatted log with exception, use {0},{1},.. for parameter replacement
* Example: LogUtil.error(logger, e, "hello,{0}, a {1} exception occurred here", "dear", "database operation");
*/
public static void error(Logger logger, Throwable e, String msg, Object... params) {
if (logger.isErrorEnabled()) {
if (ArrayUtils.isEmpty(params)) {
logger.error(LogUtil.buildLocationInfo() + msg, e);
} else {
logger.error(LogUtil.buildLocationInfo() + format(msg, params), e);
}
}
}
/**
* Get the class name, method and line number that calls LogUtil
*
* @return location information string
*/
private static String buildLocationInfo() {
StringBuilder header = new StringBuilder();
// LOG4J2-1029 new Throwable().getStackTrace is faster than Thread.currentThread().getStackTrace().
final StackTraceElement[] stackTraceElements = new Throwable().getStackTrace();
for (int i = 0; i < stackTraceElements.length - 1; i++) {
StackTraceElement currentStackTrace = stackTraceElements[i];
StackTraceElement nextStackTrace = stackTraceElements[i + 1];
// If current stack trace is in LogUtil
// and next stack trace is not in LogUtil
// then the next node is the caller of LogUtil
if (LogUtil.class.getName().equals(currentStackTrace.getClassName())
&& !LogUtil.class.getName().equals(nextStackTrace.getClassName())) {
String stackTrace = nextStackTrace.toString();
header.append(" ").append(StringUtils.removeStart(stackTrace, nextStackTrace.getClassName() + "."));
break;
}
}
return header.append(":").toString();
}
private static String format(String msg, Object... params) {
if (StringUtils.isEmpty(msg)) {
return StringUtils.EMPTY;
}
if (params != null && params.length > 0) {
msg = MessageFormat.format(msg, params);
}
return msg.replaceAll(TEMPLATE_REGEX, StringUtils.EMPTY);
}
private static String toString(Object object) {
return ToStringBuilder.reflectionToString(object, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
@@ -90,4 +90,15 @@ class AesUtilTest {
assertFalse(isCiphertext(encryptedText, invalidKey));
}
@Test
void testDefaultKeyCompatibility() {
// Test with default key
String originalText = "This is a secret message";
// encode use default secret key
String encryptedText = aesEncode(originalText, AesUtil.DEFAULT_ENCODE_RULES);
// decode use new secret key
String decryptedText = aesDecode(encryptedText, "newkey1234567890");
// old data can decode with default secret key
assertEquals(originalText, decryptedText);
}
}
@@ -21,6 +21,7 @@ package org.apache.hertzbeat.common.util;
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 java.util.HashMap;
@@ -69,4 +70,21 @@ public class JexlExpressionRunnerTest {
contains("Abc", "a")
"""));
}
@Test
void testJsonFunction() {
Map<String, Object> context = new HashMap<String, Object>();
context.put("$.info", "{\"current\":537132,\"grantTotal\":63129,\"instanceCode\":\"CSDZMC\",\"timeMillis\":1754579874357}");
String expression = "json:apply($.info).current";
JexlExpression expObj = JexlExpressionRunner.compile(expression);
assertEquals(537132, JexlExpressionRunner.evaluate(expObj, context));
context.put("$.info", "{\"grantTotal\":63129,\"instanceCode\":\"CSDZMC\",\"timeMillis\":1754579874357}");
assertNull(JexlExpressionRunner.evaluate(expObj, context));
context.clear();
assertNull(JexlExpressionRunner.evaluate(expObj, context));
}
}
@@ -0,0 +1,117 @@
/*
* 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.common.util;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.contains;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LogUtilTest {
@Mock
private Logger mockLogger;
private AutoCloseable mocks;
@BeforeEach
void setUp() {
mocks = MockitoAnnotations.openMocks(this);
}
@AfterEach
void tearDown() throws Exception {
if (mocks != null) {
mocks.close();
}
}
@Test
void testFormat_noParams_returnsOriginalMessage() throws Exception {
String original = "hello world";
Method formatMethod = LogUtil.class.getDeclaredMethod("format", String.class, Object[].class);
formatMethod.setAccessible(true);
String formatted = (String) formatMethod.invoke(null, original, new Object[0]);
assertEquals(original, formatted);
}
@Test
void testFormat_withParams_replacesPlaceholders() throws Exception {
String template = "hello,{0}, world {1}!";
Method formatMethod = LogUtil.class.getDeclaredMethod("format", String.class, Object[].class);
formatMethod.setAccessible(true);
Object[] params = {"Alice", 123};
String result = (String) formatMethod.invoke(null, template, params);
assertTrue(result.contains("hello,Alice"));
assertTrue(result.contains("world 123!"));
}
@Test
void testDebug_noParams_logsRawMessage() {
when(mockLogger.isDebugEnabled()).thenReturn(true);
String msg = "test-debug";
LogUtil.debug(mockLogger, msg);
verify(mockLogger).debug(contains(msg));
}
@Test
void testDebug_withParams_logsFormattedMessage() {
when(mockLogger.isDebugEnabled()).thenReturn(true);
LogUtil.debug(mockLogger, "user={0}", "Bob");
verify(mockLogger).debug(contains("user=Bob"));
}
@Test
void testInfo_levelOff_doesNotLog() {
when(mockLogger.isInfoEnabled()).thenReturn(false);
LogUtil.info(mockLogger, "should-not-log");
verify(mockLogger, never()).info(anyString());
}
@Test
void testWarn_withException_logsMessageAndException() {
when(mockLogger.isWarnEnabled()).thenReturn(true);
RuntimeException ex = new RuntimeException("warn-ex");
LogUtil.warn(mockLogger, ex, "warning {0}", "occurred");
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(mockLogger).warn(captor.capture(), eq(ex));
assertTrue(captor.getValue().contains("warning occurred"));
}
@Test
void testError_withExceptionAndParams_logsError() {
when(mockLogger.isErrorEnabled()).thenReturn(true);
RuntimeException ex = new RuntimeException("err");
LogUtil.error(mockLogger, ex, "fail code {0}", 500);
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(mockLogger).error(captor.capture(), eq(ex));
assertTrue(captor.getValue().contains("fail code 500"));
}
}
@@ -31,7 +31,6 @@
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mysql.version>8.0.30</mysql.version>
<postgresql.version>42.5.5</postgresql.version>
</properties>
@@ -82,9 +81,8 @@
<!-- JDBC Drivers -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
@@ -79,7 +79,7 @@ public class KafkaCollectE2eTest {
.withNetwork(network)
.withNetworkAliases(ZOOKEEPER_NAME)
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(Duration.ofSeconds(30));
.withStartupTimeout(Duration.ofSeconds(120));
zookeeperContainer.setPortBindings(Collections.singletonList(ZOOKEEPER_PORT + ":" + ZOOKEEPER_PORT));
Startables.deepStart(Stream.of(zookeeperContainer)).join();
@@ -90,7 +90,8 @@ public class KafkaCollectE2eTest {
.withNetworkAliases(KAFKA_NAME)
.withLogConsumer(
new Slf4jLogConsumer(
DockerLoggerFactory.getLogger(KAFKA_IMAGE_NAME)));
DockerLoggerFactory.getLogger(KAFKA_IMAGE_NAME)))
.withStartupTimeout(Duration.ofSeconds(120));
Startables.deepStart(Stream.of(kafkaContainer)).join();
}
@@ -113,11 +114,12 @@ public class KafkaCollectE2eTest {
// Create Topic
Properties properties = new Properties();
properties.put("bootstrap.servers", bootstrapServers);
AdminClient adminClient = KafkaAdminClient.create(properties);
int numPartitions = 1;
short replicationFactor = 1;
NewTopic newTopic = new NewTopic(topicName, numPartitions, replicationFactor);
adminClient.createTopics(Collections.singletonList(newTopic)).all().get(60, TimeUnit.SECONDS);
try (AdminClient adminClient = KafkaAdminClient.create(properties)) {
NewTopic newTopic = new NewTopic(topicName, numPartitions, replicationFactor);
adminClient.createTopics(Collections.singletonList(newTopic)).all().get(60, TimeUnit.SECONDS);
}
// Verify the information of topic list monitoring
builder = CollectRep.MetricsData.newBuilder();
+4
View File
@@ -210,6 +210,10 @@
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-memory-netty</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
<build>
@@ -20,14 +20,15 @@ package org.apache.hertzbeat.manager.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.usthe.sureness.util.JsonWebTokenUtil;
import jakarta.annotation.Resource;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.TimeZoneUtil;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.manager.pojo.dto.MuteConfig;
@@ -62,6 +63,9 @@ public class ConfigInitializer implements SmartLifecycle {
@Value("${sureness.jwt.secret:" + DEFAULT_JWT_SECRET + "}")
private String currentJwtSecret;
@Value("${common.secret:" + AesUtil.DEFAULT_ENCODE_RULES + "}")
private String currentAesSecret;
@Resource
private SystemGeneralConfigServiceImpl systemGeneralConfigService;
@@ -110,30 +114,41 @@ public class ConfigInitializer implements SmartLifecycle {
// for template config, flush the template config in db to memory
TemplateConfig templateConfig = templateConfigService.getConfig();
appService.updateCustomTemplateConfig(templateConfig);
// for system secrets
// for system jwt secrets and aes secret
boolean needUpdate = false;
SystemSecret.SystemSecretBuilder systemSecretBuilder = SystemSecret.builder();
SystemSecret systemSecret = systemSecretService.getConfig();
if (systemSecret != null) {
systemSecretBuilder.jwtSecret(systemSecret.getJwtSecret());
systemSecretBuilder.aesSecret(systemSecret.getAesSecret());
}
if (DEFAULT_JWT_SECRET.equals(currentJwtSecret)) {
// use the random jwt secret
SystemSecret systemSecret = systemSecretService.getConfig();
if (systemSecret == null || StringUtils.isBlank(systemSecret.getJwtSecret())) {
char[] chars = DEFAULT_JWT_SECRET.toCharArray();
Random rand = new Random();
for (int i = 0; i < chars.length; i++) {
int index = rand.nextInt(chars.length);
char temp = chars[i];
chars[i] = chars[index];
chars[index] = temp;
}
currentJwtSecret = new String(chars);
systemSecret = SystemSecret.builder().jwtSecret(currentJwtSecret).build();
systemSecretService.saveConfig(systemSecret);
currentJwtSecret = randomizeSecret(DEFAULT_JWT_SECRET);
systemSecretBuilder.jwtSecret(currentJwtSecret);
needUpdate = true;
} else {
currentJwtSecret = systemSecret.getJwtSecret();
}
}
// else use the user custom jwt secret
// set the jwt secret token in util
// else use the user custom jwt secret, set the jwt secret token in util
JsonWebTokenUtil.setDefaultSecretKey(currentJwtSecret);
// Aes secret config
if (AesUtil.DEFAULT_ENCODE_RULES.equals(currentAesSecret)) {
// use the random aes secret
if (systemSecret == null || StringUtils.isBlank(systemSecret.getAesSecret())) {
currentAesSecret = randomizeSecret(AesUtil.DEFAULT_ENCODE_RULES);
systemSecretBuilder.aesSecret(currentAesSecret);
needUpdate = true;
} else {
currentAesSecret = systemSecret.getAesSecret();
}
}
AesUtil.setDefaultSecretKey(currentAesSecret);
if (needUpdate) {
systemSecretService.saveConfig(systemSecretBuilder.build());
}
// init web-app mute config
MuteConfig muteConfig = muteGeneralConfigService.getConfig();
if (muteConfig == null) {
@@ -162,4 +177,18 @@ public class ConfigInitializer implements SmartLifecycle {
public int getPhase() {
return Ordered.HIGHEST_PRECEDENCE;
}
private String randomizeSecret(String secret) {
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder(secret.length());
for (int i = 0; i < secret.length(); i++) {
char ch;
do {
int codePoint = random.nextInt('z' - '0' + 1) + '0';
ch = (char) codePoint;
} while (!Character.isLetterOrDigit(ch));
sb.append(ch);
}
return sb.toString();
}
}
@@ -43,12 +43,12 @@ public class SwaggerConfig {
.info(new Info()
.title("HertzBeat")
.description("An Open-Source Real-time Monitoring Tool.")
.termsOfService("https://hertzbeat.com/")
.termsOfService("https://hertzbeat.apache.org/")
.contact(new Contact().name("tom").url("https://github.com/tomsun28").email("tomsun28@outlook.com"))
.version("v1.0")
.license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0")))
.externalDocs(new ExternalDocumentation()
.description("HertzBeat Docs").url("https://hertzbeat.com/docs/"))
.description("HertzBeat Docs").url("https://hertzbeat.apache.org/docs/"))
.addSecurityItem(new SecurityRequirement().addList(SECURITY_SCHEME_NAME))
.components(new Components().addSecuritySchemes(SECURITY_SCHEME_NAME,
new SecurityScheme()
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.manager.controller;
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;
@@ -25,6 +24,7 @@ import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.ResponseUtil;
import org.apache.hertzbeat.manager.pojo.dto.TemplateConfig;
import org.apache.hertzbeat.manager.service.ConfigService;
@@ -37,6 +37,19 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.TextStyle;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* Alert sender Configuration API
*/
@@ -45,6 +58,9 @@ import org.springframework.web.bind.annotation.RestController;
@Tag(name = "Alert sender Configuration API")
@Slf4j
public class GeneralConfigController {
private static final Set<String> ZONE_IDS = ZoneId.getAvailableZoneIds();
@Resource
private ConfigService configService;
@@ -74,4 +90,28 @@ public class GeneralConfigController {
@RequestBody TemplateConfig.AppTemplate template) {
return ResponseUtil.handle(() -> configService.updateTemplateAppConfig(app, template));
}
@GetMapping(path = "/timezones")
@Operation(summary = "Get all available timezones and their current UTC offset", description = "Get all available timezones and their current UTC offset")
public ResponseEntity<Message<List<Map<String, String>>>> getTimezones() {
List<Map<String, String>> timezones = ZONE_IDS.stream()
.map(id -> {
try {
ZoneId zoneId = ZoneId.of(id);
ZonedDateTime now = ZonedDateTime.now(zoneId);
int totalSeconds = now.getOffset().getTotalSeconds();
String offset = String.format("UTC%+03d:%02d", totalSeconds / 3600, Math.abs((totalSeconds / 60) % 60));
String displayName = zoneId.getDisplayName(TextStyle.FULL, Locale.getDefault());
return Map.of("zoneId", id, "offset", offset, "displayName", displayName);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Query Timezone failed. {} ", errorMsg);
return null;
}
})
.filter(t -> Objects.nonNull(t) && Objects.nonNull(t.get("zoneId")))
.sorted(Comparator.comparing(m -> m.get("zoneId")))
.collect(Collectors.toList());
return ResponseEntity.ok(Message.success(timezones));
}
}
@@ -121,6 +121,14 @@ public class MonitorsController {
monitorService.export(ids, type, res);
}
@GetMapping("/export/all")
@Operation(summary = "export all monitor config", description = "export all monitor config")
public void exportAll(
@Parameter(description = "Export Type:JSON,EXCEL,YAML") @RequestParam(defaultValue = "JSON") String type,
HttpServletResponse res) throws Exception {
monitorService.exportAll(type, res);
}
@PostMapping("/import")
@Operation(summary = "import monitor config", description = "import monitor config")
public ResponseEntity<Message<Void>> export(MultipartFile file) throws Exception {
@@ -35,4 +35,9 @@ public class SystemSecret {
* secret key for jwt
*/
private String jwtSecret;
/**
* secret key for aes
*/
private String aesSecret;
}
@@ -36,6 +36,7 @@ import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobServi
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.dto.CollectorInfo;
import org.apache.hertzbeat.common.entity.dto.ServerInfo;
import org.apache.hertzbeat.common.entity.job.Configmap;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.manager.Collector;
@@ -45,6 +46,7 @@ import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
import org.apache.hertzbeat.manager.dao.CollectorDao;
@@ -147,6 +149,9 @@ public class CollectorJobScheduler implements CollectorScheduling, CollectJobSch
appDefine.setDefaultInterval(monitor.getIntervals());
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
appDefine.setMetadata(metadata);
List<Param> params = paramDao.findParamsByMonitorId(monitor.getId());
List<Configmap> configmaps = params.stream()
.map(param -> Configmap.builder()
@@ -258,9 +263,11 @@ public class CollectorJobScheduler implements CollectorScheduling, CollectJobSch
if (Objects.isNull(collector)) {
return false;
}
ServerInfo serverInfo = ServerInfo.builder().aesSecret(AesUtil.getDefaultSecretKey()).build();
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setDirection(ClusterMsg.Direction.REQUEST)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(serverInfo)))
.setIdentity(identity)
.build();
ClusterMsg.Message response = this.manageServer.sendMsgSync(identity, message);
@@ -17,12 +17,15 @@
package org.apache.hertzbeat.manager.scheduler.netty.process;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import java.net.InetSocketAddress;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.common.entity.dto.CollectorInfo;
import org.apache.hertzbeat.common.entity.dto.ServerInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.scheduler.netty.ManageServer;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -52,6 +55,12 @@ public class CollectorOnlineProcessor implements NettyRemotingProcessor {
}
this.manageServer.addChannel(collector, ctx.channel());
this.manageServer.getCollectorAndJobScheduler().collectorGoOnline(collector, collectorInfo);
return null;
ServerInfo serverInfo = ServerInfo.builder().aesSecret(AesUtil.getDefaultSecretKey()).build();
return ClusterMsg.Message.newBuilder()
.setIdentity(message.getIdentity())
.setDirection(ClusterMsg.Direction.RESPONSE)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(serverInfo)))
.setType(ClusterMsg.MessageType.GO_ONLINE)
.build();
}
}
@@ -173,6 +173,15 @@ public interface MonitorService {
*/
void export(List<Long> ids, String type, HttpServletResponse res) throws Exception;
/**
* Export All Monitoring Configuration
*
* @param type file type
* @param res response
* @throws Exception This exception will be thrown if the export fails
*/
void exportAll(String type, HttpServletResponse res) throws Exception;
/**
* Import Monitoring Configuration
*
@@ -41,6 +41,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.JexlKeywordsEnum;
import org.apache.hertzbeat.common.entity.job.Configmap;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
@@ -458,6 +459,10 @@ public class AppServiceImpl implements AppService, InitializingBean {
throw new IllegalArgumentException(app.getApp() + " " + metrics.getName() + " "
+ field.getField() + " can not duplicated.");
}
if (JexlKeywordsEnum.match(field.getField())) {
throw new IllegalArgumentException(app.getApp() + " " + metrics.getName() + " "
+ field.getField() + " prohibited keywords.");
}
fieldsSet.add(field.getField());
}
}
@@ -518,16 +523,15 @@ public class AppServiceImpl implements AppService, InitializingBean {
} else {
if (objectStoreConfig.getType() == ObjectStoreDTO.Type.OBS) {
appDefineStore = new ObjectStoreAppDefineStoreImpl();
} else if (objectStoreConfig.getType() == ObjectStoreDTO.Type.DATABASE){
} else if (objectStoreConfig.getType() == ObjectStoreDTO.Type.DATABASE) {
appDefineStore = new DatabaseAppDefineStoreImpl();
} else {
appDefineStore = new LocalFileAppDefineStoreImpl();
}
}
var success = appDefineStore.loadAppDefines();
if (!success) {
new JarAppDefineStoreImpl().loadAppDefines();
}
jarAppDefineStore.loadAppDefines();
// merge define yml files inside jars
appDefineStore.loadAppDefines();
}
private interface AppDefineStore {
@@ -709,7 +713,7 @@ public class AppServiceImpl implements AppService, InitializingBean {
}
}
});
return false;
return true;
}
@Override
@@ -769,8 +773,7 @@ public class AppServiceImpl implements AppService, InitializingBean {
appDefines.put(define.getApp().toLowerCase(), app);
}
}
// merge define yml files inside jars
return false;
return true;
}
@Override
@@ -120,6 +120,9 @@ public class BulletinServiceImpl implements BulletinService {
List<BulletinMetricsData.Data> dataList = new ArrayList<>();
for (Long monitorId : bulletin.getMonitorIds()) {
Monitor monitor = monitorService.getMonitor(monitorId);
if (null == monitor) {
continue;
}
BulletinMetricsData.Data.DataBuilder dataBuilder = BulletinMetricsData.Data.builder()
.monitorId(monitorId)
.monitorName(monitor.getName())
@@ -67,7 +67,7 @@ public class LabelServiceImpl implements LabelService {
public void modifyLabel(Label label) {
Optional<Label> optional = labelDao.findById(label.getId());
if (optional.isPresent()) {
Optional<Label> existOptional = labelDao.findLabelByNameAndTagValue(label.getName(), label.getTagValue());
if (existOptional.isPresent() && !existOptional.get().getId().equals(label.getId())) {
throw new IllegalArgumentException("The label with same key and value already exists.");
@@ -116,33 +116,38 @@ public class LabelServiceImpl implements LabelService {
@Override
public void deleteLabels(HashSet<Long> ids) {
if (CollectionUtils.isEmpty(ids)){
if (CollectionUtils.isEmpty(ids)) {
return;
}
labelDao.deleteLabelsByIdIn(ids);
}
public List<Label> determineNewLabels(Set<Map.Entry<String, String>> originLabels){
@Override
public List<Label> determineNewLabels(Set<Map.Entry<String, String>> originLabels) {
if (originLabels == null || originLabels.isEmpty()) return List.of();
// Get all labels from the database
Set<Map.Entry<String, String>> allLabels = labelDao.findAll().stream()
.map(label -> Map.entry(label.getName(), label.getTagValue()))
Set<Label> allLabels = labelDao.findAll().stream()
.map(label -> Label.builder()
.name(label.getName())
.tagValue(label.getTagValue())
.build())
.collect(Collectors.toSet());
// If the bound label (key:value) does not exist, then add it
Set<Map.Entry<String, String>> addLabelsKv = originLabels.stream()
return originLabels.stream()
.map(entry -> Label.builder()
.name(entry.getKey())
.tagValue(entry.getValue())
.build())
.filter(label -> !allLabels.contains(label))
.collect(Collectors.toCollection(HashSet::new));
return addLabelsKv.stream().map(kv -> {
Label label = new Label();
label.setId(null);
label.setName(kv.getKey());
label.setTagValue(kv.getValue());
label.setType((byte) 0);
return label;
}).toList();
.map(label -> Label.builder()
.id(null)
.name(label.getName())
.tagValue(label.getTagValue())
.type((byte) 0)
.build())
.toList();
}
}
@@ -27,6 +27,7 @@ import org.apache.hertzbeat.alert.dao.AlertDefineBindDao;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.ExportFileConstants;
import org.apache.hertzbeat.common.constants.JexlKeywordsEnum;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
@@ -235,6 +236,18 @@ public class MonitorServiceImpl implements MonitorService {
imExportService.exportConfig(res.getOutputStream(), ids);
}
@Override
public void exportAll(String type, HttpServletResponse res) throws Exception {
// Get all monitor IDs from the database
List<Long> allMonitorIds = monitorDao.findAll()
.stream()
.map(Monitor::getId)
.collect(Collectors.toList());
// Use the existing export method to export all monitors
export(allMonitorIds, type, res);
}
@Override
public void importConfig(MultipartFile file) throws Exception {
var fileName = FileUtil.getFileName(file);
@@ -424,6 +437,27 @@ public class MonitorServiceImpl implements MonitorService {
}
}
}
checkJobFields(monitorDto.getMonitor().getApp());
}
private void checkJobFields(String app) {
if (null == app || app.trim().isEmpty()) {
return;
}
Job job = appService.getAppDefine(app);
if (null != job && !CollectionUtils.isEmpty(job.getMetrics())) {
for (Metrics metrics : job.getMetrics()) {
if (null == metrics.getFields() || metrics.getFields().isEmpty()) {
continue;
}
for (Metrics.Field field : metrics.getFields()) {
if (JexlKeywordsEnum.match(field.getField())) {
throw new IllegalArgumentException(job.getApp() + " " + metrics.getName() + " "
+ field.getField() + " prohibited keywords, please modify the template information.");
}
}
}
}
}
@Override
@@ -22,6 +22,7 @@ spring:
url: jdbc:h2:./data/test;MODE=MYSQL
hikari:
max-lifetime: 120000
jpa:
show-sql: false
database-platform: org.eclipse.persistence.platform.database.MySQLPlatform
@@ -30,8 +31,10 @@ spring:
eclipselink:
logging:
level: SEVERE
flyway:
enabled: false
mail:
host: smtp.exmail.qq.com
username: example@tancloud.cn
@@ -55,26 +58,57 @@ warehouse:
jpa:
enabled: true
expire-time: 1h
max-history-record-num: 6000
victoria-metrics:
enabled: false
url: http://localhost:8428
username: root
password: root
insert:
buffer-size: 1000
buffer-size: 100
flush-interval: 3
compression:
enabled: false
cluster:
enabled: false
select:
url: http://localhost:8481
username: root
password: root
insert:
url: http://localhost:8480
username: root
password: root
buffer-size: 1000
flush-interval: 3
td-engine:
enabled: false
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
url: jdbc:TAOS-RS://127.0.0.1:6041/hertzbeat
username: root
password: taosdata
greptime:
enabled: false
grpc-endpoints: localhost:4001
http-endpoint: http://localhost:4000
database: public
username: greptime
password: greptime
iot-db:
enabled: false
host: 127.0.0.1
rpc-port: 6667
username: root
password: root
query-timeout-in-ms: -1
expire-time: '7776000000'
influxdb:
enabled: false
server-url: http://127.0.0.1:8086
username: root
password: root
expire-time: '30d'
replication: 1
# store real-time metrics data, enable only one below
real-time:
memory:
@@ -92,6 +126,58 @@ warehouse:
# redis db index, default: DB0
db: 0
alerter:
# custom console url
console-url: https://console.tancloud.io
# we work
we-work-webhook-url: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=
# ding ding talk
ding-talk-webhook-url: https://oapi.dingtalk.com/robot/send?access_token=
# fei shu fly book
fly-book-webhook-url: https://open.feishu.cn/open-apis/bot/v2/hook/
# telegram
telegram-webhook-url: https://api.telegram.org/bot%s/sendMessage
# discord
discord-webhook-url: https://discord.com/api/v9/channels/%s/messages
# serverChan
server-chan-webhook-url: https://sctapi.ftqq.com/%s.send
# gotify
gotify-webhook-url: http://127.0.0.1/message?token=%s
# alert inhibit ttl unit ms, default 14400000(4 hours)
inhibit:
ttl: 14400000
sms:
enable: false
type: tencent
tencent:
secret-id:
secret-key:
app-id:
sign-name:
template-id:
alibaba:
access-key-id:
access-key-secret:
sign-name:
template-code:
unisms:
# auth-mode: simple or hmac
auth-mode: simple
access-key-id: YOUR_ACCESS_KEY_ID
# hmac mode need to fill in access-key-secret
access-key-secret: YOUR_ACCESS_KEY_SECRET
signature: YOUR_SMS_SIGNATURE
template-id: YOUR_TEMPLATE_ID
smslocal:
api-key: YOUR_API_KEY_HERE
aws:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -100,13 +186,13 @@ scheduler:
grafana:
enabled: false
url: http://127.0.0.1:3000
expose-url: http://127.0.0.1:3000
username: admin
password: admin
# AI config
# See the documentation for details : https://hertzbeat.apache.org/zh-cn/docs/help/aiConfig
ai:
# AI Type:zhiPu、alibabaAi、kimiAi、sparkDesk
# AI Type:zhiPu、alibabaAi、kimiAi、sparkDesk、ollama、openRouter
type:
# Model nameglm-4、qwen-turboo、moonshot-v1-8k、generalv3.5
model:
@@ -114,3 +200,5 @@ ai:
api-key:
#At present, only IFLYTEK large model needs to be filled in
api-secret:
# The URL of the ollama AI service
api-url:
@@ -48,7 +48,19 @@ management:
include:
- 'metrics'
- 'health'
- 'prometheus'
enabled-by-default: on
endpoint:
prometheus:
access: read_only
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
prometheus:
metrics:
export:
enabled: true
sureness:
container: jakarta_servlet
@@ -62,14 +74,6 @@ sureness:
8tVt4bisXQ13rbN0oxhUZR73M6EByXIO+SV5
dKhaX0csgOCTlCxq20yhmUea6H6JIpSE2Rwp'
otel:
traces:
exporter: none
metrics:
exporter: none
logs:
exporter: none
---
spring:
config:
@@ -146,8 +150,10 @@ warehouse:
username: root
password: root
insert:
buffer-size: 1000
buffer-size: 100
flush-interval: 3
compression:
enabled: false
cluster:
enabled: false
select:
@@ -0,0 +1,24 @@
-- 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.
-- ensure every sql can rerun without error
-- Modify common_annotations column to TEXT (H2 TEXT is equivalent to CLOB)
ALTER TABLE HZB_ALERT_GROUP ALTER COLUMN common_annotations CLOB;
-- Modify alert_fingerprints column to TEXT (H2 TEXT is equivalent to CLOB)
ALTER TABLE HZB_ALERT_GROUP ALTER COLUMN alert_fingerprints CLOB;
@@ -0,0 +1,64 @@
-- 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.
-- ensure every sql can rerun without error
-- Modify hzb_alert_group table columns to TEXT type to resolve MySQL row size limit issue
DELIMITER //
CREATE PROCEDURE ModifyGroupAlertColumns()
BEGIN
DECLARE table_exists INT;
DECLARE col_exists INT;
-- Check if the table exists
SELECT COUNT(*) INTO table_exists
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'HZB_ALERT_GROUP';
IF table_exists = 1 THEN
-- Check and modify common_annotations column to TEXT
SELECT COUNT(*) INTO col_exists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'HZB_ALERT_GROUP'
AND COLUMN_NAME = 'common_annotations'
AND DATA_TYPE != 'text';
IF col_exists = 1 THEN
ALTER TABLE HZB_ALERT_GROUP MODIFY COLUMN common_annotations TEXT;
END IF;
-- Check and modify alert_fingerprints column to TEXT
SELECT COUNT(*) INTO col_exists
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'HZB_ALERT_GROUP'
AND COLUMN_NAME = 'alert_fingerprints'
AND DATA_TYPE != 'text';
IF col_exists = 1 THEN
ALTER TABLE HZB_ALERT_GROUP MODIFY COLUMN alert_fingerprints TEXT;
END IF;
END IF;
END //
DELIMITER ;
CALL ModifyGroupAlertColumns();
DROP PROCEDURE IF EXISTS ModifyGroupAlertColumns;
COMMIT;
@@ -0,0 +1,25 @@
-- 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.
-- ensure every sql can rerun without error
-- Modify hzb_alert_group table columns to TEXT type to resolve row size limit issue
ALTER TABLE HZB_ALERT_GROUP ALTER COLUMN common_annotations TYPE TEXT;
ALTER TABLE HZB_ALERT_GROUP ALTER COLUMN alert_fingerprints TYPE TEXT;
commit;
@@ -26,8 +26,8 @@ name:
help:
zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMX 协议</a> 对 Apache ActiveMQ 消息中间件的运行状态,节点,Topic等相关指标(broker、topic、memory pool、class loading、thread)进行监测。<br><span class='help_module_span'>⚠️注意:您需要在 ActiveMQ 开启 JMX 服务。<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/activemq'>点击查看开启步骤</a>。</span>
en-US: "HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMX protocol</a> to monitor the running status, nodes, topics, and other metrics of Apache ActiveMQ message-oriented middleware. <br><span class='help_module_span'>⚠️Note: You should enable the JMX service in ActiveMQ. <a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/activemq'>Click here to view the specific steps.</a></span>"
zh-TW: HertzBeat使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMX 協定</a> 對 Apache ActiveMQ 消息中介軟體的運行狀態,節點,Topic 等相關名額(broker、topic、memory pool、class loading、thread)進行監測。<br><span class='help_module_span'> ⚠️注意:您需要在 ActiveMQ 開啟 JMX 服務。<a class='help_module_content' href=' https://hertzbeat.apache.org/zh-cn/docs/help/activemq'>點擊查看開啟步驟</a>。</span>
ja-JP: HertzBeatは <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMXプロトコルを介して</a> Apache ActiveMQメッセージングシステムのランタイムステータス、ノード、トピック、その他の関連メトリクを監視します。<br><span class='help_module_span'> ⚠️注意:ActiveMQ で JMX サービスを有効にする必要があります。<a class='help_module_content' href=' https://hertzbeat.apache.org/zh-cn/docs/help/activemq'>クリックしてガイドを見ます</a>。</span>
zh-TW: HertzBeat使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMX 協定</a> 對 Apache ActiveMQ 消息中介軟體的運行狀態,節點,Topic 等相關名額(broker、topic、memory pool、class loading、thread)進行監測。<br><span class='help_module_span'> ⚠️注意:您需要在 ActiveMQ 開啟 JMX 服務。<a class='help_module_content' href='https://hertzbeat.apache.org/zh-cn/docs/help/activemq'>點擊查看開啟步驟</a>。</span>
ja-JP: HertzBeatは <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jmx'> JMXプロトコルを介して</a> Apache ActiveMQメッセージングシステムのランタイムステータス、ノード、トピック、その他の関連メトリクを監視します。<br><span class='help_module_span'> ⚠️注意:ActiveMQ で JMX サービスを有効にする必要があります。<a class='help_module_content' href='https://hertzbeat.apache.org/docs/help/activemq'>クリックしてガイドを見ます</a>。</span>
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/activemq
en-US: https://hertzbeat.apache.org/docs/help/activemq
@@ -196,7 +196,7 @@ metrics:
i18n:
zh-CN: 持久化
en-US: Persistence
ja-JP: 永続
ja-JP: 永続
type: 1
- field: DataDirectory
i18n:
@@ -271,19 +271,19 @@ metrics:
i18n:
zh-CN: 平均消息大小
en-US: Average Message Size
ja-JP: 平均メッセージサイズ
ja-JP: メッセージの平均サイズ
type: 0
- field: MaxMessageSize
i18n:
zh-CN: 最大消息大小
en-US: Max Message Size
ja-JP: 最大メッセージサイズ
ja-JP: メッセージの最大サイズ
type: 0
- field: MinMessageSize
i18n:
zh-CN: 最小消息大小
en-US: Min Message Size
ja-JP: 最小メッセージサイズ
ja-JP: メッセージの最小サイズ
type: 0
protocol: jmx
jmx:
@@ -372,7 +372,7 @@ metrics:
i18n:
zh-CN: 存储消息大小
en-US: Store Message Size
ja-JP: ストレージメッセージサイズ
ja-JP: ストレージメッセージサイズ
- field: AverageEnqueueTime
type: 0
unit: ms
@@ -407,21 +407,21 @@ metrics:
i18n:
zh-CN: 平均消息大小
en-US: Average Message Size
ja-JP: 平均メッセージサイズ
ja-JP: メッセージの平均サイズ
- field: MaxMessageSize
type: 0
unit: B
i18n:
zh-CN: 最大消息大小
en-US: Max Message Size
ja-JP: 最大メッセージサイズ
ja-JP: メッセージの最大サイズ
- field: MinMessageSize
type: 0
unit: B
i18n:
zh-CN: 最小消息大小
en-US: Min Message Size
ja-JP: 最小メッセージサイズ
ja-JP: メッセージの最小サイズ
units:
- MemoryLimit=B->MB
protocol: jmx
@@ -446,14 +446,14 @@ metrics:
i18n:
zh-CN: 指标名称
en-US: Name
ja-JP: 指標
ja-JP: メトリクス
- field: committed
type: 0
unit: MB
i18n:
zh-CN: 已分配内存
en-US: Committed
ja-JP: コミットメモリ
ja-JP: コミットされたメモリ
- field: init
type: 0
unit: MB
@@ -474,7 +474,7 @@ metrics:
i18n:
zh-CN: 已使用内存
en-US: Used
ja-JP: 使用済みメモリ
ja-JP: 使用したメモリ
units:
- committed=B->MB
- init=B->MB
@@ -27,7 +27,7 @@ help:
zh-CN: Hertzbeat 对 Apache Airflow 通用性能指标(airflow health、airflow version)进行采集监控。<br>您可以点击“<i>新建 Apache Airflow </i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。
en-US: HertzBeat monitors Apache Airflow through general performance metrics such as airflow health and airflow version. You could click the "<i>New Apache Airflow</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu.
zh-TW: Hertzbeat對Apache Airflow通用性能指標(airflow health、airflow version)進行採集監控。<br>您可以點擊“<i>新建 Apache Airflow</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。
ja-JP: Hertzbeatは、Apache Airflowの一般的なパフォーマンスのメトリクを監視します。 <br><i>新規Apache Airflow</i>をクリックして設定しましょう。
ja-JP: Hertzbeatは、Apache Airflowの一般的なパフォーマンスのメトリクを監視します。 <br><i>新規Apache Airflow</i>をクリックして設定しましょう。
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/airflow/
en-US: https://hertzbeat.apache.org/docs/help/airflow/
@@ -27,7 +27,7 @@ help:
zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 AlmaLinux 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 AlmaLinux</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。
en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors AlmaLinux operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New AlmaLinux</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu.
zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 AlmaLinux 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建AlmaLinux</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。
ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> AlmaLinuxシステムの一般的なパフォーマンスのメトリク監視します。<br>「<i>新規 AlmaLinux</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。
ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> AlmaLinuxシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 AlmaLinux</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/almalinux
en-US: https://hertzbeat.apache.org/docs/help/almalinux
@@ -80,7 +80,7 @@ params:
name:
zh-CN: 复用连接
en-US: Reuse Connection
ja-JP: コネクション再利用
ja-JP: 接続再利用
# type-param field type(most mapping the html input type)
type: boolean
# required-true or false
@@ -92,7 +92,7 @@ params:
name:
zh-CN: 使用代理
en-US: Use Proxy Connection
ja-JP: プロキシコネクション利用
ja-JP: プロキシ接続利用
# type-param field type(most mapping the html input type)
type: boolean
# required-true or false
@@ -246,7 +246,7 @@ metrics:
i18n:
zh-CN: 操作系统版本
en-US: System Version
ja-JP: システムバージョン
ja-JP: オーエスバージョン
- field: uptime
type: 1
i18n:
@@ -415,7 +415,7 @@ metrics:
i18n:
zh-CN: 剩余可用内存
en-US: Available Memory
ja-JP: 使用可能メモリ
ja-JP: 使用可能メモリ
- field: usage
type: 0
unit: '%'
@@ -544,14 +544,14 @@ metrics:
i18n:
zh-CN: 入站数据流量
en-US: Receive Bytes
ja-JP: 受信バイト数
ja-JP: 受信されたバイト数
- field: transmit_bytes
type: 0
unit: Mb
i18n:
zh-CN: 出站数据流量
en-US: Transmit Bytes
ja-JP: 送信バイト数
ja-JP: 転送されたバイト数
units:
- receive_bytes=B->MB
- transmit_bytes=B->MB
@@ -679,7 +679,7 @@ metrics:
i18n:
zh-CN: 执行命令
en-US: Command
ja-JP: 指令
ja-JP: コマンド
protocol: ssh
ssh:
host: ^_^host^_^
@@ -738,7 +738,7 @@ metrics:
i18n:
zh-CN: 执行命令
en-US: Command
ja-JP: 指令
ja-JP: コマンド
protocol: ssh
ssh:
host: ^_^host^_^
@@ -27,7 +27,7 @@ help:
zh-CN: Hertzbeat 将调用 HTTP API 接口,查看接口是否可用,并以 ms 为指标单位对其响应时间等指标进行监测。您可以先点击“新增 HTTP API”按钮并进行配置,或在“更多操作”中导入已有配置。
en-US: The platform will invoke an HTTP API endpoint to verify its accessibility and monitor various metrics, including response time, measured in milliseconds (ms). <br>To set up this functionality, you could click the "Add HTTP API" button and proceed with the configuration or import an existing setup through the "More Actions" menu.
zh-TW: Hertzbeat將調用HTTP API介面,查看介面是否可用,並以ms為名額組織對其回應時間等名額進行監測。 您可以先點擊“新增HTTP API”按鈕並進行配寘,或在“更多操作”中導入已有配寘。
ja-JP: Hertzbeatは、HTTP APIを呼び出して利用可能かどうかを確認し、応答時間(ms)などのメトリクを監視します。「新規HTTP API」をクリックして設定しましょう。
ja-JP: Hertzbeatは、HTTP APIを呼び出して利用可能かどうかを確認し、応答時間(ms)などのメトリクを監視します。「新規HTTP API」をクリックして設定しましょう。
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/api
en-US: https://hertzbeat.apache.org/docs/help/api
@@ -170,9 +170,9 @@ params:
name:
zh-CN: 请求BODY
en-US: BODY
ja-JP: ボディ
# type-param field type(most mapping the html input type)
type: textarea
# 参数输入框提示信息
# param field input placeholder
placeholder: 'Available When POST PUT'
# dependent parameter values list
@@ -304,6 +304,12 @@ metrics:
zh-CN: 关键词数量
en-US: Keyword
ja-JP: キーワード
- field: statusCode
type: 0
i18n:
zh-CN: 状态码
en-US: Status Code
ja-JP: 状態コード
# the protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk
protocol: http
# the config content when protocol is http
@@ -27,7 +27,7 @@ help:
zh-CN: 监控 HTTP API 接口,对 API 返回的业务自定义状态码(非 Http 状态码)进行监控。此需通过配置 JsonPath 来解析您 API 的业务状态码路径。<br>您可以点击 “<i>新建 API Code</i>” 并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。
en-US: Monitor HTTP API to monitor business-defined status codes (non-HTTP status codes) returned by API. To do this, you need to configure JsonPath to resolve the business status code path of your API. <br>You could click the "<i>New API Codes</i>" button and proceed with the configuration or import an existing setup through the "<i>More Actions</i>" menu.
zh-TW: 監控 HTTP API 接口,對 API 返回的業務自定義狀態碼(非 Http 狀態碼)進行監控。此需通過配置 JsonPath 來解析您 API 的業務狀態碼路徑。<br>您可以點擊“<i>新建API Code</i>”並進行配寘,或者選擇“<i>更多操作</i>”,導入已有配寘。
ja-JP: Hertzbeatは、HTTP APIを呼び出して利用可能かどうかを確認し、ビジネスステータスコード(HTTPステータスコードではない)のメトリクを監視します。これには、API のビジネスステータスコードのパスを解析するように JsonPath を構成する必要があります。「新規API Code」をクリックして設定しましょう。
ja-JP: Hertzbeatは、HTTP APIを呼び出して利用可能かどうかを確認し、ビジネスステータスコード(HTTPステータスコードではない)のメトリクを監視します。これには、API のビジネスステータスコードのパスを解析するように JsonPath を構成する必要があります。「新規API Code」をクリックして設定しましょう。
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/api_code
en-US: https://hertzbeat.apache.org/docs/help/api_code
@@ -27,7 +27,7 @@ help:
zh-CN: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 对 Centos 操作系统的通用性能指标 (系统信息、CPU、内存、磁盘、网卡、文件系统、TOP资源进程等) 进行采集监控。<br>您可以点击“<i>新建 Centos</i>”并配置HOST端口账户等相关参数进行添加,支持SSH账户密码或密钥认证。或者选择“<i>更多操作</i>”,导入已有配置。
en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH protocol</a> to monitors Centos operating system's general performance metrics such as cpu, memory, disk, basic, interface, disk_free, top_process etc. <br>You can click the "<i>New Centos</i>" and config host port and other related params to add, auth support password or secretKey. Or import an existing setup through the "<i>More Actions</i>" menu.
zh-TW: Hertzbeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSH 协议</a> 對 Centos 操作系统的通用性能指標 (系統信息、CPU、內存、磁盤、網卡、文件系統、TOP資源進程等) 進行採集監控。<br>您可以點擊“<i>新建 Centos</i>”並配置HOST端口賬戶等相關參數進行添加,支持SSH賬戶密碼或密鑰認證。或者選擇“<i>更多操作</i>”,導入已有配寘。
ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Centosシステムの一般的なパフォーマンスのメトリク監視します。<br>「<i>新規 Centos Linux</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。
ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-ssh'> SSHプロトコルを介して</a> Centosシステムの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 Centos Linux</i>」をクリックしてホストなどのパラメタを設定した後、新規することができます。SSHまたはキー認証をサポートします。
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/centos/
en-US: https://hertzbeat.apache.org/docs/help/centos/
@@ -73,7 +73,6 @@ params:
# required-true or false
required: false
# default value
# 默认值
defaultValue: 6000
# field-param field key
- field: reuseConnection
@@ -81,7 +80,7 @@ params:
name:
zh-CN: 复用连接
en-US: Reuse Connection
ja-JP: コネクション再利用
ja-JP: 接続再利用
# type-param field type(most mapping the html input type)
type: boolean
# required-true or false
@@ -93,7 +92,7 @@ params:
name:
zh-CN: 使用代理
en-US: Use Proxy Connection
ja-JP: プロキシコネクション利用
ja-JP: プロキシ接続利用
# type-param field type(most mapping the html input type)
type: boolean
# required-true or false
@@ -247,7 +246,7 @@ metrics:
i18n:
zh-CN: 操作系统版本
en-US: System Version
ja-JP: システムバージョン
ja-JP: オーエスバージョン
- field: uptime
type: 1
i18n:
@@ -416,7 +415,7 @@ metrics:
i18n:
zh-CN: 剩余可用内存
en-US: Available Memory
ja-JP: 使用可能メモリ
ja-JP: 使用可能メモリ
- field: usage
type: 0
unit: '%'
@@ -545,14 +544,14 @@ metrics:
i18n:
zh-CN: 入站数据流量
en-US: Receive Bytes
ja-JP: 受信バイト数
ja-JP: 受信されたバイト数
- field: transmit_bytes
type: 0
unit: Mb
i18n:
zh-CN: 出站数据流量
en-US: Transmit Bytes
ja-JP: 送信バイト数
ja-JP: 転送されたバイト数
units:
- receive_bytes=B->MB
- transmit_bytes=B->MB
@@ -680,7 +679,7 @@ metrics:
i18n:
zh-CN: 执行命令
en-US: Command
ja-JP: 指令
ja-JP: コマンド
protocol: ssh
ssh:
host: ^_^host^_^
@@ -739,7 +738,7 @@ metrics:
i18n:
zh-CN: 执行命令
en-US: Command
ja-JP: 指令
ja-JP: コマンド
protocol: ssh
ssh:
host: ^_^host^_^

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