Compare commits

..
Author SHA1 Message Date
tomsun28 c33aa86ba2 [webapp] update ui
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-07-06 15:26:03 +08:00
tomsun28 b6bca5a10d [improve] make jackson serialize field all visibility
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-07-06 15:08:21 +08:00
Calvin c6f1f07155 [doc] japanese jvm (#3545) 2025-07-05 12:35:45 +08:00
Calvinandtomsun28 0ebd3499b7 [doc] japanese kafka (#3548)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-05 12:04:24 +08:00
淞筱 c88d4a7b0e [doc] update download page (#3549) 2025-07-05 11:56:39 +08:00
tomsun28 8bebae9db6 [doc] add hertzbeat maturity and fix doc (#3544) 2025-07-04 19:55:27 +08:00
b6b1a48a5c [doc] recommend newer mysql connector jar (#3541)
Signed-off-by: PJ Fanning <pjfanning@users.noreply.github.com>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-07-03 23:35:53 +08:00
63ed50ef45 [doc] Fix some issues in the documentation (#3526)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-07-03 23:04:31 +08:00
PJ Fanning e63715d1b0 enable GitHub Dependabot to create PRs for security alerts but not for every dependency update (#3543) 2025-07-03 21:27:48 +08:00
Calvin edf3eac6d6 [doc] japanese jetty (#3539) 2025-07-03 11:03:54 +08:00
Calvin 743f285c8d [doc] japanese ipmi (#3537) 2025-07-02 22:55:09 +08:00
Logicandaias00 458433464c [doc]update download page text (#3532)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-07-02 17:45:28 +08:00
淞筱 64740a4f85 [doc] update outdated links (#3535) 2025-07-02 17:15:19 +08:00
淞筱andaias00 b5a2bce261 download version deal (#3531)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-07-01 13:11:31 +08:00
Calvin 5e351c854b [doc] japanese iotdb (#3530) 2025-07-01 12:29:54 +08:00
0875bea046 fix: fix something (#3524)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
2025-06-30 10:56:55 +08:00
Calvin 9ad5f77816 [doc] japanese influxdb promql (#3527) 2025-06-30 09:46:56 +08:00
aias00andCalvin 998f9059bc [feat]parallel ci (#3523)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-06-30 00:04:10 +08:00
a1439c3ec2 [fix] fix i18n error and append two metrics about disk infos (#3514)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-06-29 20:20:05 +08:00
742b6f6755 [doc] Add new commiter blog (#3522)
Co-authored-by: yinyijun <yingey2011>
Co-authored-by: 淞筱 <2030509072@qq.com>
2025-06-29 15:15:50 +08:00
5fdb34598c [doc] japanese hikvision (#3499)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: 淞筱 <2030509072@qq.com>
2025-06-29 12:16:17 +08:00
Calvin 4f982d1ab0 [doc] japanese influxdb (#3521) 2025-06-29 11:50:42 +08:00
4a28056d9f [fix] Update the correct replacement of outdated functions and placeholders (#3516)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-28 22:31:18 +08:00
Calvinandaias00 3f2fa71e22 [doc] japanese iceberg (#3519)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-28 22:25:22 +08:00
Jastandaias00 08c81bf62f [Improve] Improve kafka e2e (#3520)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-28 22:23:07 +08:00
e16d6cb4ba [feature] Support for Periodic Threshold preview (#3505)
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-06-28 20:38:38 +08:00
Calvin a54d14e803 [doc] japanese hugeGraph (#3518) 2025-06-28 07:24:44 +08:00
Calvinandaias00 01ae281769 [doc] japanese hive (#3506)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-28 00:10:22 +08:00
Calvin 9cb10ff14d [doc] japanese huawei switch (#3515) 2025-06-27 20:55:27 +08:00
Calvin 62f40d3ad1 [doc] japanese hpe switch (#3511) 2025-06-26 00:07:54 +08:00
tomsun28 57601ef989 [release] release new version 1.7.2 (#3510)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-06-25 22:01:18 +08:00
puruidong 0a05edbec5 [bugfix]Fixed an error in the format of the flying book notification (#3508)
Signed-off-by: puruidong <pruidong@gmail.com>
2025-06-25 19:18:21 +08:00
Yang Chenandtomsun28 fcef24fdbf [bugfix] Correctly parse binary comparison expressions (#3504)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-25 11:01:12 +08:00
e45c31abfb [bugfix] Fix incorrect expression parsing in alert setting component (#3497)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-24 22:56:16 +08:00
198c190ca7 fixed:a minor issue change (#3428)
Signed-off-by: 淞筱 <2030509072@qq.com>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
Co-authored-by: 淞筱 <2030509072@qq.com>
2025-06-24 22:50:22 +08:00
铁甲小宝anda-little-fool 392a59f9a9 add:a small jdbc modified. (#3500)
Co-authored-by: a-little-fool <2030509072@qq.com>
2025-06-24 22:07:18 +08:00
aias00 c01b7f7bce [feat] update_mvnd_version (#3498) 2025-06-23 21:18:04 +08:00
Calvin 0323b044b8 [doc] japanese hertzbeat token (#3493) 2025-06-23 08:54:47 +08:00
256302fdcf [fix] Fix antlr4 parsing of or and unless logical and set binary operators (#3488)
Co-authored-by: wordofwordof <black.jack.kaiqi@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-22 19:46:34 +08:00
Calvin 3f3330af41 [doc] japanese hertzbeat (#3492) 2025-06-22 19:13:18 +08:00
Calvin 33342baa3f [doc] japanese hdfs namenode (#3490) 2025-06-22 06:43:10 +08:00
Logic 563cbd7943 [ci]: add mvnd support and update backend build (#3491) 2025-06-21 20:07:10 +08:00
Duansgandaias00 ddb1601290 [fix] antlr4 vectors and parse semantic fixes and optimizations (#3482)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-21 07:55:54 +08:00
Calvin 8e9d3c09f3 [doc] japanese hdfs datanode (#3487) 2025-06-21 00:07:53 +08:00
Calvin ade04d2cf2 [doc] japanese hbase region server (#3479) 2025-06-20 09:03:09 +08:00
Calvinandaias00 53555c88c7 [doc] japanese hbase master (#3477)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-19 19:14:43 +08:00
Logic 03bf31269d [docs](webhook): update Chinese documentation for alert integration (#3478) 2025-06-19 19:05:21 +08:00
Calvin 5c329a8a66 [doc] japanese hadoop (#3476) 2025-06-18 23:34:44 +08:00
dda9250243 [feature] Add statusCode metrics data. (#3446)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-06-17 22:32:31 +08:00
Calvin 65a97aed8f [doc] japanese h3c switch (#3471) 2025-06-17 08:07:17 +08:00
Calvin 56528f4fc0 [doc] japanese greptime (#3469) 2025-06-15 22:12:43 +08:00
Duansgandliutianyou f218ba018a [fix] Fix Monitors paging display. (#3467)
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-06-15 11:44:23 +08:00
d6ce0c153a [improvement] change column definitions for commonAnnotations and alertFingerprints to TEXT type (#3463)
Co-authored-by: Jast <shenghang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-06-14 21:52:27 +08:00
Calvin a1b23cacdc [doc] japanese greenplum (#3468) 2025-06-14 17:40:04 +08:00
ed6214d0c1 [doc] Alert notification template (#3466)
Co-authored-by: yinyijun <yingey2011>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-14 13:04:32 +08:00
Duansgandtomsun28 9a3fc5911d [fix] fix jacoco can not generate test reports. (#3455)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-14 00:21:07 +08:00
d5d01459e7 [Fix] fix custom monitoring bulletin NullPointerException (#3448)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-06-13 08:48:51 +08:00
liutianyouandtomsun28 d077c72211 [doc] add doc for integrate volcengine alerts (#3460)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-13 00:32:15 +08:00
Yang Chen 11e3e4cc22 [improvement] disable default OpenTelemetry exporters to prevent connection errors (#3461) 2025-06-12 23:40:28 +08:00
Calvin c24c6ab7aa [doc] japanese freebsd (#3456) 2025-06-12 08:50:13 +08:00
Calvinandaias00 a183176e83 [doc] japanese flink on yarn (#3452)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-11 08:56:10 +08:00
tomsun28 a2e8739fec [doc] add publish version 1.7.1 blog (#3449) 2025-06-11 06:57:02 +08:00
liutianyouandCalvin ebb6e3d4ff [feature] support volcengine alert source (#3451)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-06-11 00:48:07 +08:00
tomsun28 fc3f26368d [security] update hertzbeat security model (#3450) 2025-06-11 00:22:26 +08:00
Calvin 7967cfabf0 [doc] japanese flink (#3447) 2025-06-10 07:31:31 +08:00
199466cd59 [feat] add support for OpenRouter AI provider (#3439)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Jast <shenghang@apache.org>
2025-06-09 23:35:08 +08:00
8065b63057 [docs] Add one-click installation for Rainbond Cloud (#3440)
Signed-off-by: songyg <songyg@goodrain.com>
Co-authored-by: songyg <songyg@goodrain.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-09 23:32:14 +08:00
Calvinandaias00 88655fcf19 [doc] japanese euleros (#3442)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-06-09 12:03:31 +08:00
Duansgandtomsun28 99b5460dd1 [feature] Support Huawei Cloud Cloud Eye alert source (#3443)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-09 00:22:31 +08:00
d681420c11 [feature] add support for darwin (#3431)
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-09 00:04:02 +08:00
Yang Chen 8618145e44 [feat] add support for ollama and update docs (#3441) 2025-06-07 18:48:41 +08:00
Duansgandkangli dd0045c6a4 [bugfix] Fix the issue where Prometheus RealTime Threshold is not eff… (#3434)
Co-authored-by: kangli <likang@apache.org>
2025-06-06 13:00:55 +08:00
Yang Chen 84e0a604e7 [improvement] disable default OpenTelemetry exporters to prevent connection errors (#3437) 2025-06-06 11:29:51 +08:00
Calvinandtomsun28 ce253f7069 [doc] japanese emqx (#3433)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-06 00:52:26 +08:00
Jast 767b70f70b [Improve] add mvnw (#3430) 2025-06-06 00:49:50 +08:00
Duansgandtomsun28 4aad08ca17 [feature]Support Alibaba Cloud 'Simple Log Service(SLS)' alert source (#3422)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-04 21:05:57 +08:00
c176bb2988 [doc] japanese elasticsearch (#3423)
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-04 09:31:22 +08:00
3cb4fc71f9 [doc] japanese dynamic_tp (#3419)
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-04 09:23:57 +08:00
c3f32132ca [feat] support sql("...") and promql("...") expressions including SQL condition parsing (#3410)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-06-04 09:07:18 +08:00
e1affb169f [feature]Make GreptimeDB as a grafana data source (#3403)
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-06-03 23:37:31 +08:00
shown db27159947 [infra]: Update issue tmpl (#3421)
Signed-off-by: shown <yuluo08290126@gmail.com>
2025-06-02 18:52:51 +08:00
63c187488f [doc] japanese doris_fe (#3416)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-31 09:14:37 +08:00
f3f66ada0d [doc] Update anonymous user auth configuration of the grafana dashboard document (#3407)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-05-31 09:12:33 +08:00
tomsun28 d5c934dc01 [doc] update home doc (#3418) 2025-05-30 21:26:14 +08:00
Jast 518eebb6b1 [Doc] Update 1.7.1 (#3411) 2025-05-29 16:12:24 +08:00
Calvin 210454f969 [doc] japanese doris_be (#3409) 2025-05-29 08:03:06 +08:00
Calvin a8db5e31a0 [doc] japanese docker (#3408) 2025-05-28 07:16:33 +08:00
Calvin c01653aff3 [doc] japanese dns sd (#3405) 2025-05-27 21:27:58 +08:00
Calvin 480bd0e449 [doc] japanese dns (#3404) 2025-05-27 08:47:33 +08:00
97faca6753 [docs] update alert threshold doc (#3399)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-26 16:12:50 +08:00
Calvinandtomsun28 e271bd7417 [doc] japanese deepseek & dm (#3400)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-26 08:53:40 +08:00
7de4fd1cbf [improve] http sd collector adds authentication (#3388)
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-05-26 08:42:21 +08:00
Calvin c2a7d0db35 [doc] japanese Debian (#3398) 2025-05-25 15:42:54 +08:00
Calvin 93b2630bf3 [doc] japanese dahua (#3396) 2025-05-25 13:09:06 +08:00
Calvinandliutianyou 23749e7fac [doc] japanese coreos (#3393)
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-24 09:35:46 +08:00
kangli d5eae40754 [doc]add new committer blog (#3395) 2025-05-24 08:23:17 +08:00
Calvin 7ef79de1de [doc] japanese consul sd (#3392) 2025-05-23 13:07:11 +08:00
tomsun28 8496771c8b [doc] new contributor and committer, update doc (#3391)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-05-23 12:15:09 +08:00
Calvin f31c835181 [doc] japanese clickhouse (#3390) 2025-05-23 08:42:33 +08:00
Logicandtomsun28 552554b96d [fix](warehouse): improve GreptimeDB data storage and querying (#3387)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-22 23:31:06 +08:00
zhangyaxiandtomsun28 4adcfd2a4e [test] Add unit tests for HttpSdCollectImpl (#3386)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-22 23:29:23 +08:00
Calvin e630beecb4 [doc] japanese cisco switch (#3389) 2025-05-22 22:46:53 +08:00
Duansgandtomsun28 6c2b38e180 [doc] modify supplement related documentation (#3383)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-21 23:17:50 +08:00
84986dd1bf [feature] support auto nacos service discovery (#3324)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: zhangyaxi <yaxi.zhang950408@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-05-21 22:01:09 +08:00
af868e083a [Task] Batch import metrics data in victoria-metrics (#3337)
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-05-20 23:56:56 +08:00
be3e2f3558 [doc] modify chinese words (#3380)
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 21:42:37 +08:00
d32818e8ef [feat] Support Zookeeper Service Discovery (#3377)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 21:30:48 +08:00
10b283522c [improve] add path validation for pluginservice (#3375)
Signed-off-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: liutianyou <tianyou.liu@qq.com>
2025-05-20 12:32:24 +08:00
31ab2da41e [doc] Add blog (#3379)
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:16:06 +08:00
LL-LINandtomsun28 a9436049e3 [doc] fix incorrect time template syntax usercase in doc (#3378)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:12:20 +08:00
Calvinandtomsun28 41c010511c [doc] japanese centos (#3376)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-05-20 00:08:39 +08:00
aias00andCalvin b5281c1274 update maven dep (#3359)
Co-authored-by: Calvin <naruse_shinji@163.com>
2025-05-20 00:06:59 +08:00
62cf3d1b2b [improve] improve jndi validation (#3358)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
2025-05-18 17:45:09 +08:00
386 changed files with 18364 additions and 2343 deletions
+19
View File
@@ -2485,6 +2485,25 @@
"contributions": [
"code"
]
},
{
"login": "Duansg",
"name": "Duansg",
"avatar_url": "https://avatars.githubusercontent.com/u/112607719?v=4",
"profile": "https://github.com/Duansg",
"contributions": [
"doc"
]
},
{
"login": "xiaomizhou2",
"name": "zhangyaxi",
"avatar_url": "https://avatars.githubusercontent.com/u/47807926?v=4",
"profile": "https://github.com/xiaomizhou2",
"contributions": [
"code",
"test"
]
}
],
"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
+2 -1
View File
@@ -53,4 +53,5 @@ body:
validations:
required: false
- type: markdown
value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR"
attributes:
value: "Please read the [Contribution Guideline](https://hertzbeat.apache.org/docs/community/contribution) before submitting the PR"
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
- uses: ./script/ci/github-actions/setup-deps
- name: Build with Maven
run: mvn clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
run: mvnd clean -B package -Prelease -Dmaven.test.skip=false --file pom.xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
+3 -5
View File
@@ -49,11 +49,9 @@ jobs:
- name: Dead Link Check
run: |
sudo npm install -g markdown-link-check@3.8.7
for file in $(find ./home -name "*.md"); do
if ! grep -Fxq "$file" ./script/ci/exclude_files.txt; then
markdown-link-check -c ./script/ci/link_check.json -q "$file"
fi
done
find ./home -name "*.md" > all_md_files.txt
grep -vFf ./script/ci/exclude_files.txt all_md_files.txt > to_check.txt
xargs -P 8 -a to_check.txt -I{} markdown-link-check -c ./script/ci/link_check.json -q "{}"
- name: NPM INSTALL
working-directory: home
+4
View File
@@ -523,6 +523,10 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
</tr>
</tbody>
</table>
+9 -5
View File
@@ -33,8 +33,8 @@
### 特点
-**监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,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酱` 等方式消息及时送达。
@@ -59,8 +59,8 @@
## ⛄ 已支持
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模,用户可以导入这些模来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模,用户可以导入这些模来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
@@ -98,7 +98,7 @@
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
- 和更多自定义监控模
- 和更多自定义监控模
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`
## 🐕 快速开始
@@ -521,6 +521,10 @@ 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>
</tr>
</tbody>
</table>
@@ -139,6 +139,9 @@ public class RealTimeAlertCalculator {
String instanceHost = metricsData.getInstanceHost();
String app = metricsData.getApp();
String metrics = metricsData.getMetrics();
if ((CommonConstants.PROMETHEUS_APP_PREFIX + instanceName).equals(metricsData.getApp())) {
app = CommonConstants.PROMETHEUS;
}
int priority = metricsData.getPriority();
int code = metricsData.getCode().getNumber();
Map<String, String> labels = metricsData.getLabels();
@@ -17,17 +17,16 @@
package org.apache.hertzbeat.alert.controller;
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.Objects;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -36,8 +35,17 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
import static org.apache.hertzbeat.common.constants.CommonConstants.MONITOR_NOT_EXIST_CODE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* Alarm definition management API
*/
@@ -90,4 +98,18 @@ public class AlertDefineController {
return ResponseEntity.ok(Message.success("Delete success"));
}
@GetMapping(path = "/preview/{datasource}")
@Operation(summary = "Alarm definition expression preview",
description = "If the expression is formal, then the result of the query will be returned, otherwise it will respond with an error")
public ResponseEntity<Message<List<Map<String, Object>>>> getDefinePreview(
@Parameter(description = "Data Source Type", example = "promql") @PathVariable("datasource") String datasource,
@Parameter(description = "alert threshold type:realtime,periodic") @RequestParam String type,
@Parameter(description = "alert threshold expression") @RequestParam String expr) {
try {
return ResponseEntity.ok(Message.successWithData(alertDefineService.getDefinePreview(datasource, type, expr)));
} catch (AlertExpressionException ae) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Message.fail(FAIL_CODE, ae.getMessage()));
}
}
}
@@ -0,0 +1,184 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Alibaba Cloud 'Simple Log Service(SLS)' alert content entity.
*
* @see <a href="https://help.aliyun.com/zh/sls/user-guide/variables-in-new-alert-templates"/>
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AlibabaCloudSlsExternAlert {
/**
* The id of the instance on which the alarm was triggered
*/
@JsonProperty("alert_instance_id")
private String alertInstanceId;
/**
* Alarm rule id, unique within project
*/
@JsonProperty("alert_id")
private String alertId;
/**
* Alarm rule name
*/
@JsonProperty("alert_name")
private String alertName;
/**
* Region
*/
private String region;
/**
* Alarm rule belongs to Project
*/
private String project;
/**
* Time of this evaluation
*/
@JsonProperty("alert_time")
private int alertTime;
/**
* First trigger time
*/
@JsonProperty("fire_time")
private int fireTime;
/**
* Alarm recovery time
* If the alarm status is firing, the value is 0.
* If the alarm state is resolved, the value is the specific recovery time.
*/
@JsonProperty("resolve_time")
private int resolveTime;
/**
* Alarm status.
* firing: Triggers an alarm.
* resolved: Notification of resumption.
*/
private String status;
/**
* The total number of entries in the data that triggered the alert,
* which may be more than 100, for example after a Cartesian product operation.
*/
@JsonProperty("fire_results_count")
private int fireResultsCount;
/**
* Tag list
* Example: {"env":"test"}
*/
private Map<String, String> labels;
/**
* Labeled lists
* Example: { "title": "Alarm title","desc": "Alarm desc" }
*/
private Map<String, String> annotations;
/**
* Alarm severity.
*
* 10: Critical
* 8: High
* 6: Medium
* 4: Low
* 2: Report only
*/
private int severity;
/**
*
*/
@JsonProperty("signin_url")
private String signinUrl;
public String getAnnotation(String key) {
if (null == this.annotations || this.annotations.isEmpty()) {
return "N/A";
}
return this.annotations.get(key);
}
/**
* Severity
*/
public enum Severity {
CRITICAL(10, "Critical"),
HIGH(8, "High"),
MEDIUM(6, "Medium"),
LOW(4, "Low"),
REPORT_ONLY(2, "Report only");
private static final Map<Integer, Severity> STATUS_MAP;
static {
STATUS_MAP = Arrays.stream(Severity.values()).collect(Collectors.toMap(Severity::getStatus, t -> t, (oldVal, newVal) -> newVal));
}
private final int status;
private final String alias;
Severity(int status, String alias) {
this.status = status;
this.alias = alias;
}
public static Optional<Severity> convert(int severity) {
return Optional.ofNullable(STATUS_MAP.get(severity));
}
public int getStatus() {
return status;
}
public String getAlias() {
return alias;
}
}
}
@@ -0,0 +1,237 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
/**
* Huawei Cloud (CES) alert content entity.
*
* @see <a href="https://support.huaweicloud.com/usermanual-ces/ces_01_0218.html"/>
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HuaweiCloudExternAlert {
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_MESSAGE_ID = "message_id";
public static final String FIELD_TIMESTAMP = "timestamp";
public static final String FIELD_TOPIC_URN = "topic_urn";
public static final String FIELD_TYPE = "type";
public static final String FIELD_SUBJECT = "subject";
public static final String FIELD_SUBSCRIBE_URL = "subscribe_url";
/**
* Signature information.
*/
private String signature;
/**
* Subject
*/
private String subject;
/**
* The unique identifier of a topic, indicating the topic to which the message belongs.
*/
@JsonProperty("topic_urn")
private String topicUrn;
/**
* Message unique identifier.
*/
@JsonProperty("message_id")
private String messageId;
/**
* Message
*/
private String message;
/**
* message types, the message types are respectively:
* SubscriptionConfirmation、Notification、UnsubscribeConfirmation
*/
private String type;
/**
* Subscription confirms the URL that needs to be accessed
*/
@JsonProperty("subscribe_url")
private String subscribeUrl;
/**
* The certificate URL used for message signing, which does not require authentication and can be accessed directly.
*/
@JsonProperty("signing_cert_url")
private String signingCertUrl;
/**
* The timestamp of when the message was first sent.
*/
private String timestamp;
/**
* Alert message
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class AlertMessage {
private String version;
private AlertData data;
}
/**
* Alert data
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class AlertData {
/**
* Whether an alarm occurs.
* Note: Empty and false are both recovery notifications.
* Note: There are no recovery notifications for event types
*/
@JsonProperty("IsAlarm")
private Boolean alarm;
/**
* Alarm time
*/
@JsonProperty("AlarmTime")
private String alarmTime;
/**
* Resource ID
*/
@JsonProperty("ResourceId")
private String resourceId;
/**
* The name of the metric
*/
@JsonProperty("MetricName")
private String metricName;
/**
* Specifies the alarm severity, which can be Critical, Major, Minor, or Informational.
*/
@JsonProperty("AlarmLevel")
private String alarmLevel;
/**
* Namespace
*/
@JsonProperty("Namespace")
private String namespace;
/**
* Region
*/
@JsonProperty("Region")
private String region;
/**
* Dimension name
*/
@JsonProperty("DimensionName")
private String dimensionName;
/**
* Resource name
*/
@JsonProperty("ResourceName")
private String resourceName;
/**
* Alarm record ID
*/
@JsonProperty("AlarmRecordID")
private String alarmRecordId;
/**
* Current data
*/
@JsonProperty("CurrentData")
private String currentData;
/**
* The comparison conditions for the alarm thresholds can be >, =, <, >=, <=.
*/
@JsonProperty("ComparisonOperator")
private String comparisonOperator;
/**
* Alarm value
*/
@JsonProperty("Value")
private String value;
/**
* Number of consecutive occurrences of triggered alarms
*/
@JsonProperty("Count")
private int count;
}
/**
* Huawei cloud alert type
*/
public enum AlertType {
SUBSCRIPTION("SubscriptionConfirmation"),
UNSUBSCRIBE("UnsubscribeConfirmation"),
NOTIFICATION("Notification");
private final String type;
AlertType(String type) {
this.type = type;
}
public static boolean valid(String type) {
if (null == type || type.isEmpty()) {
return false;
}
return Arrays.stream(AlertType.values()).anyMatch(alertType -> alertType.getType().equals(type));
}
public String getType() {
return type;
}
}
}
@@ -0,0 +1,121 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* volcengine event alert entity class
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VolcEngineExternEventAlert {
@JsonProperty("Type")
private String type;
@JsonProperty("AccountId")
private String accountId;
@JsonProperty("Source")
private String source;
@JsonProperty("ProjectName")
private String projectName;
@JsonProperty("Id")
private String id;
@JsonProperty("EventType")
private String eventType;
@JsonProperty("DescriptionCN")
private String descriptionCn;
@JsonProperty("HappenedAt")
private Long happenedAt;
@JsonProperty("Region")
private String region;
@JsonProperty("Details")
private EventDetails details;
@JsonProperty("Rules")
private List<EventRule> rules;
/**
* event details
*/
@Data
public static class EventDetails {
@JsonProperty("specversion")
private String specVersion;
@JsonProperty("id")
private String id;
@JsonProperty("source")
private String source;
@JsonProperty("type")
private String type;
@JsonProperty("volcpublishtime")
private String volcPublishTime;
@JsonProperty("volcregion")
private String volcRegion;
@JsonProperty("volcaccountid")
private String volcAccountId;
@JsonProperty("volceventbusname")
private String volcEventBusName;
@JsonProperty("volcresourcename")
private String volcResourceName;
@JsonProperty("subject")
private String subject;
@JsonProperty("time")
private String time;
}
/**
* event rule
*/
@Data
public static class EventRule {
@JsonProperty("RuleId")
private String ruleId;
@JsonProperty("RuleName")
private String ruleName;
@JsonProperty("Level")
private String level;
}
}
@@ -0,0 +1,203 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Volcengine alarm entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VolcEngineExternMetricAlert {
@JsonProperty("Type")
private String type;
@JsonProperty("AccountId")
private String accountId;
@JsonProperty("RuleName")
private String ruleName;
@JsonProperty("RuleId")
private String ruleId;
@JsonProperty("Namespace")
private String namespace;
@JsonProperty("SubNamespace")
private String subNamespace;
@JsonProperty("Level")
private String level;
@JsonProperty("HappenedAt")
private String happenedAt;
@JsonProperty("RuleCondition")
private String ruleCondition;
@JsonProperty("Resources")
private List<Resource> resources;
@JsonProperty("RecoveredResources")
private List<Resource> recoveredResources;
@JsonProperty("NoDataResources")
private List<NoDataResource> noDataResources;
@JsonProperty("NoDataRecoveredResources")
private List<NoDataRecoveredResource> noDataRecoveredResources;
/**
* alert resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Resource {
@JsonProperty("Id")
private String id;
@JsonProperty("Name")
private String name;
@JsonProperty("Region")
private String region;
@JsonProperty("FirstAlertTime")
private Long firstAlertTime;
@JsonProperty("LastAlertTime")
private Long lastAlertTime;
@JsonProperty("Metrics")
private List<Metric> metrics;
@JsonProperty("AlertGroupId")
private String alertGroupId;
@JsonProperty("Dimensions")
private List<Dimension> dimensions;
}
/**
* alert no data resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class NoDataResource extends Resource {
@JsonProperty("NoDataMetrics")
private List<Metric> noDataMetrics;
}
/**
* alert no data recovered resource entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class NoDataRecoveredResource extends Resource {
@JsonProperty("DroppedMetrics")
private List<DroppedMetric> droppedMetrics;
}
/**
* alert metric entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Metric {
@JsonProperty("Name")
private String name;
@JsonProperty("Unit")
private String unit;
@JsonProperty("Threshold")
private Double threshold;
@JsonProperty("CurrentValue")
private Object currentValue;
@JsonProperty("Description")
private String description;
@JsonProperty("Warning")
private Boolean warning;
}
/**
* alert dropped metric entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public static class DroppedMetric extends Metric {
@JsonProperty("Reason")
private String reason;
}
/**
* resource dimension entity class
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Dimension {
@JsonProperty("Name")
private String name;
@JsonProperty("NameCN")
private String nameCn;
@JsonProperty("Value")
private String value;
@JsonProperty("Description")
private String description;
}
public static final String ALERT_TYPE_METRIC = "Metric";
public static final String ALERT_TYPE_EVENT = "Event";
public static final String ALERT_TYPE_METRIC_RECOVERED = "MetricRecovered";
public static final String ALERT_TYPE_METRICS_NODATA = "MetricsNoData";
public static final String ALERT_TYPE_NO_DATA_RECOVERED = "NoDataRecovered";
}
@@ -47,14 +47,6 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
*/
@Override public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitQueryExpr(AlertExpressionParser.QueryExprContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
@@ -71,6 +63,22 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
*/
@Override public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <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); }
/**
* {@inheritDoc}
*
* <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); }
/**
* {@inheritDoc}
*
@@ -88,7 +96,23 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
@Override public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
@@ -101,7 +125,23 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitIdentifier(AlertExpressionParser.IdentifierContext ctx) { return visitChildren(ctx); }
@Override public T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitParameterList(AlertExpressionParser.ParameterListContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitParameter(AlertExpressionParser.ParameterContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
@@ -110,4 +150,204 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitNumber(AlertExpressionParser.NumberContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitString(AlertExpressionParser.StringContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitDuration(AlertExpressionParser.DurationContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitSelectField(AlertExpressionParser.SelectFieldContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitGroupByList(AlertExpressionParser.GroupByListContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitOrderByList(AlertExpressionParser.OrderByListContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitRelList(AlertExpressionParser.RelListContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitRelation(AlertExpressionParser.RelationContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitConditionList(AlertExpressionParser.ConditionListContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitCompOp(AlertExpressionParser.CompOpContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitCondition(AlertExpressionParser.ConditionContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitPromql(AlertExpressionParser.PromqlContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitLabelList(AlertExpressionParser.LabelListContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) { return visitChildren(ctx); }
/**
* {@inheritDoc}
*
* <p>The default implementation returns the result of calling
* {@link #visitChildren} on {@code ctx}.</p>
*/
@Override public T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) { return visitChildren(ctx); }
/**
* {@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); }
}
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.alert.expr;
import org.antlr.v4.runtime.CommonTokenStream;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import java.util.ArrayList;
@@ -24,6 +25,8 @@ 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
@@ -31,12 +34,16 @@ import java.util.Map;
public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<Map<String, Object>>> {
private static final String THRESHOLD = "__threshold__";
private static final String NAME = "__name__";
private static final String VALUE = "__value__";
private static final String TIMESTAMP = "__timestamp__";
private final QueryExecutor executor;
private final CommonTokenStream tokens;
public AlertExpressionEvalVisitor(QueryExecutor executor) {
public AlertExpressionEvalVisitor(QueryExecutor executor, CommonTokenStream tokens) {
this.executor = executor;
this.tokens = tokens;
}
@Override
@@ -66,10 +73,11 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
}
// queryValues may be a list of values, or a single value
Object matchValue = evaluateCondition(queryValues, operator, threshold);
item.put(VALUE, matchValue);
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(new HashMap<>(item));
result.add(resultMap);
}
return result;
}
@@ -80,42 +88,26 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
public List<Map<String, Object>> visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
List<Map<String, Object>> results = new ArrayList<>();
Map<String, Object> leftMap = null;
boolean leftMatch = false;
Map<String, Object> rightMap = null;
boolean rightMatch = false;
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
// build a hash set of the right-side tag collection
Set<String> rightLabelsSet = rightOperand.stream()
.filter(item -> item.get(VALUE) != null)
.map(this::labelKey)
.collect(Collectors.toSet());
// iterate over the left side, O(1) match
for (Map<String, Object> leftItem : leftOperand) {
Object leftVal = leftItem.get(VALUE);
if (leftVal == null) {
continue;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
String labelKey = labelKey(leftItem);
if (rightLabelsSet.contains(labelKey)) {
results.add(new HashMap<>(leftItem));
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && rightMatch) {
rightMap.putAll(leftMap);
return new LinkedList<>(List.of(rightMap));
} else if (leftMap != null) {
leftMap.put(VALUE, null);
return new LinkedList<>(List.of(leftMap));
} else if (rightMap != null) {
rightMap.put(VALUE, null);
return new LinkedList<>(List.of(rightMap));
}
return new LinkedList<>();
return results;
}
@Override
@@ -123,98 +115,51 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
Map<String, Object> leftMap = null;
boolean leftMatch = false;
Map<String, Object> rightMap = null;
boolean rightMatch = false;
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
// build a hashMap of the left-hand label collection
Map<String, Map<String, Object>> leftLabelMap = leftOperand.stream()
.filter(item -> item.get(VALUE) != null)
.collect(Collectors.toMap(this::labelKey, HashMap::new, (k1, k2) -> k1));
// first add all the non-empty items on the left side
List<Map<String, Object>> results = new ArrayList<>(leftLabelMap.values());
// add the term that has a value on the right side and not on the left side
for (Map<String, Object> rightItem : rightOperand) {
Object rightVal = rightItem.get(VALUE);
if (rightVal == null) {
continue;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
String key = labelKey(rightItem);
if (!leftLabelMap.containsKey(key)) {
results.add(new HashMap<>(rightItem));
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && rightMatch) {
rightMap.putAll(leftMap);
return new LinkedList<>(List.of(rightMap));
} else if (leftMatch) {
return new LinkedList<>(List.of(leftMap));
} else if (rightMatch) {
return new LinkedList<>(List.of(rightMap));
} else {
if (leftMap != null && rightMap != null) {
rightMap.putAll(leftMap);
return new LinkedList<>(List.of(rightMap));
} else if (leftMap != null) {
return new LinkedList<>(List.of(leftMap));
} else if (rightMap != null) {
return new LinkedList<>(List.of(rightMap));
}
}
return new LinkedList<>();
return results;
}
@Override
public List<Map<String, Object>> visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
List<Map<String, Object>> leftOperand = visit(ctx.left);
List<Map<String, Object>> rightOperand = visit(ctx.right);
Map<String, Object> leftMap = null;
boolean leftMatch = false;
Map<String, Object> rightMap = null;
boolean rightMatch = false;
for (Map<String, Object> item : leftOperand) {
if (leftMap == null) {
leftMap = item;
}
if (item.get(VALUE) != null) {
leftMap = item;
leftMatch = true;
break;
}
}
for (Map<String, Object> item : rightOperand) {
if (rightMap == null) {
rightMap = item;
}
if (item.get(VALUE) != null) {
rightMap = item;
rightMatch = true;
break;
}
}
if (leftMatch && !rightMatch) {
return new LinkedList<>(List.of(leftMap));
} else {
if (leftMap != null) {
leftMap.put(VALUE, null);
return new LinkedList<>(List.of(leftMap));
} else {
if (rightMap != null) {
rightMap.put(VALUE, null);
return new LinkedList<>(List.of(rightMap));
}
}
}
return new LinkedList<>();
}
List<Map<String, Object>> results = new ArrayList<>();
@Override
public List<Map<String, Object>> visitQueryExpr(AlertExpressionParser.QueryExprContext ctx) {
String query = ctx.identifier().getText();
return executor.execute(query);
// build a hash set of the right-side tag collection
Set<String> rightLabelSet = rightOperand.stream()
.filter(item -> item.get(VALUE) != null)
.map(this::labelKey)
.collect(Collectors.toSet());
// iterate over the left side, O(1) match
for (Map<String, Object> leftItem : leftOperand) {
Object leftVal = leftItem.get(VALUE);
if (leftVal == null) {
continue;
}
if (!rightLabelSet.contains(labelKey(leftItem))) {
results.add(new HashMap<>(leftItem));
}
}
return results;
}
@Override
@@ -227,6 +172,28 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
return numAsList;
}
@Override
public List<Map<String, Object>> visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
String rawPromql = tokens.getText(ctx.promql());
return executor.execute(rawPromql);
}
@Override
public List<Map<String, Object>> visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
String rawSql = tokens.getText(ctx.selectSql());
return executor.execute(rawSql);
}
@Override
public List<Map<String, Object>> visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
return callSqlOrPromql(tokens.getText(ctx.string()));
}
@Override
public List<Map<String, Object>> visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
return callSqlOrPromql(tokens.getText(ctx.string()));
}
private Object evaluateCondition(Object value, String operator, Double threshold) {
// value may be a list of values, or a single value
switch (operator) {
@@ -302,4 +269,25 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
return null;
}
}
}
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;
}
}
@@ -36,37 +36,66 @@ public class AlertExpressionLexer extends Lexer {
protected static final DFA[] _decisionToDFA;
protected static final PredictionContextCache _sharedContextCache =
new PredictionContextCache();
new PredictionContextCache();
public static final int
AND=1, OR=2, UNLESS=3, GT=4, GE=5, LT=6, LE=7, EQ=8, NE=9, LPAREN=10,
RPAREN=11, IDENTIFIER=12, NUMBER=13, WS=14;
AND=1, OR=2, UNLESS=3, NOT=4, SELECT=5, FROM=6, WHERE=7, GROUP=8, BY=9,
HAVING=10, ORDER=11, LIMIT=12, OFFSET=13, AS=14, ASC=15, DESC=16, IN=17,
IS=18, NULL=19, LIKE=20, BETWEEN=21, STAR=22, COUNT=23, SUM=24, AVG=25,
MIN=26, MAX=27, STDDEV=28, STDVAR=29, VARIANCE=30, RATE_FUNCTION=31, INCREASE_FUNCTION=32,
HISTOGRAM_QUANTILE_FUNCTION=33, TOPK=34, BOTTOMK=35, QUANTILE=36, BY_FUNCTION=37,
WITHOUT_FUNCTION=38, GROUP_LEFT_FUNCTION=39, GROUP_RIGHT_FUNCTION=40,
IGNORING_FUNCTION=41, ON_FUNCTION=42, SQL_FUNCTION=43, PROMQL_FUNCTION=44,
GT=45, GE=46, LT=47, LE=48, EQ=49, NE=50, LPAREN=51, RPAREN=52, LBRACE=53,
RBRACE=54, LBRACKET=55, RBRACKET=56, COMMA=57, DOT=58, COLON=59, SEMICOLON=60,
SCIENTIFIC_NUMBER=61, FLOAT=62, NUMBER=63, DURATION=64, STRING=65, IDENTIFIER=66,
WS=67, LINE_COMMENT=68, BLOCK_COMMENT=69;
public static String[] channelNames = {
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
};
public static String[] modeNames = {
"DEFAULT_MODE"
"DEFAULT_MODE"
};
private static String[] makeRuleNames() {
return new String[] {
"AND", "OR", "UNLESS", "GT", "GE", "LT", "LE", "EQ", "NE", "LPAREN",
"RPAREN", "IDENTIFIER", "NUMBER", "WS"
"AND", "OR", "UNLESS", "NOT", "SELECT", "FROM", "WHERE", "GROUP", "BY",
"HAVING", "ORDER", "LIMIT", "OFFSET", "AS", "ASC", "DESC", "IN", "IS",
"NULL", "LIKE", "BETWEEN", "STAR", "COUNT", "SUM", "AVG", "MIN", "MAX",
"STDDEV", "STDVAR", "VARIANCE", "RATE_FUNCTION", "INCREASE_FUNCTION",
"HISTOGRAM_QUANTILE_FUNCTION", "TOPK", "BOTTOMK", "QUANTILE", "BY_FUNCTION",
"WITHOUT_FUNCTION", "GROUP_LEFT_FUNCTION", "GROUP_RIGHT_FUNCTION", "IGNORING_FUNCTION",
"ON_FUNCTION", "SQL_FUNCTION", "PROMQL_FUNCTION", "GT", "GE", "LT", "LE",
"EQ", "NE", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET",
"COMMA", "DOT", "COLON", "SEMICOLON", "SCIENTIFIC_NUMBER", "FLOAT", "NUMBER",
"DURATION", "STRING", "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT"
};
}
public static final String[] ruleNames = makeRuleNames();
private static String[] makeLiteralNames() {
return new String[] {
null, "'and'", "'or'", "'unless'", "'>'", "'>='", "'<'", "'<='", "'=='",
"'!='", "'('", "')'"
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, "'*'", null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, "'>'", "'>='",
"'<'", "'<='", null, "'!='", "'('", "')'", "'{'", "'}'", "'['", "']'",
"','", "'.'", "':'", "';'"
};
}
private static final String[] _LITERAL_NAMES = makeLiteralNames();
private static String[] makeSymbolicNames() {
return new String[] {
null, "AND", "OR", "UNLESS", "GT", "GE", "LT", "LE", "EQ", "NE", "LPAREN",
"RPAREN", "IDENTIFIER", "NUMBER", "WS"
null, "AND", "OR", "UNLESS", "NOT", "SELECT", "FROM", "WHERE", "GROUP",
"BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "AS", "ASC", "DESC", "IN",
"IS", "NULL", "LIKE", "BETWEEN", "STAR", "COUNT", "SUM", "AVG", "MIN",
"MAX", "STDDEV", "STDVAR", "VARIANCE", "RATE_FUNCTION", "INCREASE_FUNCTION",
"HISTOGRAM_QUANTILE_FUNCTION", "TOPK", "BOTTOMK", "QUANTILE", "BY_FUNCTION",
"WITHOUT_FUNCTION", "GROUP_LEFT_FUNCTION", "GROUP_RIGHT_FUNCTION", "IGNORING_FUNCTION",
"ON_FUNCTION", "SQL_FUNCTION", "PROMQL_FUNCTION", "GT", "GE", "LT", "LE",
"EQ", "NE", "LPAREN", "RPAREN", "LBRACE", "RBRACE", "LBRACKET", "RBRACKET",
"COMMA", "DOT", "COLON", "SEMICOLON", "SCIENTIFIC_NUMBER", "FLOAT", "NUMBER",
"DURATION", "STRING", "IDENTIFIER", "WS", "LINE_COMMENT", "BLOCK_COMMENT"
};
}
private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames();
@@ -128,62 +157,363 @@ public class AlertExpressionLexer extends Lexer {
public ATN getATN() { return _ATN; }
public static final String _serializedATN =
"\u0004\u0000\u000eZ\u0006\uffff\uffff\u0002\u0000\u0007\u0000\u0002\u0001"+
"\u0007\u0001\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002\u0004"+
"\u0007\u0004\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002\u0007"+
"\u0007\u0007\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002\u000b"+
"\u0007\u000b\u0002\f\u0007\f\u0002\r\u0007\r\u0001\u0000\u0001\u0000\u0001"+
"\u0000\u0001\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0001\u0002\u0001"+
"\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001"+
"\u0003\u0001\u0003\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0005\u0001"+
"\u0005\u0001\u0006\u0001\u0006\u0001\u0006\u0001\u0007\u0001\u0007\u0001"+
"\u0007\u0001\b\u0001\b\u0001\b\u0001\t\u0001\t\u0001\n\u0001\n\u0001\u000b"+
"\u0001\u000b\u0005\u000bB\b\u000b\n\u000b\f\u000bE\t\u000b\u0001\f\u0004"+
"\fH\b\f\u000b\f\f\fI\u0001\f\u0001\f\u0004\fN\b\f\u000b\f\f\fO\u0003\f"+
"R\b\f\u0001\r\u0004\rU\b\r\u000b\r\f\rV\u0001\r\u0001\r\u0000\u0000\u000e"+
"\u0001\u0001\u0003\u0002\u0005\u0003\u0007\u0004\t\u0005\u000b\u0006\r"+
"\u0007\u000f\b\u0011\t\u0013\n\u0015\u000b\u0017\f\u0019\r\u001b\u000e"+
"\u0001\u0000\u0004\u0003\u0000AZ__az\n\u0000\"\"..09==A[]]__a{}}\u8000"+
"\uff5e\u8000\uff5e\u0001\u000009\u0003\u0000\t\n\r\r ^\u0000\u0001\u0001"+
"\u0000\u0000\u0000\u0000\u0003\u0001\u0000\u0000\u0000\u0000\u0005\u0001"+
"\u0000\u0000\u0000\u0000\u0007\u0001\u0000\u0000\u0000\u0000\t\u0001\u0000"+
"\u0000\u0000\u0000\u000b\u0001\u0000\u0000\u0000\u0000\r\u0001\u0000\u0000"+
"\u0000\u0000\u000f\u0001\u0000\u0000\u0000\u0000\u0011\u0001\u0000\u0000"+
"\u0000\u0000\u0013\u0001\u0000\u0000\u0000\u0000\u0015\u0001\u0000\u0000"+
"\u0000\u0000\u0017\u0001\u0000\u0000\u0000\u0000\u0019\u0001\u0000\u0000"+
"\u0000\u0000\u001b\u0001\u0000\u0000\u0000\u0001\u001d\u0001\u0000\u0000"+
"\u0000\u0003!\u0001\u0000\u0000\u0000\u0005$\u0001\u0000\u0000\u0000\u0007"+
"+\u0001\u0000\u0000\u0000\t-\u0001\u0000\u0000\u0000\u000b0\u0001\u0000"+
"\u0000\u0000\r2\u0001\u0000\u0000\u0000\u000f5\u0001\u0000\u0000\u0000"+
"\u00118\u0001\u0000\u0000\u0000\u0013;\u0001\u0000\u0000\u0000\u0015="+
"\u0001\u0000\u0000\u0000\u0017?\u0001\u0000\u0000\u0000\u0019G\u0001\u0000"+
"\u0000\u0000\u001bT\u0001\u0000\u0000\u0000\u001d\u001e\u0005a\u0000\u0000"+
"\u001e\u001f\u0005n\u0000\u0000\u001f \u0005d\u0000\u0000 \u0002\u0001"+
"\u0000\u0000\u0000!\"\u0005o\u0000\u0000\"#\u0005r\u0000\u0000#\u0004"+
"\u0001\u0000\u0000\u0000$%\u0005u\u0000\u0000%&\u0005n\u0000\u0000&\'"+
"\u0005l\u0000\u0000\'(\u0005e\u0000\u0000()\u0005s\u0000\u0000)*\u0005"+
"s\u0000\u0000*\u0006\u0001\u0000\u0000\u0000+,\u0005>\u0000\u0000,\b\u0001"+
"\u0000\u0000\u0000-.\u0005>\u0000\u0000./\u0005=\u0000\u0000/\n\u0001"+
"\u0000\u0000\u000001\u0005<\u0000\u00001\f\u0001\u0000\u0000\u000023\u0005"+
"<\u0000\u000034\u0005=\u0000\u00004\u000e\u0001\u0000\u0000\u000056\u0005"+
"=\u0000\u000067\u0005=\u0000\u00007\u0010\u0001\u0000\u0000\u000089\u0005"+
"!\u0000\u00009:\u0005=\u0000\u0000:\u0012\u0001\u0000\u0000\u0000;<\u0005"+
"(\u0000\u0000<\u0014\u0001\u0000\u0000\u0000=>\u0005)\u0000\u0000>\u0016"+
"\u0001\u0000\u0000\u0000?C\u0007\u0000\u0000\u0000@B\u0007\u0001\u0000"+
"\u0000A@\u0001\u0000\u0000\u0000BE\u0001\u0000\u0000\u0000CA\u0001\u0000"+
"\u0000\u0000CD\u0001\u0000\u0000\u0000D\u0018\u0001\u0000\u0000\u0000"+
"EC\u0001\u0000\u0000\u0000FH\u0007\u0002\u0000\u0000GF\u0001\u0000\u0000"+
"\u0000HI\u0001\u0000\u0000\u0000IG\u0001\u0000\u0000\u0000IJ\u0001\u0000"+
"\u0000\u0000JQ\u0001\u0000\u0000\u0000KM\u0005.\u0000\u0000LN\u0007\u0002"+
"\u0000\u0000ML\u0001\u0000\u0000\u0000NO\u0001\u0000\u0000\u0000OM\u0001"+
"\u0000\u0000\u0000OP\u0001\u0000\u0000\u0000PR\u0001\u0000\u0000\u0000"+
"QK\u0001\u0000\u0000\u0000QR\u0001\u0000\u0000\u0000R\u001a\u0001\u0000"+
"\u0000\u0000SU\u0007\u0003\u0000\u0000TS\u0001\u0000\u0000\u0000UV\u0001"+
"\u0000\u0000\u0000VT\u0001\u0000\u0000\u0000VW\u0001\u0000\u0000\u0000"+
"WX\u0001\u0000\u0000\u0000XY\u0006\r\u0000\u0000Y\u001c\u0001\u0000\u0000"+
"\u0000\u0006\u0000CIOQV\u0001\u0006\u0000\u0000";
"\u0004\u0000E\u0228\u0006\uffff\uffff\u0002\u0000\u0007\u0000\u0002\u0001"+
"\u0007\u0001\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002\u0004"+
"\u0007\u0004\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002\u0007"+
"\u0007\u0007\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002\u000b"+
"\u0007\u000b\u0002\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e\u0002"+
"\u000f\u0007\u000f\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011\u0002"+
"\u0012\u0007\u0012\u0002\u0013\u0007\u0013\u0002\u0014\u0007\u0014\u0002"+
"\u0015\u0007\u0015\u0002\u0016\u0007\u0016\u0002\u0017\u0007\u0017\u0002"+
"\u0018\u0007\u0018\u0002\u0019\u0007\u0019\u0002\u001a\u0007\u001a\u0002"+
"\u001b\u0007\u001b\u0002\u001c\u0007\u001c\u0002\u001d\u0007\u001d\u0002"+
"\u001e\u0007\u001e\u0002\u001f\u0007\u001f\u0002 \u0007 \u0002!\u0007"+
"!\u0002\"\u0007\"\u0002#\u0007#\u0002$\u0007$\u0002%\u0007%\u0002&\u0007"+
"&\u0002\'\u0007\'\u0002(\u0007(\u0002)\u0007)\u0002*\u0007*\u0002+\u0007"+
"+\u0002,\u0007,\u0002-\u0007-\u0002.\u0007.\u0002/\u0007/\u00020\u0007"+
"0\u00021\u00071\u00022\u00072\u00023\u00073\u00024\u00074\u00025\u0007"+
"5\u00026\u00076\u00027\u00077\u00028\u00078\u00029\u00079\u0002:\u0007"+
":\u0002;\u0007;\u0002<\u0007<\u0002=\u0007=\u0002>\u0007>\u0002?\u0007"+
"?\u0002@\u0007@\u0002A\u0007A\u0002B\u0007B\u0002C\u0007C\u0002D\u0007"+
"D\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0000\u0001\u0001\u0001\u0001"+
"\u0001\u0001\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002"+
"\u0001\u0002\u0001\u0002\u0001\u0003\u0001\u0003\u0001\u0003\u0001\u0003"+
"\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004\u0001\u0004"+
"\u0001\u0004\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005\u0001\u0005"+
"\u0001\u0006\u0001\u0006\u0001\u0006\u0001\u0006\u0001\u0006\u0001\u0006"+
"\u0001\u0007\u0001\u0007\u0001\u0007\u0001\u0007\u0001\u0007\u0001\u0007"+
"\u0001\b\u0001\b\u0001\b\u0001\t\u0001\t\u0001\t\u0001\t\u0001\t\u0001"+
"\t\u0001\t\u0001\n\u0001\n\u0001\n\u0001\n\u0001\n\u0001\n\u0001\u000b"+
"\u0001\u000b\u0001\u000b\u0001\u000b\u0001\u000b\u0001\u000b\u0001\f\u0001"+
"\f\u0001\f\u0001\f\u0001\f\u0001\f\u0001\f\u0001\r\u0001\r\u0001\r\u0001"+
"\u000e\u0001\u000e\u0001\u000e\u0001\u000e\u0001\u000f\u0001\u000f\u0001"+
"\u000f\u0001\u000f\u0001\u000f\u0001\u0010\u0001\u0010\u0001\u0010\u0001"+
"\u0011\u0001\u0011\u0001\u0011\u0001\u0012\u0001\u0012\u0001\u0012\u0001"+
"\u0012\u0001\u0012\u0001\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0001"+
"\u0013\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001"+
"\u0014\u0001\u0014\u0001\u0014\u0001\u0015\u0001\u0015\u0001\u0016\u0001"+
"\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0017\u0001"+
"\u0017\u0001\u0017\u0001\u0017\u0001\u0018\u0001\u0018\u0001\u0018\u0001"+
"\u0018\u0001\u0019\u0001\u0019\u0001\u0019\u0001\u0019\u0001\u001a\u0001"+
"\u001a\u0001\u001a\u0001\u001a\u0001\u001b\u0001\u001b\u0001\u001b\u0001"+
"\u001b\u0001\u001b\u0001\u001b\u0001\u001b\u0001\u001c\u0001\u001c\u0001"+
"\u001c\u0001\u001c\u0001\u001c\u0001\u001c\u0001\u001c\u0001\u001d\u0001"+
"\u001d\u0001\u001d\u0001\u001d\u0001\u001d\u0001\u001d\u0001\u001d\u0001"+
"\u001d\u0001\u001d\u0001\u001e\u0001\u001e\u0001\u001e\u0001\u001e\u0001"+
"\u001e\u0001\u001f\u0001\u001f\u0001\u001f\u0001\u001f\u0001\u001f\u0001"+
"\u001f\u0001\u001f\u0001\u001f\u0001\u001f\u0001 \u0001 \u0001 \u0001"+
" \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001 \u0001"+
" \u0001 \u0001 \u0001 \u0001 \u0001 \u0001!\u0001!\u0001!\u0001!\u0001"+
"!\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001\"\u0001"+
"#\u0001#\u0001#\u0001#\u0001#\u0001#\u0001#\u0001#\u0001#\u0001$\u0001"+
"$\u0001$\u0001%\u0001%\u0001%\u0001%\u0001%\u0001%\u0001%\u0001%\u0001"+
"&\u0001&\u0001&\u0001&\u0001&\u0001&\u0001&\u0001&\u0001&\u0001&\u0001"+
"&\u0001\'\u0001\'\u0001\'\u0001\'\u0001\'\u0001\'\u0001\'\u0001\'\u0001"+
"\'\u0001\'\u0001\'\u0001\'\u0001(\u0001(\u0001(\u0001(\u0001(\u0001(\u0001"+
"(\u0001(\u0001(\u0001)\u0001)\u0001)\u0001*\u0001*\u0001*\u0001*\u0001"+
"+\u0001+\u0001+\u0001+\u0001+\u0001+\u0001+\u0001,\u0001,\u0001-\u0001"+
"-\u0001-\u0001.\u0001.\u0001/\u0001/\u0001/\u00010\u00010\u00010\u0003"+
"0\u01a3\b0\u00011\u00011\u00011\u00012\u00012\u00013\u00013\u00014\u0001"+
"4\u00015\u00015\u00016\u00016\u00017\u00017\u00018\u00018\u00019\u0001"+
"9\u0001:\u0001:\u0001;\u0001;\u0001<\u0004<\u01bd\b<\u000b<\f<\u01be\u0001"+
"<\u0001<\u0004<\u01c3\b<\u000b<\f<\u01c4\u0003<\u01c7\b<\u0001<\u0001"+
"<\u0003<\u01cb\b<\u0001<\u0004<\u01ce\b<\u000b<\f<\u01cf\u0001=\u0004"+
"=\u01d3\b=\u000b=\f=\u01d4\u0001=\u0001=\u0004=\u01d9\b=\u000b=\f=\u01da"+
"\u0001>\u0004>\u01de\b>\u000b>\f>\u01df\u0001?\u0004?\u01e3\b?\u000b?"+
"\f?\u01e4\u0001?\u0004?\u01e8\b?\u000b?\f?\u01e9\u0001@\u0001@\u0001@"+
"\u0001@\u0005@\u01f0\b@\n@\f@\u01f3\t@\u0001@\u0001@\u0001@\u0001@\u0001"+
"@\u0005@\u01fa\b@\n@\f@\u01fd\t@\u0001@\u0003@\u0200\b@\u0001A\u0001A"+
"\u0005A\u0204\bA\nA\fA\u0207\tA\u0001B\u0004B\u020a\bB\u000bB\fB\u020b"+
"\u0001B\u0001B\u0001C\u0001C\u0001C\u0001C\u0005C\u0214\bC\nC\fC\u0217"+
"\tC\u0001C\u0001C\u0001D\u0001D\u0001D\u0001D\u0005D\u021f\bD\nD\fD\u0222"+
"\tD\u0001D\u0001D\u0001D\u0001D\u0001D\u0001\u0220\u0000E\u0001\u0001"+
"\u0003\u0002\u0005\u0003\u0007\u0004\t\u0005\u000b\u0006\r\u0007\u000f"+
"\b\u0011\t\u0013\n\u0015\u000b\u0017\f\u0019\r\u001b\u000e\u001d\u000f"+
"\u001f\u0010!\u0011#\u0012%\u0013\'\u0014)\u0015+\u0016-\u0017/\u0018"+
"1\u00193\u001a5\u001b7\u001c9\u001d;\u001e=\u001f? A!C\"E#G$I%K&M\'O("+
"Q)S*U+W,Y-[.]/_0a1c2e3g4i5k6m7o8q9s:u;w<y={>}?\u007f@\u0081A\u0083B\u0085"+
"C\u0087D\u0089E\u0001\u0000!\u0002\u0000AAaa\u0002\u0000NNnn\u0002\u0000"+
"DDdd\u0002\u0000OOoo\u0002\u0000RRrr\u0002\u0000UUuu\u0002\u0000LLll\u0002"+
"\u0000EEee\u0002\u0000SSss\u0002\u0000TTtt\u0002\u0000CCcc\u0002\u0000"+
"FFff\u0002\u0000MMmm\u0002\u0000WWww\u0002\u0000HHhh\u0002\u0000GGgg\u0002"+
"\u0000PPpp\u0002\u0000BBbb\u0002\u0000YYyy\u0002\u0000VVvv\u0002\u0000"+
"IIii\u0002\u0000KKkk\u0002\u0000XXxx\u0002\u0000QQqq\u0001\u000009\u0002"+
"\u0000++--\u0002\u0000AZaz\u0004\u0000\n\n\r\r\"\"\\\\\u0004\u0000\n\n"+
"\r\r\'\'\\\\\u0003\u0000AZ__az\n\u0000\"\"-.09==A[]]__a{}~\u8000\uff5e"+
"\u8000\uff5e\u0003\u0000\t\n\r\r \u0002\u0000\n\n\r\r\u023b\u0000\u0001"+
"\u0001\u0000\u0000\u0000\u0000\u0003\u0001\u0000\u0000\u0000\u0000\u0005"+
"\u0001\u0000\u0000\u0000\u0000\u0007\u0001\u0000\u0000\u0000\u0000\t\u0001"+
"\u0000\u0000\u0000\u0000\u000b\u0001\u0000\u0000\u0000\u0000\r\u0001\u0000"+
"\u0000\u0000\u0000\u000f\u0001\u0000\u0000\u0000\u0000\u0011\u0001\u0000"+
"\u0000\u0000\u0000\u0013\u0001\u0000\u0000\u0000\u0000\u0015\u0001\u0000"+
"\u0000\u0000\u0000\u0017\u0001\u0000\u0000\u0000\u0000\u0019\u0001\u0000"+
"\u0000\u0000\u0000\u001b\u0001\u0000\u0000\u0000\u0000\u001d\u0001\u0000"+
"\u0000\u0000\u0000\u001f\u0001\u0000\u0000\u0000\u0000!\u0001\u0000\u0000"+
"\u0000\u0000#\u0001\u0000\u0000\u0000\u0000%\u0001\u0000\u0000\u0000\u0000"+
"\'\u0001\u0000\u0000\u0000\u0000)\u0001\u0000\u0000\u0000\u0000+\u0001"+
"\u0000\u0000\u0000\u0000-\u0001\u0000\u0000\u0000\u0000/\u0001\u0000\u0000"+
"\u0000\u00001\u0001\u0000\u0000\u0000\u00003\u0001\u0000\u0000\u0000\u0000"+
"5\u0001\u0000\u0000\u0000\u00007\u0001\u0000\u0000\u0000\u00009\u0001"+
"\u0000\u0000\u0000\u0000;\u0001\u0000\u0000\u0000\u0000=\u0001\u0000\u0000"+
"\u0000\u0000?\u0001\u0000\u0000\u0000\u0000A\u0001\u0000\u0000\u0000\u0000"+
"C\u0001\u0000\u0000\u0000\u0000E\u0001\u0000\u0000\u0000\u0000G\u0001"+
"\u0000\u0000\u0000\u0000I\u0001\u0000\u0000\u0000\u0000K\u0001\u0000\u0000"+
"\u0000\u0000M\u0001\u0000\u0000\u0000\u0000O\u0001\u0000\u0000\u0000\u0000"+
"Q\u0001\u0000\u0000\u0000\u0000S\u0001\u0000\u0000\u0000\u0000U\u0001"+
"\u0000\u0000\u0000\u0000W\u0001\u0000\u0000\u0000\u0000Y\u0001\u0000\u0000"+
"\u0000\u0000[\u0001\u0000\u0000\u0000\u0000]\u0001\u0000\u0000\u0000\u0000"+
"_\u0001\u0000\u0000\u0000\u0000a\u0001\u0000\u0000\u0000\u0000c\u0001"+
"\u0000\u0000\u0000\u0000e\u0001\u0000\u0000\u0000\u0000g\u0001\u0000\u0000"+
"\u0000\u0000i\u0001\u0000\u0000\u0000\u0000k\u0001\u0000\u0000\u0000\u0000"+
"m\u0001\u0000\u0000\u0000\u0000o\u0001\u0000\u0000\u0000\u0000q\u0001"+
"\u0000\u0000\u0000\u0000s\u0001\u0000\u0000\u0000\u0000u\u0001\u0000\u0000"+
"\u0000\u0000w\u0001\u0000\u0000\u0000\u0000y\u0001\u0000\u0000\u0000\u0000"+
"{\u0001\u0000\u0000\u0000\u0000}\u0001\u0000\u0000\u0000\u0000\u007f\u0001"+
"\u0000\u0000\u0000\u0000\u0081\u0001\u0000\u0000\u0000\u0000\u0083\u0001"+
"\u0000\u0000\u0000\u0000\u0085\u0001\u0000\u0000\u0000\u0000\u0087\u0001"+
"\u0000\u0000\u0000\u0000\u0089\u0001\u0000\u0000\u0000\u0001\u008b\u0001"+
"\u0000\u0000\u0000\u0003\u008f\u0001\u0000\u0000\u0000\u0005\u0092\u0001"+
"\u0000\u0000\u0000\u0007\u0099\u0001\u0000\u0000\u0000\t\u009d\u0001\u0000"+
"\u0000\u0000\u000b\u00a4\u0001\u0000\u0000\u0000\r\u00a9\u0001\u0000\u0000"+
"\u0000\u000f\u00af\u0001\u0000\u0000\u0000\u0011\u00b5\u0001\u0000\u0000"+
"\u0000\u0013\u00b8\u0001\u0000\u0000\u0000\u0015\u00bf\u0001\u0000\u0000"+
"\u0000\u0017\u00c5\u0001\u0000\u0000\u0000\u0019\u00cb\u0001\u0000\u0000"+
"\u0000\u001b\u00d2\u0001\u0000\u0000\u0000\u001d\u00d5\u0001\u0000\u0000"+
"\u0000\u001f\u00d9\u0001\u0000\u0000\u0000!\u00de\u0001\u0000\u0000\u0000"+
"#\u00e1\u0001\u0000\u0000\u0000%\u00e4\u0001\u0000\u0000\u0000\'\u00e9"+
"\u0001\u0000\u0000\u0000)\u00ee\u0001\u0000\u0000\u0000+\u00f6\u0001\u0000"+
"\u0000\u0000-\u00f8\u0001\u0000\u0000\u0000/\u00fe\u0001\u0000\u0000\u0000"+
"1\u0102\u0001\u0000\u0000\u00003\u0106\u0001\u0000\u0000\u00005\u010a"+
"\u0001\u0000\u0000\u00007\u010e\u0001\u0000\u0000\u00009\u0115\u0001\u0000"+
"\u0000\u0000;\u011c\u0001\u0000\u0000\u0000=\u0125\u0001\u0000\u0000\u0000"+
"?\u012a\u0001\u0000\u0000\u0000A\u0133\u0001\u0000\u0000\u0000C\u0146"+
"\u0001\u0000\u0000\u0000E\u014b\u0001\u0000\u0000\u0000G\u0153\u0001\u0000"+
"\u0000\u0000I\u015c\u0001\u0000\u0000\u0000K\u015f\u0001\u0000\u0000\u0000"+
"M\u0167\u0001\u0000\u0000\u0000O\u0172\u0001\u0000\u0000\u0000Q\u017e"+
"\u0001\u0000\u0000\u0000S\u0187\u0001\u0000\u0000\u0000U\u018a\u0001\u0000"+
"\u0000\u0000W\u018e\u0001\u0000\u0000\u0000Y\u0195\u0001\u0000\u0000\u0000"+
"[\u0197\u0001\u0000\u0000\u0000]\u019a\u0001\u0000\u0000\u0000_\u019c"+
"\u0001\u0000\u0000\u0000a\u01a2\u0001\u0000\u0000\u0000c\u01a4\u0001\u0000"+
"\u0000\u0000e\u01a7\u0001\u0000\u0000\u0000g\u01a9\u0001\u0000\u0000\u0000"+
"i\u01ab\u0001\u0000\u0000\u0000k\u01ad\u0001\u0000\u0000\u0000m\u01af"+
"\u0001\u0000\u0000\u0000o\u01b1\u0001\u0000\u0000\u0000q\u01b3\u0001\u0000"+
"\u0000\u0000s\u01b5\u0001\u0000\u0000\u0000u\u01b7\u0001\u0000\u0000\u0000"+
"w\u01b9\u0001\u0000\u0000\u0000y\u01bc\u0001\u0000\u0000\u0000{\u01d2"+
"\u0001\u0000\u0000\u0000}\u01dd\u0001\u0000\u0000\u0000\u007f\u01e2\u0001"+
"\u0000\u0000\u0000\u0081\u01ff\u0001\u0000\u0000\u0000\u0083\u0201\u0001"+
"\u0000\u0000\u0000\u0085\u0209\u0001\u0000\u0000\u0000\u0087\u020f\u0001"+
"\u0000\u0000\u0000\u0089\u021a\u0001\u0000\u0000\u0000\u008b\u008c\u0007"+
"\u0000\u0000\u0000\u008c\u008d\u0007\u0001\u0000\u0000\u008d\u008e\u0007"+
"\u0002\u0000\u0000\u008e\u0002\u0001\u0000\u0000\u0000\u008f\u0090\u0007"+
"\u0003\u0000\u0000\u0090\u0091\u0007\u0004\u0000\u0000\u0091\u0004\u0001"+
"\u0000\u0000\u0000\u0092\u0093\u0007\u0005\u0000\u0000\u0093\u0094\u0007"+
"\u0001\u0000\u0000\u0094\u0095\u0007\u0006\u0000\u0000\u0095\u0096\u0007"+
"\u0007\u0000\u0000\u0096\u0097\u0007\b\u0000\u0000\u0097\u0098\u0007\b"+
"\u0000\u0000\u0098\u0006\u0001\u0000\u0000\u0000\u0099\u009a\u0007\u0001"+
"\u0000\u0000\u009a\u009b\u0007\u0003\u0000\u0000\u009b\u009c\u0007\t\u0000"+
"\u0000\u009c\b\u0001\u0000\u0000\u0000\u009d\u009e\u0007\b\u0000\u0000"+
"\u009e\u009f\u0007\u0007\u0000\u0000\u009f\u00a0\u0007\u0006\u0000\u0000"+
"\u00a0\u00a1\u0007\u0007\u0000\u0000\u00a1\u00a2\u0007\n\u0000\u0000\u00a2"+
"\u00a3\u0007\t\u0000\u0000\u00a3\n\u0001\u0000\u0000\u0000\u00a4\u00a5"+
"\u0007\u000b\u0000\u0000\u00a5\u00a6\u0007\u0004\u0000\u0000\u00a6\u00a7"+
"\u0007\u0003\u0000\u0000\u00a7\u00a8\u0007\f\u0000\u0000\u00a8\f\u0001"+
"\u0000\u0000\u0000\u00a9\u00aa\u0007\r\u0000\u0000\u00aa\u00ab\u0007\u000e"+
"\u0000\u0000\u00ab\u00ac\u0007\u0007\u0000\u0000\u00ac\u00ad\u0007\u0004"+
"\u0000\u0000\u00ad\u00ae\u0007\u0007\u0000\u0000\u00ae\u000e\u0001\u0000"+
"\u0000\u0000\u00af\u00b0\u0007\u000f\u0000\u0000\u00b0\u00b1\u0007\u0004"+
"\u0000\u0000\u00b1\u00b2\u0007\u0003\u0000\u0000\u00b2\u00b3\u0007\u0005"+
"\u0000\u0000\u00b3\u00b4\u0007\u0010\u0000\u0000\u00b4\u0010\u0001\u0000"+
"\u0000\u0000\u00b5\u00b6\u0007\u0011\u0000\u0000\u00b6\u00b7\u0007\u0012"+
"\u0000\u0000\u00b7\u0012\u0001\u0000\u0000\u0000\u00b8\u00b9\u0007\u000e"+
"\u0000\u0000\u00b9\u00ba\u0007\u0000\u0000\u0000\u00ba\u00bb\u0007\u0013"+
"\u0000\u0000\u00bb\u00bc\u0007\u0014\u0000\u0000\u00bc\u00bd\u0007\u0001"+
"\u0000\u0000\u00bd\u00be\u0007\u000f\u0000\u0000\u00be\u0014\u0001\u0000"+
"\u0000\u0000\u00bf\u00c0\u0007\u0003\u0000\u0000\u00c0\u00c1\u0007\u0004"+
"\u0000\u0000\u00c1\u00c2\u0007\u0002\u0000\u0000\u00c2\u00c3\u0007\u0007"+
"\u0000\u0000\u00c3\u00c4\u0007\u0004\u0000\u0000\u00c4\u0016\u0001\u0000"+
"\u0000\u0000\u00c5\u00c6\u0007\u0006\u0000\u0000\u00c6\u00c7\u0007\u0014"+
"\u0000\u0000\u00c7\u00c8\u0007\f\u0000\u0000\u00c8\u00c9\u0007\u0014\u0000"+
"\u0000\u00c9\u00ca\u0007\t\u0000\u0000\u00ca\u0018\u0001\u0000\u0000\u0000"+
"\u00cb\u00cc\u0007\u0003\u0000\u0000\u00cc\u00cd\u0007\u000b\u0000\u0000"+
"\u00cd\u00ce\u0007\u000b\u0000\u0000\u00ce\u00cf\u0007\b\u0000\u0000\u00cf"+
"\u00d0\u0007\u0007\u0000\u0000\u00d0\u00d1\u0007\t\u0000\u0000\u00d1\u001a"+
"\u0001\u0000\u0000\u0000\u00d2\u00d3\u0007\u0000\u0000\u0000\u00d3\u00d4"+
"\u0007\b\u0000\u0000\u00d4\u001c\u0001\u0000\u0000\u0000\u00d5\u00d6\u0007"+
"\u0000\u0000\u0000\u00d6\u00d7\u0007\b\u0000\u0000\u00d7\u00d8\u0007\n"+
"\u0000\u0000\u00d8\u001e\u0001\u0000\u0000\u0000\u00d9\u00da\u0007\u0002"+
"\u0000\u0000\u00da\u00db\u0007\u0007\u0000\u0000\u00db\u00dc\u0007\b\u0000"+
"\u0000\u00dc\u00dd\u0007\n\u0000\u0000\u00dd \u0001\u0000\u0000\u0000"+
"\u00de\u00df\u0007\u0014\u0000\u0000\u00df\u00e0\u0007\u0001\u0000\u0000"+
"\u00e0\"\u0001\u0000\u0000\u0000\u00e1\u00e2\u0007\u0014\u0000\u0000\u00e2"+
"\u00e3\u0007\b\u0000\u0000\u00e3$\u0001\u0000\u0000\u0000\u00e4\u00e5"+
"\u0007\u0001\u0000\u0000\u00e5\u00e6\u0007\u0005\u0000\u0000\u00e6\u00e7"+
"\u0007\u0006\u0000\u0000\u00e7\u00e8\u0007\u0006\u0000\u0000\u00e8&\u0001"+
"\u0000\u0000\u0000\u00e9\u00ea\u0007\u0006\u0000\u0000\u00ea\u00eb\u0007"+
"\u0014\u0000\u0000\u00eb\u00ec\u0007\u0015\u0000\u0000\u00ec\u00ed\u0007"+
"\u0007\u0000\u0000\u00ed(\u0001\u0000\u0000\u0000\u00ee\u00ef\u0007\u0011"+
"\u0000\u0000\u00ef\u00f0\u0007\u0007\u0000\u0000\u00f0\u00f1\u0007\t\u0000"+
"\u0000\u00f1\u00f2\u0007\r\u0000\u0000\u00f2\u00f3\u0007\u0007\u0000\u0000"+
"\u00f3\u00f4\u0007\u0007\u0000\u0000\u00f4\u00f5\u0007\u0001\u0000\u0000"+
"\u00f5*\u0001\u0000\u0000\u0000\u00f6\u00f7\u0005*\u0000\u0000\u00f7,"+
"\u0001\u0000\u0000\u0000\u00f8\u00f9\u0007\n\u0000\u0000\u00f9\u00fa\u0007"+
"\u0003\u0000\u0000\u00fa\u00fb\u0007\u0005\u0000\u0000\u00fb\u00fc\u0007"+
"\u0001\u0000\u0000\u00fc\u00fd\u0007\t\u0000\u0000\u00fd.\u0001\u0000"+
"\u0000\u0000\u00fe\u00ff\u0007\b\u0000\u0000\u00ff\u0100\u0007\u0005\u0000"+
"\u0000\u0100\u0101\u0007\f\u0000\u0000\u01010\u0001\u0000\u0000\u0000"+
"\u0102\u0103\u0007\u0000\u0000\u0000\u0103\u0104\u0007\u0013\u0000\u0000"+
"\u0104\u0105\u0007\u000f\u0000\u0000\u01052\u0001\u0000\u0000\u0000\u0106"+
"\u0107\u0007\f\u0000\u0000\u0107\u0108\u0007\u0014\u0000\u0000\u0108\u0109"+
"\u0007\u0001\u0000\u0000\u01094\u0001\u0000\u0000\u0000\u010a\u010b\u0007"+
"\f\u0000\u0000\u010b\u010c\u0007\u0000\u0000\u0000\u010c\u010d\u0007\u0016"+
"\u0000\u0000\u010d6\u0001\u0000\u0000\u0000\u010e\u010f\u0007\b\u0000"+
"\u0000\u010f\u0110\u0007\t\u0000\u0000\u0110\u0111\u0007\u0002\u0000\u0000"+
"\u0111\u0112\u0007\u0002\u0000\u0000\u0112\u0113\u0007\u0007\u0000\u0000"+
"\u0113\u0114\u0007\u0013\u0000\u0000\u01148\u0001\u0000\u0000\u0000\u0115"+
"\u0116\u0007\b\u0000\u0000\u0116\u0117\u0007\t\u0000\u0000\u0117\u0118"+
"\u0007\u0002\u0000\u0000\u0118\u0119\u0007\u0013\u0000\u0000\u0119\u011a"+
"\u0007\u0000\u0000\u0000\u011a\u011b\u0007\u0004\u0000\u0000\u011b:\u0001"+
"\u0000\u0000\u0000\u011c\u011d\u0007\u0013\u0000\u0000\u011d\u011e\u0007"+
"\u0000\u0000\u0000\u011e\u011f\u0007\u0004\u0000\u0000\u011f\u0120\u0007"+
"\u0014\u0000\u0000\u0120\u0121\u0007\u0000\u0000\u0000\u0121\u0122\u0007"+
"\u0001\u0000\u0000\u0122\u0123\u0007\n\u0000\u0000\u0123\u0124\u0007\u0007"+
"\u0000\u0000\u0124<\u0001\u0000\u0000\u0000\u0125\u0126\u0007\u0004\u0000"+
"\u0000\u0126\u0127\u0007\u0000\u0000\u0000\u0127\u0128\u0007\t\u0000\u0000"+
"\u0128\u0129\u0007\u0007\u0000\u0000\u0129>\u0001\u0000\u0000\u0000\u012a"+
"\u012b\u0007\u0014\u0000\u0000\u012b\u012c\u0007\u0001\u0000\u0000\u012c"+
"\u012d\u0007\n\u0000\u0000\u012d\u012e\u0007\u0004\u0000\u0000\u012e\u012f"+
"\u0007\u0007\u0000\u0000\u012f\u0130\u0007\u0000\u0000\u0000\u0130\u0131"+
"\u0007\b\u0000\u0000\u0131\u0132\u0007\u0007\u0000\u0000\u0132@\u0001"+
"\u0000\u0000\u0000\u0133\u0134\u0007\u000e\u0000\u0000\u0134\u0135\u0007"+
"\u0014\u0000\u0000\u0135\u0136\u0007\b\u0000\u0000\u0136\u0137\u0007\t"+
"\u0000\u0000\u0137\u0138\u0007\u0003\u0000\u0000\u0138\u0139\u0007\u000f"+
"\u0000\u0000\u0139\u013a\u0007\u0004\u0000\u0000\u013a\u013b\u0007\u0000"+
"\u0000\u0000\u013b\u013c\u0007\f\u0000\u0000\u013c\u013d\u0005_\u0000"+
"\u0000\u013d\u013e\u0007\u0017\u0000\u0000\u013e\u013f\u0007\u0005\u0000"+
"\u0000\u013f\u0140\u0007\u0000\u0000\u0000\u0140\u0141\u0007\u0001\u0000"+
"\u0000\u0141\u0142\u0007\t\u0000\u0000\u0142\u0143\u0007\u0014\u0000\u0000"+
"\u0143\u0144\u0007\u0006\u0000\u0000\u0144\u0145\u0007\u0007\u0000\u0000"+
"\u0145B\u0001\u0000\u0000\u0000\u0146\u0147\u0007\t\u0000\u0000\u0147"+
"\u0148\u0007\u0003\u0000\u0000\u0148\u0149\u0007\u0010\u0000\u0000\u0149"+
"\u014a\u0007\u0015\u0000\u0000\u014aD\u0001\u0000\u0000\u0000\u014b\u014c"+
"\u0007\u0011\u0000\u0000\u014c\u014d\u0007\u0003\u0000\u0000\u014d\u014e"+
"\u0007\t\u0000\u0000\u014e\u014f\u0007\t\u0000\u0000\u014f\u0150\u0007"+
"\u0003\u0000\u0000\u0150\u0151\u0007\f\u0000\u0000\u0151\u0152\u0007\u0015"+
"\u0000\u0000\u0152F\u0001\u0000\u0000\u0000\u0153\u0154\u0007\u0017\u0000"+
"\u0000\u0154\u0155\u0007\u0005\u0000\u0000\u0155\u0156\u0007\u0000\u0000"+
"\u0000\u0156\u0157\u0007\u0001\u0000\u0000\u0157\u0158\u0007\t\u0000\u0000"+
"\u0158\u0159\u0007\u0014\u0000\u0000\u0159\u015a\u0007\u0006\u0000\u0000"+
"\u015a\u015b\u0007\u0007\u0000\u0000\u015bH\u0001\u0000\u0000\u0000\u015c"+
"\u015d\u0007\u0011\u0000\u0000\u015d\u015e\u0007\u0012\u0000\u0000\u015e"+
"J\u0001\u0000\u0000\u0000\u015f\u0160\u0007\r\u0000\u0000\u0160\u0161"+
"\u0007\u0014\u0000\u0000\u0161\u0162\u0007\t\u0000\u0000\u0162\u0163\u0007"+
"\u000e\u0000\u0000\u0163\u0164\u0007\u0003\u0000\u0000\u0164\u0165\u0007"+
"\u0005\u0000\u0000\u0165\u0166\u0007\t\u0000\u0000\u0166L\u0001\u0000"+
"\u0000\u0000\u0167\u0168\u0007\u000f\u0000\u0000\u0168\u0169\u0007\u0004"+
"\u0000\u0000\u0169\u016a\u0007\u0003\u0000\u0000\u016a\u016b\u0007\u0005"+
"\u0000\u0000\u016b\u016c\u0007\u0010\u0000\u0000\u016c\u016d\u0005_\u0000"+
"\u0000\u016d\u016e\u0007\u0006\u0000\u0000\u016e\u016f\u0007\u0007\u0000"+
"\u0000\u016f\u0170\u0007\u000b\u0000\u0000\u0170\u0171\u0007\t\u0000\u0000"+
"\u0171N\u0001\u0000\u0000\u0000\u0172\u0173\u0007\u000f\u0000\u0000\u0173"+
"\u0174\u0007\u0004\u0000\u0000\u0174\u0175\u0007\u0003\u0000\u0000\u0175"+
"\u0176\u0007\u0005\u0000\u0000\u0176\u0177\u0007\u0010\u0000\u0000\u0177"+
"\u0178\u0005_\u0000\u0000\u0178\u0179\u0007\u0004\u0000\u0000\u0179\u017a"+
"\u0007\u0014\u0000\u0000\u017a\u017b\u0007\u000f\u0000\u0000\u017b\u017c"+
"\u0007\u000e\u0000\u0000\u017c\u017d\u0007\t\u0000\u0000\u017dP\u0001"+
"\u0000\u0000\u0000\u017e\u017f\u0007\u0014\u0000\u0000\u017f\u0180\u0007"+
"\u000f\u0000\u0000\u0180\u0181\u0007\u0001\u0000\u0000\u0181\u0182\u0007"+
"\u0003\u0000\u0000\u0182\u0183\u0007\u0004\u0000\u0000\u0183\u0184\u0007"+
"\u0014\u0000\u0000\u0184\u0185\u0007\u0001\u0000\u0000\u0185\u0186\u0007"+
"\u000f\u0000\u0000\u0186R\u0001\u0000\u0000\u0000\u0187\u0188\u0007\u0003"+
"\u0000\u0000\u0188\u0189\u0007\u0001\u0000\u0000\u0189T\u0001\u0000\u0000"+
"\u0000\u018a\u018b\u0007\b\u0000\u0000\u018b\u018c\u0007\u0017\u0000\u0000"+
"\u018c\u018d\u0007\u0006\u0000\u0000\u018dV\u0001\u0000\u0000\u0000\u018e"+
"\u018f\u0007\u0010\u0000\u0000\u018f\u0190\u0007\u0004\u0000\u0000\u0190"+
"\u0191\u0007\u0003\u0000\u0000\u0191\u0192\u0007\f\u0000\u0000\u0192\u0193"+
"\u0007\u0017\u0000\u0000\u0193\u0194\u0007\u0006\u0000\u0000\u0194X\u0001"+
"\u0000\u0000\u0000\u0195\u0196\u0005>\u0000\u0000\u0196Z\u0001\u0000\u0000"+
"\u0000\u0197\u0198\u0005>\u0000\u0000\u0198\u0199\u0005=\u0000\u0000\u0199"+
"\\\u0001\u0000\u0000\u0000\u019a\u019b\u0005<\u0000\u0000\u019b^\u0001"+
"\u0000\u0000\u0000\u019c\u019d\u0005<\u0000\u0000\u019d\u019e\u0005=\u0000"+
"\u0000\u019e`\u0001\u0000\u0000\u0000\u019f\u01a0\u0005=\u0000\u0000\u01a0"+
"\u01a3\u0005=\u0000\u0000\u01a1\u01a3\u0005=\u0000\u0000\u01a2\u019f\u0001"+
"\u0000\u0000\u0000\u01a2\u01a1\u0001\u0000\u0000\u0000\u01a3b\u0001\u0000"+
"\u0000\u0000\u01a4\u01a5\u0005!\u0000\u0000\u01a5\u01a6\u0005=\u0000\u0000"+
"\u01a6d\u0001\u0000\u0000\u0000\u01a7\u01a8\u0005(\u0000\u0000\u01a8f"+
"\u0001\u0000\u0000\u0000\u01a9\u01aa\u0005)\u0000\u0000\u01aah\u0001\u0000"+
"\u0000\u0000\u01ab\u01ac\u0005{\u0000\u0000\u01acj\u0001\u0000\u0000\u0000"+
"\u01ad\u01ae\u0005}\u0000\u0000\u01ael\u0001\u0000\u0000\u0000\u01af\u01b0"+
"\u0005[\u0000\u0000\u01b0n\u0001\u0000\u0000\u0000\u01b1\u01b2\u0005]"+
"\u0000\u0000\u01b2p\u0001\u0000\u0000\u0000\u01b3\u01b4\u0005,\u0000\u0000"+
"\u01b4r\u0001\u0000\u0000\u0000\u01b5\u01b6\u0005.\u0000\u0000\u01b6t"+
"\u0001\u0000\u0000\u0000\u01b7\u01b8\u0005:\u0000\u0000\u01b8v\u0001\u0000"+
"\u0000\u0000\u01b9\u01ba\u0005;\u0000\u0000\u01bax\u0001\u0000\u0000\u0000"+
"\u01bb\u01bd\u0007\u0018\u0000\u0000\u01bc\u01bb\u0001\u0000\u0000\u0000"+
"\u01bd\u01be\u0001\u0000\u0000\u0000\u01be\u01bc\u0001\u0000\u0000\u0000"+
"\u01be\u01bf\u0001\u0000\u0000\u0000\u01bf\u01c6\u0001\u0000\u0000\u0000"+
"\u01c0\u01c2\u0005.\u0000\u0000\u01c1\u01c3\u0007\u0018\u0000\u0000\u01c2"+
"\u01c1\u0001\u0000\u0000\u0000\u01c3\u01c4\u0001\u0000\u0000\u0000\u01c4"+
"\u01c2\u0001\u0000\u0000\u0000\u01c4\u01c5\u0001\u0000\u0000\u0000\u01c5"+
"\u01c7\u0001\u0000\u0000\u0000\u01c6\u01c0\u0001\u0000\u0000\u0000\u01c6"+
"\u01c7\u0001\u0000\u0000\u0000\u01c7\u01c8\u0001\u0000\u0000\u0000\u01c8"+
"\u01ca\u0007\u0007\u0000\u0000\u01c9\u01cb\u0007\u0019\u0000\u0000\u01ca"+
"\u01c9\u0001\u0000\u0000\u0000\u01ca\u01cb\u0001\u0000\u0000\u0000\u01cb"+
"\u01cd\u0001\u0000\u0000\u0000\u01cc\u01ce\u0007\u0018\u0000\u0000\u01cd"+
"\u01cc\u0001\u0000\u0000\u0000\u01ce\u01cf\u0001\u0000\u0000\u0000\u01cf"+
"\u01cd\u0001\u0000\u0000\u0000\u01cf\u01d0\u0001\u0000\u0000\u0000\u01d0"+
"z\u0001\u0000\u0000\u0000\u01d1\u01d3\u0007\u0018\u0000\u0000\u01d2\u01d1"+
"\u0001\u0000\u0000\u0000\u01d3\u01d4\u0001\u0000\u0000\u0000\u01d4\u01d2"+
"\u0001\u0000\u0000\u0000\u01d4\u01d5\u0001\u0000\u0000\u0000\u01d5\u01d6"+
"\u0001\u0000\u0000\u0000\u01d6\u01d8\u0005.\u0000\u0000\u01d7\u01d9\u0007"+
"\u0018\u0000\u0000\u01d8\u01d7\u0001\u0000\u0000\u0000\u01d9\u01da\u0001"+
"\u0000\u0000\u0000\u01da\u01d8\u0001\u0000\u0000\u0000\u01da\u01db\u0001"+
"\u0000\u0000\u0000\u01db|\u0001\u0000\u0000\u0000\u01dc\u01de\u0007\u0018"+
"\u0000\u0000\u01dd\u01dc\u0001\u0000\u0000\u0000\u01de\u01df\u0001\u0000"+
"\u0000\u0000\u01df\u01dd\u0001\u0000\u0000\u0000\u01df\u01e0\u0001\u0000"+
"\u0000\u0000\u01e0~\u0001\u0000\u0000\u0000\u01e1\u01e3\u0007\u0018\u0000"+
"\u0000\u01e2\u01e1\u0001\u0000\u0000\u0000\u01e3\u01e4\u0001\u0000\u0000"+
"\u0000\u01e4\u01e2\u0001\u0000\u0000\u0000\u01e4\u01e5\u0001\u0000\u0000"+
"\u0000\u01e5\u01e7\u0001\u0000\u0000\u0000\u01e6\u01e8\u0007\u001a\u0000"+
"\u0000\u01e7\u01e6\u0001\u0000\u0000\u0000\u01e8\u01e9\u0001\u0000\u0000"+
"\u0000\u01e9\u01e7\u0001\u0000\u0000\u0000\u01e9\u01ea\u0001\u0000\u0000"+
"\u0000\u01ea\u0080\u0001\u0000\u0000\u0000\u01eb\u01f1\u0005\"\u0000\u0000"+
"\u01ec\u01f0\b\u001b\u0000\u0000\u01ed\u01ee\u0005\\\u0000\u0000\u01ee"+
"\u01f0\t\u0000\u0000\u0000\u01ef\u01ec\u0001\u0000\u0000\u0000\u01ef\u01ed"+
"\u0001\u0000\u0000\u0000\u01f0\u01f3\u0001\u0000\u0000\u0000\u01f1\u01ef"+
"\u0001\u0000\u0000\u0000\u01f1\u01f2\u0001\u0000\u0000\u0000\u01f2\u01f4"+
"\u0001\u0000\u0000\u0000\u01f3\u01f1\u0001\u0000\u0000\u0000\u01f4\u0200"+
"\u0005\"\u0000\u0000\u01f5\u01fb\u0005\'\u0000\u0000\u01f6\u01fa\b\u001c"+
"\u0000\u0000\u01f7\u01f8\u0005\\\u0000\u0000\u01f8\u01fa\t\u0000\u0000"+
"\u0000\u01f9\u01f6\u0001\u0000\u0000\u0000\u01f9\u01f7\u0001\u0000\u0000"+
"\u0000\u01fa\u01fd\u0001\u0000\u0000\u0000\u01fb\u01f9\u0001\u0000\u0000"+
"\u0000\u01fb\u01fc\u0001\u0000\u0000\u0000\u01fc\u01fe\u0001\u0000\u0000"+
"\u0000\u01fd\u01fb\u0001\u0000\u0000\u0000\u01fe\u0200\u0005\'\u0000\u0000"+
"\u01ff\u01eb\u0001\u0000\u0000\u0000\u01ff\u01f5\u0001\u0000\u0000\u0000"+
"\u0200\u0082\u0001\u0000\u0000\u0000\u0201\u0205\u0007\u001d\u0000\u0000"+
"\u0202\u0204\u0007\u001e\u0000\u0000\u0203\u0202\u0001\u0000\u0000\u0000"+
"\u0204\u0207\u0001\u0000\u0000\u0000\u0205\u0203\u0001\u0000\u0000\u0000"+
"\u0205\u0206\u0001\u0000\u0000\u0000\u0206\u0084\u0001\u0000\u0000\u0000"+
"\u0207\u0205\u0001\u0000\u0000\u0000\u0208\u020a\u0007\u001f\u0000\u0000"+
"\u0209\u0208\u0001\u0000\u0000\u0000\u020a\u020b\u0001\u0000\u0000\u0000"+
"\u020b\u0209\u0001\u0000\u0000\u0000\u020b\u020c\u0001\u0000\u0000\u0000"+
"\u020c\u020d\u0001\u0000\u0000\u0000\u020d\u020e\u0006B\u0000\u0000\u020e"+
"\u0086\u0001\u0000\u0000\u0000\u020f\u0210\u0005/\u0000\u0000\u0210\u0211"+
"\u0005/\u0000\u0000\u0211\u0215\u0001\u0000\u0000\u0000\u0212\u0214\b"+
" \u0000\u0000\u0213\u0212\u0001\u0000\u0000\u0000\u0214\u0217\u0001\u0000"+
"\u0000\u0000\u0215\u0213\u0001\u0000\u0000\u0000\u0215\u0216\u0001\u0000"+
"\u0000\u0000\u0216\u0218\u0001\u0000\u0000\u0000\u0217\u0215\u0001\u0000"+
"\u0000\u0000\u0218\u0219\u0006C\u0001\u0000\u0219\u0088\u0001\u0000\u0000"+
"\u0000\u021a\u021b\u0005/\u0000\u0000\u021b\u021c\u0005*\u0000\u0000\u021c"+
"\u0220\u0001\u0000\u0000\u0000\u021d\u021f\t\u0000\u0000\u0000\u021e\u021d"+
"\u0001\u0000\u0000\u0000\u021f\u0222\u0001\u0000\u0000\u0000\u0220\u0221"+
"\u0001\u0000\u0000\u0000\u0220\u021e\u0001\u0000\u0000\u0000\u0221\u0223"+
"\u0001\u0000\u0000\u0000\u0222\u0220\u0001\u0000\u0000\u0000\u0223\u0224"+
"\u0005*\u0000\u0000\u0224\u0225\u0005/\u0000\u0000\u0225\u0226\u0001\u0000"+
"\u0000\u0000\u0226\u0227\u0006D\u0001\u0000\u0227\u008a\u0001\u0000\u0000"+
"\u0000\u0015\u0000\u01a2\u01be\u01c4\u01c6\u01ca\u01cf\u01d4\u01da\u01df"+
"\u01e4\u01e9\u01ef\u01f1\u01f9\u01fb\u01ff\u0205\u020b\u0215\u0220\u0002"+
"\u0000\u0001\u0000\u0006\u0000\u0000";
public static final ATN _ATN =
new ATNDeserializer().deserialize(_serializedATN.toCharArray());
new ATNDeserializer().deserialize(_serializedATN.toCharArray());
static {
_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
@@ -39,23 +39,15 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code AndExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitAndExpr(AlertExpressionParser.AndExprContext ctx);
/**
* Visit a parse tree produced by the {@code QueryExpr}
* labeled alternative in AlertExpressionParser#expr
* @param ctx the parse tree
* @return the visitor result
*/
T visitQueryExpr(AlertExpressionParser.QueryExprContext ctx);
/**
* Visit a parse tree produced by the {@code ComparisonExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
@@ -63,15 +55,31 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code UnlessExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx);
/**
* Visit a parse tree produced by the {@code SqlExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx);
/**
* Visit a parse tree produced by the {@code SqlCallExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx);
/**
* Visit a parse tree produced by the {@code LiteralExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
@@ -79,26 +87,56 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
/**
* Visit a parse tree produced by the {@code ParenExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitParenExpr(AlertExpressionParser.ParenExprContext ctx);
/**
* Visit a parse tree produced by the {@code PromqlCallExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx);
/**
* Visit a parse tree produced by the {@code PromqlExpr}
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx);
/**
* Visit a parse tree produced by the {@code OrExpr}
* labeled alternative in AlertExpressionParser#expr
* labeled alternative in {@link AlertExpressionParser#expr}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrExpr(AlertExpressionParser.OrExprContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#identifier}.
* Visit a parse tree produced by {@link AlertExpressionParser#functionCall}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitIdentifier(AlertExpressionParser.IdentifierContext ctx);
T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#parameterList}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitParameterList(AlertExpressionParser.ParameterListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#parameter}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitParameter(AlertExpressionParser.ParameterContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#number}.
@@ -106,4 +144,179 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
* @return the visitor result
*/
T visitNumber(AlertExpressionParser.NumberContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#string}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitString(AlertExpressionParser.StringContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#duration}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitDuration(AlertExpressionParser.DurationContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#functionName}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectSql}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectFieldList}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#selectField}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitSelectField(AlertExpressionParser.SelectFieldContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#groupByList}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitGroupByList(AlertExpressionParser.GroupByListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#orderByList}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrderByList(AlertExpressionParser.OrderByListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#orderByField}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#limitClause}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#relList}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitRelList(AlertExpressionParser.RelListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#relation}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitRelation(AlertExpressionParser.RelationContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#conditionList}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitConditionList(AlertExpressionParser.ConditionListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#compOp}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitCompOp(AlertExpressionParser.CompOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#condition}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitCondition(AlertExpressionParser.ConditionContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#conditionUnit}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#promql}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitPromql(AlertExpressionParser.PromqlContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#metricSelector}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherList}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherItem}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherOp}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#labelList}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitLabelList(AlertExpressionParser.LabelListContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#instantVectorOp}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx);
/**
* Visit a parse tree produced by {@link AlertExpressionParser#aggregationOperator}.
* @param ctx the parse tree
* @return the visitor result
*/
T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx);
/**
* 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
@@ -32,8 +32,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Gotify alert notify handler
*/
@@ -65,9 +63,6 @@ public class GotifyAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<GotifyWebHookDto> httpEntity = new HttpEntity<>(gotifyWebHookDto, headers);
String webHookUrl = String.format(alerterProperties.getGotifyWebhookUrl(), receiver.getGotifyToken());
if (!isValidWebhookUrl(webHookUrl)) {
throw new AlertNoticeException("Invalid Gotify webhook URL: " + webHookUrl);
}
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
httpEntity, CommonRobotNotifyResp.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
@@ -109,19 +104,4 @@ public class GotifyAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
}
}
/**
* Validate the webhook URL against a whitelist of allowed base URLs.
*
* @param url the webhook URL to validate
* @return true if the URL is valid, false otherwise
*/
private boolean isValidWebhookUrl(String url) {
// Define a whitelist of allowed base URLs
List<String> allowedBaseUrls = List.of(
"https://trusted-gotify-server.com",
"https://another-trusted-server.com"
);
return allowedBaseUrls.stream().anyMatch(url::startsWith);
}
}
@@ -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);
}
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.alert.service.impl;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -29,7 +28,9 @@ import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.common.cache.CacheFactory;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.ExportFileConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
@@ -72,18 +73,21 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Autowired
private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
private final DataSourceService dataSourceService;
private final Map<String, AlertDefineImExportService> alertDefineImExportServiceMap = new HashMap<>();
private static final String CONTENT_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + "charset=" + StandardCharsets.UTF_8;
public AlertDefineServiceImpl(List<AlertDefineImExportService> alertDefineImExportServiceList) {
public AlertDefineServiceImpl(List<AlertDefineImExportService> alertDefineImExportServiceList, DataSourceService dataSourceService) {
alertDefineImExportServiceList.forEach(it -> alertDefineImExportServiceMap.put(it.type(), it));
this.dataSourceService = dataSourceService;
}
@Override
public void validate(AlertDefine alertDefine, boolean isModify) throws IllegalArgumentException {
if (StringUtils.hasText(alertDefine.getExpr())) {
if (ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
if (CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME.equals(alertDefine.getType())) {
try {
JexlExpressionRunner.compile(alertDefine.getExpr());
} catch (Exception e) {
@@ -213,9 +217,23 @@ public class AlertDefineServiceImpl implements AlertDefineService {
public List<AlertDefine> getRealTimeAlertDefines() {
List<AlertDefine> alertDefines = CacheFactory.getAlertDefineCache();
if (alertDefines == null) {
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_REALTIME);
alertDefines = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME);
CacheFactory.setAlertDefineCache(alertDefines);
}
return alertDefines;
}
@Override
public List<Map<String, Object>> getDefinePreview(String datasource, String type, String expr) {
if (!StringUtils.hasText(expr) || !StringUtils.hasText(datasource) || !StringUtils.hasText(type)) {
return Collections.emptyList();
}
switch (type) {
case CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC:
return dataSourceService.calculate(datasource, expr);
default:
log.error("Get define preview unsupported type: {}", type);
return Collections.emptyList();
}
}
}
@@ -0,0 +1,185 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.stereotype.Service;
import java.text.MessageFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* Alibaba Cloud 'Simple Log Service(SLS)' external alarm service impl
*/
@Slf4j
@Service
public class AlibabaCloudSlsExternAlertService implements ExternAlertService {
private final AlarmCommonReduce alarmCommonReduce;
public AlibabaCloudSlsExternAlertService(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
}
@Override
public void addExternAlert(String content) {
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
if (externAlert == null) {
log.warn("Failure to parse external alert content. content: {}", content);
return;
}
SingleAlert singleAlert = new AlibabaCloudSlsConverter().convert(externAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
@Override
public String supportSource() {
return "alibabacloud-sls";
}
/**
*
*/
public static class AlibabaCloudSlsConverter {
/**
* convert
*
* @param externAlert alert content entity
* @return Single alert
*/
public SingleAlert convert(AlibabaCloudSlsExternAlert externAlert) {
return SingleAlert.builder()
.triggerTimes(1)
.status(externAlert.getStatus())
.startAt(Instant.ofEpochSecond(externAlert.getFireTime()).toEpochMilli())
.activeAt(Instant.ofEpochSecond(externAlert.getAlertTime()).toEpochMilli())
.endAt(convertResolveTime(externAlert.getStatus(), externAlert.getResolveTime()))
.labels(buildLabels(externAlert))
.annotations(buildAnnotations(externAlert))
.content(formatContent(externAlert))
.build();
}
/**
* todo i18n
*
* @param externAlert alert content entity
* @return content
*/
private String formatContent(AlibabaCloudSlsExternAlert externAlert) {
// convet severity
Optional<AlibabaCloudSlsExternAlert.Severity> severity = AlibabaCloudSlsExternAlert.Severity.convert(externAlert.getSeverity());
// If the alarm state is resolved, the value is the specific recovery time.
Long resolveTimeMilli = convertResolveTime(externAlert.getStatus(), externAlert.getResolveTime());
return MessageFormat.format(
"AlibabaCloud-sls alert , {0} - [{1}], level: [{2}], desc: {3}, fire_time:{4}, resolve_time:{5}",
externAlert.getAnnotation("title"),
externAlert.getStatus(),
severity.isPresent() ? severity.get().getAlias() : "N/A",
externAlert.getAnnotation("desc"),
timeSecondToDate(Instant.ofEpochSecond(externAlert.getFireTime()).toEpochMilli()),
null != resolveTimeMilli ? timeSecondToDate(resolveTimeMilli) : "N/A"
);
}
/**
* Converts a timestamp (milliseconds) to a formatted date-time string.
*
* @param timestampMillis timestamp in milliseconds
* @return formatted date-time string in the pattern: yyyy-MM-dd HH:mm:ss
*/
private String timeSecondToDate(long timestampMillis) {
LocalDateTime dateTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(timestampMillis),
ZoneId.systemDefault()
);
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* Build basic annotations and fill annotations for alibaba cloud sls.
*
* @param externAlert alert content entity
* @return annotations
*/
private Map<String, String> buildAnnotations(AlibabaCloudSlsExternAlert externAlert) {
Map<String, String> annotations = new HashMap<>(8);
Optional<AlibabaCloudSlsExternAlert.Severity> severity = AlibabaCloudSlsExternAlert.Severity.convert(externAlert.getSeverity());
severity.ifPresent(value -> annotations.put("severity", value.getAlias()));
// Notification templates for sls need to be configured.
if (StringUtils.isNotBlank(externAlert.getSigninUrl()) && IpDomainUtil.isHasSchema(externAlert.getSigninUrl())) {
annotations.put("signinUrl", "<a target=\"_blank\" href=\"" + externAlert.getSigninUrl() + "\">View Details</a>");
}
// Filling the annotations with the alibaba cloud sls.
if (null != externAlert.getAnnotations() && !externAlert.getAnnotations().isEmpty()) {
annotations.putAll(externAlert.getAnnotations());
}
return annotations;
}
/**
* Build basic labels and fill labels for alibaba cloud sls.
*
* @param externAlert alert content entity
* @return labels
*/
private Map<String, String> buildLabels(AlibabaCloudSlsExternAlert externAlert) {
Map<String, String> labels = new HashMap<>(8);
labels.put("__source__", "alibabacloud-sls");
labels.put("alertname", externAlert.getAlertName());
labels.put("region", externAlert.getRegion());
// The project name is globally unique.
labels.put("project", externAlert.getProject());
// Filling the labels with the alibaba cloud sls.
if (null != externAlert.getLabels() && !externAlert.getLabels().isEmpty()){
labels.putAll(externAlert.getLabels());
}
return labels;
}
/**
* If the alarm status is firing, the value is 0.
* If the alarm state is resolved, the value is the specific recovery time.
*
* @param status alert status
* @param resolveTimeSecond recovery time
* @return milliseconds
*/
private Long convertResolveTime(String status, int resolveTimeSecond) {
return CommonConstants.ALERT_STATUS_RESOLVED.equals(status) ? Instant.ofEpochSecond(resolveTimeSecond).toEpochMilli() : null;
}
}
}
@@ -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;
@@ -80,6 +86,9 @@ public class DataSourceServiceImpl implements DataSourceService {
expr = expr.replaceAll("\\s+", " ");
try {
return evaluate(expr, executor);
} catch (AlertExpressionException ae) {
log.error("Calculate query parse error {}: {}", datasource, ae.getMessage());
throw ae;
} catch (Exception e) {
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
throw new RuntimeException("Query execution failed", e);
@@ -87,14 +96,14 @@ public class DataSourceServiceImpl implements DataSourceService {
}
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
ParseTree tree = expressionCache.get(expr, e -> {
CommonTokenStream tokens = tokenStreamCache.get(e, this::createTokenStream);
AlertExpressionParser parser = new AlertExpressionParser(tokens);
return parser.expr();
});
AlertExpressionEvalVisitor visitor = new AlertExpressionEvalVisitor(executor);
CommonTokenStream tokens = tokenStreamCache.get(expr, this::createTokenStream);
AlertExpressionParser parser = new AlertExpressionParser(tokens);
ParseTree tree = expressionCache.get(expr, e -> parser.expr());
if (null != tokens && tokens.LA(1) != Token.EOF) {
throw new AlertExpressionException(bundle.getString("alerter.calculate.parse.error"));
}
AlertExpressionEvalVisitor visitor = new AlertExpressionEvalVisitor(executor, tokens);
return visitor.visit(tree);
}
private CommonTokenStream createTokenStream(String expr) {
@@ -0,0 +1,343 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.alert.util.DateUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.exception.IgnoreException;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.NOTIFICATION;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.SUBSCRIPTION;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.UNSUBSCRIBE;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE_ID;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBJECT;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBSCRIBE_URL;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TIMESTAMP;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TOPIC_URN;
import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TYPE;
/**
* Huawei cloud external alarm service impl
*/
@Slf4j
@Service
public class HuaweiCloudExternAlertService implements ExternAlertService {
private static final String CERTIFICATE_TYPE = "X.509";
private static final String CHARSET_UTF8 = StandardCharsets.UTF_8.name();
private final AlarmCommonReduce alarmCommonReduce;
public HuaweiCloudExternAlertService(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
}
@Override
public void addExternAlert(String content) {
HuaweiCloudExternAlert externAlert = JsonUtil.fromJson(content, HuaweiCloudExternAlert.class);
if (externAlert == null || StringUtils.isBlank(externAlert.getMessage())) {
log.warn("Failure to parse external alert content. content: {}", content);
return;
}
if (!isMessageValid(externAlert)) {
log.warn("Huawei cloud alert verify failed. content: {}", content);
return;
}
process(externAlert);
}
/**
* Process according to different types
*
* @param externAlert alert content entity
*/
private void process(HuaweiCloudExternAlert externAlert) {
if (NOTIFICATION.getType().equals(externAlert.getType())) {
Optional.ofNullable(buildSendAlert(externAlert)).ifPresent(alarmCommonReduce::reduceAndSendAlarm);
} else if (SUBSCRIPTION.getType().equals(externAlert.getType())) {
autoSubscribeForUrl(externAlert.getSubscribeUrl());
} else if (UNSUBSCRIBE.getType().equals(externAlert.getType())) {
log.warn("Huawei cloud notifies the recipient of the notification to cancel the subscription.");
}
}
/**
* Build single alert.
*
* @param externAlert alert content entity
* @return single alert
*/
private SingleAlert buildSendAlert(HuaweiCloudExternAlert externAlert) {
HuaweiCloudExternAlert.AlertMessage message = JsonUtil.fromJson(externAlert.getMessage(), HuaweiCloudExternAlert.AlertMessage.class);
if (null == message || null == message.getData()) {
log.warn("Failure to parse external alert message. message: {}", externAlert.getMessage());
return null;
}
// Note: Empty and false are both recovery notifications.
// Note: There are no recovery notifications for event types
boolean isAlarm = null != message.getData().getAlarm() && message.getData().getAlarm();
Long alarmTime = DateUtil.getZonedTimeStampFromFormat(message.getData().getAlarmTime(), "yyyy/MM/dd HH:mm:ss 'GMT'XXX");
return SingleAlert.builder()
.triggerTimes(1)
.status(isAlarm ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED)
.startAt(alarmTime)
.activeAt(Instant.now().toEpochMilli())
.endAt(isAlarm ? null : alarmTime)
.labels(buildLabels(message.getData()))
.annotations(buildAnnotations(message.getData()))
.content(formatContent(externAlert.getSubject(), message.getData()))
.build();
}
/**
* Build basic annotations and fill annotations for huawei cloud.
*
* @param alertData alert content entity
* @return annotations
*/
private Map<String, String> buildAnnotations(HuaweiCloudExternAlert.AlertData alertData) {
Map<String, String> annotations = new HashMap<>(8);
if (null != alertData) {
putIfNotBlank(annotations, "region", alertData.getRegion());
putIfNotBlank(annotations, "dimensionName", alertData.getDimensionName());
putIfNotBlank(annotations, "resourceName", alertData.getResourceName());
putIfNotBlank(annotations, "alarmRecordId", alertData.getAlarmRecordId());
}
return annotations;
}
/**
* Build basic labels and fill labels for huawei cloud.
*
* @param alertData alert content entity
* @return labels
*/
private Map<String, String> buildLabels(HuaweiCloudExternAlert.AlertData alertData) {
Map<String, String> labels = new HashMap<>(8);
labels.put("__source__", "huaweicloud-ces");
if (null != alertData) {
putIfNotBlank(labels, "namespace", alertData.getNamespace());
putIfNotBlank(labels, "metricName", alertData.getMetricName());
putIfNotBlank(labels, "resourceId", alertData.getResourceId());
putIfNotBlank(labels, "level", alertData.getAlarmLevel());
}
return labels;
}
/**
* todo i18n
*
* @param subject alert subject
* @param alertData alert content entity
* @return content
*/
private String formatContent(String subject, HuaweiCloudExternAlert.AlertData alertData) {
if (null == alertData) {
return subject;
}
return MessageFormat.format(
"{0} threshold:{1}{2}, current{3}",
subject,
alertData.getComparisonOperator(),
alertData.getValue(),
alertData.getCurrentData()
);
}
/**
* Automatic subscription url.
*
* @param subscribeUrl subscribeUrl
*/
public void autoSubscribeForUrl(String subscribeUrl) {
if (StringUtils.isBlank(subscribeUrl)) {
return;
}
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(subscribeUrl);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
if (statusCode != 200) {
log.error("Subscribe url request failed with status code: " + statusCode + ", response: " + responseBody);
return;
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
if (jsonResponse == null) {
throw new IgnoreException("Subscribe url failed with status code: " + statusCode + ", response: " + responseBody);
}
JsonNode surnNode = jsonResponse.get("subscription_urn");
if (surnNode == null || StringUtils.isBlank(surnNode.asText())) {
throw new IgnoreException("Subscribe url failed with status code: " + statusCode + ", response: " + responseBody);
}
log.info("Successfully subscribed to Huawei Cloud(SMN) url.");
}
} catch (Exception e) {
log.error("Failed to subscribe url request: {}", e.getMessage());
}
}
/**
* Verifying the signature of huawei cloud alert message.
*
* @param externAlert alert content entity
* @return verification result
* @throws SecurityException thrown when validation fails
*/
private boolean isMessageValid(HuaweiCloudExternAlert externAlert) {
try {
String signMessage = buildSignMessage(externAlert);
if (StringUtils.isBlank(signMessage)) {
throw new SecurityException("Verify sign message is null");
}
X509Certificate cert = getCertificate(externAlert.getSigningCertUrl());
return verifySignature(signMessage, cert, externAlert.getSignature());
} catch (Exception e) {
log.error("Failed to verify message signature: ", e);
return false;
}
}
/**
* Build sign message.
*
* @param externAlert alert content entity
* @return sign message
*/
private String buildSignMessage(HuaweiCloudExternAlert externAlert) {
if (NOTIFICATION.getType().equals(externAlert.getType())) {
return buildNotificationMessage(externAlert);
} else if (SUBSCRIPTION.getType().equals(externAlert.getType()) || UNSUBSCRIBE.getType().equals(externAlert.getType())){
return buildSubscriptionMessage(externAlert);
}
return null;
}
/**
* Building sign message of 'Notification' type
*
* @param externAlert alert content entity
* @return sign message
*/
private String buildNotificationMessage(HuaweiCloudExternAlert externAlert) {
StringBuilder message = new StringBuilder();
appendField(message, FIELD_MESSAGE, externAlert.getMessage());
appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId());
if (StringUtils.isNotBlank(externAlert.getSubject())) {
appendField(message, FIELD_SUBJECT, externAlert.getSubject());
}
appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp());
appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn());
appendField(message, FIELD_TYPE, externAlert.getType());
return message.toString();
}
/**
* Building sign message of 'SubscriptionConfirmation' or 'UnsubscribeConfirmation' type
*
* @param externAlert alert content entity
* @return sign message
*/
private String buildSubscriptionMessage(HuaweiCloudExternAlert externAlert) {
StringBuilder message = new StringBuilder();
appendField(message, FIELD_MESSAGE, externAlert.getMessage());
appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId());
appendField(message, FIELD_SUBSCRIBE_URL, externAlert.getSubscribeUrl());
appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp());
appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn());
appendField(message, FIELD_TYPE, externAlert.getType());
return message.toString();
}
/**
* Obtain certificate
*
* @param signCertUrl sign cert url
* @return X509 certificate
* @throws Exception Thrown when certificate acquisition fails
*/
private X509Certificate getCertificate(String signCertUrl) throws Exception {
URL url = new URL(signCertUrl);
try (InputStream in = url.openStream()) {
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
return (X509Certificate) cf.generateCertificate(in);
}
}
/**
* Verify signature
*
* @param message sign message
* @param cert cert
* @param signature signature
* @return verification result
* @throws Exception thrown when an error occurs in the validation process
*/
private boolean verifySignature(String message, X509Certificate cert, String signature) throws Exception {
Signature sig = Signature.getInstance(cert.getSigAlgName());
sig.initVerify(cert.getPublicKey());
sig.update(message.getBytes(CHARSET_UTF8));
return sig.verify(Base64.getDecoder().decode(signature));
}
private void putIfNotBlank(Map<String, String> map, String key, String value) {
if (StringUtils.isNotBlank(value)){
map.put(key, value);
}
}
private void appendField(StringBuilder builder, String fieldName, String value) {
builder.append(fieldName).append("\n").append(value).append("\n");
}
@Override
public String supportSource() {
return "huaweicloud-ces";
}
}
@@ -0,0 +1,244 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.hertzbeat.alert.dto.VolcEngineExternEventAlert;
import org.apache.hertzbeat.alert.dto.VolcEngineExternMetricAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.ExternAlertService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Volcengine alarm entity class
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VolcEngineExternAlertService implements ExternAlertService {
private final AlarmCommonReduce alarmCommonReduce;
private static final Map<String, Integer> severityOrder = Map.of(
CommonConstants.ALERT_SEVERITY_CRITICAL, 1,
CommonConstants.ALERT_SEVERITY_WARNING, 2,
CommonConstants.ALERT_SEVERITY_INFO, 3
);
@Override
@SneakyThrows
public void addExternAlert(String content) {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(content);
String type = root.get("Type").asText();
if (VolcEngineExternMetricAlert.ALERT_TYPE_EVENT.equals(type)) {
VolcEngineExternEventAlert eventAlert = JsonUtil.fromJson(content, VolcEngineExternEventAlert.class);
if (eventAlert == null) {
log.warn("parse extern event alert content failed! content: {}", content);
return;
}
SingleAlert singleAlert = new VolcEngineAlertConverter().convertEventToSingleAlert(eventAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
} else {
// deal with metric alert
VolcEngineExternMetricAlert report = JsonUtil.fromJson(content, VolcEngineExternMetricAlert.class);
if (report == null) {
log.warn("parse extern metrics alert content failed! content: {}", content);
return;
}
for (SingleAlert singleAlert : new VolcEngineAlertConverter().convertMetricAlertToSingeAlert(report)) {
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
}
}
@Override
public String supportSource() {
return "volcengine";
}
/**
* Converter: VolcEngine alert to SingleAlert
*/
public static class VolcEngineAlertConverter {
/**
* Convert VolcEngine metric alert to SingleAlert List
*/
public List<SingleAlert> convertMetricAlertToSingeAlert(VolcEngineExternMetricAlert alert) {
String status = convertStatus(alert);
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRIC)) {
return convertMetricAlert(alert, status, alert.getResources());
}
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRICS_NODATA)) {
return convertMetricAlert(alert, status, alert.getNoDataResources());
}
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_NO_DATA_RECOVERED)) {
return convertMetricAlert(alert, status, alert.getNoDataRecoveredResources());
}
if (Objects.equals(alert.getType(), VolcEngineExternMetricAlert.ALERT_TYPE_METRIC_RECOVERED)) {
return convertMetricAlert(alert, status, alert.getRecoveredResources());
}
return List.of();
}
public SingleAlert convertEventToSingleAlert(VolcEngineExternEventAlert alert) {
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
.status(CommonConstants.ALERT_STATUS_FIRING)
.startAt(alert.getHappenedAt() * 1000)
.activeAt(alert.getHappenedAt() * 1000)
.labels(buildEventLabels(alert))
.content(alert.getDescriptionCn())
.annotations(new HashMap<>());
return builder.build();
}
/**
* build labels for volcengine event alert
*/
private Map<String, String> buildEventLabels(VolcEngineExternEventAlert event) {
Map<String, String> labels = new HashMap<>();
labels.put("severity", convertEventSeverity(event));
labels.put("__source__", "volcengine");
labels.put("resource_name", event.getDetails().getVolcResourceName());
labels.put("account_id", event.getAccountId());
labels.put("region", event.getRegion());
labels.put("event_type", event.getEventType());
labels.put("source", event.getSource());
return labels;
}
/**
* convert volcengine event alert status to heartbeat alert status
* use the most severe level in the rules
*
* @param alert volcengine event alert
* @return status
*/
private String convertEventSeverity(VolcEngineExternEventAlert alert) {
return alert.getRules().stream().map(VolcEngineExternEventAlert.EventRule::getLevel)
.min((o1, o2) -> severityOrder.get(convertCommonSeverity(o1)).compareTo(severityOrder.get(convertCommonSeverity(o2))))
.orElse(CommonConstants.ALERT_SEVERITY_INFO);
}
private String convertCommonSeverity(String level) {
return switch (level) {
case "critical" -> CommonConstants.ALERT_SEVERITY_CRITICAL;
case "warning" -> CommonConstants.ALERT_SEVERITY_WARNING;
default -> CommonConstants.ALERT_SEVERITY_INFO;
};
}
/**
* create SingleAlert for each resources in volcengine alert
*
* @param alert volcengine alert
* @param status status
* @param resources resources
* @return List of SingleAlert
*/
private List<SingleAlert> convertMetricAlert(VolcEngineExternMetricAlert alert, String status,
List<? extends VolcEngineExternMetricAlert.Resource> resources) {
if (CollectionUtils.isEmpty(resources)) {
return List.of();
}
List<SingleAlert> result = new ArrayList<>();
for (VolcEngineExternMetricAlert.Resource resource : resources) {
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
.status(status)
.startAt(resource.getFirstAlertTime() * 1000)
.endAt(resource.getLastAlertTime() * 1000)
.labels(buildLabels(alert, resource))
.activeAt(convertHappenAt(alert.getHappenedAt()))
.content(resource.getName() + alert.getRuleCondition())
.annotations(buildAnnotations(resource));
result.add(builder.build());
}
return result;
}
private Long convertHappenAt(String happenAt) {
String cleanedStr = happenAt.replace("UTC", "").replace("(", "").replace(")", "");
DateTimeFormatter altFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX");
OffsetDateTime odt = OffsetDateTime.parse(cleanedStr, altFormatter);
return odt.toEpochSecond() * 1000;
}
private Map<String, String> buildLabels(VolcEngineExternMetricAlert alert, VolcEngineExternMetricAlert.Resource resource) {
Map<String, String> labels = new HashMap<>();
labels.put("__source__", "volcengine");
labels.put("resource_name", resource.getName());
labels.put("resource_id", resource.getId());
labels.put("rule_id", alert.getRuleId());
return labels;
}
private Map<String, String> buildAnnotations(VolcEngineExternMetricAlert.Resource resource) {
Map<String, String> annotations = new HashMap<>();
if (resource instanceof VolcEngineExternMetricAlert.NoDataResource noDataResource) {
for (VolcEngineExternMetricAlert.Metric noDataMetric : noDataResource.getNoDataMetrics()) {
annotations.put(noDataMetric.getName(), "N/A");
}
}
for (VolcEngineExternMetricAlert.Metric metric : resource.getMetrics()) {
annotations.put(metric.getName(), metric.getCurrentValue() + metric.getUnit());
}
annotations.put("region", resource.getRegion());
List<VolcEngineExternMetricAlert.Dimension> dimensions = resource.getDimensions();
if (CollectionUtils.isNotEmpty(dimensions)) {
for (VolcEngineExternMetricAlert.Dimension dimension : dimensions) {
annotations.put(dimension.getNameCn(), dimension.getValue());
}
}
return annotations;
}
/**
* convert volcengine alert status to heartbeat alert status
*
* @param alert volcengine alert
* @return status
*/
private String convertStatus(VolcEngineExternMetricAlert alert) {
String type = alert.getType();
if (Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_METRIC)
|| Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_EVENT)
|| Objects.equals(type, VolcEngineExternMetricAlert.ALERT_TYPE_METRICS_NODATA)) {
return CommonConstants.ALERT_STATUS_FIRING;
}
return CommonConstants.ALERT_STATUS_RESOLVED;
}
}
}
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.alert.util;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Optional;
@@ -80,4 +81,19 @@ public final class DateUtil {
return Optional.empty();
}
/**
* convert format data to timestamp
*/
public static Long getZonedTimeStampFromFormat(String dateStr, String format) {
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
// the parsed zoned date-time, not null
return ZonedDateTime.parse(dateStr, formatter).toInstant().toEpochMilli();
} catch (Exception e) {
log.error("Error parsing date '{}' with format '{}': {}",
dateStr, format, e.getMessage());
}
return null;
}
}
@@ -32,3 +32,4 @@ 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
@@ -32,3 +32,4 @@ alerter.notify.console = 登入控制台
alerter.priority.0 = 紧急告警
alerter.priority.1 = 严重告警
alerter.priority.2 = 警告告警
alerter.calculate.parse.error = 表达式未完全解析,可能存在语法错误或输入不完整
@@ -32,3 +32,4 @@ alerter.notify.console = 控制台登錄
alerter.priority.0 = 緊急警報
alerter.priority.1 = 嚴重警報
alerter.priority.2 = 警告警報
alerter.calculate.parse.error = 表達式未完全解析,可能存在語法錯誤或輸入不完整
@@ -22,36 +22,272 @@ expression
;
expr
: '(' expr ')' # ParenExpr
| left=expr op=('>='|'<='|'>'|'<'|'=='|'!=') right=expr # ComparisonExpr
| left=expr 'and' right=expr # AndExpr
| left=expr 'unless' right=expr # UnlessExpr
| left=expr 'or' right=expr # OrExpr
| identifier # QueryExpr
| number # LiteralExpr
: LPAREN expr RPAREN # ParenExpr
| left=expr op=(GE|LE|GT|LT|EQ|NE) right=expr # ComparisonExpr
| left=expr AND right=expr # AndExpr
| left=expr UNLESS right=expr # UnlessExpr
| left=expr OR right=expr # OrExpr
| promql # PromqlExpr
| selectSql # SqlExpr
| number # LiteralExpr
| SQL_FUNCTION LPAREN string RPAREN # SqlCallExpr
| PROMQL_FUNCTION LPAREN string RPAREN # PromqlCallExpr
;
// Lexer rules
AND : 'and' ;
OR : 'or' ;
UNLESS : 'unless' ;
GT : '>' ;
GE : '>=' ;
LT : '<' ;
LE : '<=' ;
EQ : '==' ;
NE : '!=' ;
LPAREN : '(' ;
RPAREN : ')' ;
functionCall
: functionName LPAREN parameterList RPAREN
;
identifier
: IDENTIFIER
parameterList
: parameter (COMMA parameter)*
;
parameter
: expr
| STAR
| string
| duration
;
number
: NUMBER
| FLOAT
| SCIENTIFIC_NUMBER
;
IDENTIFIER : [a-zA-Z_] [a-zA-Z0-9_={}[\]".]*;
NUMBER : [0-9]+ ('.' [0-9]+)? ;
WS : [ \t\r\n]+ -> skip ;
string
: STRING
;
duration
: DURATION
;
functionName
: COUNT
| AVG
| SUM
| MIN
| MAX
| RATE_FUNCTION
| INCREASE_FUNCTION
| HISTOGRAM_QUANTILE_FUNCTION
| BY_FUNCTION
| WITHOUT_FUNCTION
| GROUP_LEFT_FUNCTION
| GROUP_RIGHT_FUNCTION
| IGNORING_FUNCTION
| ON_FUNCTION
| IDENTIFIER
;
// SQL grammar for complex queries
selectSql
: SELECT selectFieldList FROM relList (WHERE conditionList)?
(GROUP BY groupByList)? (HAVING conditionList)?
(ORDER BY orderByList)? (LIMIT limitClause)?
;
selectFieldList
: selectField (COMMA selectField)*
;
selectField
: functionCall (AS? IDENTIFIER)?
| IDENTIFIER (AS? IDENTIFIER)?
| STAR (AS? IDENTIFIER)?
| IDENTIFIER DOT IDENTIFIER (AS? IDENTIFIER)?
;
groupByList
: IDENTIFIER (COMMA IDENTIFIER)*
;
orderByList
: orderByField (COMMA orderByField)*
;
orderByField
: IDENTIFIER (ASC | DESC)?
| functionCall (ASC | DESC)?
;
limitClause
: NUMBER
;
relList
: relation (COMMA relation)*
;
relation
: IDENTIFIER (AS? IDENTIFIER)?
| LPAREN selectSql RPAREN AS IDENTIFIER
;
conditionList
: conditionList AND conditionList
| conditionList OR conditionList
| condition
| LPAREN conditionList RPAREN
;
compOp
: EQ | LT | GT | LE | GE | NE | LIKE | NOT LIKE | IN | NOT IN | IS | IS NOT
;
condition
: conditionUnit compOp conditionUnit
| LPAREN condition RPAREN
| IDENTIFIER BETWEEN number AND number
;
conditionUnit
: number
| string
| IDENTIFIER
| IDENTIFIER DOT IDENTIFIER
| NULL
| LPAREN selectSql RPAREN
| functionCall
;
// PromQL query expressions
promql
: metricSelector instantVectorOp?
| aggregationOperator LPAREN promql (BY labelList)? RPAREN
| functionCall
| promql LBRACKET duration RBRACKET
| promql LBRACKET duration COLON duration RBRACKET
| promql OFFSET duration
| IDENTIFIER
;
metricSelector
: LBRACE labelMatcherList? RBRACE
;
labelMatcherList
: labelMatcherItem (COMMA labelMatcherItem)*
;
labelMatcherItem
: IDENTIFIER labelMatcherOp string
;
labelMatcherOp
: EQ | NE
;
labelList
: LPAREN IDENTIFIER (COMMA IDENTIFIER)* RPAREN
;
instantVectorOp
: LBRACKET duration RBRACKET
;
aggregationOperator
: SUM | AVG | COUNT | MIN | MAX | STDDEV | STDVAR | TOPK | BOTTOMK | QUANTILE
;
binaryOperator
: EQ | NE | GT | LT | GE | LE
| AND | OR | UNLESS
;
// Lexer rules
// Boolean operators
AND : [Aa][Nn][Dd] ;
OR : [Oo][Rr] ;
UNLESS : [Uu][Nn][Ll][Ee][Ss][Ss] ;
NOT : [Nn][Oo][Tt] ;
// SQL keywords
SELECT : [Ss][Ee][Ll][Ee][Cc][Tt] ;
FROM : [Ff][Rr][Oo][Mm] ;
WHERE : [Ww][Hh][Ee][Rr][Ee] ;
GROUP : [Gg][Rr][Oo][Uu][Pp] ;
BY : [Bb][Yy] ;
HAVING : [Hh][Aa][Vv][Ii][Nn][Gg] ;
ORDER : [Oo][Rr][Dd][Ee][Rr] ;
LIMIT : [Ll][Ii][Mm][Ii][Tt] ;
OFFSET : [Oo][Ff][Ff][Ss][Ee][Tt] ;
AS : [Aa][Ss] ;
ASC : [Aa][Ss][Cc] ;
DESC : [Dd][Ee][Ss][Cc] ;
IN : [Ii][Nn] ;
IS : [Ii][Ss] ;
NULL : [Nn][Uu][Ll][Ll] ;
LIKE : [Ll][Ii][Kk][Ee] ;
BETWEEN : [Bb][Ee][Tt][Ww][Ee][Ee][Nn] ;
STAR : '*' ;
// Aggregate functions
COUNT : [Cc][Oo][Uu][Nn][Tt] ;
SUM : [Ss][Uu][Mm] ;
AVG : [Aa][Vv][Gg] ;
MIN : [Mm][Ii][Nn] ;
MAX : [Mm][Aa][Xx] ;
STDDEV : [Ss][Tt][Dd][Dd][Ee][Vv] ;
STDVAR : [Ss][Tt][Dd][Vv][Aa][Rr] ;
VARIANCE: [Vv][Aa][Rr][Ii][Aa][Nn][Cc][Ee] ;
// PromQL specific functions
RATE_FUNCTION : [Rr][Aa][Tt][Ee] ;
INCREASE_FUNCTION: [Ii][Nn][Cc][Rr][Ee][Aa][Ss][Ee] ;
HISTOGRAM_QUANTILE_FUNCTION: [Hh][Ii][Ss][Tt][Oo][Gg][Rr][Aa][Mm] '_' [Qq][Uu][Aa][Nn][Tt][Ii][Ll][Ee] ;
TOPK : [Tt][Oo][Pp][Kk] ;
BOTTOMK : [Bb][Oo][Tt][Tt][Oo][Mm][Kk] ;
QUANTILE: [Qq][Uu][Aa][Nn][Tt][Ii][Ll][Ee] ;
BY_FUNCTION: [Bb][Yy] ;
WITHOUT_FUNCTION: [Ww][Ii][Tt][Hh][Oo][Uu][Tt] ;
GROUP_LEFT_FUNCTION: [Gg][Rr][Oo][Uu][Pp] '_' [Ll][Ee][Ff][Tt] ;
GROUP_RIGHT_FUNCTION: [Gg][Rr][Oo][Uu][Pp] '_' [Rr][Ii][Gg][Hh][Tt] ;
IGNORING_FUNCTION: [Ii][Gg][Nn][Oo][Rr][Ii][Nn][Gg] ;
ON_FUNCTION: [Oo][Nn] ;
// Other functions
SQL_FUNCTION: [Ss][Qq][Ll] ;
PROMQL_FUNCTION: [Pp][Rr][Oo][Mm][Qq][Ll] ;
// Comparison operators
GT : '>' ;
GE : '>=' ;
LT : '<' ;
LE : '<=' ;
EQ : '==' | '=' ;
NE : '!=' ;
// Delimiters
LPAREN : '(' ;
RPAREN : ')' ;
LBRACE : '{' ;
RBRACE : '}' ;
LBRACKET: '[' ;
RBRACKET: ']' ;
COMMA : ',' ;
DOT : '.' ;
COLON : ':' ;
SEMICOLON: ';' ;
// number formats
SCIENTIFIC_NUMBER: [0-9]+ ('.' [0-9]+)? [eE] [+-]? [0-9]+ ;
FLOAT : [0-9]+ '.' [0-9]+ ;
NUMBER : [0-9]+ ;
// Duration literals for PromQL (e.g., 5m, 1h, 30s)
DURATION : [0-9]+ [a-zA-Z]+ ;
// String literals
STRING : '"' (~["\r\n\\] | '\\' .)* '"'
| '\'' (~['\r\n\\] | '\\' .)* '\'' ;
// Identifiers and metric names
IDENTIFIER : [a-zA-Z_] [a-zA-Z0-9_={}[\]".~-]* ;
// Whitespace and comments
WS : [ \t\r\n]+ -> channel(HIDDEN) ;
LINE_COMMENT : '//' ~[\r\n]* -> skip ;
BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
@@ -0,0 +1,251 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.calculate;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.MetricDataConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.queue.impl.InMemoryCommonDataQueue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
*
*/
public class RealTimeAlertCalculatorMatchTest {
private final AlerterWorkerPool workerPool = new AlerterWorkerPool();
@Mock
private CommonDataQueue dataQueue = new InMemoryCommonDataQueue();
@Mock
private AlertDefineService alertDefineService;
@Mock
private SingleAlertDao singleAlertDao;
@Mock
private AlarmCommonReduce alarmCommonReduce;
@Mock
private AlarmCacheManager alarmCacheManager;
private RealTimeAlertCalculator realTimeAlertCalculator;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
when(singleAlertDao.querySingleAlertsByStatus(any())).thenReturn(new ArrayList<>());
realTimeAlertCalculator = new RealTimeAlertCalculator(
workerPool,
dataQueue,
alertDefineService,
singleAlertDao,
alarmCommonReduce,
alarmCacheManager,
false
);
}
@Test
void testFilterThresholdsByAppAndMetrics_withInstanceExpr_HasSpace() {
String app = "redis";
String instanceId = "501045327364864";
int priority = 0;
AlertDefine matchDefine = new AlertDefine();
matchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__, \"501045327364864\")");
AlertDefine unmatchDefine = new AlertDefine();
unmatchDefine.setExpr("equals(__app__,\"redis\") && equals(__instance__, \"999999999\")");
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
List<AlertDefine> filtered = realTimeAlertCalculator.filterThresholdsByAppAndMetrics(allDefines, app, "", Map.of(), instanceId, priority);
// It should filter out 999999999.
assertEquals(1, filtered.size());
assertEquals("equals(__app__,\"redis\") && equals(__instance__, \"501045327364864\")",
filtered.get(0).getExpr());
}
@Test
void testPrometheusReplaceMultipleJobsApp() throws InterruptedException {
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
builder.setId(518789738974464L)
.setApp("_prometheus_Cool_Stingray_34Nj_copy")
.setMetrics("canal_instance")
.setPriority(0)
.setCode(CollectRep.Code.SUCCESS);
CollectRep.Field destination = CollectRep.Field.newBuilder().setName("destination").setType(CommonConstants.TYPE_STRING).setLabel(true).build();
CollectRep.Field mode = CollectRep.Field.newBuilder().setName("mode").setType(CommonConstants.TYPE_STRING).setLabel(true).build();
CollectRep.Field metricValue = CollectRep.Field.newBuilder().setName("metric_value").setType(CommonConstants.TYPE_NUMBER).setLabel(true).build();
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj_copy");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(destination, mode, metricValue));
builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("example").addColumn("spring").addColumn("1.0").build());
CollectRep.MetricsData metricsData = builder.build();
AlertDefine matchDefine = new AlertDefine();
matchDefine.setName("test");
matchDefine.setExpr(
"equals(__app__,\"prometheus\") && "
+ "equals(__metrics__,\"canal_instance\") && "
+ "(equals(__instance__, \"515224274242816\") or equals(__instance__, \"518789738974464\")) && "
+ "metric_value > 0"
);
matchDefine.setTemplate("Canal instance val: ${value}%");
matchDefine.setTimes(1);
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@Test
void testPrometheusReplaceApp() throws InterruptedException {
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
builder.setId(1)
.setApp("_prometheus_Cool_Stingray_34Nj")
.setMetrics("canal_instance")
.setPriority(0)
.setCode(CollectRep.Code.SUCCESS);
CollectRep.Field destination = CollectRep.Field.newBuilder().setName("destination").setType(CommonConstants.TYPE_STRING).setLabel(true).build();
CollectRep.Field mode = CollectRep.Field.newBuilder().setName("mode").setType(CommonConstants.TYPE_STRING).setLabel(true).build();
CollectRep.Field metricValue = CollectRep.Field.newBuilder().setName("metric_value").setType(CommonConstants.TYPE_NUMBER).setLabel(true).build();
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(destination, mode, metricValue));
builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("example").addColumn("spring").addColumn("1.0").build());
CollectRep.MetricsData metricsData = builder.build();
AlertDefine matchDefine = new AlertDefine();
matchDefine.setName("test");
matchDefine.setExpr("equals(__app__,\"prometheus\") && equals(__metrics__,\"canal_instance\") && metric_value > 0");
matchDefine.setTemplate("Canal instance val: ${value}%");
matchDefine.setTimes(1);
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(any(), any());
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
}
@Test
void testCalculateWithNormalApp() throws InterruptedException {
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
builder.setId(1)
.setApp("springboot3")
.setMetrics("available")
.setPriority(0)
.setCode(CollectRep.Code.SUCCESS)
.setTenantId(0).setId(518679137103104L)
.setTime(1749110170834L)
.setPriority(0);
CollectRep.Field responseTime = CollectRep.Field.newBuilder()
.setName("responseTime")
.setType(CommonConstants.TYPE_STRING)
.setUnit("ms")
.setLabel(false)
.build();
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Vibrant_Gazelle_83vJ");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(responseTime));
builder.addValueRow(CollectRep.ValueRow.newBuilder().addColumn("18").build());
CollectRep.MetricsData metricsData = builder.build();
AlertDefine matchDefine = new AlertDefine();
matchDefine.setName("test");
matchDefine.setExpr("equals(__app__,\"springboot3\") && equals(__metrics__,\"available\") && equals(__instance__, \"518679137103104\") && responseTime > 0");
matchDefine.setTemplate("Canal instance val: ${value}%");
matchDefine.setTimes(1);
List<AlertDefine> allDefines = Collections.singletonList(matchDefine);
when(alertDefineService.getRealTimeAlertDefines()).thenReturn(allDefines);
when(dataQueue.pollMetricsDataToAlerter()).thenReturn(metricsData).thenThrow(new InterruptedException());
realTimeAlertCalculator.startCalculate();
Thread.sleep(3000);
verify(alarmCacheManager, times(1)).getPending(any());
verify(alarmCacheManager, times(1)).putFiring(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;
/**
@@ -38,76 +40,130 @@ import static org.mockito.Mockito.when;
class AlertExpressionEvalVisitorTest {
private QueryExecutor mockExecutor;
private AlertExpressionEvalVisitor visitor;
@BeforeEach
void setUp() {
mockExecutor = Mockito.mock(QueryExecutor.class);
visitor = new AlertExpressionEvalVisitor(mockExecutor);
}
@Test
void testGreaterThan() {
when(mockExecutor.execute("cpu")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80.0))));
when(mockExecutor.execute("select cpu from cpu_table where id = 1")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 80.0))));
// promql
List<Map<String, Object>> result = evaluate("cpu > 70");
assertEquals(1, result.size());
assertEquals(80.0, result.get(0).get("__value__"));
//sql
result = evaluate("(select cpu from cpu_table where id = 1) > 70");
assertEquals(1, result.size());
assertEquals(80.0, result.get(0).get("__value__"));
}
@Test
void testGreaterThanWithInteger() {
when(mockExecutor.execute("cpu")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80))));
when(mockExecutor.execute("select cpu_usage from system_metrics where host = 'server1'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 80))));
// promql
List<Map<String, Object>> result = evaluate("cpu > 70");
assertEquals(1, result.size());
assertEquals(80, result.get(0).get("__value__"));
// sql
result = evaluate("(select cpu_usage from system_metrics where host = 'server1') > 70");
assertEquals(1, result.size());
assertEquals(80, result.get(0).get("__value__"));
}
@Test
void testLessThan() {
when(mockExecutor.execute("memory")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 65.0))));
when(mockExecutor.execute("select memory_usage from memory_table where instance = 'web1'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 65.0))));
// promql
List<Map<String, Object>> result = evaluate("memory < 70");
assertEquals(1, result.size());
assertEquals(65.0, result.get(0).get("__value__"));
// sql
result = evaluate("(select memory_usage from memory_table where instance = 'web1') < 70");
assertEquals(1, result.size());
assertEquals(65.0, result.get(0).get("__value__"));
}
@Test
void testEqualWithTolerance() {
when(mockExecutor.execute("disk")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 99.999))));
when(mockExecutor.execute("select disk_usage from storage_metrics where partition = '/'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 99.999))));
// promql
List<Map<String, Object>> result = evaluate("disk == 100");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
// sql
result = evaluate("(select disk_usage from storage_metrics where partition = '/') == 100");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
}
@Test
void testNotEqual() {
when(mockExecutor.execute("network")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 50.0))));
when(mockExecutor.execute("select bandwidth from network_stats where interface = 'eth0'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 50.0))));
// promql
List<Map<String, Object>> result = evaluate("network != 60");
assertEquals(1, result.size());
assertEquals(50.0, result.get(0).get("__value__"));
// sql
result = evaluate("(select bandwidth from network_stats where interface = 'eth0') != 60");
assertEquals(1, result.size());
assertEquals(50.0, result.get(0).get("__value__"));
}
@Test
void testExactlyEqual() {
when(mockExecutor.execute("threshold")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 100.0))));
when(mockExecutor.execute("select alert_threshold from alert_config where rule_id = 'cpu_high'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 100.0))));
// promql
List<Map<String, Object>> result = evaluate("threshold == 100");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
// sql
result = evaluate("(select alert_threshold from alert_config where rule_id = 'cpu_high') == 100");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
void testMaxValueBoundary() {
when(mockExecutor.execute("max_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", Double.MAX_VALUE))));
when(mockExecutor.execute("select max_value from boundary_test where test_case = 'extreme'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", Double.MAX_VALUE))));
// promql
List<Map<String, Object>> result = evaluate("max_val > 100");
assertEquals(1, result.size());
assertEquals(Double.MAX_VALUE, result.get(0).get("__value__"));
// sql
result = evaluate("(select max_value from boundary_test where test_case = 'extreme') > 100");
assertEquals(1, result.size());
assertEquals(Double.MAX_VALUE, result.get(0).get("__value__"));
}
@Test
void testMinValueBoundary() {
when(mockExecutor.execute("min_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", Double.MIN_VALUE))));
when(mockExecutor.execute("select min_value from boundary_test where test_case = 'minimal'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", Double.MIN_VALUE))));
// promql
List<Map<String, Object>> result = evaluate("min_val > 0");
assertEquals(1, result.size());
assertEquals(Double.MIN_VALUE, result.get(0).get("__value__"));
// sql
result = evaluate("(select min_value from boundary_test where test_case = 'minimal') > 0");
assertEquals(1, result.size());
assertEquals(Double.MIN_VALUE, result.get(0).get("__value__"));
}
@Test
@@ -121,25 +177,46 @@ class AlertExpressionEvalVisitorTest {
@Test
void testListValueWithMax() {
when(mockExecutor.execute("multi_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
when(mockExecutor.execute("select values from multi_metrics where group_id = 'test_group'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
// promql
List<Map<String, Object>> result = evaluate("multi_val > 25");
assertEquals(1, result.size());
assertEquals(30.0, result.get(0).get("__value__"));
// 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__"));
}
@Test
void testListValueWithMin() {
when(mockExecutor.execute("multi_val")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
when(mockExecutor.execute("select response_times from performance_data where service = 'api'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
// promql
List<Map<String, Object>> result = evaluate("multi_val < 15");
assertEquals(1, result.size());
assertEquals(10.0, result.get(0).get("__value__"));
// 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__"));
}
@Test
void testEmptyListValue() {
when(mockExecutor.execute("empty_list")).thenReturn(List.of(new HashMap<>(Map.of("__value__", List.of()))));
when(mockExecutor.execute("select error_codes from error_log where date = '2024-01-01'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", List.of()))));
// promql
List<Map<String, Object>> result = evaluate("empty_list > 50");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
// 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__"));
}
@Test
@@ -147,19 +224,219 @@ class AlertExpressionEvalVisitorTest {
when(mockExecutor.execute("a")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 10.0))));
when(mockExecutor.execute("b")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 20.0))));
when(mockExecutor.execute("c")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 30.0))));
when(mockExecutor.execute("select cpu from server_a where region = 'us-east'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 10.0))));
when(mockExecutor.execute("select memory from server_b where region = 'us-west'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 20.0))));
when(mockExecutor.execute("select disk from server_c where region = 'eu-central'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 30.0))));
// promql
List<Map<String, Object>> result = evaluate("(a > 5) and (b > 15 or c < 25)");
assertEquals(1, result.size());
assertEquals(10.0, result.get(0).get("__value__"));
// sql
result = evaluate("((select cpu from server_a where region = 'us-east') > 5)"
+ " and ((select memory from server_b where region = 'us-west') > 15"
+ " or (select disk from server_c where region = 'eu-central') < 25)");
assertEquals(1, result.size());
assertEquals(10.0, result.get(0).get("__value__"));
}
@Test
void testMultipleUnlessConditions() {
when(mockExecutor.execute("metric1")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 40.0))));
when(mockExecutor.execute("metric2")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 50.0))));
when(mockExecutor.execute("metric3")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 60.0))));
List<Map<String, Object>> result = evaluate("metric1 > 30 unless metric2 > 45 unless metric3 < 70");
void testUnlessOpPromql() {
String promql = "http_server_requests_seconds_count > 10 unless http_server_requests_seconds_max > 0";
Map<String, Object> countValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> maxValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 10.007799125);
put("method", "GET");
put("__name__", "http_server_requests_seconds_max");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/health");
put("outcome", "SUCCESS");
put("status", "200");
}
};
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1));
when(mockExecutor.execute("http_server_requests_seconds_max")).thenReturn(List.of(maxValue1));
List<Map<String, Object>> result = evaluate(promql);
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(1307, result.get(0).get("__value__"));
maxValue1.put("uri", "/actuator/prometheus");
result = evaluate(promql);
assertEquals(0, result.size());
Map<String, Object> sumValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 20.018);
put("method", "GET");
put("__name__", "http_server_requests_seconds_sum");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/**");
put("outcome", "SUCCESS");
put("status", "200");
}
};
maxValue1.put("uri", "/actuator/health");
when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(sumValue1));
promql = "(http_server_requests_seconds_count > 10 unless http_server_requests_seconds_max > 0) unless http_server_requests_seconds_sum > 10";
result = evaluate(promql);
assertEquals(1, result.size());
assertEquals(1307, result.get(0).get("__value__"));
}
@Test
void testOrOpPromql() {
String promql = "http_server_requests_seconds_count > 10 or jvm_threads_states_threads > 0";
Map<String, Object> countValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> countValue2 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 16);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/**");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> countValue3 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 7);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/health");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> threadsValue1 = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 10.007799125);
put("__name__", "jvm_threads_states_threads");
put("__timestamp__", "1.750320922467E9");
put("job", "spring-boot-app");
put("state=", "runnable");
}
};
Map<String, Object> threadsValue2 = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 1);
put("__name__", "jvm_threads_states_threads");
put("__timestamp__", "1.750320922467E9");
put("job", "spring-boot-app");
put("state=", "timed-waiting");
}
};
Map<String, Object> threadsValue3 = new HashMap<>() {
{
put("instance", "host.docker.internal:8989");
put("__value__", 19.02);
put("__name__", "jvm_threads_states_threads");
put("__timestamp__", "1.750320922467E9");
put("job", "spring-boot-app");
put("state=", "waiting");
}
};
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1, countValue2, countValue3));
when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(List.of(threadsValue1, threadsValue2, threadsValue3));
List<Map<String, Object>> result = evaluate(promql);
assertEquals(5, result.size());
assertTrue(result.stream().allMatch(t -> null != t.get("__value__")));
when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(new ArrayList<>());
result = evaluate(promql);
assertEquals(2, result.size());
assertTrue(result.stream().allMatch(t -> null != t.get("__value__")));
assertEquals(1307, result.get(0).get("__value__"));
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(new ArrayList<>());
when(mockExecutor.execute("jvm_threads_states_threads")).thenReturn(List.of(threadsValue1, threadsValue2, threadsValue3));
result = evaluate(promql);
assertEquals(3, result.size());
assertTrue(result.stream().allMatch(t -> null != t.get("__value__")));
assertEquals(10.007799125, result.get(0).get("__value__"));
}
@Test
void testMultipleUnlessConditions() {
when(mockExecutor.execute("metric1")).thenReturn(List.of(new HashMap<>(Map.of("job", "api", "__value__", 40.0))));
when(mockExecutor.execute("metric2")).thenReturn(List.of(new HashMap<>(Map.of("job", "web", "__value__", 50.0))));
when(mockExecutor.execute("metric3")).thenReturn(List.of(new HashMap<>(Map.of("job", "api", "__value__", 60.0))));
when(mockExecutor.execute("select cpu_usage from metrics where service = 'web'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 40.0))));
when(mockExecutor.execute("select memory_usage from metrics where service = 'db'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 50.0))));
when(mockExecutor.execute("select disk_usage from metrics where service = 'cache'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 60.0))));
// promql
List<Map<String, Object>> result = evaluate("(metric1 > 30 unless metric2 > 45) unless metric3 > 50");
assertEquals(0, result.size());
// sql
result = evaluate("((select cpu_usage from metrics where service = 'web') > 30"
+ " unless (select memory_usage from metrics where service = 'db') > 55)"
+ " unless (select disk_usage from metrics where service = 'cache') > 50");
assertEquals(0, result.size());
}
@Test
@@ -167,15 +444,324 @@ class AlertExpressionEvalVisitorTest {
when(mockExecutor.execute("cpu_temp")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 75.0))));
when(mockExecutor.execute("gpu_temp")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 85.0))));
when(mockExecutor.execute("fan_speed")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 2000.0))));
when(mockExecutor.execute("select cpu_temperature from hardware_metrics where component = 'cpu'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 75.0))));
when(mockExecutor.execute("select gpu_temperature from hardware_metrics where component = 'gpu'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 85.0))));
when(mockExecutor.execute("select fan_rpm from hardware_metrics where component = 'fan'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 2000.0))));
// promql
List<Map<String, Object>> result = evaluate("(cpu_temp > 70 and gpu_temp < 90) or fan_speed > 1500");
assertEquals(1, result.size());
assertEquals(75.0, result.get(0).get("__value__"));
// sql
result = evaluate("((select cpu_temperature from hardware_metrics where component = 'cpu') > 70"
+ " and (select gpu_temperature from hardware_metrics where component = 'gpu') < 90)"
+ " or (select fan_rpm from hardware_metrics where component = 'fan') > 1500");
assertEquals(1, result.size());
assertEquals(75.0, result.get(0).get("__value__"));
}
@Test
void testSqlAggregateCount() {
when(mockExecutor.execute("select count(*) from cpu_metrics where host = 'server1'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 150))));
List<Map<String, Object>> result = evaluate("(select count(*) from cpu_metrics where host = 'server1') > 100");
assertEquals(1, result.size());
assertEquals(150, result.get(0).get("__value__"));
}
@Test
void testSqlAggregateAvg() {
when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where region = 'us-east'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 75.5))));
List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where region = 'us-east') > 70");
assertEquals(1, result.size());
assertEquals(75.5, result.get(0).get("__value__"));
}
@Test
void testSqlAggregateSum() {
when(mockExecutor.execute("select sum(memory_used) from memory_stats where service = 'web'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 2048.0))));
List<Map<String, Object>> result = evaluate("(select sum(memory_used) from memory_stats where service = 'web') < 3000");
assertEquals(1, result.size());
assertEquals(2048.0, result.get(0).get("__value__"));
}
@Test
void testSqlAggregateMaxMin() {
when(mockExecutor.execute("select max(response_time) from api_metrics where endpoint = '/api/users'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 250.0))));
when(mockExecutor.execute("select min(response_time) from api_metrics where endpoint = '/api/users'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 50.0))));
List<Map<String, Object>> result = evaluate("(select max(response_time) from api_metrics where endpoint = '/api/users') > 200");
assertEquals(1, result.size());
assertEquals(250.0, result.get(0).get("__value__"));
result = evaluate("(select min(response_time) from api_metrics where endpoint = '/api/users') < 100");
assertEquals(1, result.size());
assertEquals(50.0, result.get(0).get("__value__"));
}
@Test
void testSqlGroupBy() {
when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where timestamp > '2024-01-01' group by host")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 80.5))));
List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where timestamp > '2024-01-01' group by host) > 75");
assertEquals(1, result.size());
assertEquals(80.5, result.get(0).get("__value__"));
}
@Test
void testSqlGroupByHaving() {
when(mockExecutor.execute("select count(*) from error_logs where level = 'ERROR' group by service having count(*) > 10")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 25))));
List<Map<String, Object>> result = evaluate("(select count(*) from error_logs where level = 'ERROR' group by service having count(*) > 10) > 20");
assertEquals(1, result.size());
assertEquals(25, result.get(0).get("__value__"));
}
@Test
void testSqlOrderByLimit() {
when(mockExecutor.execute("select cpu_usage from system_metrics where host like 'web%' order by cpu_usage desc limit 1")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 95.2))));
List<Map<String, Object>> result = evaluate("(select cpu_usage from system_metrics where host like 'web%' order by cpu_usage desc limit 1) > 90");
assertEquals(1, result.size());
assertEquals(95.2, result.get(0).get("__value__"));
}
@Test
void testSqlMultipleColumns() {
when(mockExecutor.execute("select avg(cpu_usage), max(memory_usage) from system_metrics where region = 'us-west'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 65.8))));
List<Map<String, Object>> result = evaluate("(select avg(cpu_usage), max(memory_usage) from system_metrics where region = 'us-west') < 70");
assertEquals(1, result.size());
assertEquals(65.8, result.get(0).get("__value__"));
}
@Test
void testSqlSubquery() {
when(mockExecutor.execute("select avg(cpu_usage) from system_metrics where host in (select host from active_servers where status = 'running')")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 72.3))));
List<Map<String, Object>> result = evaluate("(select avg(cpu_usage) from system_metrics where host in (select host from active_servers where status = 'running')) > 70");
assertEquals(1, result.size());
assertEquals(72.3, result.get(0).get("__value__"));
}
@Test
void testSqlComplexSubquery() {
when(mockExecutor.execute("select count(*) from alerts where severity = 'HIGH' and service_id in (select id from services where category = 'critical')")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 8))));
List<Map<String, Object>> result = evaluate("(select count(*) from alerts where severity = 'HIGH' and service_id in (select id from services where category = 'critical')) >= 5");
assertEquals(1, result.size());
assertEquals(8, result.get(0).get("__value__"));
}
@Test
void testSqlWithJoins() {
when(mockExecutor.execute("select avg(m.cpu_usage) from metrics m, servers s where m.server_id = s.id and s.environment = 'production'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 78.9))));
List<Map<String, Object>> result = evaluate("(select avg(m.cpu_usage) from metrics m, servers s where m.server_id = s.id and s.environment = 'production') < 80");
assertEquals(1, result.size());
assertEquals(78.9, result.get(0).get("__value__"));
}
@Test
void testSqlBetweenCondition() {
when(mockExecutor.execute("select count(*) from performance_logs where response_time between 100 and 500")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 1200))));
List<Map<String, Object>> result = evaluate("(select count(*) from performance_logs where response_time between 100 and 500) > 1000");
assertEquals(1, result.size());
assertEquals(1200, result.get(0).get("__value__"));
}
@Test
void testSqlComplexGroupByOrderBy() {
when(mockExecutor.execute("select service, avg(response_time) from api_metrics where timestamp > '2024-01-01'"
+ " group by service order by avg(response_time) desc limit 5")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 180.5))));
List<Map<String, Object>> result = evaluate("(select service, avg(response_time) from api_metrics where timestamp > '2024-01-01'"
+ " group by service order by avg(response_time) desc limit 5) > 150");
assertEquals(1, result.size());
assertEquals(180.5, result.get(0).get("__value__"));
}
@Test
void testSqlVarianceStddev() {
when(mockExecutor.execute("select stddev(cpu_usage) from system_metrics where host = 'db-server' and timestamp > '2024-01-01'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 12.5))));
List<Map<String, Object>> result = evaluate("(select stddev(cpu_usage) from system_metrics where host = 'db-server' and timestamp > '2024-01-01') < 15");
assertEquals(1, result.size());
assertEquals(12.5, result.get(0).get("__value__"));
}
@Test
void testSqlNestedAggregation() {
when(mockExecutor.execute("select max(daily_avg) from (select date, avg(cpu_usage) as daily_avg from system_metrics group by date) as daily_stats")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 88.7))));
List<Map<String, Object>> result = evaluate("(select max(daily_avg) from (select date, avg(cpu_usage) as daily_avg from system_metrics group by date) as daily_stats) > 85");
assertEquals(1, result.size());
assertEquals(88.7, result.get(0).get("__value__"));
}
@Test
void testSqlComplexWhereConditions() {
when(mockExecutor.execute("select count(*) from alerts where (severity = 'HIGH' or severity = 'CRITICAL') and status = 'ACTIVE' and created_at > '2024-01-01'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 42))));
List<Map<String, Object>> result = evaluate("(select count(*) from alerts where (severity = 'HIGH' or severity = 'CRITICAL') and status = 'ACTIVE' and created_at > '2024-01-01') != 50");
assertEquals(1, result.size());
assertEquals(42, result.get(0).get("__value__"));
}
@Test
void testSqlWithNullChecks() {
when(mockExecutor.execute("select count(*) from system_metrics where cpu_usage is not null and memory_usage is not null")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 9876))));
List<Map<String, Object>> result = evaluate("(select count(*) from system_metrics where cpu_usage is not null and memory_usage is not null) > 9000");
assertEquals(1, result.size());
assertEquals(9876, result.get(0).get("__value__"));
}
@Test
void testSqlMultipleAggregatesWithAlias() {
when(mockExecutor.execute("select max(cpu_usage) as max_cpu, min(cpu_usage) as min_cpu, avg(cpu_usage) as avg_cpu from system_metrics where region = 'asia'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 92.1))));
List<Map<String, Object>> result = evaluate("(select max(cpu_usage) as max_cpu, min(cpu_usage) as min_cpu, avg(cpu_usage) as avg_cpu from system_metrics where region = 'asia') > 90");
assertEquals(1, result.size());
assertEquals(92.1, result.get(0).get("__value__"));
}
@Test
void testSqlAndPromqlCallExpr() {
when(mockExecutor.execute("sum(rate(http_requests_total{job='your-service'}[1m]))")).thenReturn(List.of(new HashMap<>(Map.of("__value__", 80))));
when(mockExecutor.execute("select cpu_usage from system_metrics where host = 'server1'")).thenReturn(
List.of(new HashMap<>(Map.of("__value__", 80))));
// promql
List<Map<String, Object>> result = evaluate("promql(\"sum(rate(http_requests_total{job='your-service'}[1m]))\") > 70");
assertEquals(1, result.size());
assertEquals(80, result.get(0).get("__value__"));
// sql
result = evaluate("sql(\"select cpu_usage from system_metrics where host = 'server1'\") > 70");
assertEquals(1, result.size());
assertEquals(80, result.get(0).get("__value__"));
}
@Test
void testAndOpPromql() {
String promql = "http_server_requests_seconds_count > 10 and http_server_requests_seconds_max > 5";
Map<String, Object> countValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> countValue2 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 16);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/**");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> countValue3 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 7);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/health");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> maxValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 10.007799125);
put("method", "GET");
put("__name__", "http_server_requests_seconds_max");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> maxValue2 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 10);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/**");
put("outcome", "SUCCESS");
put("status", "200");
}
};
Map<String, Object> maxValue3 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 0);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/health");
put("outcome", "SUCCESS");
put("status", "200");
}
};
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(countValue1, countValue2, countValue3));
when(mockExecutor.execute("http_server_requests_seconds_max")).thenReturn(List.of(maxValue1, maxValue2, maxValue3));
List<Map<String, Object>> result = evaluate(promql);
assertEquals(2, result.size());
assertEquals(1307, result.get(0).get("__value__"));
assertEquals(16, result.get(1).get("__value__"));
}
private List<Map<String, Object>> evaluate(String expression) {
AlertExpressionLexer lexer = new AlertExpressionLexer(CharStreams.fromString(expression));
AlertExpressionParser parser = new AlertExpressionParser(new CommonTokenStream(lexer));
return visitor.visit(parser.expression());
CommonTokenStream tokens = new CommonTokenStream(lexer);
AlertExpressionParser parser = new AlertExpressionParser(tokens);
return new AlertExpressionEvalVisitor(mockExecutor, tokens).visit(parser.expression());
}
}
@@ -20,7 +20,6 @@ package org.apache.hertzbeat.alert.notice.impl;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import org.apache.hertzbeat.alert.AlerterProperties;
@@ -49,7 +48,7 @@ import java.util.ResourceBundle;
*/
@ExtendWith(MockitoExtension.class)
class GotifyAlertNotifyHandlerImplTest {
@Mock
private RestTemplate restTemplate;
@@ -58,21 +57,20 @@ class GotifyAlertNotifyHandlerImplTest {
@Mock
private ResourceBundle bundle;
@InjectMocks
private GotifyAlertNotifyHandlerImpl gotifyAlertNotifyHandler;
private NoticeReceiver receiver;
private GroupAlert groupAlert;
private NoticeTemplate template;
@BeforeEach
public void setUp() {
receiver = new NoticeReceiver();
receiver.setId(1L);
receiver.setName("test-receiver");
receiver.setAccessToken("test-token");
receiver.setGotifyToken("A845h__ZMqDxZlO");
groupAlert = new GroupAlert();
SingleAlert singleAlert = new SingleAlert();
@@ -89,30 +87,41 @@ class GotifyAlertNotifyHandlerImplTest {
template.setName("test-template");
template.setContent("test content");
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
lenient().when(alerterProperties.getGotifyWebhookUrl())
.thenReturn("https://trusted-gotify-server.com/%s");
when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
when(alerterProperties.getGotifyWebhookUrl()).thenReturn("http://localhost:8080/gotify/%s");
}
@Test
public void testNotifyAlertSuccess() {
CommonRobotNotifyResp successResp = new CommonRobotNotifyResp();
successResp.setErrCode(0);
ResponseEntity<CommonRobotNotifyResp> responseEntity = new ResponseEntity<>(successResp, HttpStatus.OK);
ResponseEntity<CommonRobotNotifyResp> responseEntity =
new ResponseEntity<>(successResp, HttpStatus.OK);
when(restTemplate.postForEntity(
any(String.class),
any(),
eq(CommonRobotNotifyResp.class))).thenReturn(responseEntity);
eq(CommonRobotNotifyResp.class)
)).thenReturn(responseEntity);
gotifyAlertNotifyHandler.send(receiver, template, groupAlert);
}
@Test
public void testNotifyAlertWithInvalidUrl() {
when(alerterProperties.getGotifyWebhookUrl()).thenReturn("http://untrusted-server.com/%s");
public void testNotifyAlertFailure() {
CommonRobotNotifyResp failResp = new CommonRobotNotifyResp();
failResp.setCode(1);
failResp.setErrMsg("Test Error");
ResponseEntity<CommonRobotNotifyResp> responseEntity =
new ResponseEntity<>(failResp, HttpStatus.BAD_REQUEST);
when(restTemplate.postForEntity(
any(String.class),
any(),
eq(CommonRobotNotifyResp.class)
)).thenReturn(responseEntity);
assertThrows(AlertNoticeException.class,
assertThrows(AlertNoticeException.class,
() -> gotifyAlertNotifyHandler.send(receiver, template, groupAlert));
}
}
@@ -17,19 +17,7 @@
package org.apache.hertzbeat.alert.service;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anySet;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl;
@@ -45,6 +33,27 @@ import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_REALTIME;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anySet;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test case for {@link AlertDefineService}
*/
@@ -62,6 +71,9 @@ class AlertDefineServiceTest {
@Mock
private List<AlertDefineImExportService> alertDefineImExportServiceList;
@Mock
private DataSourceService dataSourceService;
@InjectMocks
private AlertDefineServiceImpl alertDefineService;
@@ -131,4 +143,36 @@ class AlertDefineServiceTest {
assertNotNull(alertDefineService.getAlertDefines(null, null, "id", "desc", 1, 10));
verify(alertDefineDao, times(1)).findAll(any(Specification.class), any(PageRequest.class));
}
@Test
void getDefinePreview() {
String expr = "http_server_requests_seconds_count > 10";
Map<String, Object> countValue1 = new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
};
when(dataSourceService.calculate(eq("promql"), eq(expr))).thenReturn(Lists.newArrayList(countValue1));
List<Map<String, Object>> result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_PERIODIC, expr);
assertNotNull(result);
assertEquals(1307, result.get(0).get("__value__"));
result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_PERIODIC, null);
assertEquals(0, result.size());
result = alertDefineService.getDefinePreview("promql", ALERT_THRESHOLD_TYPE_REALTIME, null);
assertEquals(0, result.size());
}
}
@@ -0,0 +1,150 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.impl.AlibabaCloudSlsExternAlertService;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* unit test for {@link AlibabaCloudSlsExternAlertServiceTest }
*/
@ExtendWith(MockitoExtension.class)
public class AlibabaCloudSlsExternAlertServiceTest {
@Mock
private AlarmCommonReduce alarmCommonReduce;
@InjectMocks
private AlibabaCloudSlsExternAlertService externAlertService;
@Test
void testAddExternAlertWithInvalidContent() {
String invalidContent = "invalid json";
externAlertService.addExternAlert(invalidContent);
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlert() {
String source = externAlertService.supportSource();
assertEquals("alibabacloud-sls", source);
AlibabaCloudSlsExternAlert externAlert = new AlibabaCloudSlsExternAlert();
externAlert.setAlertName("Test SLS alert");
externAlert.setFireTime((int) Instant.now().getEpochSecond());
externAlert.setAlertTime((int) Instant.now().getEpochSecond());
externAlert.setRegion("cn-hangzhou");
externAlert.setProject("project");
externAlert.setStatus("firing");
externAlert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus());
Map<String, String> labels = new HashMap<>();
labels.put("labels-k", "labels-v");
externAlert.setLabels(labels);
Map<String, String> annotations = new HashMap<>();
annotations.put("annotations-k", "annotations-v");
externAlert.setAnnotations(annotations);
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlertWithSigninUrl() {
AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert();
alert.setAlertName("Test Alert");
alert.setFireTime((int) Instant.now().getEpochSecond());
alert.setAlertTime((int) Instant.now().getEpochSecond());
alert.setRegion("cn-hangzhou");
alert.setProject("test-project");
alert.setStatus("firing");
alert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus());
alert.setSigninUrl("https://example.com");
externAlertService.addExternAlert(JsonUtil.toJson(alert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlertWithDifferentSeverityLevels() {
for (AlibabaCloudSlsExternAlert.Severity severity : AlibabaCloudSlsExternAlert.Severity.values()) {
AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert();
alert.setAlertName("Test Alert");
alert.setFireTime((int) Instant.now().getEpochSecond());
alert.setAlertTime((int) Instant.now().getEpochSecond());
alert.setRegion("cn-hangzhou");
alert.setProject("test-project");
alert.setStatus("firing");
alert.setSeverity(severity.getStatus());
externAlertService.addExternAlert(JsonUtil.toJson(alert));
}
verify(alarmCommonReduce, times(AlibabaCloudSlsExternAlert.Severity.values().length))
.reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlertWithEmptyLabelsAndAnnotations() {
AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert();
alert.setAlertName("Test Alert");
alert.setFireTime((int) Instant.now().getEpochSecond());
alert.setAlertTime((int) Instant.now().getEpochSecond());
alert.setRegion("cn-hangzhou");
alert.setProject("test-project");
alert.setStatus("firing");
alert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus());
alert.setLabels(new HashMap<>());
alert.setAnnotations(new HashMap<>());
externAlertService.addExternAlert(JsonUtil.toJson(alert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlertWithInvalidSeverity() {
AlibabaCloudSlsExternAlert alert = new AlibabaCloudSlsExternAlert();
alert.setAlertName("Test Alert");
alert.setFireTime((int) Instant.now().getEpochSecond());
alert.setAlertTime((int) Instant.now().getEpochSecond());
alert.setRegion("cn-hangzhou");
alert.setProject("test-project");
alert.setStatus("firing");
alert.setSeverity(-99);
externAlertService.addExternAlert(JsonUtil.toJson(alert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
}
@@ -17,29 +17,35 @@
package org.apache.hertzbeat.alert.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.HashMap;
import com.github.benmanes.caffeine.cache.Cache;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.hertzbeat.alert.service.impl.DataSourceServiceImpl;
import org.apache.hertzbeat.common.support.exception.AlertExpressionException;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* test case for {@link DataSourceService}
*/
class DataSourceServiceTest {
private DataSourceServiceImpl dataSourceService;
@BeforeEach
void setUp() {
dataSourceService = new DataSourceServiceImpl();
@@ -51,12 +57,12 @@ class DataSourceServiceTest {
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 150");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
@@ -296,45 +302,36 @@ class DataSourceServiceTest {
@Test
void calculate15() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
new HashMap<>(Map.of("__value__", 1))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"user\"} > 250)")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"idle\"} < 220 )")).thenReturn(new ArrayList<>());
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 and node_cpu_seconds_total{mode=\"idle\"} < 220");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "count(node_cpu_seconds_total{mode=\"user\"} > 250) > 0 and count(node_cpu_seconds_total{mode=\"idle\"} < 220 ) > 0");
assertEquals(0, result.size());
}
@Test
void calculate16() {
List<Map<String, Object>> prometheusData1 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
new HashMap<>(Map.of("__value__", 1))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "instance", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "instance", "node2"))
new HashMap<>(Map.of("__value__", 1))
);
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
Mockito.when(mockExecutor.support("promql")).thenReturn(true);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"user\"}")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("node_cpu_seconds_total{mode=\"idle\"}")).thenReturn(prometheusData2);
Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"user\"} > 250)")).thenReturn(prometheusData1);
Mockito.when(mockExecutor.execute("count(node_cpu_seconds_total{mode=\"idle\"} < 220 )")).thenReturn(prometheusData2);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 and node_cpu_seconds_total{mode=\"idle\"} < 20");
List<Map<String, Object>> result = dataSourceService.calculate("promql", "count(node_cpu_seconds_total{mode=\"user\"} > 250) > 0 and count(node_cpu_seconds_total{mode=\"idle\"} < 220 ) > 0");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertNotNull(result.get(0).get("__value__"));
}
@Test
@@ -399,8 +396,7 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 or node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(0, result.size());
}
@Test
@@ -421,8 +417,7 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 or node_cpu_seconds_total{mode=\"idle\"} < 320");
assertEquals(1, result.size());
assertNotNull(result.get(0).get("__value__"));
assertEquals(4, result.size());
}
@Test
@@ -443,8 +438,8 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 unless node_cpu_seconds_total{mode=\"idle\"} < 320");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
@@ -465,8 +460,8 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 50 unless node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNotNull(result.get(0).get("__value__"));
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
@@ -487,8 +482,7 @@ class DataSourceServiceTest {
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 250 unless node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(0, result.size());
}
@Test
@@ -565,7 +559,6 @@ class DataSourceServiceTest {
tokenStreamCache.invalidateAll();
long beforeHits = tokenStreamCache.stats().hitCount();
dataSourceService.calculate("promql", expr);
expressionCache.invalidateAll();
dataSourceService.calculate("promql", expr);
long actualHits = tokenStreamCache.stats().hitCount() - beforeHits;
assertEquals(1, actualHits, "expression cache should hit but miss");
@@ -616,4 +609,34 @@ class DataSourceServiceTest {
long actualHits = tokenStreamCache.stats().hitCount() - beforeHits;
assertEquals(0, actualHits, "expression cache should miss but hit");
}
@Test
void testAlertExpressionException() {
List<Map<String, Object>> prometheusData = List.of(
new HashMap<>() {
{
put("exception", "none");
put("instance", "host.docker.internal:8989");
put("__value__", 1307);
put("method", "GET");
put("__name__", "http_server_requests_seconds_count");
put("__timestamp__", "1.750320922467E9");
put("error", "none");
put("job", "spring-boot-app");
put("uri", "/actuator/prometheus");
put("outcome", "SUCCESS");
put("status", "200");
}
});
QueryExecutor mockExecutor = Mockito.mock(QueryExecutor.class);
when(mockExecutor.support(eq("promql"))).thenReturn(true);
when(mockExecutor.execute(eq("http_server_requests_seconds_count"))).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "http_server_requests_seconds_count > 10");
assertNotNull(result);
assertEquals(1307, result.get(0).get("__value__"));
assertThrows(AlertExpressionException.class, () -> dataSourceService.calculate("promql", "http_server_requests_seconds_count{!@~!!#$%^&}"));
}
}
@@ -0,0 +1,152 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.impl.HuaweiCloudExternAlertService;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* unit test for {@link AlibabaCloudSlsExternAlertServiceTest }
*/
@ExtendWith(MockitoExtension.class)
public class HuaweiCloudExternAlertServiceTest {
@Mock
private AlarmCommonReduce alarmCommonReduce;
@InjectMocks
private HuaweiCloudExternAlertService externAlertService;
@BeforeEach
void setUp() {
assertEquals("huaweicloud-ces", externAlertService.supportSource());
}
@Test
void testAddExternAlertWithInvalidContent() {
String invalidContent = "invalid json";
externAlertService.addExternAlert(invalidContent);
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testMessageValidFailed() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
externAlert.setType("failedType");
externAlert.setMessage("failedMessage");
externAlert.setTimestamp("2025-06-07T15:12:09Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testCertFailed() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLb"
+ "vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX"
+ "w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E"
+ "0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
externAlert.setType("Notification");
externAlert.setMessage("{}");
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/failedUrl");
externAlert.setTimestamp("2025-06-07T15:12:09Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddExternAlert() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSignature("Igs0bBhzw0JGmlgBH+9ejw2xWfPTXjAatAEsKDkkWcC5bZ/jveckdRZdgp/S0JER9eiJfMF427YDABufIN0sv/vBRXaRQKfRBLTJYbSTl+AQpEbIW5yUfJSRLEG3HNEhUDjASolbrW7zdPCoGkkqjifE23FCvw"
+ "+4tewMzqmHnfJHcFBq3W89CJzdPBjwO1UcY9C39moUZgqZk+qDVLpxb4bHSrEYAwPOSrOPR7TZpETJ30UOgFYajJydQk692edfs0NeVutHoQiOJ5/YC83ULHft0aXhichjtfZE4KF69nROAKez0ubk3l"
+ "Ey/mBIM9Ylbxn5b84OIrzzZQrIWe8Syw==");
externAlert.setSubject("[华为云][紧急告警]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已触发告警。");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("1565df032a19494590d61e05f7b0dc0e");
externAlert.setType("Notification");
externAlert.setMessage("{\"version\":\"v1\",\"data\":{\"AccountName\":\"hid_hk6tij5o1v-95zn\",\"Namespace\":\"分布式缓存服务\",\"DimensionName\":\"DCS Redis实例\",\"ResourceName\""
+ ":\"dcs-h4tv\",\"MetricName\":\"每秒并发操作数\",\"IsAlarm\":true,\"AlarmLevel\":\"紧急\",\"Region\":\"华东-上海一\",\"RegionId\":\"cn-east-3\",\"ResourceId\":\"3dc7b9ea"
+ "-70b4-4c38-942d-e2636e6d844c\",\"PrivateIp\":\"192.168.0.54\",\"CurrentData\":\"6.00 count\",\"AlarmTime\":\"2025/06/02 22:56:15 GMT+08:00\","
+ "\"AlarmRecordID\":\"ah1748876175242njvndyzMZ\","
+ "\"AlarmRuleName\":\"alarm-c5jj\",\"IsOriginalValue\":true,\"Filter\":\"原始值\",\"ComparisonOperator\":\"\\u003e\",\"Value\":\"5 count\",\"Unit\":\"count\",\"Count\":2}}");
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
externAlert.setTimestamp("2025-06-02T14:56:17Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testSubscriptionUrl() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSubscribeUrl("https://console.huaweicloud.com/smn/subscription/confirm?token=477a784601d744e4ab9ab83986502d31c4b938"
+ "0ec0b64392b134e517c3aa17eb7b3a12dc9f3b4ab495e61c4dee654b435d7223ea934345bf8ae8901cef912b1d&topic_urn=urn:smn:cn-north-4"
+ ":477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX&region=cn-north-4");
externAlert.setSignature("ottf37C/2RdDgqimRQMIBU6i7XjUfPPMU760jJn71wwP3825YPoIT22uw2A9399rkm9Jrt1qUEFrDLuA5yHFLd5n/XoM4FghIgyFn7VIfgpuVM31a+co78s"
+ "YBiZ1egOCE/AwFm2oygRhfIceUj9Kw9vmc06el9TXY6RtE5tAEF6qEmICtTh45KwtCO/WRs3DY72dQi5hm0w7/tktS4WFZ1iP4LHt5eCwFvnH0u29Y96cJNI0fLUQxI5MkhgjK"
+ "77JkFK7UT6ZYJZhzgSp/B7OQGStOQx+3Duvx4T4CzccZQM3sca81Z0B0GFGWeVXuEHyCPLsayY/Iz+5Tco51elT8w==");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("242fac183d3a4936b5ead6c725a32ed0");
externAlert.setType("SubscriptionConfirmation");
externAlert.setMessage("You are invited to subscribe to topic: urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:"
+ "CES_notification_group_bngJ2aMpX. To confirm this subscription, please visit the subscribe_url included in this message. The subscribe_url is valid only within 48 hours.");
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
externAlert.setTimestamp("2025-06-07T15:07:14Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testUnsubscribe() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLbvaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np"
+ "1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwXw1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9l"
+ "YyeL7vVI0e4sfRbuQruz3E0+ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例 “dcs-h4tv” 的每秒并发操作数已恢复正常。");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
externAlert.setType("UnsubscribeConfirmation");
externAlert.setMessage("{}");
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
externAlert.setTimestamp("2025-06-07T15:12:09Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
}
@@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.when;
/**
* unit test for {@link SmsClientFactory }
*/
@ExtendWith(MockitoExtension.class)
public class SmsClientFactoryTest {
@Mock
private GeneralConfigDao generalConfigDao;
@Mock
private ObjectMapper objectMapper;
@Mock
private SmsConfig yamlSmsConfig;
@InjectMocks
private SmsClientFactory smsClientFactory;
@Test
void testloadDbConfig() throws JsonProcessingException {
GeneralConfig generalConfig = new GeneralConfig();
SmsConfig smsConfig = new SmsConfig();
smsConfig.setType("smslocal");
smsConfig.setEnable(true);
smsConfig.setSmslocal(new SmslocalSmsProperties("11"));
generalConfig.setContent(JsonUtil.toJson(smsConfig));
when(objectMapper.readValue(generalConfig.getContent(), SmsConfig.class)).thenReturn(smsConfig);
when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(generalConfig);
assertNotNull(smsClientFactory.getSmsClient());
}
@Test
void testloadYamlConfig() {
when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(null);
when(yamlSmsConfig.getType()).thenReturn("smslocal");
when(yamlSmsConfig.isEnable()).thenReturn(true);
when(yamlSmsConfig.getSmslocal()).thenReturn(new SmslocalSmsProperties("11"));
assertNotNull(smsClientFactory.getSmsClient());
}
@Test
void testNull() {
when(generalConfigDao.findByType(GeneralConfigTypeEnum.sms.name())).thenReturn(null);
when(yamlSmsConfig.getType()).thenReturn("");
assertNull(smsClientFactory.getSmsClient());
}
}
@@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
/**
* Test case for {@link SmsLocalSmsClientImpl}
*/
@ExtendWith(MockitoExtension.class)
public class SmsLocalSmsClientImplTest {
@Mock
private SmslocalSmsProperties smslocalSmsProperties;
private SmsLocalSmsClientImpl smsLocalSmsClient;
@BeforeEach
void setUp() {
smsLocalSmsClient = new SmsLocalSmsClientImpl(smslocalSmsProperties);
when(smslocalSmsProperties.getApiKey()).thenReturn("2");
}
@Test
void testSendMessage() {
assertEquals("smslocal", smsLocalSmsClient.getType());
assertTrue(smsLocalSmsClient.checkConfig());
//
NoticeReceiver noticeReceiver = new NoticeReceiver();
noticeReceiver.setPhone("13888888888");
SingleAlert singleAlert = new SingleAlert();
singleAlert.setContent("test");
GroupAlert groupAlert = new GroupAlert();
groupAlert.setAlerts(Lists.newArrayList(singleAlert));
assertThrows(SendMessageException.class,
() -> smsLocalSmsClient.sendMessage(noticeReceiver, null, groupAlert));
}
}
@@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service.impl;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
/**
* Test case for {@link UniSmsClientImpl}
*/
@ExtendWith(MockitoExtension.class)
public class UniSmsClientImplTest {
@Mock
private UniSmsProperties uniSmsProperties;
private UniSmsClientImpl uniSmsClient;
@BeforeEach
void setUp() {
uniSmsClient = new UniSmsClientImpl(uniSmsProperties);
when(uniSmsProperties.getSignature()).thenReturn("2");
when(uniSmsProperties.getTemplateId()).thenReturn("any(String.class)");
when(uniSmsProperties.getAuthMode()).thenReturn("hmac");
when(uniSmsProperties.getAccessKeyId()).thenReturn("hmac");
when(uniSmsProperties.getAccessKeySecret()).thenReturn("hmac");
}
@Test
void testSendMessage() {
assertEquals("unisms", uniSmsClient.getType());
assertTrue(uniSmsClient.checkConfig());
//
NoticeReceiver noticeReceiver = new NoticeReceiver();
noticeReceiver.setPhone("13888888888");
Map<String, String> commonLabels = new HashMap<>();
commonLabels.put("instance", "");
commonLabels.put("priority", "unknown");
Map<String, String> commonAnnotations = new HashMap<>();
commonAnnotations.put("test", "test");
GroupAlert groupAlert = new GroupAlert();
groupAlert.setCommonLabels(commonLabels);
groupAlert.setCommonAnnotations(commonAnnotations);
assertThrows(SendMessageException.class,
() -> uniSmsClient.sendMessage(noticeReceiver, null, groupAlert));
}
}
@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Test case for {@link CryptoUtils}
*/
public class CryptoUtilsTest {
@Test
void testSha256Hex() {
String sign = CryptoUtils.sha256Hex("Hello world.");
assertEquals("aa3ec16e6acc809d8b2818662276256abfd2f1b441cb51574933f3d4bd115d11", sign);
}
@Test
void testHmacSha256Base64Debug() {
String signature = CryptoUtils.hmacSha256Base64("your-real-key", "your-real-data");
assertEquals("8JrfX0v5Tt3s8PfI85o6jcf5XM3C+vLlMwvFp45LupU=", signature);
}
@Test
void testHmacSha256Hex() {
String signature = CryptoUtils.hmacSha256Hex("your-real-key", "your-real-data");;
assertEquals("41878ccd7ecd795a2dd7ec39be7f33fed4be3ec75f5307689e39dd6f41fdbaac", signature);
}
}
@@ -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);
}
}
@@ -0,0 +1,19 @@
# 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.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
@@ -69,8 +69,8 @@
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>provided</scope>
</dependency>
<!-- clickhouse -->
@@ -164,5 +164,11 @@
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
</dependency>
<!-- Zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>${zookeeper.version}</version>
</dependency>
</dependencies>
</project>
@@ -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);
url = url.toLowerCase();
// url format check
if (!url.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 (url.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 = url.replaceAll("[\\x00-\\x1F\\x7F\\xA0]", " ").toLowerCase();
// 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 normalizedUrl;
}
assert jdbcProtocol.getPlatform() != null;
return switch (jdbcProtocol.getPlatform()) {
case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase())
@@ -166,7 +166,7 @@ public class HttpCollectImpl extends AbstractCollect {
case DispatchConstants.PARSE_XML_PATH ->
parseResponseByXmlPath(resp, metrics, builder, responseTime);
case DispatchConstants.PARSE_WEBSITE ->
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime);
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime, statusCode);
case DispatchConstants.PARSE_SITE_MAP ->
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
case DispatchConstants.PARSE_HEADER ->
@@ -237,11 +237,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());
}
@@ -66,7 +66,7 @@ public class JmxCollectImpl extends AbstractCollect {
private static final String JMX_URL_PREFIX = "service:jmx:rmi:///jndi/rmi://";
private static final String JMX_URL_SUFFIX = "/jmxrmi";
private static final String IGNORED_STUB = "/stub/";
private static final String SUB_ATTRIBUTE = "->";
@@ -75,7 +75,6 @@ public class JmxCollectImpl extends AbstractCollect {
private final ClassLoader jmxClassLoader;
public JmxCollectImpl() {
jmxClassLoader = new JmxClassLoader(ClassLoader.getSystemClassLoader());
}
@@ -83,13 +82,66 @@ public class JmxCollectImpl extends AbstractCollect {
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
Assert.isTrue(metrics != null && metrics.getJmx() != null, "JMX collect must have JMX params");
JmxProtocol jmxProtocol = metrics.getJmx();
String url = metrics.getJmx().getUrl();
// Validate JMX URL if provided
String url = jmxProtocol.getUrl();
if (StringUtils.hasText(url)) {
Assert.doesNotContain(url, IGNORED_STUB, "JMX url prohibit contains stub, please check");
// Prevent JNDI injection by validating URL format
validateJmxUrl(url);
} else {
// Validate host and port inputs
String host = jmxProtocol.getHost();
int port = Integer.parseInt(jmxProtocol.getPort());
// Validate host format (only allow valid hostnames or IP addresses)
Assert.isTrue(isValidHostname(host), "Invalid hostname format");
Assert.isTrue(port > 0 && port <= 65535, "Port must be between 1 and 65535");
}
}
/**
* Validate JMX URL
*
* @param url JMX URL to validate
* @throws IllegalArgumentException if URL is potentially malicious
*/
private void validateJmxUrl(String url) throws IllegalArgumentException {
// Only allow service:jmx:rmi protocol
Assert.isTrue(url.startsWith("service:jmx:rmi:"), "Only service:jmx:rmi protocol is supported");
String[] disallowedPatterns = { "ldap:", "rmi:", "iiop:", "nis:", "dns:", "corbaname:", "http:", "https:" };
for (String pattern : disallowedPatterns) {
if (url.contains(pattern) && !pattern.equals("rmi:///jndi/rmi:")) {
throw new IllegalArgumentException("Potentially unsafe JNDI protocol detected in URL: " + pattern);
}
}
// Check for suspicious patterns
if (url.contains("${") || url.contains("$[") || url.contains(":#") || url.contains(":/")) {
throw new IllegalArgumentException("Potentially malicious pattern detected in JMX URL");
}
}
/**
* Validate hostname format
*
* @param hostname Hostname to validate
* @return true if hostname is valid
*/
private boolean isValidHostname(String hostname) {
if (hostname == null || hostname.isEmpty()) {
return false;
}
// Simplified hostname/IP validation regex
// This regex accepts valid hostnames, IPv4 and IPv6 addresses
String hostnameRegex = "^([a-zA-Z0-9][-a-zA-Z0-9]*\\.)+[a-zA-Z0-9][-a-zA-Z0-9]*$|^(\\d{1,3}\\.){3}\\d{1,3}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
return hostname.matches(hostnameRegex);
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
@@ -172,16 +224,18 @@ public class JmxCollectImpl extends AbstractCollect {
log.info("attribute {} value is null.", attribute.getName());
continue;
}
if (value instanceof Number || value instanceof String || value instanceof ObjectName
if (value instanceof Number || value instanceof String || value instanceof ObjectName
|| value instanceof Boolean || value instanceof Date || value instanceof TimeUnit) {
attributeValueMap.put(attribute.getName(), value.toString());
} else if (value instanceof CompositeData compositeData) {
} else if (value instanceof CompositeData) {
CompositeData compositeData = (CompositeData) value;
CompositeType compositeType = compositeData.getCompositeType();
for (String typeKey : compositeType.keySet()) {
Object fieldValue = compositeData.get(typeKey);
attributeValueMap.put(attribute.getName() + SUB_ATTRIBUTE + typeKey, fieldValue.toString());
}
} else if (value instanceof String[] values) {
} else if (value instanceof String[]) {
String[] values = (String[]) value;
StringBuilder builder = new StringBuilder();
for (int index = 0; index < values.length; index++) {
builder.append(values[index]);
@@ -219,12 +273,33 @@ public class JmxCollectImpl extends AbstractCollect {
String url;
if (jmxProtocol.getUrl() != null) {
url = jmxProtocol.getUrl();
// Double check URL format for security
if (!url.startsWith("service:jmx:rmi:")) {
throw new IOException("Unsupported JMX URL protocol. Only service:jmx:rmi: is allowed.");
}
} else {
url = JMX_URL_PREFIX + jmxProtocol.getHost() + ":" + jmxProtocol.getPort() + JMX_URL_SUFFIX;
// More strict formatting with proper escaping
String host = jmxProtocol.getHost();
int port = Integer.parseInt(jmxProtocol.getPort());
// Additional validation at connection time
if (!isValidHostname(host)) {
throw new IOException("Invalid hostname format for JMX connection: " + host);
}
if (port <= 0 || port > 65535) {
throw new IOException("Invalid port for JMX connection: " + port);
}
url = JMX_URL_PREFIX + host + ":" + port + JMX_URL_SUFFIX;
}
// Set security properties to prevent remote class loading
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false");
Map<String, Object> environment = new HashMap<>(4);
if (StringUtils.hasText(jmxProtocol.getUsername()) && StringUtils.hasText(jmxProtocol.getPassword())) {
String[] credential = new String[] {jmxProtocol.getUsername(), jmxProtocol.getPassword()};
String[] credential = new String[] { jmxProtocol.getUsername(), jmxProtocol.getPassword() };
environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credential);
}
if (Boolean.TRUE.toString().equals(jmxProtocol.getSsl())) {
@@ -233,10 +308,20 @@ public class JmxCollectImpl extends AbstractCollect {
environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
}
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
// Limit JMX connection timeout
environment.put("jmx.remote.x.client.connection.timeout", 10000);
environment.put("jmx.remote.x.server.connection.timeout", 10000);
try {
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
} catch (Exception e) {
log.error("Failed to connect to JMX server: {}", e.getMessage());
throw new IOException("Failed to connect to JMX server: " + e.getMessage(), e);
}
}
}
@@ -72,12 +72,13 @@ import org.springframework.util.StringUtils;
* prometheus auto collect
*/
@Slf4j
public class PrometheusAutoCollectImpl {
public class PrometheusAutoCollectImpl implements PrometheusCollect {
private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED,
HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet());
@Override
public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder,
Metrics metrics) {
try {
@@ -139,6 +140,7 @@ public class PrometheusAutoCollectImpl {
return Collections.singletonList(builder.build());
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_PROMETHEUS;
}
@@ -179,7 +181,7 @@ public class PrometheusAutoCollectImpl {
builder.addField(CollectRep.Field.newBuilder().setName(label.getName())
.setType(CommonConstants.TYPE_STRING).setLabel(true).build());
});
builder.addField(CollectRep.Field.newBuilder().setName("value")
builder.addField(CollectRep.Field.newBuilder().setName("metric_value")
.setType(CommonConstants.TYPE_NUMBER).setLabel(false).build());
}
Map<String, String> labelMap = metric.getLabels()
@@ -0,0 +1,45 @@
/*
* 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.prometheus;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import java.util.List;
/**
* Prometheus metrics collector interface
*/
public interface PrometheusCollect {
/**
* Collect prometheus metrics data
* @param builder metrics data builder
* @param metrics metrics config
* @return list of metrics data
*/
List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder, Metrics metrics);
/**
* Get the protocol name this collector supported
* @return protocol name
*/
String supportProtocol();
}
@@ -0,0 +1,308 @@
/*
* 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.prometheus;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.PrometheusProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
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.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.SSLException;
import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH;
@Slf4j
public class PrometheusProxyCollectImpl implements PrometheusCollect {
private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED,
HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet());
public static final String RAW_TEXT_CONTENT_FIELD_NAME = "raw_text_content";
@Override
public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
PrometheusProtocol prometheusProtocol = metrics.getPrometheus();
HttpUriRequest request;
try {
validateParams(metrics);
} catch (Exception e) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(e.getMessage());
return Collections.singletonList(builder.build());
}
HttpContext httpContext = createHttpContext(prometheusProtocol);
request = createHttpRequest(prometheusProtocol);
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
log.debug("Prometheus proxy collect, response status: {}", statusCode);
if (!defaultSuccessStatusCodes.contains(statusCode)) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return Collections.singletonList(builder.build());
}
String rawTextContent = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
builder.clearFields();
builder.clearValues();
CollectRep.Field rawDataField = CollectRep.Field.newBuilder()
.setName(RAW_TEXT_CONTENT_FIELD_NAME)
.setType(CommonConstants.TYPE_STRING)
.build();
builder.addField(rawDataField);
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(rawTextContent);
builder.addValueRow(valueRowBuilder.build());
builder.setCode(CollectRep.Code.SUCCESS);
} catch (ClientProtocolException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.error("Prometheus proxy collect error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e1);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(errorMsg);
} catch (UnknownHostException e2) {
String errorMsg = CommonUtil.getMessageFromThrowable(e2);
log.info("Prometheus proxy collect unknown host: {}. Host: {}", errorMsg, prometheusProtocol.getHost(), e2);
builder.setCode(CollectRep.Code.UN_REACHABLE);
builder.setMsg("unknown host:" + errorMsg);
} catch (InterruptedIOException | ConnectException | SSLException e3) {
String errorMsg = CommonUtil.getMessageFromThrowable(e3);
log.info("Prometheus proxy collect connect error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e3);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(errorMsg);
} catch (IOException e4) {
String errorMsg = CommonUtil.getMessageFromThrowable(e4);
log.info("Prometheus proxy collect IO error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e4);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("Prometheus proxy collect unknown error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
return Collections.singletonList(builder.build());
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_PROMETHEUS;
}
private void validateParams(Metrics metrics) throws Exception {
if (metrics == null || metrics.getPrometheus() == null) {
throw new Exception("Prometheus collect must has prometheus params");
}
PrometheusProtocol protocol = metrics.getPrometheus();
if (!StringUtils.hasText(protocol.getHost())
|| !StringUtils.hasText(protocol.getPort())) {
throw new Exception("Prometheus collect must has host and port params");
}
if (protocol.getPath() == null
|| !StringUtils.hasText(protocol.getPath())
|| !protocol.getPath().startsWith(RIGHT_DASH)) {
protocol.setPath(protocol.getPath() == null ? RIGHT_DASH : RIGHT_DASH + protocol.getPath().trim());
}
}
/**
* create httpContext
* This method is adapted from PrometheusAutoCollectImpl
* @param protocol prometheus protocol
* @return context
*/
public HttpContext createHttpContext(PrometheusProtocol protocol) {
PrometheusProtocol.Authorization auth = protocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (StringUtils.hasText(auth.getDigestAuthUsername())
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials =
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword());
provider.setCredentials(AuthScope.ANY, credentials);
AuthCache authCache = new BasicAuthCache();
HttpHost targetHost = new HttpHost(protocol.getHost(), Integer.parseInt(protocol.getPort()));
authCache.put(targetHost, new DigestScheme());
clientContext.setCredentialsProvider(provider);
clientContext.setAuthCache(authCache);
return clientContext;
}
}
return null;
}
/**
* create http request
* This method is adapted from PrometheusAutoCollectImpl
* @param protocol http params
* @return http uri request
*/
public HttpUriRequest createHttpRequest(PrometheusProtocol protocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
// params
Map<String, String> params = protocol.getParams();
if (params != null && !params.isEmpty()) {
for (Map.Entry<String, String> param : params.entrySet()) {
if (StringUtils.hasText(param.getValue())) {
requestBuilder.addParameter(param.getKey(), param.getValue());
}
}
}
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
// headers The custom request header is overwritten here
Map<String, String> headers = protocol.getHeaders();
if (headers != null && !headers.isEmpty()) {
for (Map.Entry<String, String> header : headers.entrySet()) {
if (StringUtils.hasText(header.getValue())) {
requestBuilder.addHeader(CollectUtil.replaceUriSpecialChar(header.getKey()),
CollectUtil.replaceUriSpecialChar(header.getValue()));
}
}
}
if (headers == null || headers.keySet().stream().noneMatch(HttpHeaders.ACCEPT::equalsIgnoreCase)) {
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE + ";version=0.0.4,*/*;q=0.1");
}
if (protocol.getAuthorization() != null) {
PrometheusProtocol.Authorization authorization = protocol.getAuthorization();
if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) {
if (StringUtils.hasText(authorization.getBearerTokenToken())) {
String value = DispatchConstants.BEARER + " " + authorization.getBearerTokenToken();
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value);
}
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth);
}
}
}
if (StringUtils.hasLength(protocol.getPayload())) {
requestBuilder.setEntity(new StringEntity(protocol.getPayload(), StandardCharsets.UTF_8));
if (headers == null || headers.keySet().stream().noneMatch(HttpHeaders.CONTENT_TYPE::equalsIgnoreCase)) {
requestBuilder.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
}
}
String uriPath = CollectUtil.replaceUriSpecialChar(protocol.getPath());
if (IpDomainUtil.isHasSchema(protocol.getHost())) {
requestBuilder.setUri(protocol.getHost() + SignConstants.DOUBLE_MARK + protocol.getPort() + uriPath);
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(protocol.getHost());
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uriPath)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uriPath);
boolean ssl = Boolean.parseBoolean(protocol.getSsl());
if (ssl) {
requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri);
} else {
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
}
}
// custom timeout
int timeout = CollectUtil.getTimeout(protocol.getTimeout());
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setConnectionRequestTimeout(timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
} else {
RequestConfig requestConfig = RequestConfig.custom()
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
}
/**
* get collect instance
* @return instance
*/
public static PrometheusProxyCollectImpl getInstance() {
return PrometheusProxyCollectImpl.SingleInstance.INSTANCE;
}
/**
* static instance
*/
private static class SingleInstance {
private static final PrometheusProxyCollectImpl INSTANCE = new PrometheusProxyCollectImpl();
}
}
@@ -20,24 +20,42 @@ package org.apache.hertzbeat.collector.collect.sd;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.sd.ConnectionConfig;
import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriUtils;
/**
@@ -52,9 +70,10 @@ public class HttpSdCollectImpl extends AbstractCollect {
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
List<ConnectionConfig> configList = Lists.newArrayList();
HttpUriRequest request = RequestBuilder.get().setUri(metrics.getHttp_sd().getUrl()).build();
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request)) {
HttpUriRequest request = createHttpRequest(metrics.getHttp_sd());
HttpContext httpContext = createHttpContext(metrics.getHttp_sd());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
log.warn("Failed to fetch sd...");
@@ -113,4 +132,111 @@ public class HttpSdCollectImpl extends AbstractCollect {
public String supportProtocol() {
return DispatchConstants.PROTOCOL_HTTP_SD;
}
/**
* create httpContext
*
* @param httpSdProtocol http sd protocol
* @return context
*/
public HttpContext createHttpContext(HttpProtocol httpSdProtocol) {
HttpProtocol.Authorization auth = httpSdProtocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (org.springframework.util.StringUtils.hasText(auth.getDigestAuthUsername())
&& org.springframework.util.StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(),
auth.getDigestAuthPassword());
URL url;
try {
url = new URL(httpSdProtocol.getUrl());
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid URI format.", e);
}
int port = url.getPort() != -1 ? url.getPort() : ("https".equals(url.getProtocol()) ? 443 : 80);
AuthScope authScope = new AuthScope(url.getHost(), port);
provider.setCredentials(authScope, credentials);
clientContext.setCredentialsProvider(provider);
return clientContext;
}
}
return null;
}
/**
* create http request
*
* @param httpSdProtocol http request set
* @return http uri request
*/
public HttpUriRequest createHttpRequest(HttpProtocol httpSdProtocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
// The default request header can be overridden if customized
// keep-alive
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
// headers The custom request header is overwritten here
Map<String, String> headers = httpSdProtocol.getHeaders();
if (headers != null && !headers.isEmpty()) {
for (Map.Entry<String, String> header : headers.entrySet()) {
if (org.springframework.util.StringUtils.hasText(header.getValue())) {
requestBuilder.addHeader(header.getKey(), header.getValue());
}
}
}
// add accept
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
// add authorization
if (httpSdProtocol.getAuthorization() != null) {
HttpProtocol.Authorization authorization = httpSdProtocol.getAuthorization();
if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) {
String value = DispatchConstants.BEARER + SignConstants.BLANK + authorization.getBearerTokenToken();
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value);
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (org.springframework.util.StringUtils.hasText(authorization.getBasicAuthUsername())
&& org.springframework.util.StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + SignConstants.DOUBLE_MARK + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth);
}
}
}
// uri encode, default true
boolean enableUrlEncoding = Boolean.parseBoolean(httpSdProtocol.getEnableUrlEncoding());
if (enableUrlEncoding) {
// if the url contains parameters directly
if (httpSdProtocol.getUrl().contains("?")) {
String path = httpSdProtocol.getUrl().substring(0, httpSdProtocol.getUrl().indexOf("?"));
String query = httpSdProtocol.getUrl().substring(httpSdProtocol.getUrl().indexOf("?") + 1);
httpSdProtocol.setUrl(UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8"));
} else {
httpSdProtocol.setUrl(UriUtils.encodePath(httpSdProtocol.getUrl(), "UTF-8"));
}
}
// set uri
try {
requestBuilder.setUri(httpSdProtocol.getUrl());
} catch (IllegalArgumentException e) {
log.warn("Invalid URI with illegal characters: {}. User has disabled URL encoding, not applying any encoding.", httpSdProtocol.getUrl());
throw e;
}
// custom timeout
int timeout = CollectUtil.getTimeout(httpSdProtocol.getTimeout(), 0);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
}
}
@@ -0,0 +1,114 @@
/*
* 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.sd;
import java.util.List;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.registry.constant.DiscoveryClientInstance;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement;
import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.springframework.util.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
/**
* Nacos Service Discovery Collector
* This collector integrates with Nacos to discover registered services
*/
@Slf4j
public class NacosSdCollectImpl extends AbstractCollect {
/**
* Client management to interact with discovery services
*/
private final DiscoveryClientManagement discoveryClientManagement = new DiscoveryClientManagement();
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
// Validate the required configuration is present
if (metrics == null || metrics.getNacos_sd() == null) {
throw new IllegalArgumentException("Nacos service discovery monitoring, the config is null");
}
if (metrics.getNacos_sd().isInvalid()) {
throw new IllegalArgumentException("Nacos service discovery monitoring, the config is invalid");
}
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
// Create Registry protocol from NacosSd protocol
RegistryProtocol registryProtocol = RegistryProtocol.builder()
.host(metrics.getNacos_sd().getHost())
.port(metrics.getNacos_sd().getPort())
.discoveryClientTypeName(DiscoveryClientInstance.NACOS.name())
.build();
DiscoveryClient discoveryClient = null;
try {
// Use the existing NacosDiscoveryClient through DiscoveryClientManagement
discoveryClient = discoveryClientManagement.getClient(registryProtocol);
if (discoveryClient == null) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Failed to get Nacos discovery client");
return;
}
// Get all services registered in Nacos
List<ServiceInstance> services = discoveryClient.getServices();
if (CollectionUtils.isEmpty(services)) {
return;
}
// Populate the response data with service information
services.forEach(service -> {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(service.getAddress());
valueRowBuilder.addColumn(String.valueOf(service.getPort()));
valueRowBuilder.addColumn(service.getServiceName());
valueRowBuilder.addColumn(service.getHealthStatus());
builder.addValueRow(valueRowBuilder.build());
});
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch services from Nacos: {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
// Close the discovery client to release resources
if (discoveryClient != null) {
try {
discoveryClient.close();
} catch (Exception e) {
log.warn("Failed to close Nacos discovery client: {}", CommonUtil.getMessageFromThrowable(e));
}
}
}
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_NACOS_SD;
}
}
@@ -0,0 +1,95 @@
/*
* 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.sd;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol;
import org.apache.hertzbeat.common.entity.sd.ConnectionConfig;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;
import org.apache.hertzbeat.common.util.CommonUtil;
import java.io.IOException;
import java.util.List;
/**
* zookeeper sd collector
*/
@Slf4j
public class ZookeeperSdCollectImpl extends AbstractCollect {
private static int TIMEOUT = 30000;
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
ZookeeperSdProtocol zookeeperSdProtocol = metrics.getZookeeper_sd();
String url = zookeeperSdProtocol.getUrl();
String pathPrefix = zookeeperSdProtocol.getPathPrefix();
try (ZooKeeper zk = new ZooKeeper(url, TIMEOUT, event -> {})){
List<String> children = zk.getChildren(pathPrefix, false);
List<ConnectionConfig> connectionConfigs = children.stream().map(node -> {
String[] split = node.split(":");
if (split.length != 2) log.warn("Node format is incorrect: {}, expected format is 'host:port'", node);
return new ConnectionConfig(split[0], split[1]);
}).toList();
connectionConfigs.forEach(config -> {
CollectRep.ValueRow valueRow = CollectRep.ValueRow.newBuilder()
.addColumn(config.getHost())
.addColumn(config.getPort())
.build();
builder.addValueRow(valueRow);
});
} catch (IOException e){
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("Failed to connect to Zookeeper: {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (InterruptedException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("Zookeeper connection interrupted: {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (KeeperException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error("Zookeeper operation failed: {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
if (metrics.getZookeeper_sd() == null) {
throw new IllegalArgumentException("Zookeeper SD configuration cannot be null");
}
if (metrics.getZookeeper_sd().getUrl() == null || metrics.getZookeeper_sd().getUrl().isEmpty()) {
throw new IllegalArgumentException("Zookeeper URL cannot be null or empty");
}
if (metrics.getZookeeper_sd().getPathPrefix() == null || metrics.getZookeeper_sd().getPathPrefix().isEmpty()) {
throw new IllegalArgumentException("Zookeeper path prefix cannot be null or empty");
}
}
@Override
public String supportProtocol() {
return "zookeeper_sd";
}
}
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.collector.collect.http;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -25,6 +26,7 @@ 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;
@@ -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
@@ -0,0 +1,200 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.collector.collect.sd;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import lombok.SneakyThrows;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.HttpEntity;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.protocol.HttpContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* Test case for {@link HttpSdCollectImpl}
*/
@ExtendWith(MockitoExtension.class)
class HttpSdCollectImplTest {
@InjectMocks
private HttpSdCollectImpl httpSdCollectImpl;
@Mock
private CloseableHttpClient httpClient;
@Mock
private CloseableHttpResponse httpResponse;
private Metrics metrics;
private HttpProtocol httpProtocol;
private CollectRep.MetricsData.Builder builder;
@BeforeEach
void setUp() {
httpProtocol = HttpProtocol.builder()
.url("http://localhost:8080/services")
.build();
metrics = Metrics.builder()
.http_sd(httpProtocol)
.build();
builder = CollectRep.MetricsData.newBuilder();
}
@Test
void testCollectSuccess() throws IOException {
List<ServiceDiscoveryResponseEntity> responseEntities = new ArrayList<>();
ServiceDiscoveryResponseEntity entity1 = new ServiceDiscoveryResponseEntity();
entity1.setTarget(Arrays.asList("192.168.1.10:8080", "192.168.1.11:8081"));
ServiceDiscoveryResponseEntity entity2 = new ServiceDiscoveryResponseEntity();
entity2.setTarget(Arrays.asList("192.168.1.12:8082"));
responseEntities.add(entity1);
responseEntities.add(entity2);
String jsonResponse = JsonUtil.toJson(responseEntities);
try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) {
mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient);
when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse);
StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK");
when(httpResponse.getStatusLine()).thenReturn(statusLine);
HttpEntity entity = new StringEntity(jsonResponse, ContentType.APPLICATION_JSON);
when(httpResponse.getEntity()).thenReturn(entity);
httpSdCollectImpl.collect(builder, metrics);
assertEquals(3, builder.getValuesCount());
CollectRep.ValueRow firstRow = builder.getValues(0);
assertEquals("192.168.1.10", firstRow.getColumns(0));
assertEquals("8080", firstRow.getColumns(1));
CollectRep.ValueRow secondRow = builder.getValues(1);
assertEquals("192.168.1.11", secondRow.getColumns(0));
assertEquals("8081", secondRow.getColumns(1));
CollectRep.ValueRow thirdRow = builder.getValues(2);
assertEquals("192.168.1.12", thirdRow.getColumns(0));
assertEquals("8082", thirdRow.getColumns(1));
}
}
@Test
void testCollectWithNon200Status() throws IOException {
try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) {
mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient);
when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse);
StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 404, "Not Found");
when(httpResponse.getStatusLine()).thenReturn(statusLine);
httpSdCollectImpl.collect(builder, metrics);
assertEquals(CollectRep.Code.FAIL, builder.getCode());
assertEquals("StatusCode 404", builder.getMsg());
}
}
@SneakyThrows
@Test
void testCollectWithException() {
String exceptionMessage = "Connection refused";
IOException testException = new IOException(exceptionMessage);
try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class);
MockedStatic<CommonUtil> mockedCommonUtil = Mockito.mockStatic(CommonUtil.class)) {
mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient);
when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenThrow(testException);
mockedCommonUtil.when(() -> CommonUtil.getMessageFromThrowable(any(Throwable.class)))
.thenReturn(exceptionMessage);
httpSdCollectImpl.collect(builder, metrics);
assertEquals(CollectRep.Code.FAIL, builder.getCode());
assertEquals(exceptionMessage, builder.getMsg());
}
}
@Test
void testCollectWithEmptyServiceList() throws IOException {
List<ServiceDiscoveryResponseEntity> emptyList = Collections.emptyList();
String jsonResponse = JsonUtil.toJson(emptyList);
try (MockedStatic<CommonHttpClient> mockedHttpClient = Mockito.mockStatic(CommonHttpClient.class)) {
mockedHttpClient.when(CommonHttpClient::getHttpClient).thenReturn(httpClient);
when(httpClient.execute(any(HttpUriRequest.class), nullable(HttpContext.class))).thenReturn(httpResponse);
StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK");
when(httpResponse.getStatusLine()).thenReturn(statusLine);
HttpEntity entity = new StringEntity(jsonResponse, ContentType.APPLICATION_JSON);
when(httpResponse.getEntity()).thenReturn(entity);
httpSdCollectImpl.collect(builder, metrics);
assertEquals(0, builder.getValuesCount());
}
}
@Test
void testSupportProtocol() {
assertEquals(DispatchConstants.PROTOCOL_HTTP_SD, httpSdCollectImpl.supportProtocol());
}
}
@@ -0,0 +1,233 @@
/*
* 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.sd;
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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import lombok.SneakyThrows;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClient;
import org.apache.hertzbeat.collector.collect.registry.discovery.DiscoveryClientManagement;
import org.apache.hertzbeat.collector.collect.registry.discovery.entity.ServiceInstance;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.NacosSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
/**
* Test case for {@link NacosSdCollectImpl}
*/
@ExtendWith(MockitoExtension.class)
class NacosSdCollectImplTest {
@InjectMocks
private NacosSdCollectImpl nacosSdCollectImpl;
@Mock
private DiscoveryClientManagement discoveryClientManagement;
@Mock
private DiscoveryClient discoveryClient;
private Metrics metrics;
private NacosSdProtocol nacosSdProtocol;
private CollectRep.MetricsData.Builder builder;
@BeforeEach
void setUp() {
nacosSdProtocol = NacosSdProtocol.builder()
.host("localhost")
.port("8848")
.username("nacos")
.password("nacos")
.build();
metrics = Metrics.builder()
.nacos_sd(nacosSdProtocol)
.build();
builder = CollectRep.MetricsData.newBuilder();
ReflectionTestUtils.setField(nacosSdCollectImpl, "discoveryClientManagement", discoveryClientManagement);
}
@Test
void testPreCheckWithNullMetrics() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> nacosSdCollectImpl.preCheck(null));
assertEquals("Nacos service discovery monitoring, the config is null", exception.getMessage());
}
@Test
void testPreCheckWithNullNacosSdConfig() {
Metrics metricsWithoutNacosSd = Metrics.builder().build();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> nacosSdCollectImpl.preCheck(metricsWithoutNacosSd));
assertEquals("Nacos service discovery monitoring, the config is null", exception.getMessage());
}
@Test
void testPreCheckWithInvalidNacosSdConfig() {
NacosSdProtocol invalidNacosSdProtocol = NacosSdProtocol.builder()
.host("")
.port("8848")
.build();
Metrics metricsWithInvalidNacosSd = Metrics.builder()
.nacos_sd(invalidNacosSdProtocol)
.build();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> nacosSdCollectImpl.preCheck(metricsWithInvalidNacosSd));
assertEquals("Nacos service discovery monitoring, the config is invalid", exception.getMessage());
NacosSdProtocol portInvalidNacosSdProtocol = NacosSdProtocol.builder()
.host("localhost")
.port("")
.build();
Metrics portMetricsWithInvalidNacosSd = Metrics.builder()
.nacos_sd(portInvalidNacosSdProtocol)
.build();
exception = assertThrows(IllegalArgumentException.class,
() -> nacosSdCollectImpl.preCheck(portMetricsWithInvalidNacosSd));
assertEquals("Nacos service discovery monitoring, the config is invalid", exception.getMessage());
}
@Test
void testPreCheckWithValidConfig() {
assertDoesNotThrow(() -> nacosSdCollectImpl.preCheck(metrics));
}
@Test
void testCollectSuccess() throws Exception {
List<ServiceInstance> services = new ArrayList<>();
services.add(ServiceInstance.builder()
.serviceId("service-1")
.serviceName("user-service")
.address("192.168.1.10")
.port(8080)
.healthStatus("UP")
.build());
services.add(ServiceInstance.builder()
.serviceId("service-2")
.serviceName("order-service")
.address("192.168.1.11")
.port(8081)
.healthStatus("UP")
.build());
when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient);
when(discoveryClient.getServices()).thenReturn(services);
nacosSdCollectImpl.collect(builder, metrics);
verify(discoveryClient, times(1)).close();
assertEquals(2, builder.getValuesCount());
CollectRep.ValueRow firstRow = builder.getValues(0);
assertEquals("192.168.1.10", firstRow.getColumns(0));
assertEquals("8080", firstRow.getColumns(1));
assertEquals("user-service", firstRow.getColumns(2));
assertEquals("UP", firstRow.getColumns(3));
CollectRep.ValueRow secondRow = builder.getValues(1);
assertEquals("192.168.1.11", secondRow.getColumns(0));
assertEquals("8081", secondRow.getColumns(1));
assertEquals("order-service", secondRow.getColumns(2));
assertEquals("UP", secondRow.getColumns(3));
}
@Test
void testCollectClientNull() {
when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(null);
nacosSdCollectImpl.collect(builder, metrics);
assertEquals(CollectRep.Code.FAIL, builder.getCode());
assertEquals("Failed to get Nacos discovery client", builder.getMsg());
}
@SneakyThrows
@Test
void testCollectWithException() {
String exceptionMessage = "Connection refused";
Exception testException = new RuntimeException(exceptionMessage);
when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient);
when(discoveryClient.getServices()).thenThrow(testException);
try (MockedStatic<CommonUtil> mockedCommonUtil = Mockito.mockStatic(CommonUtil.class)) {
mockedCommonUtil.when(() -> CommonUtil.getMessageFromThrowable(any(Throwable.class)))
.thenReturn(exceptionMessage);
nacosSdCollectImpl.collect(builder, metrics);
assertEquals(CollectRep.Code.FAIL, builder.getCode());
assertEquals(exceptionMessage, builder.getMsg());
verify(discoveryClient, times(1)).close();
}
}
@Test
void testCollectWithEmptyServiceList() throws Exception {
when(discoveryClientManagement.getClient(any(RegistryProtocol.class))).thenReturn(discoveryClient);
when(discoveryClient.getServices()).thenReturn(Collections.emptyList());
nacosSdCollectImpl.collect(builder, metrics);
assertEquals(0, builder.getValuesCount());
verify(discoveryClient, times(1)).close();
}
@Test
void testSupportProtocol() {
assertEquals(DispatchConstants.PROTOCOL_NACOS_SD, nacosSdCollectImpl.supportProtocol());
}
}
@@ -0,0 +1,113 @@
/*
* 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.sd;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.zookeeper.ZooKeeper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class ZookeeperSdCollectImplTest {
private ZookeeperSdCollectImpl zookeeperSdCollect;
private Metrics metrics;
private ZookeeperSdProtocol protocol;
private CollectRep.MetricsData.Builder builder;
@BeforeEach
void setUp() {
zookeeperSdCollect = new ZookeeperSdCollectImpl();
protocol = ZookeeperSdProtocol.builder()
.url("localhost:2181")
.pathPrefix("/services")
.build();
metrics = Metrics.builder()
.zookeeper_sd(protocol)
.build();
builder = CollectRep.MetricsData.newBuilder();
}
@Test
void testPreCheckWithNullConfig() {
Metrics invalid = Metrics.builder().build();
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> zookeeperSdCollect.preCheck(invalid));
assertEquals("Zookeeper SD configuration cannot be null", e.getMessage());
}
@Test
void testPreCheckWithEmptyUrl() {
ZookeeperSdProtocol badProtocol = ZookeeperSdProtocol.builder().url("").pathPrefix("/path").build();
Metrics m = Metrics.builder().zookeeper_sd(badProtocol).build();
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> zookeeperSdCollect.preCheck(m));
assertEquals("Zookeeper URL cannot be null or empty", e.getMessage());
}
@Test
void testPreCheckWithEmptyPath() {
ZookeeperSdProtocol badProtocol = ZookeeperSdProtocol.builder().url("host").pathPrefix("").build();
Metrics m = Metrics.builder().zookeeper_sd(badProtocol).build();
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> zookeeperSdCollect.preCheck(m));
assertEquals("Zookeeper path prefix cannot be null or empty", e.getMessage());
}
@Test
void testPreCheckValid() {
assertDoesNotThrow(() -> zookeeperSdCollect.preCheck(metrics));
}
@Test
void testSupportProtocol() {
assertEquals("zookeeper_sd", zookeeperSdCollect.supportProtocol());
}
@Test
void testCollectSuccess() throws Exception {
try (MockedConstruction<ZooKeeper> mocked = Mockito.mockConstruction(ZooKeeper.class,
(mock, context) -> Mockito.when(mock.getChildren("/services", false)).thenReturn(List.of("host1:8080", "host2:9090")))) {
zookeeperSdCollect.collect(builder, metrics);
assertEquals(2, builder.getValuesCount());
assertEquals("host1", builder.getValues(0).getColumns(0));
assertEquals("8080", builder.getValues(0).getColumns(1));
assertEquals("host2", builder.getValues(1).getColumns(0));
assertEquals("9090", builder.getValues(1).getColumns(1));
}
}
}
@@ -28,7 +28,7 @@ import java.util.concurrent.ScheduledExecutorService;
import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.CommonThreadPool;
import org.apache.hertzbeat.remoting.RemotingClient;
@@ -32,7 +32,7 @@ import java.util.concurrent.CountDownLatch;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.WorkerPool;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.CollectRep;
@@ -23,9 +23,9 @@ 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.dispatch.timer.Timeout;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.dispatch.timer.WheelTimerTask;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
import org.apache.hertzbeat.collector.dispatch.unit.UnitConvert;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.entity.job.Configmap;
@@ -23,8 +23,8 @@ import org.apache.commons.jexl3.JexlExpression;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.prometheus.PrometheusAutoCollectImpl;
import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.collector.dispatch.timer.WheelTimerTask;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
import org.apache.hertzbeat.collector.dispatch.unit.UnitConvert;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -115,6 +115,10 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
* Whether it is a service discovery job, true is yes, false is no
*/
protected boolean isSd;
/**
* Whether to use the Prometheus proxy
*/
protected boolean prometheusProxyMode;
protected List<UnitConvert> unitConvertList;
@@ -137,6 +141,7 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
this.collectDataDispatch = collectDataDispatch;
this.isCyclic = job.isCyclic();
this.isSd = job.isSd();
this.prometheusProxyMode = job.isPrometheusProxyMode();
this.unitConvertList = unitConvertList;
// Temporary one-time tasks are executed with high priority
if (isCyclic) {
@@ -153,11 +158,31 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
CollectRep.MetricsData.Builder response = CollectRep.MetricsData.newBuilder();
response.setApp(app).setId(id).setTenantId(tenantId)
.setLabels(labels).setAnnotations(annotations).addMetadataAll(metadata);
// for prometheus auto
// for prometheus auto or proxy mode
if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(metrics.getProtocol())) {
List<CollectRep.MetricsData> metricsData = PrometheusAutoCollectImpl
.getInstance().collect(response, metrics);
validateResponse(metricsData.stream().findFirst().orElse(null));
List<CollectRep.MetricsData> metricsData;
// TODO: Refactor Prometheus metrics collection logic.
// The current implementation for proxy mode and auto mode needs review and potential simplification.
// Consider a more unified approach or clarify the conditions for each mode.
/*
// TODO USE PROXY MODE
if (prometheusProxyMode) {
List<CollectRep.MetricsData> proxyData = PrometheusProxyCollectImpl.getInstance().collect(response, metrics);
List<CollectRep.MetricsData> autoData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics);
metricsData = new LinkedList<>();
if (proxyData != null) {
metricsData.addAll(proxyData);
}
if (autoData != null) {
metricsData.addAll(autoData);
}
} else {
metricsData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics);
}
*/
metricsData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics);
validateResponse(metricsData == null ? null : metricsData.stream().findFirst().orElse(null));
collectDataDispatch.dispatchCollectData(timeout, metrics, metricsData);
return;
}
@@ -29,6 +29,7 @@ org.apache.hertzbeat.collector.collect.mqtt.MqttCollectImpl
org.apache.hertzbeat.collector.collect.ipmi2.IpmiCollectImpl
org.apache.hertzbeat.collector.collect.kafka.KafkaCollectImpl
org.apache.hertzbeat.collector.collect.sd.HttpSdCollectImpl
org.apache.hertzbeat.collector.collect.sd.NacosSdCollectImpl
org.apache.hertzbeat.collector.collect.sd.DnsSdCollectImpl
org.apache.hertzbeat.collector.collect.sd.EurekaSdCollectImpl
org.apache.hertzbeat.collector.collect.sd.ConsulSdCollectImpl
@@ -39,7 +39,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";
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.collector.dispatch;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.message.CollectRep;
@@ -127,6 +127,10 @@ public interface DispatchConstants {
* protocol http sd
*/
String PROTOCOL_HTTP_SD = "http_sd";
/**
* protocol nacos sd
*/
String PROTOCOL_NACOS_SD = "nacos_sd";
/**
* protocol dns sd
*/
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.collector.dispatch;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.common.timer.Timeout;
/**
* Metrics collection task scheduler interface
@@ -31,7 +31,7 @@ import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoCloseProcess
import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOfflineProcessor;
import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOnlineProcessor;
import org.apache.hertzbeat.collector.dispatch.entrance.processor.HeartbeatProcessor;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.dto.CollectorInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.CommonThreadPool;
@@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.WorkerPool;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.timer.TimerDispatch;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.support.SpringContextHolder;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.collector.timer;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponseEventListener;
import org.apache.hertzbeat.common.entity.job.Job;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.collector.timer;
import java.util.List;
import java.util.Map;
@@ -28,6 +28,9 @@ import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectResponse
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.timer.HashedWheelTimer;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.timer.Timer;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.collector.timer;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
@@ -28,6 +28,8 @@ import org.apache.hertzbeat.common.entity.job.Configmap;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.common.timer.TimerTask;
import org.apache.hertzbeat.common.util.AesUtil;
import java.util.ArrayList;
Vendored Executable
+259
View File
@@ -0,0 +1,259 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"
+149
View File
@@ -0,0 +1,149 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
@@ -142,5 +142,58 @@ public interface AiConstants {
float TEMPERATURE = 0.7f;
}
/**
* Ollama constants
*/
interface OllamaConstants {
/**
* request role param
*/
String REQUEST_ROLE = "user";
/**
* The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 3072
*/
Integer MAX_TOKENS = 3072;
/**
* The sampling temperature, which controls the randomness of the output, must be positive
* The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95. The larger the value,
* the more random and creative the output will be. The smaller the value, the more stable or certain the output will be
* You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time
*/
float TEMPERATURE = 0.7f;
}
/**
* OpenRouter constants
*/
interface OpenRouterConstants {
/**
* OpenRouter Ai URL
*/
String URL = "https://openrouter.ai/api/v1/chat/completions";
/**
* request role param
*/
String REQUEST_ROLE = "user";
/**
* The model outputs the maximum tokens, with a maximum output of 8192 and a default value of 3072
*/
Integer MAX_TOKENS = 3072;
/**
* The sampling temperature, which controls the randomness of the output, must be positive
* The value ranges from 0.0 to 1.0, and cannot be equal to 0. The default value is 0.95. The larger the value,
* the more random and creative the output will be. The smaller the value, the more stable or certain the output will be
* You are advised to adjust top_p or temperature parameters based on application scenarios, but do not adjust the two parameters at the same time
*/
float TEMPERATURE = 0.7f;
}
}
@@ -42,8 +42,17 @@ public enum AiTypeEnum {
/**
* Moonshot AI
*/
kimiAi;
kimiAi,
/**
* Ollama AI
*/
ollama,
/**
* OpenRouter
*/
openRouter;
/**
* get type
@@ -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")
@@ -138,6 +138,11 @@ public class Job {
*/
private boolean isSd = false;
/**
* Whether to use the Prometheus proxy
*/
private boolean prometheusProxyMode = false;
/**
* the collect data response metrics as env configmap for other collect use. ^o^xxx^o^
*/
@@ -36,6 +36,7 @@ import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.EurekaSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.ConsulSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.ModbusProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.NacosSdProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.PlcProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.RegistryProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.IcmpProtocol;
@@ -66,11 +67,12 @@ import org.apache.hertzbeat.common.entity.job.protocol.TelnetProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.UdpProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.WebsocketProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.entity.job.protocol.ZookeeperSdProtocol;
/**
* Details of the monitoring metrics collected
* eg: cpu | memory | health
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -278,6 +280,14 @@ public class Metrics {
* consul sd protocol
*/
private ConsulSdProtocol consul_sd;
/**
* nacos sd protocol
*/
private NacosSdProtocol nacos_sd;
/*
* zookeeper sd protocol
*/
private ZookeeperSdProtocol zookeeper_sd;
/**
* Monitoring configuration information using the public plc protocol
*/
@@ -0,0 +1,63 @@
/*
* 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.job.protocol;
import org.apache.commons.lang3.StringUtils;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Nacos Service Discovery Protocol
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class NacosSdProtocol implements Protocol {
/**
* Nacos server host
*/
private String host;
/**
* Nacos server port
*/
private String port;
/**
* Nacos namespace
*/
private String namespace;
/**
* Nacos username for authentication
*/
private String username;
/**
* Nacos password for authentication
*/
private String password;
/**
* Check if the essential protocol parameters are invalid
* @return true if essential parameters are missing
*/
public boolean isInvalid() {
return StringUtils.isAnyBlank(host, port);
}
}
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.job.protocol;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
/**
* Zookeeper service discovery protocol
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ZookeeperSdProtocol implements Protocol{
private String url;
private String pathPrefix;
}
@@ -77,7 +77,7 @@ public class Monitor {
@Size(max = 100)
private String app;
@Schema(title = "Scrape type: static | http_sd | dns_sd", example = "static", accessMode = READ_WRITE)
@Schema(title = "Scrape type: static | http_sd | dns_sd | zookeeper_sd", example = "static", accessMode = READ_WRITE)
@Size(max = 100)
private String scrape;
@@ -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);
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.common.timer;
import org.apache.hertzbeat.common.util.NetworkUtil;
import org.slf4j.Logger;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.common.timer;
/**
* Derived from Apache Dubbo org.apache.dubbo.common.timer.Timeout
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.common.timer;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.dispatch.timer;
package org.apache.hertzbeat.common.timer;
import java.util.concurrent.TimeUnit;
@@ -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());
}
@@ -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>
@@ -20,8 +20,8 @@ package org.apache.hertzbeat.collector.collect;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.CollectDataDispatch;
import org.apache.hertzbeat.collector.dispatch.MetricsCollect;
import org.apache.hertzbeat.collector.dispatch.timer.Timeout;
import org.apache.hertzbeat.collector.dispatch.timer.WheelTimerTask;
import org.apache.hertzbeat.common.timer.Timeout;
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.job.Metrics;
@@ -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();
@@ -18,6 +18,7 @@
package org.apache.hertzbeat.grafana.common;
import org.apache.hertzbeat.common.constants.ConfigConstants;
import java.net.URL;
/**
* Grafana Common Constants
@@ -31,32 +32,64 @@ public interface GrafanaConstants {
String REFRESH = "&refresh=15s";
String INSTANCE = "&var-instance=";
String CREATE_DASHBOARD_API = "/api/dashboards/db";
String DELETE_DASHBOARD_API = "/api/dashboards/uid/%s";
String DATASOURCE_NAME = "hertzbeat-victoria-metrics";
String USE_DATASOURCE = "&var-ds=" + DATASOURCE_NAME;
String DATASOURCE_BASE_NAME = "hertzbeat";
String DATASOURCE_TYPE = "prometheus";
String DATASOURCE_ACCESS = "proxy";
String CREATE_DATASOURCE_API = "/api/datasources";
String QUERY_DATASOURCE_API = "/api/datasources/name/" + DATASOURCE_NAME;
String QUERY_DATASOURCE_API = "/api/datasources/name/";
String GET_SERVICE_ACCOUNTS_API = "%s/api/serviceaccounts/search";
String ACCOUNT_NAME = ConfigConstants.SystemConstant.PROJECT_NAME;
String ACCOUNT_ROLE = "Admin";
String CREATE_SERVICE_ACCOUNT_API = "%s/api/serviceaccounts";
String CREATE_SERVICE_TOKEN_API = "%s/api/serviceaccounts/%d/tokens";
String GRAFANA_CONFIG = "grafanaConfig";
}
/**
* Generate data source name based on type and URL
* @param type Data source type (vm or greptime)
* @param url Data source URL
* @return Generated unique data source name
*/
static String generateDatasourceName(String type, String url) {
try {
URL parsedUrl = new URL(url);
String host = parsedUrl.getHost();
int port = parsedUrl.getPort();
// Extract host without subdomain for cleaner names
String hostPart = host != null ? host.replaceAll("^www\\.", "") : "localhost";
// Include port if it's not default
String portPart = (port > 0 && port != 80 && port != 443) ? "-" + port : "";
return String.format("%s-%s-%s%s", DATASOURCE_BASE_NAME, type, hostPart, portPart);
} catch (Exception e) {
// Fallback to simple naming if URL parsing fails
return String.format("%s-%s-%s", DATASOURCE_BASE_NAME, type, url.hashCode());
}
}
/**
* Generate USE_DATASOURCE parameter with dynamic datasource name
* @param datasourceName The datasource name
* @return USE_DATASOURCE parameter string
*/
static String generateUseDatasource(String datasourceName) {
return "&var-ds=" + datasourceName;
}
}
@@ -45,10 +45,7 @@ public class GrafanaInit implements CommandLineRunner {
if (grafanaProperties.enabled()) {
log.info("grafana init start");
try {
String token = serviceAccountService.getToken();
if (token == null) {
token = serviceAccountService.applyForToken();
}
String token = serviceAccountService.applyForToken();
datasourceService.existOrCreateDatasource(token);
} catch (Exception e) {
log.error("grafana init error", e);
@@ -22,7 +22,6 @@ import static org.apache.hertzbeat.grafana.common.GrafanaConstants.DELETE_DASHBO
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.INSTANCE;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.KIOSK;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.REFRESH;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.USE_DATASOURCE;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -30,6 +29,7 @@ import java.util.Objects;
import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.grafana.common.GrafanaConstants;
import org.apache.hertzbeat.grafana.config.GrafanaProperties;
import org.apache.hertzbeat.grafana.dao.DashboardDao;
import org.springframework.beans.factory.annotation.Autowired;
@@ -63,10 +63,15 @@ public class DashboardService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DatasourceService datasourceService;
/**
* Creates a new dashboard in Grafana.
* Creates or updates a dashboard in Grafana.
* The "id" field will be removed from the dashboard JSON before sending
* to Grafana to ensure new dashboards are created correctly.
*
* @param dashboardJson the JSON representation of the dashboard
* @param dashboardJson the JSON representation of the dashboard definition
* @param monitorId the ID of the monitor associated with the dashboard
* @return ResponseEntity containing the response from Grafana
*/
@@ -84,11 +89,31 @@ public class DashboardService {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
Map<String, Object> body = new HashMap<>();
body.put("dashboard", JsonUtil.fromJson(dashboardJson, Object.class));
body.put("overwrite", true);
Map<String, Object> dashboardObjectMap;
try {
dashboardObjectMap = JsonUtil.fromJson(dashboardJson, Map.class);
} catch (Exception e) {
log.error("Failed to parse dashboardJson. Monitor ID: {}. JSON: {}", monitorId, dashboardJson, e);
throw new RuntimeException("Invalid dashboard JSON structure", e);
}
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(body, headers);
if (dashboardObjectMap == null) {
log.error("Parsed dashboardJson is null. Monitor ID: {}. Original JSON: {}", monitorId, dashboardJson);
throw new RuntimeException("Parsed dashboard JSON is null");
}
if (dashboardObjectMap.containsKey("id")) {
dashboardObjectMap.remove("id");
log.debug("Removed 'id' field from dashboard JSON for monitorId: {}", monitorId);
}
// Construct the full request payload for Grafana API
Map<String, Object> requestPayload = new HashMap<>();
requestPayload.put("dashboard", dashboardObjectMap);
requestPayload.put("overwrite", true); // Overwrite if a dashboard with the same UID exists
String finalJsonPayload = JsonUtil.toJson(requestPayload);
HttpEntity<String> requestEntity = new HttpEntity<>(finalJsonPayload, headers);
try {
ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
@@ -97,30 +122,49 @@ public class DashboardService {
GrafanaDashboard grafanaDashboard = JsonUtil.fromJson(response.getBody(), GrafanaDashboard.class);
if (grafanaDashboard != null) {
grafanaDashboard.setEnabled(true);
grafanaDashboard.setUrl(grafanaProperties.exposeUrl()
+ grafanaDashboard.getUrl().replace(grafanaProperties.getUrl(), "")
+ KIOSK + REFRESH + INSTANCE + monitorId + USE_DATASOURCE);
String currentDatasourceName = datasourceService.getCurrentDatasourceName();
String useDatasource = currentDatasourceName != null
? GrafanaConstants.generateUseDatasource(currentDatasourceName) : "";
String relativeDashboardUrl = grafanaDashboard.getUrl();
if (relativeDashboardUrl != null && grafanaProperties.getUrl() != null && relativeDashboardUrl.startsWith(grafanaProperties.getUrl())) {
relativeDashboardUrl = relativeDashboardUrl.substring(grafanaProperties.getUrl().length());
}
String fullDashboardUrl = grafanaProperties.exposeUrl().replaceAll("/$", "")
+ (relativeDashboardUrl != null ? relativeDashboardUrl.replaceAll("^/", "") : "");
grafanaDashboard.setUrl(fullDashboardUrl + KIOSK + REFRESH + INSTANCE + monitorId + useDatasource);
grafanaDashboard.setMonitorId(monitorId);
dashboardDao.save(grafanaDashboard);
log.info("create dashboard success, token: {}", response.getBody());
log.info("Successfully created/updated Grafana dashboard for monitorId: {}. Response: {}", monitorId, response.getBody());
} else {
log.error("Failed to parse Grafana response into GrafanaDashboard object. MonitorId: {}. Response body: {}", monitorId, response.getBody());
}
return response;
} else {
log.error("create dashboard error: {}", response.getStatusCode());
throw new RuntimeException("create dashboard error");
log.error("Failed to create/update Grafana dashboard for monitorId: {}. Status: {}, Response: {}",
monitorId, response.getStatusCode(), response.getBody());
throw new RuntimeException("Failed to create/update Grafana dashboard: " + response.getStatusCode() + " - " + response.getBody());
}
} catch (HttpClientErrorException.Forbidden ex) {
log.error("Grafana Access denied to save dashboard", ex);
throw new RuntimeException("Grafana Access denied to save dashboard", ex);
} catch (HttpClientErrorException.NotFound ex){
log.error("Grafana Dashboard not found", ex);
throw new RuntimeException("Grafana Dashboard not found", ex);
} catch (HttpClientErrorException ex) {
String responseBody = ex.getResponseBodyAsString();
log.error("Grafana API request failed for monitorId: {}. Status: {}. URL: {}. Request: {}. Response: {}",
monitorId, ex.getStatusCode(), url, finalJsonPayload, responseBody, ex);
if (ex instanceof HttpClientErrorException.Forbidden) {
throw new RuntimeException("Grafana Access Denied: " + responseBody, ex);
} else if (ex instanceof HttpClientErrorException.NotFound) {
throw new RuntimeException("Grafana API endpoint or resource not found: " + responseBody, ex);
}
throw new RuntimeException("Grafana API client error (" + ex.getStatusCode() + "): " + responseBody, ex);
} catch (Exception ex) {
log.error("create dashboard error", ex);
throw new RuntimeException("create dashboard error", ex);
log.error("An unexpected error occurred while creating/updating Grafana dashboard for monitorId: {}. URL: {}. Request: {}",
monitorId, url, finalJsonPayload, ex);
throw new RuntimeException("Error during Grafana dashboard operation: " + ex.getMessage(), ex);
}
}
/**
* Deletes a dashboard in Grafana by monitor ID.
*
@@ -130,6 +174,7 @@ public class DashboardService {
public void deleteDashboard(Long monitorId) {
GrafanaDashboard grafanaDashboard = dashboardDao.findByMonitorId(monitorId);
if (Objects.isNull(grafanaDashboard)) {
log.info("No Grafana dashboard found for monitorId {} to delete.", monitorId);
return;
}
String uid = grafanaDashboard.getUid();
@@ -137,6 +182,7 @@ public class DashboardService {
if (grafanaDashboards.size() > 1) {
dashboardDao.deleteByMonitorId(monitorId);
log.info("Deleted hertzbeat dashboard record for monitorId: {}, Grafana dashboard with UID: {} still used by other monitors.", monitorId, uid);
} else {
String token = serviceAccountService.getToken();
String url = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + String.format(DELETE_DASHBOARD_API, uid);
@@ -146,19 +192,36 @@ public class DashboardService {
headers.setBearerAuth(token);
HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
dashboardDao.deleteByMonitorId(monitorId);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, requestEntity, String.class);
try {
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, requestEntity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.info("delete dashboard success");
} else {
log.error("delete dashboard error: {}", response.getStatusCode());
throw new RuntimeException("delete dashboard error");
if (response.getStatusCode().is2xxSuccessful()) {
// Delete from local DB only after successful Grafana deletion
dashboardDao.deleteByMonitorId(monitorId);
log.info("Successfully deleted Grafana dashboard with UID: {} and corresponding hertzbeat record for monitorId: {}", uid, monitorId);
} else {
log.error("Failed to delete Grafana dashboard with UID: {}. Status: {}, Response: {}",
uid, response.getStatusCode(), response.getBody());
throw new RuntimeException("Failed to delete Grafana dashboard: " + response.getStatusCode() + " - " + response.getBody());
}
} catch (HttpClientErrorException ex) {
String responseBody = ex.getResponseBodyAsString();
log.error("Grafana API request failed during dashboard deletion for UID: {}. Status: {}. URL: {}. Response: {}",
uid, ex.getStatusCode(), url, responseBody, ex);
if (ex.getStatusCode() == org.springframework.http.HttpStatus.NOT_FOUND) {
log.warn("Grafana dashboard with UID: {} not found during deletion attempt. Assuming already deleted. Deleting local record for monitorId: {}", uid, monitorId);
dashboardDao.deleteByMonitorId(monitorId);
} else {
throw new RuntimeException("Grafana API client error during deletion (" + ex.getStatusCode() + "): " + responseBody, ex);
}
} catch (Exception ex) {
log.error("An unexpected error occurred while deleting Grafana dashboard with UID: {}. URL: {}", uid, url, ex);
throw new RuntimeException("Error during Grafana dashboard deletion: " + ex.getMessage(), ex);
}
}
}
/**
* Retrieves a dashboard by monitor ID.
*
@@ -171,14 +234,22 @@ public class DashboardService {
/**
* Disables a Grafana dashboard by monitor ID.
* (This only updates the local HertzBeat database flag, does not interact with Grafana API)
*
* @param monitorId the ID of the monitor associated with the dashboard
*/
public void closeGrafanaDashboard(Long monitorId) {
GrafanaDashboard grafanaDashboard = dashboardDao.findByMonitorId(monitorId);
if (grafanaDashboard != null) {
grafanaDashboard.setEnabled(false);
dashboardDao.save(grafanaDashboard);
if (grafanaDashboard.isEnabled()) { // Only save if there's a change
grafanaDashboard.setEnabled(false);
dashboardDao.save(grafanaDashboard);
log.info("Disabled Grafana dashboard link in HertzBeat for monitorId: {}", monitorId);
} else {
log.info("Grafana dashboard link for monitorId: {} was already disabled.", monitorId);
}
} else {
log.warn("No Grafana dashboard record found for monitorId {} to disable.", monitorId);
}
}
}
}
@@ -19,12 +19,18 @@ package org.apache.hertzbeat.grafana.service;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.CREATE_DATASOURCE_API;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.DATASOURCE_ACCESS;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.DATASOURCE_NAME;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.DATASOURCE_TYPE;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.QUERY_DATASOURCE_API;
import static org.apache.hertzbeat.grafana.common.GrafanaConstants.generateDatasourceName;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.grafana.config.GrafanaProperties;
import org.apache.hertzbeat.warehouse.store.history.vm.VictoriaMetricsProperties;
import org.apache.hertzbeat.warehouse.store.history.tsdb.vm.VictoriaMetricsProperties;
import org.apache.hertzbeat.warehouse.store.history.tsdb.greptime.GreptimeProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@@ -36,7 +42,8 @@ import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
/**
* Service for managing Grafana datasource.
* Service for managing Grafana datasources.
* This service checks if a datasource exists and creates it if not.
*/
@Service
@Slf4j
@@ -44,57 +51,163 @@ public class DatasourceService {
private final GrafanaProperties grafanaProperties;
private final VictoriaMetricsProperties warehouseProperties;
private final GreptimeProperties greptimeProperties;
private final RestTemplate restTemplate;
@Autowired
public DatasourceService(
GrafanaProperties grafanaProperties,
VictoriaMetricsProperties warehouseProperties,
GreptimeProperties greptimeProperties,
RestTemplate restTemplate
) {
this.grafanaProperties = grafanaProperties;
this.warehouseProperties = warehouseProperties;
this.greptimeProperties = greptimeProperties;
this.restTemplate = restTemplate;
}
/**
* Create a new datasource in Grafana.
*/
public void existOrCreateDatasource(String token) {
if (!warehouseProperties.enabled()) {
log.info("HertzBeat VictoriaMetrics config not enabled");
boolean vmEnabled = warehouseProperties.enabled();
boolean greptimeEnabled = greptimeProperties.enabled();
if (vmEnabled && greptimeEnabled) {
throw new IllegalStateException("Conflict: Both VictoriaMetrics and Greptime are enabled, only one can be used for Grafana datasource");
}
if (!vmEnabled && !greptimeEnabled) {
log.info("HertzBeat warehouse config not enabled");
return;
}
// Determine datasource type and URL
String datasourceType;
String datasourceUrl;
if (vmEnabled) {
datasourceType = "vm";
datasourceUrl = warehouseProperties.url();
} else {
datasourceType = "greptime";
datasourceUrl = greptimeProperties.httpEndpoint();
}
// Generate unique datasource name
String datasourceName = generateDatasourceName(datasourceType, datasourceUrl);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
// query if exist this datasource
String queryUrl = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + QUERY_DATASOURCE_API;
String queryUrl = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + QUERY_DATASOURCE_API + datasourceName;
HttpEntity<Void> entity = new HttpEntity<>(headers);
try {
ResponseEntity<String> response = restTemplate.exchange(queryUrl, HttpMethod.GET, entity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.info("{} datasource exist", DATASOURCE_NAME);
log.info("{} datasource already exists", datasourceName);
return;
}
} catch (HttpClientErrorException.NotFound notFound) {
String createUrl = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + CREATE_DATASOURCE_API;
String body = String.format(
"{\"name\":\"%s\",\"type\":\"%s\",\"access\":\"%s\",\"url\":\"%s\",\"basicAuth\":%s}",
DATASOURCE_NAME, DATASOURCE_TYPE, DATASOURCE_ACCESS, warehouseProperties.url(), false
);
HttpEntity<String> createEntity = new HttpEntity<>(body, headers);
try {
ResponseEntity<String> createResponse = restTemplate.postForEntity(createUrl, createEntity, String.class);
if (createResponse.getStatusCode().is2xxSuccessful()) {
log.info("Create datasource success");
}
} catch (Exception e) {
log.error("Create datasource error", e);
}
log.info("Datasource {} not found, creating new one", datasourceName);
} catch (Exception e) {
log.error("Query datasource error", e);
return;
}
// Create new datasource
createDatasource(token, datasourceName, datasourceUrl, datasourceType);
}
}
public void createDatasource(String token, String datasourceName, String datasourceUrl, String datasourceType) {
String createUrl = grafanaProperties.getPrefix() + grafanaProperties.getUrl() + CREATE_DATASOURCE_API;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(token);
DatasourceRequest datasourceRequest;
if ("greptime".equals(datasourceType)) {
datasourceUrl += "/v1/prometheus";
Map<String, Object> jsonData = new HashMap<>();
Map<String, Object> secureJsonData = new HashMap<>();
jsonData.put("httpHeaderName1", "x-greptime-db-name");
secureJsonData.put("httpHeaderValue1", greptimeProperties.database());
datasourceRequest = new DatasourceRequest(
datasourceName,
DATASOURCE_TYPE,
DATASOURCE_ACCESS,
datasourceUrl,
false,
jsonData,
secureJsonData
);
} else {
datasourceRequest = new DatasourceRequest(
datasourceName,
DATASOURCE_TYPE,
DATASOURCE_ACCESS,
datasourceUrl,
false,
null,
null
);
}
try {
String body = JsonUtil.toJson(datasourceRequest);
HttpEntity<String> createEntity = new HttpEntity<>(body, headers);
ResponseEntity<String> createResponse = restTemplate.postForEntity(createUrl, createEntity, String.class);
if (createResponse.getStatusCode().is2xxSuccessful()) {
log.info("Create datasource success");
}
} catch (HttpClientErrorException.Conflict conflict) {
log.info("Datasource already exists");
} catch (Exception e) {
log.error("Create datasource error", e);
}
}
/**
* Request object for creating a Grafana datasource.
* Fields are annotated with @JsonInclude to exclude null values from serialization.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class DatasourceRequest {
public String name;
public String type;
public String access;
public String url;
public boolean basicAuth;
public Map<String, Object> jsonData;
public Map<String, Object> secureJsonData;
public DatasourceRequest(String name, String type, String access, String url, boolean basicAuth, Map<String, Object> jsonData, Map<String, Object> secureJsonData) {
this.name = name;
this.type = type;
this.access = access;
this.url = url;
this.basicAuth = basicAuth;
this.jsonData = jsonData;
this.secureJsonData = secureJsonData;
}
}
/**
* Get the current active datasource name
* @return Current datasource name or null if none active
*/
public String getCurrentDatasourceName() {
boolean vmEnabled = warehouseProperties.enabled();
boolean greptimeEnabled = greptimeProperties.enabled();
if (vmEnabled) {
return generateDatasourceName("vm", warehouseProperties.url());
} else if (greptimeEnabled) {
return generateDatasourceName("greptime", greptimeProperties.httpEndpoint());
}
return null;
}
}

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