Compare commits

..
Author SHA1 Message Date
a-little-fool 5c98843efb correct home's new_committer_process 2025-02-21 17:52:39 +08:00
Nick Guo 398a40bba4 [improve] optimize kafka collect test (#3093) 2025-02-21 17:47:46 +08:00
yunfan24andtomsun28 bb14681621 [feature] supports alibabacloud sms. (#3084)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-21 13:55:30 +08:00
yunfan24 d3a080fbaf [improve] Improve and unify the search. (#3085) 2025-02-20 14:58:56 +08:00
tomsun28 ce6f9feac6 [webapp] update and fix alert ui when theme dark (#3082) 2025-02-16 23:25:04 +08:00
yunfan24 7d18118bbd [feature] SMS notification supports unisms. (#3077) 2025-02-15 22:40:03 +08:00
yunfan24andaias00 d5d2b6bc71 [doc] Update the SMS configuration document. (#3073)
Co-authored-by: aias00 <rokkki@163.com>
2025-02-13 20:32:48 +08:00
Logicandtomsun28 e11a4da2ac [improve](web-app): update monitor chart configuration and springboot GreptimeDB version (#3071)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-13 10:19:49 +08:00
58360442ca [Bugfix] fix when the monitor is modified, the status is erroneously changed (#3067)
Co-authored-by: aias00 <rokkki@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-13 00:14:37 +08:00
yunfan24andaias00 4d76c0abf4 [improve] Refactor SMS sending and replace Tencent Cloud SDK with HTTP API. (#3063)
Co-authored-by: aias00 <rokkki@163.com>
2025-02-11 19:40:36 +08:00
Jastandtomsun28 f761e18739 [Feature]Add Chinese check (#3066)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-11 10:36:55 +08:00
yunfan24 6ac073a54e [improve] Complete the missing labels in the i18n file. (#3065) 2025-02-11 10:16:36 +08:00
65a8d19c5d [feature] Support SSH Tunnel (#3060)
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: aias00 <rokkki@163.com>
2025-02-10 10:20:03 +08:00
tomsun28 060b4d875d [webapp] update ui theme (#3064)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-10 10:09:31 +08:00
kangliandLogic 6c88c1ba03 [API DOC] Change Swagger description (#3061)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-02-08 19:29:12 +08:00
tomsun28 8c58a35476 [bugfix] fix singleton not support remove, search id error, audio fetch 401 (#3062)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-08 18:36:42 +08:00
2ab05a5669 [feature] Add Japanese i18n (#3013)
Co-authored-by: aias00 <rokkki@163.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-02-08 09:19:13 +08:00
f3bc7f55c5 [feature] Add Sftp config for monitor (#3038)
Co-authored-by: yinyijun <yingey2011>
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
2025-02-07 23:57:06 +08:00
7effbc1f0d [Feature]Add zookeeper e2e code (#3030)
Co-authored-by: Calvin <naruse_shinji@163.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-02-07 23:22:10 +08:00
Logicandtomsun28 05e42182d9 [bugfix]style(alert-center): enhance 3D transformation and z-index layers (#3059)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: Logic <zqr10159@dromara.org>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 20:28:15 +08:00
yunfan24andtomsun28 5e69f4dfd0 [bugfix] Fix the issue where the monitoring status is not updated. (#3056)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 16:12:47 +08:00
tomsun28 aeb0083394 [improve] update theme ui color (#3057)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-02-07 15:47:19 +08:00
Logic 02f635bdd3 [improve](alert-center) enhance alert card animations and interactions (#3055) 2025-02-07 10:18:52 +08:00
7dbcaaeda8 [alert] support multi query expr threshold (#3054)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-06 21:33:56 +08:00
Logicandgithub-actions[bot] 4ddf0a9f3f [alert] support sse for alert center, no need to manually refresh the page, add slide-in animation (#3051)
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-02-06 16:13:01 +08:00
yunfan24andtomsun28 f1b1e3f7d6 [improve] Search ignores case sensitivity. (#3042)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-02-05 15:56:31 +08:00
yunfan24 921b8e5713 [update] upgrade actions upload-artifact to v4 (#3046) 2025-02-05 11:30:42 +08:00
yunfan24andtomsun28 4009525f81 [improve] Replaced hardcoded text with internationalized string. (#3035)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-30 22:01:32 +08:00
yunfan24 e0c7b4e111 [bugfix] Fixed the bug in the threshold rules search box. (#3034) 2025-01-30 21:43:51 +08:00
yunfan24andaias00 5e01482caa [feature] Add pagination and name-based search functionality in notification module (#2948)
Co-authored-by: aias00 <rokkki@163.com>
2025-01-27 14:12:55 +08:00
yunfan24 028e2fbcbe [bugfix] fix and enable some unit tests. (#3031) 2025-01-27 12:58:14 +08:00
tomsun28 d00cc6dcf4 [feature] periodic alert threshold (#3024) 2025-01-26 18:37:57 +08:00
4b7fdd6985 OnlineParserTest doesn't test anything (#3010)
Co-authored-by: aias00 <rokkki@163.com>
Co-authored-by: leo <1552443053@qq.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-26 16:13:21 +08:00
tomsun28andJast e45ec431aa [doc] update new contributor wall (#3025)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Jast <shenghang@apache.org>
2025-01-26 15:50:05 +08:00
Jastandtomsun28 8fa52fbca1 [Feature] Add API e2e code (#3029)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-26 11:23:52 +08:00
Jastandtomsun28 132fd00ab1 [Improve] update english doc (#3028)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-26 11:12:43 +08:00
12ba2faa19 [MINOR UPDATE] Use Encode to string when possible (Base64) (#3016)
Co-authored-by: aias00 <rokkki@163.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-24 18:44:14 +08:00
PJ Fanningandaias00 2f3abfbad4 [issue-2998] remove invalid check in isValidLabelValue (#3015)
Co-authored-by: aias00 <rokkki@163.com>
2025-01-24 18:38:43 +08:00
myangle1120andaias00 95739d9114 [bugfix] Modify mask issue (#3018)
Co-authored-by: aias00 <rokkki@163.com>
2025-01-24 09:27:43 +08:00
Logicandtomsun28 428ae1f7d5 [bugfix]Fix page not found (#3014)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-23 19:49:02 +08:00
PJ Fanningandaias00 6509b8bfc6 [MINOR UPDATE] always specify the char encoding in getBytes (#3011)
Co-authored-by: aias00 <rokkki@163.com>
2025-01-23 18:40:59 +08:00
yunfan24andtomsun28 783bd8de89 [improve] Freeze the 'Operate' column on the right side of the list. (#3009)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-23 15:52:10 +08:00
Logicandtomsun28 d1115b44d1 [feature](alert) implement drag-and-drop functionality for alert templates (#3005)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-22 23:43:50 +08:00
NikhilMurugesanandaias00 68e37d613a [bugfix] fix some unit tests that failed to run (#3007)
Co-authored-by: aias00 <rokkki@163.com>
2025-01-22 23:41:08 +08:00
jonasHanhan 7db0df11ba [feature] Add alerter_zh_TW.properties configuration to adapt to mult… (#3004) 2025-01-21 16:30:48 +08:00
wanhaoandLogic e244bdc5a7 [feature] Complete multiple languages (#3002)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-01-21 09:13:21 +08:00
tomsun28andLogic 0b7aa487f0 [webapp] bugfix edit monitor http query params error (#3001)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Logic <zqr10159@dromara.org>
2025-01-20 13:09:27 +08:00
PJ FanningandLogic 59d8764dfb [MINOR UPDATE] refactor base64 code to simplify the conversions (#2999)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-01-20 10:13:52 +08:00
yunfan24 8a38a34965 [feature] Support monitoring for StarRocks FE and StarRocks BE. (#2997) 2025-01-20 10:03:18 +08:00
tomsun28 fd7e1b0e05 [bugfix] fix wrong http user-agent content (#2996) 2025-01-20 01:16:50 +08:00
PJ Fanning 5441838888 [MINOR UPDATE] fix more instances of unclosed Http Responses (#2995) 2025-01-19 22:45:28 +08:00
PJ FanningandLogic 56b1dcb502 [MINOR UPDATE] close http response in PrometheusAutoCollectImpl (#2994)
Signed-off-by: PJ Fanning <pjfanning@users.noreply.github.com>
Co-authored-by: Logic <zqr10159@dromara.org>
2025-01-19 17:56:16 +08:00
PJ Fanningandtomsun28 efbaf7feac [MINOR UPDATE] close HttpResponse in HttpCollectImpl (#2993)
Signed-off-by: PJ Fanning <pjfanning@users.noreply.github.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-01-19 17:48:21 +08:00
Logic 7003b8a150 [Improve]Add Copy token button (#2992) 2025-01-19 14:54:01 +08:00
241 changed files with 45665 additions and 38070 deletions
+73
View File
@@ -2225,6 +2225,79 @@
"contributions": [
"code"
]
},
{
"login": "helei1030",
"name": "helei1030",
"avatar_url": "https://avatars.githubusercontent.com/u/11839080?v=4",
"profile": "https://github.com/helei1030",
"contributions": [
"doc"
]
},
{
"login": "pjfanning",
"name": "PJ Fanning",
"avatar_url": "https://avatars.githubusercontent.com/u/11783444?v=4",
"profile": "https://medium.com/@pjfanning",
"contributions": [
"code"
]
},
{
"login": "MonsterChenzhuo",
"name": "monster",
"avatar_url": "https://avatars.githubusercontent.com/u/60029759?v=4",
"profile": "https://github.com/MonsterChenzhuo",
"contributions": [
"code"
]
},
{
"login": "MasamiYui",
"name": "Sherlock Yin",
"avatar_url": "https://avatars.githubusercontent.com/u/22274133?v=4",
"profile": "https://github.com/MasamiYui",
"contributions": [
"code"
]
},
{
"login": "wanhao23",
"name": "wanhao",
"avatar_url": "https://avatars.githubusercontent.com/u/29560961?v=4",
"profile": "https://github.com/wanhao23",
"contributions": [
"code",
"doc"
]
},
{
"login": "jonasHanhan",
"name": "jonasHanhan",
"avatar_url": "https://avatars.githubusercontent.com/u/130035609?v=4",
"profile": "https://github.com/jonasHanhan",
"contributions": [
"code"
]
},
{
"login": "NikhilMurugesan",
"name": "NikhilMurugesan",
"avatar_url": "https://avatars.githubusercontent.com/u/49281792?v=4",
"profile": "https://github.com/NikhilMurugesan",
"contributions": [
"code"
]
},
{
"login": "myangle1120",
"name": "myangle1120",
"avatar_url": "https://avatars.githubusercontent.com/u/19237013?v=4",
"profile": "https://github.com/myangle1120",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
+1 -1
View File
@@ -73,7 +73,7 @@ jobs:
# upload application logs
- name: Upload logs & API test reports
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: always()
with:
name: hz-logs-${{ github.run_id }}
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
run: npx docusaurus-prince-pdf -u https://hertzbeat.apache.org/docs --output docs-en.pdf
- name: Upload results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: docs-cn-pdf
path: docs-cn.pdf
@@ -47,7 +47,7 @@ jobs:
retention-days: 1
- name: Upload results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: docs-en-pdf
path: docs-en.pdf
+12
View File
@@ -496,6 +496,18 @@ Thanks to these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+12
View File
@@ -495,6 +495,18 @@ Thanks these wonderful people, welcome to join us:
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Suvrat1629"><img src="https://avatars.githubusercontent.com/u/140749446?v=4?s=100" width="100px;" alt="Suvrat1629"/><br /><sub><b>Suvrat1629</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Suvrat1629" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://ghyghoo8.github.io/"><img src="https://avatars.githubusercontent.com/u/363129?v=4?s=100" width="100px;" alt="ghy"/><br /><sub><b>ghy</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=ghyghoo8" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/helei1030"><img src="https://avatars.githubusercontent.com/u/11839080?v=4?s=100" width="100px;" alt="helei1030"/><br /><sub><b>helei1030</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=helei1030" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@pjfanning"><img src="https://avatars.githubusercontent.com/u/11783444?v=4?s=100" width="100px;" alt="PJ Fanning"/><br /><sub><b>PJ Fanning</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=pjfanning" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MonsterChenzhuo"><img src="https://avatars.githubusercontent.com/u/60029759?v=4?s=100" width="100px;" alt="monster"/><br /><sub><b>monster</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MonsterChenzhuo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MasamiYui"><img src="https://avatars.githubusercontent.com/u/22274133?v=4?s=100" width="100px;" alt="Sherlock Yin"/><br /><sub><b>Sherlock Yin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=MasamiYui" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wanhao23"><img src="https://avatars.githubusercontent.com/u/29560961?v=4?s=100" width="100px;" alt="wanhao"/><br /><sub><b>wanhao</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=wanhao23" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jonasHanhan"><img src="https://avatars.githubusercontent.com/u/130035609?v=4?s=100" width="100px;" alt="jonasHanhan"/><br /><sub><b>jonasHanhan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=jonasHanhan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NikhilMurugesan"><img src="https://avatars.githubusercontent.com/u/49281792?v=4?s=100" width="100px;" alt="NikhilMurugesan"/><br /><sub><b>NikhilMurugesan</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=NikhilMurugesan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+5
View File
@@ -29,6 +29,11 @@
"type": 0,
"paramValue": 1000
},
{
"field": "ssl",
"type": 1,
"paramValue": false
},
{
"field": "username",
"type": 1
+5 -19
View File
@@ -40,6 +40,11 @@
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-plugin</artifactId>
</dependency>
<!-- warehouse -->
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-warehouse</artifactId>
</dependency>
<!-- spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -93,25 +98,6 @@
<version>${easy-poi.version}</version>
<scope>compile</scope>
</dependency>
<!-- sms -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp</groupId>
<artifactId>logging-interceptor</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.huaweicloud.sdk</groupId>
<artifactId>huaweicloud-sdk-smn</artifactId>
@@ -17,90 +17,172 @@
package org.apache.hertzbeat.alert.calculate;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
/**
* Periodic Alert Calculator
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class PeriodicAlertCalculator {
private static final String VALUE = "__value__";
private static final String TIMESTAMP = "__timestamp__";
private final DataSourceService dataSourceService;
private final JexlExpressionRunner expressionRunner;
private final Map<String, SingleAlert> notRecoveredAlertMap = new ConcurrentHashMap<>(16);
private final AlarmCommonReduce alarmCommonReduce;
/**
* The alarm in the process is triggered
* key - labels fingerprint
*/
private final Map<String, SingleAlert> pendingAlertMap;
/**
* The not recover alert
* key - labels fingerprint
*/
private final Map<String, SingleAlert> firingAlertMap;
public PeriodicAlertCalculator(DataSourceService dataSourceService, AlarmCommonReduce alarmCommonReduce) {
this.dataSourceService = dataSourceService;
this.alarmCommonReduce = alarmCommonReduce;
this.pendingAlertMap = new ConcurrentHashMap<>(8);
this.firingAlertMap = new ConcurrentHashMap<>(8);
}
public List<SingleAlert> calculate(AlertDefine rule) {
public void calculate(AlertDefine rule) {
if (!rule.isEnable() || StringUtils.isEmpty(rule.getExpr())) {
return Collections.emptyList();
log.error("Periodic rule {} is disabled or expression is empty", rule.getName());
return;
}
// todo: implement the following logic
long currentTimeMilli = System.currentTimeMillis();
try {
// Execute query
List<Map<String, Object>> queryResults = dataSourceService.query(
rule.getDatasource(),
rule.getExpr()
);
if (CollectionUtils.isEmpty(queryResults)) {
return Collections.emptyList();
// for prometheus is instant promql query, for db is sql query
// result: [{'value': 100, 'timestamp': 1343554, 'instance': 'node1'},{'value': 200, 'timestamp': 1343555, 'instance': 'node2'}]
// the return result should be matched with threshold
try {
List<Map<String, Object>> results = dataSourceService.calculate(
rule.getDatasource(),
rule.getExpr()
);
// if no match the expr threshold, the results item map {'value': null} should be null and others field keep
// if results has multi list, should trigger multi alert
if (CollectionUtils.isEmpty(results)) {
return;
}
for (Map<String, Object> result : results) {
Map<String, String> fingerPrints = new HashMap<>(8);
// here use the alert name as finger, not care the alert name may be changed
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
fingerPrints.putAll(rule.getLabels());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
&& !TIMESTAMP.equals(entry.getKey())) {
fingerPrints.put(entry.getKey(), entry.getValue().toString());
}
}
if (result.get(VALUE) == null) {
// recovery the alert
handleRecoveredAlert(fingerPrints);
continue;
}
Map<String, Object> fieldValueMap = new HashMap<>(8);
fieldValueMap.putAll(rule.getLabels());
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
for (Map.Entry<String, Object> entry : result.entrySet()) {
if (entry.getValue() != null) {
fieldValueMap.put(entry.getKey(), entry.getValue());
}
}
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, rule);
}
} catch (Exception ignored) {
// ignore the query exception eg: no result, timeout, etc
return;
}
// Execute expression calculation on query results
List<SingleAlert> newAlerts = queryResults.stream()
.filter(result -> execAlertExpression(result, rule.getExpr()))
.map(result -> buildAlert(rule, result))
.collect(Collectors.toList());
// Handle recovery notification
if (newAlerts.isEmpty()) {
return handleAlertRecover(rule);
}
return newAlerts;
} catch (Exception e) {
log.error("Calculate periodic rule {} failed: {}", rule.getName(), e.getMessage());
return Collections.emptyList();
}
}
private boolean execAlertExpression(Map<String, Object> result, String expr) {
return false;
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
Map<String, Object> fieldValueMap, AlertDefine define) {
String fingerprint = calculateFingerprint(fingerPrints);
SingleAlert existingAlert = pendingAlertMap.get(fingerprint);
Map<String, String> labels = new HashMap<>(8);
fieldValueMap.putAll(define.getLabels());
labels.putAll(fingerPrints);
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
if (existingAlert == null) {
// First time triggering alert, create new alert and set to pending status
SingleAlert newAlert = SingleAlert.builder()
.labels(labels)
// todo render var content in annotations
.annotations(define.getAnnotations())
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
.status(CommonConstants.ALERT_STATUS_PENDING)
.triggerTimes(1)
.startAt(currentTimeMilli)
.activeAt(currentTimeMilli)
.build();
// If required trigger times is 1, set to firing status directly
if (requiredTimes <= 1) {
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
firingAlertMap.put(fingerprint, newAlert);
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
} else {
// Otherwise put into pending queue first
pendingAlertMap.put(fingerprint, newAlert);
}
} else {
// Update existing alert
existingAlert.setTriggerTimes(existingAlert.getTriggerTimes() + 1);
existingAlert.setActiveAt(currentTimeMilli);
// Check if required trigger times reached
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
// Reached trigger times threshold, change to firing status
pendingAlertMap.remove(fingerprint);
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
firingAlertMap.put(fingerprint, existingAlert);
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
}
}
}
private SingleAlert buildAlert(AlertDefine rule, Map<String, Object> metrics) {
return SingleAlert.builder()
.labels(rule.getLabels())
.annotations(rule.getAnnotations())
.triggerTimes(1)
.startAt(System.currentTimeMillis())
.activeAt(System.currentTimeMillis())
.build();
}
private List<SingleAlert> handleAlertRecover(AlertDefine rule) {
SingleAlert firingAlert = notRecoveredAlertMap.remove(rule.getId().toString());
private void handleRecoveredAlert(Map<String, String> fingerprints) {
String fingerprint = calculateFingerprint(fingerprints);
SingleAlert firingAlert = firingAlertMap.remove(fingerprint);
if (firingAlert != null) {
return Collections.singletonList(buildResolvedAlert(rule, firingAlert));
// todo consider multi times to tig for resolved alert
firingAlert.setTriggerTimes(1);
firingAlert.setEndAt(System.currentTimeMillis());
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
}
return Collections.emptyList();
pendingAlertMap.remove(fingerprint);
}
private SingleAlert buildResolvedAlert(AlertDefine rule, SingleAlert firingAlert) {
return null;
private String calculateFingerprint(Map<String, String> fingerPrints) {
List<String> keyList = fingerPrints.keySet().stream().filter(Objects::nonNull).sorted().toList();
List<String> valueList = fingerPrints.values().stream().filter(Objects::nonNull).sorted().toList();
return Arrays.hashCode(keyList.toArray(new String[0])) + "-"
+ Arrays.hashCode(valueList.toArray(new String[0]));
}
}
@@ -17,13 +17,79 @@
package org.apache.hertzbeat.alert.calculate;
import static org.apache.hertzbeat.common.constants.CommonConstants.ALERT_THRESHOLD_TYPE_PERIODIC;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
/**
* period alert rule scheduler
* Periodic Alert Rule Scheduler
*/
@Slf4j
public class PeriodicAlertRuleScheduler {
// todo implement the following logic
@Component
public class PeriodicAlertRuleScheduler implements CommandLineRunner {
private final PeriodicAlertCalculator calculator;
private final AlertDefineDao alertDefineDao;
private final ScheduledExecutorService scheduledExecutor;
private final Map<Long, ScheduledFuture<?>> scheduledFutures;
public PeriodicAlertRuleScheduler(PeriodicAlertCalculator calculator, AlertDefineDao alertDefineDao) {
this.calculator = calculator;
this.alertDefineDao = alertDefineDao;
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Scheduled periodic alert threshold has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("periodic-alert-threshold-worker-%d")
.build();
this.scheduledExecutor = Executors.newScheduledThreadPool(10, threadFactory);
this.scheduledFutures = new ConcurrentHashMap<>();
}
public void cancelSchedule(Long ruleId) {
if (ruleId == null) {
return;
}
ScheduledFuture<?> future = scheduledFutures.get(ruleId);
if (future != null) {
future.cancel(true);
scheduledFutures.remove(ruleId);
}
}
public void updateSchedule(AlertDefine rule) {
if (rule == null || rule.getId() == null) {
log.error("Alert rule is null or rule id is null.");
return;
}
cancelSchedule(rule.getId());
if (rule.getType().equals(ALERT_THRESHOLD_TYPE_PERIODIC)) {
ScheduledFuture<?> future = scheduledExecutor.scheduleAtFixedRate(() -> {
calculator.calculate(rule);
}, 0, rule.getPeriod(), java.util.concurrent.TimeUnit.SECONDS);
scheduledFutures.put(rule.getId(), future);
}
}
@Override
public void run(String... args) throws Exception {
log.info("Starting periodic alert rule scheduler...");
List<AlertDefine> periodicRules = alertDefineDao.findAlertDefinesByTypeAndEnableTrue(ALERT_THRESHOLD_TYPE_PERIODIC);
for (AlertDefine rule : periodicRules) {
updateSchedule(rule);
}
}
}
@@ -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.alert.config;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* SSE manager for alert
*/
@Component
public class AlertSseManager {
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
public SseEmitter createEmitter(Long clientId) {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
emitter.onCompletion(() -> removeEmitter(clientId));
emitter.onTimeout(() -> removeEmitter(clientId));
emitters.put(clientId, emitter);
return emitter;
}
@Async
public void broadcast(String data) {
emitters.forEach((clientId, emitter) -> {
try {
emitter.send(SseEmitter.event()
.id(String.valueOf(System.currentTimeMillis()))
.name("ALERT_EVENT")
.data(data));
} catch (IOException e) {
emitter.complete();
removeEmitter(clientId);
}
});
}
private void removeEmitter(Long clientId) {
emitters.remove(clientId);
}
}
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
import lombok.Data;
/**
* Alibaba Cloud SMS configuration properties
*/
@Data
public class AlibabaSmsProperties {
/**
* Alibaba Cloud access key id
*/
private String accessKeyId;
/**
* Alibaba Cloud access key secret
*/
private String accessKeySecret;
/**
* SMS signature
*/
private String signName;
/**
* SMS template Code
*/
private String templateCode;
}
@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* SMS configuration
*/
@Data
@Component
@ConfigurationProperties(prefix = "alerter.sms")
public class SmsConfig {
/**
* whether to enable SMS, default is false
*/
private boolean enable = false;
/**
* sms service provider
*/
private String type;
/**
* Tencent cloud SMS configuration
*/
private TencentSmsProperties tencent;
/**
* Aliyun SMS configuration
*/
private AlibabaSmsProperties alibaba;
/**
* UniSMS configuration
*/
private UniSmsProperties unisms;
}
@@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
import lombok.Data;
/**
* Tencent Cloud SMS properties
*/
@Data
public class TencentSmsProperties {
/**
* Tencent cloud account secret id
*/
private String secretId;
/**
* Tencent cloud account secret key
*/
private String secretKey;
/**
* SMS app id
*/
private String appId;
/**
* SMS signature
*/
private String signName;
/**
* SMS template ID
*/
private String templateId;
}
@@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.config;
import lombok.Data;
/**
* UniSMS properties
*/
@Data
public class UniSmsProperties {
/**
* UniSMS access key id
*/
private String accessKeyId;
/**
* UniSMS access key secret, required for HMAC mode
*/
private String accessKeySecret;
/**
* SMS signature
*/
private String signature;
/**
* SMS template ID
*/
private String templateId;
/**
* Authentication mode: simple or hmac, default is simple
*/
private String authMode = "simple";
}
@@ -38,9 +38,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Alarm Silence management API
* Alarm Inhibit management API
*/
@Tag(name = "Alert Silence API")
@Tag(name = "Alert Inhibit API")
@RestController
@RequestMapping(path = "/api/alert/inhibit", produces = {APPLICATION_JSON_VALUE})
public class AlertInhibitController {
@@ -49,7 +49,7 @@ public class AlertInhibitController {
private AlertInhibitService alertInhibitService;
@PostMapping
@Operation(summary = "New Alarm Silence", description = "Added an alarm Silence")
@Operation(summary = "New Alarm Inhibit", description = "Added an alarm Inhibit")
public ResponseEntity<Message<Void>> addNewAlertInhibit(@Valid @RequestBody AlertInhibit alertInhibit) {
alertInhibitService.validate(alertInhibit, false);
alertInhibitService.addAlertInhibit(alertInhibit);
@@ -57,7 +57,7 @@ public class AlertInhibitController {
}
@PutMapping
@Operation(summary = "Modifying an Alarm Silence", description = "Modify an existing alarm Silence")
@Operation(summary = "Modifying an Alarm Inhibit", description = "Modify an existing alarm Inhibit")
public ResponseEntity<Message<Void>> modifyAlertInhibit(@Valid @RequestBody AlertInhibit alertInhibit) {
alertInhibitService.validate(alertInhibit, true);
alertInhibitService.modifyAlertInhibit(alertInhibit);
@@ -65,10 +65,10 @@ public class AlertInhibitController {
}
@GetMapping(path = "/{id}")
@Operation(summary = "Querying Alarm Silence",
description = "You can obtain alarm Silence information based on the alarm Silence ID")
@Operation(summary = "Querying Alarm Inhibit",
description = "You can obtain alarm Inhibit information based on the alarm Inhibit ID")
public ResponseEntity<Message<AlertInhibit>> getAlertInhibit(
@Parameter(description = "Alarm Silence ID", example = "6565463543") @PathVariable("id") long id) {
@Parameter(description = "Alarm Inhibit ID", example = "6565463543") @PathVariable("id") long id) {
AlertInhibit alertInhibit = alertInhibitService.getAlertInhibit(id);
return Objects.isNull(alertInhibit)
@@ -36,9 +36,9 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* Silence the batch API for alarms
* Inhibit the batch API for alarms
*/
@Tag(name = "Alert Silence Batch API")
@Tag(name = "Alert Inhibit Batch API")
@RestController
@RequestMapping(path = "/api/alert/inhibits", produces = {APPLICATION_JSON_VALUE})
public class AlertInhibitsController {
@@ -50,7 +50,7 @@ public class AlertInhibitsController {
@Operation(summary = "Query the alarm inhibit list",
description = "You can obtain the list of alarm inhibit by querying filter items")
public ResponseEntity<Message<Page<AlertInhibit>>> getAlertInhibits(
@Parameter(description = "Alarm Silence ID", example = "6565463543") @RequestParam(required = false) List<Long> ids,
@Parameter(description = "Alarm Inhibit ID", example = "6565463543") @RequestParam(required = false) List<Long> ids,
@Parameter(description = "Search Name", example = "x") @RequestParam(required = false) String search,
@Parameter(description = "Sort field, default id", example = "id") @RequestParam(defaultValue = "id") String sort,
@Parameter(description = "Sort mode: asc: ascending, desc: descending", example = "desc") @RequestParam(defaultValue = "desc") String order,
@@ -64,7 +64,7 @@ public class AlertInhibitsController {
@Operation(summary = "Delete alarm inhibit in batches",
description = "Delete alarm inhibit in batches based on the alarm inhibit ID list")
public ResponseEntity<Message<Void>> deleteAlertDefines(
@Parameter(description = "Alarm Silence IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids
@Parameter(description = "Alarm Inhibit IDs", example = "6565463543") @RequestParam(required = false) List<Long> ids
) {
if (ids != null && !ids.isEmpty()) {
alertInhibitService.deleteAlertInhibits(new HashSet<>(ids));
@@ -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.controller;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
import org.apache.hertzbeat.alert.config.AlertSseManager;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* SSE controller for alert
*/
@RestController
@RequestMapping(path = "/api/alert/sse", produces = {TEXT_EVENT_STREAM_VALUE})
public class AlertSseController {
private final AlertSseManager emitterManager;
public AlertSseController(AlertSseManager emitterManager) {
this.emitterManager = emitterManager;
}
@GetMapping(path = "/subscribe")
public SseEmitter subscribe() {
Long clientId = SnowFlakeIdGenerator.generateId();
return emitterManager.createEmitter(clientId);
}
}
@@ -31,6 +31,7 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.alert.service.NoticeConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -82,9 +83,18 @@ public class NoticeConfigController {
@GetMapping(path = "/receivers")
@Operation(summary = "Get a list of message notification recipients based on query filter items",
description = "Get a list of message notification recipients based on query filter items")
public ResponseEntity<Message<List<NoticeReceiver>>> getReceivers(
@Parameter(description = "en: Recipient name,support fuzzy query", example = "tom") @RequestParam(required = false) final String name) {
return ResponseEntity.ok(Message.success(noticeConfigService.getNoticeReceivers(name)));
public ResponseEntity<Message<Page<NoticeReceiver>>> getReceivers(
@Parameter(description = "en: Recipient name,support fuzzy query", example = "tom") @RequestParam(required = false) final String name,
@Parameter(description = "en: List current page", example = "0") @RequestParam(defaultValue = "0") final int pageIndex,
@Parameter(description = "en: Number of list pages", example = "8") @RequestParam(defaultValue = "8") final int pageSize) {
return ResponseEntity.ok(Message.success(noticeConfigService.getNoticeReceivers(name, pageIndex, pageSize)));
}
@GetMapping(path = "/receivers/all")
@Operation(summary = "Get a list of all message notification recipients",
description = "Get a list of all message notification recipients")
public ResponseEntity<Message<List<NoticeReceiver>>> getAllReceivers() {
return ResponseEntity.ok(Message.success(noticeConfigService.getAllNoticeReceivers()));
}
@GetMapping(path = "/receiver/{id}")
@@ -129,9 +139,11 @@ public class NoticeConfigController {
@GetMapping(path = "/rules")
@Operation(summary = "Get a list of message notification policies based on query filter items",
description = "Get a list of message notification policies based on query filter items")
public ResponseEntity<Message<List<NoticeRule>>> getRules(
@Parameter(description = "en: Recipient name", example = "rule1") @RequestParam(required = false) final String name) {
return ResponseEntity.ok(Message.success(noticeConfigService.getNoticeRules(name)));
public ResponseEntity<Message<Page<NoticeRule>>> getRules(
@Parameter(description = "en: Recipient name", example = "rule1") @RequestParam(required = false) final String name,
@Parameter(description = "en: List current page", example = "0") @RequestParam(defaultValue = "0") final int pageIndex,
@Parameter(description = "en: Number of list pages", example = "8") @RequestParam(defaultValue = "8") final int pageSize) {
return ResponseEntity.ok(Message.success(noticeConfigService.getNoticeRules(name, pageIndex, pageSize)));
}
@GetMapping(path = "/rule/{id}")
@@ -176,12 +188,22 @@ public class NoticeConfigController {
@GetMapping(path = "/templates")
@Operation(summary = "Get a list of message notification templates based on query filter items",
description = "Get a list of message notification templates based on query filter items")
public ResponseEntity<Message<List<NoticeTemplate>>> getTemplates(
@Parameter(description = "Template name,support fuzzy query", example = "rule1") @RequestParam(required = false) final String name) {
List<NoticeTemplate> templatePage = noticeConfigService.getNoticeTemplates(name);
public ResponseEntity<Message<Page<NoticeTemplate>>> getTemplates(
@Parameter(description = "Template name,support fuzzy query", example = "rule1") @RequestParam(required = false) final String name,
@Parameter(description = "Whether it is a preset template", example = "true") @RequestParam(defaultValue = "true") final boolean preset,
@Parameter(description = "List current page", example = "0") @RequestParam(defaultValue = "0") final int pageIndex,
@Parameter(description = "Number of list pages", example = "8") @RequestParam(defaultValue = "8") final int pageSize) {
Page<NoticeTemplate> templatePage = noticeConfigService.getNoticeTemplates(name, preset, pageIndex, pageSize);
return ResponseEntity.ok(Message.success(templatePage));
}
@GetMapping(path = "/templates/all")
@Operation(summary = "Get a list of all message notification templates",
description = "Get a list of all message notification templates")
public ResponseEntity<Message<List<NoticeTemplate>>> getAllTemplates() {
return ResponseEntity.ok(Message.success(noticeConfigService.getAllNoticeTemplates()));
}
@GetMapping(path = "/template/{id}")
@Operation(summary = "Get the notification template information based on the template ID",
description = "Get the notification template information based on the template ID")
@@ -23,11 +23,13 @@ import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.config.AlertSseManager;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.alert.service.NoticeConfigService;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.plugin.PostAlertPlugin;
import org.apache.hertzbeat.plugin.Plugin;
import org.apache.hertzbeat.plugin.runner.PluginRunner;
@@ -45,16 +47,18 @@ public class AlertNoticeDispatch {
private final AlertStoreHandler alertStoreHandler;
private final Map<Byte, AlertNotifyHandler> alertNotifyHandlerMap;
private final PluginRunner pluginRunner;
private final AlertSseManager emitterManager;
public AlertNoticeDispatch(AlerterWorkerPool workerPool,
NoticeConfigService noticeConfigService,
AlertStoreHandler alertStoreHandler,
List<AlertNotifyHandler> alertNotifyHandlerList, PluginRunner pluginRunner) {
List<AlertNotifyHandler> alertNotifyHandlerList, PluginRunner pluginRunner, AlertSseManager emitterManager) {
this.workerPool = workerPool;
this.noticeConfigService = noticeConfigService;
this.alertStoreHandler = alertStoreHandler;
this.pluginRunner = pluginRunner;
alertNotifyHandlerMap = Maps.newHashMapWithExpectedSize(alertNotifyHandlerList.size());
this.emitterManager = emitterManager;
alertNotifyHandlerList.forEach(r -> alertNotifyHandlerMap.put(r.type(), r));
}
@@ -76,7 +80,7 @@ public class AlertNoticeDispatch {
if (noticeTemplate == null) {
noticeTemplate = noticeConfigService.getDefaultNoticeTemplateByType(alertNotifyHandler.type());
}
if (noticeTemplate == null) {
if (noticeTemplate == null && alertNotifyHandler.type() != 0) {
log.error("alert does not have mapping default notice template. type: {}.", alertNotifyHandler.type());
throw new NullPointerException(alertNotifyHandler.type() + " does not have mapping default notice template");
}
@@ -104,27 +108,27 @@ public class AlertNoticeDispatch {
public void dispatchAlarm(GroupAlert groupAlert) {
if (groupAlert != null) {
// Determining alarm type storage
alertStoreHandler.store(groupAlert);
GroupAlert storedGroupAlert = alertStoreHandler.store(groupAlert);
// Notice distribution
sendNotify(groupAlert);
sendNotify(storedGroupAlert);
// Execute the plugin if enable (Compatible with old version plugins, will be removed in later versions)
pluginRunner.pluginExecute(Plugin.class, plugin -> plugin.alert(groupAlert));
pluginRunner.pluginExecute(Plugin.class, plugin -> plugin.alert(storedGroupAlert));
// Execute the plugin if enable with params
pluginRunner.pluginExecute(PostAlertPlugin.class, (afterAlertPlugin, pluginContext) -> afterAlertPlugin.execute(groupAlert, pluginContext));
pluginRunner.pluginExecute(PostAlertPlugin.class, (afterAlertPlugin, pluginContext) -> afterAlertPlugin.execute(storedGroupAlert, pluginContext));
// Send alert to the sse client
emitterManager.broadcast(JsonUtil.toJson(storedGroupAlert));
}
}
private void sendNotify(GroupAlert alert) {
matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> {
workerPool.executeNotify(() -> rule.getReceiverId()
.forEach(receiverId -> {
try {
sendNoticeMsg(getOneReceiverById(receiverId),
getOneTemplateById(rule.getTemplateId()), alert);
} catch (AlertNoticeException e) {
log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
}
}));
}));
matchNoticeRulesByAlert(alert).ifPresent(noticeRules -> noticeRules.forEach(rule -> workerPool.executeNotify(() -> rule.getReceiverId()
.forEach(receiverId -> {
try {
sendNoticeMsg(getOneReceiverById(receiverId),
getOneTemplateById(rule.getTemplateId()), alert);
} catch (AlertNoticeException e) {
log.warn("DispatchTask sendNoticeMsg error, message: {}", e.getMessage());
}
}))));
}
}
@@ -28,7 +28,9 @@ public interface AlertStoreHandler {
* Persistent alarm records
* It is necessary to associate and assign values
* to the alert tag information tags while persisting.
*
* @param alert alarm information
* @return groupAlert
*/
void store(GroupAlert alert);
GroupAlert store(GroupAlert alert);
}
@@ -1,56 +0,0 @@
/*
* 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.notice.impl;
import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* Send alarm information through Alibaba Cloud SMS
*/
@Component
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty("common.sms.aliyun.app-id")
final class AliYunAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// SMS notification
try {
// todo send aliyun sms
} catch (Exception e) {
throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage());
}
}
@Override
public byte type() {
return 0;
}
}
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.alert.notice.impl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -46,44 +47,53 @@ final class DbAlertStoreHandlerImpl implements AlertStoreHandler {
private final SingleAlertDao singleAlertDao;
@Override
public void store(GroupAlert groupAlert) {
public GroupAlert store(GroupAlert groupAlert) {
if (groupAlert == null || groupAlert.getAlerts() == null || groupAlert.getAlerts().isEmpty()) {
log.error("The Group Alerts is empty, ignore store");
return;
return groupAlert;
}
// 1. Find existing alert group
GroupAlert existGroupAlert = groupAlertDao.findByGroupKey(groupAlert.getGroupKey());
// 2. Process individual alerts
Set<String> alertFingerprints = new HashSet<>(8);
groupAlert.getAlerts().forEach(singleAlert -> {
List<SingleAlert> originalAlerts = groupAlert.getAlerts();
List<SingleAlert> newAlerts = new ArrayList<>();
for (SingleAlert singleAlert : originalAlerts) {
SingleAlert existAlert = singleAlertDao.findByFingerprint(singleAlert.getFingerprint());
if (existAlert != null) {
// Update existing alert
// Update the existing alert with the ID and creation time from the database
singleAlert.setId(existAlert.getId());
singleAlert.setGmtCreate(existAlert.getGmtCreate());
// Status transition logic
if (CommonConstants.ALERT_STATUS_FIRING.equals(singleAlert.getStatus())) {
// If the alert is firing and the existing alert is not resolved, update the start time and trigger times
if (!CommonConstants.ALERT_STATUS_RESOLVED.equals(existAlert.getStatus())) {
singleAlert.setStartAt(existAlert.getStartAt());
int triggerTimes = Optional.ofNullable(existAlert.getTriggerTimes()).orElse(1) + Optional.ofNullable(singleAlert.getTriggerTimes()).orElse(1);
int triggerTimes = Optional.ofNullable(existAlert.getTriggerTimes()).orElse(1)
+ Optional.ofNullable(singleAlert.getTriggerTimes()).orElse(1);
singleAlert.setTriggerTimes(triggerTimes);
}
}
} else if (CommonConstants.ALERT_STATUS_RESOLVED.equals(singleAlert.getStatus())) {
// Transition to resolved state
// If the alert is resolved, set the end time (if not already set) and copy other fields from the existing alert
if (singleAlert.getEndAt() == null) {
singleAlert.setEndAt(System.currentTimeMillis());
singleAlert.setEndAt(System.currentTimeMillis());
}
singleAlert.setStartAt(existAlert.getStartAt());
singleAlert.setActiveAt(existAlert.getActiveAt());
singleAlert.setTriggerTimes(existAlert.getTriggerTimes());
}
}
alertFingerprints.add(singleAlert.getFingerprint());
singleAlertDao.save(singleAlert);
});
SingleAlert savedSingleAlert = singleAlertDao.save(singleAlert);
newAlerts.add(savedSingleAlert);
alertFingerprints.add(savedSingleAlert.getFingerprint());
}
groupAlert.setAlerts(newAlerts);
// 3. Process resolved alerts
if (existGroupAlert != null) {
List<String> existFingerprints = existGroupAlert.getAlertFingerprints();
@@ -120,6 +130,8 @@ final class DbAlertStoreHandlerImpl implements AlertStoreHandler {
// 4. Save alert group
groupAlert.setAlertFingerprints(alertFingerprints.stream().toList());
groupAlertDao.save(groupAlert);
GroupAlert savedGroupAlert = groupAlertDao.save(groupAlert);
savedGroupAlert.setAlerts(groupAlert.getAlerts());
return savedGroupAlert;
}
}
@@ -21,12 +21,12 @@ import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.alert.service.TencentSmsClient;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.service.SmsClientFactory;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
@@ -35,35 +35,22 @@ import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
@ConditionalOnProperty("common.sms.tencent.app-id")
@Deprecated
final class SmsAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
private final TencentSmsClient tencentSmsClient;
private final SmsClientFactory smsFactory;
private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// SMS notification todo use the rest api not sdk
try {
String instance = null;
String priority = null;
String content = null;
if (alert.getCommonLabels() != null) {
instance = alert.getCommonLabels().get("instance");
priority = alert.getCommonLabels().get("priority");
content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
SmsClient smsClient = smsFactory.getSmsClient();
if (smsClient == null) {
throw new AlertNoticeException("No SMS Service available, please check the configuration");
}
String[] params = new String[3];
params[0] = instance == null ? alert.getGroupKey() : instance;
params[1] = priority == null ? "unknown" : priority;
params[2] = content;
tencentSmsClient.sendMessage(params, new String[]{receiver.getPhone()});
if (!smsClient.checkConfig()) {
throw new AlertNoticeException(smsClient.getType() + " SMS Service configuration is invalid, please check the configuration");
}
smsClient.sendMessage(receiver, noticeTemplate, alert);
} catch (Exception e) {
throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage());
}
@@ -80,7 +80,6 @@ final class WeChatAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
JsonObject textContent = new JsonObject();
// Here you can construct the message content based on the NoticeTemplate and Alert information
// String alertMessage = String.format("警告:%s\n详情:%s", alert.getAlertDefineId(), alert.getContent());
String alertMessage = "Alert message content";
textContent.addProperty("content", alertMessage);
messageContent.add("text", textContent);
@@ -21,8 +21,8 @@ package org.apache.hertzbeat.alert.reduce;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -267,7 +267,7 @@ public class AlarmGroupReduce {
.groupLabels(alert.getLabels())
.commonLabels(alert.getLabels())
.commonAnnotations(alert.getAnnotations())
.alerts(Collections.singletonList(alert))
.alerts(new LinkedList<>(List.of(alert)))
.status(alert.getStatus())
.build();
@@ -26,10 +26,10 @@ import java.util.Map;
public interface DataSourceService {
/**
* execute query
* execute query expr calculate
* @param datasource datasource
* @param query query
* @param expr query expr
* @return result
*/
List<Map<String, Object>> query(String datasource, String query);
List<Map<String, Object>> calculate(String datasource, String expr);
}
@@ -23,6 +23,7 @@ import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.springframework.data.domain.Page;
/**
* Message notification configuration interface
@@ -32,23 +33,32 @@ public interface NoticeConfigService {
/**
* Dynamic conditional query
* @param name Recipient name,support fuzzy query
* @param pageIndex Page number
* @param pageSize Number of records per page
* @return Search result
*/
List<NoticeReceiver> getNoticeReceivers(String name);
Page<NoticeReceiver> getNoticeReceivers(String name, int pageIndex, int pageSize);
/**
* Dynamic conditional query
* @param name Template name,support fuzzy query
* @param preset Whether it is a system preset template
* true: System preset template
* false: Custom template
* @param pageIndex Page number
* @param pageSize Number of records per page
* @return Search result
*/
List<NoticeTemplate> getNoticeTemplates(String name);
Page<NoticeTemplate> getNoticeTemplates(String name, boolean preset, int pageIndex, int pageSize);
/**
* Dynamic conditional query
* @param name Recipient name
* @param name Recipient name ,support fuzzy query
* @param pageIndex Page number
* @param pageSize Number of records per page
* @return Search result
*/
List<NoticeRule> getNoticeRules(String name);
Page<NoticeRule> getNoticeRules(String name, int pageIndex, int pageSize);
/**
* Add a notification recipient
@@ -154,4 +164,15 @@ public interface NoticeConfigService {
*/
boolean sendTestMsg(NoticeReceiver noticeReceiver);
/**
* Query all notification recipients
* @return Recipient List
*/
List<NoticeReceiver> getAllNoticeReceivers();
/**
* Query all notification policies
* @return Notification Policy List
*/
List<NoticeTemplate> getAllNoticeTemplates();
}
@@ -0,0 +1,42 @@
/*
* 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.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
/**
* SMS client interface
*/
public interface SmsClient {
/**
* send SMS
*/
void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert);
/**
* get SMS provider type
*/
String getType();
/**
* check SMS configuration, return true if the configuration is correct
*/
boolean checkConfig();
}
@@ -0,0 +1,141 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.UniSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.AlibabaSmsClientImpl;
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.support.event.SmsConfigChangeEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA;
import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT;
import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS;
/**
* SMS client factory
*/
@Slf4j
@Component
public class SmsClientFactory {
private static final String TYPE = GeneralConfigTypeEnum.sms.name();
private final GeneralConfigDao generalConfigDao;
private final ObjectMapper objectMapper;
private final SmsConfig yamlSmsConfig;
private volatile SmsClient currentSmsClient;
public SmsClientFactory(GeneralConfigDao generalConfigDao,
ObjectMapper objectMapper,
SmsConfig yamlSmsConfig) {
this.generalConfigDao = generalConfigDao;
this.objectMapper = objectMapper;
this.yamlSmsConfig = yamlSmsConfig;
}
/**
* SMS configuration change event listener
*/
@EventListener(SmsConfigChangeEvent.class)
public void onSmsConfigChange(SmsConfigChangeEvent event) {
log.info("[SmsClientFactory] SMS configuration change event received");
synchronized (this) {
currentSmsClient = null;
}
}
public SmsClient getSmsClient() {
if (currentSmsClient != null) {
return currentSmsClient;
}
synchronized (this) {
if (currentSmsClient != null) {
return currentSmsClient;
}
loadConfig();
return currentSmsClient;
}
}
private void loadConfig() {
try {
// 1. try to load database configuration
SmsConfig dbConfig = loadDatabaseConfig();
if (dbConfig != null && !dbConfig.getType().isBlank() && dbConfig.isEnable()) {
createSmsClient(dbConfig);
if (currentSmsClient != null) {
log.info("[SmsClientFactory] Using database SMS configuration, provider: {}", dbConfig.getType());
return;
}
}
// 2. try to load YAML configuration
if (yamlSmsConfig != null && !yamlSmsConfig.getType().isBlank() && yamlSmsConfig.isEnable()) {
createSmsClient(yamlSmsConfig);
if (currentSmsClient != null) {
log.info("[SmsClientFactory] Using YAML SMS configuration, provider: {}", yamlSmsConfig.getType());
return;
}
}
log.warn("[SmsClientFactory] No valid SMS configuration found");
} catch (Exception e) {
log.error("[SmsClientFactory] Failed to load SMS configuration", e);
currentSmsClient = null;
}
}
private SmsConfig loadDatabaseConfig() {
try {
GeneralConfig config = generalConfigDao.findByType(TYPE);
if (config != null && config.getContent() != null) {
return objectMapper.readValue(config.getContent(), SmsConfig.class);
}
} catch (Exception e) {
log.error("[SmsClientFactory] Failed to load database configuration", e);
}
return null;
}
private void createSmsClient(SmsConfig smsConfig) {
switch (smsConfig.getType()) {
case TENCENT:
currentSmsClient = new TencentSmsClientImpl(smsConfig.getTencent());
break;
case UNISMS:
currentSmsClient = new UniSmsClientImpl(smsConfig.getUnisms());
break;
case ALIBABA:
currentSmsClient = new AlibabaSmsClientImpl(smsConfig.getAlibaba());
break;
default:
log.warn("[SmsClientFactory] Unsupported SMS provider type: {}", smsConfig.getType());
break;
}
}
}
@@ -1,101 +0,0 @@
/*
* 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.tencentcloudapi.common.Credential;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.config.CommonProperties;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* sms service client for tencent cloud
*/
@Component
@ConditionalOnProperty("common.sms.tencent.app-id")
@Slf4j
public class TencentSmsClient {
private static final String RESPONSE_OK = "Ok";
private static final String REGION = "ap-guangzhou";
private SmsClient smsClient;
private String appId;
private String signName;
private String templateId;
public TencentSmsClient(CommonProperties properties) {
if (properties == null || properties.getSms() == null || properties.getSms().getTencent() == null) {
log.error("init error, please config TencentSmsClient props in application.yml");
throw new IllegalArgumentException("please config TencentSmsClient props");
}
initSmsClient(properties.getSms().getTencent());
}
private void initSmsClient(CommonProperties.TencentSmsProperties tencent) {
this.appId = tencent.getAppId();
this.signName = tencent.getSignName();
this.templateId = tencent.getTemplateId();
Credential cred = new Credential(tencent.getSecretId(), tencent.getSecretKey());
smsClient = new SmsClient(cred, REGION);
}
/**
* send text message
* @param appId appId
* @param signName sign name
* @param templateId template id
* @param templateValues template values
* @param phones phones num
*/
public void sendMessage(String appId, String signName, String templateId,
String[] templateValues, String[] phones) {
SendSmsRequest req = new SendSmsRequest();
req.setSmsSdkAppId(appId);
req.setSignName(signName);
req.setTemplateId(templateId);
req.setTemplateParamSet(templateValues);
req.setPhoneNumberSet(phones);
try {
SendSmsResponse smsResponse = this.smsClient.SendSms(req);
SendStatus sendStatus = smsResponse.getSendStatusSet()[0];
if (!RESPONSE_OK.equals(sendStatus.getCode())) {
throw new SendMessageException(sendStatus.getCode() + ":" + sendStatus.getMessage());
}
} catch (Exception e) {
log.warn(e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
/**
* send text message
* @param templateValues template values
* @param phones phones num
*/
public void sendMessage(String[] templateValues, String[] phones) {
sendMessage(this.appId, this.signName, this.templateId, templateValues, phones);
}
}
@@ -25,6 +25,7 @@ import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
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;
@@ -67,6 +68,9 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Autowired
private AlertDefineDao alertDefineDao;
@Autowired
private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
private final Map<String, AlertDefineImExportService> alertDefineImExportServiceMap = new HashMap<>();
@@ -98,19 +102,22 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Override
public void addAlertDefine(AlertDefine alertDefine) throws RuntimeException {
alertDefineDao.save(alertDefine);
alertDefine = alertDefineDao.save(alertDefine);
periodicAlertRuleScheduler.updateSchedule(alertDefine);
CacheFactory.clearAlertDefineCache();
}
@Override
public void modifyAlertDefine(AlertDefine alertDefine) throws RuntimeException {
alertDefineDao.save(alertDefine);
periodicAlertRuleScheduler.updateSchedule(alertDefine);
CacheFactory.clearAlertDefineCache();
}
@Override
public void deleteAlertDefine(long alertId) throws RuntimeException {
alertDefineDao.deleteById(alertId);
periodicAlertRuleScheduler.cancelSchedule(alertId);
CacheFactory.clearAlertDefineCache();
}
@@ -123,6 +130,9 @@ public class AlertDefineServiceImpl implements AlertDefineService {
@Override
public void deleteAlertDefines(Set<Long> alertIds) throws RuntimeException {
alertDefineDao.deleteAlertDefinesByIdIn(alertIds);
for (Long alertId : alertIds) {
periodicAlertRuleScheduler.cancelSchedule(alertId);
}
CacheFactory.clearAlertDefineCache();
}
@@ -0,0 +1,234 @@
/*
* 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.hertzbeat.alert.config.AlibabaSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.SimpleTimeZone;
import java.util.TreeMap;
import java.util.UUID;
import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA;
/**
* Alibaba Cloud SMS Client Implementation<br>
* API doc: <a href="https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms">https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms</a><br>
* Singnature doc: <a href="https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature">https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature</a>
*/
@Slf4j
public class AlibabaSmsClientImpl implements SmsClient {
private static final String API_VERSION = "2017-05-25";
private static final String ACTION = "SendSms";
private static final String HOST = "dysmsapi.aliyuncs.com";
private static final String ALGORITHM = "ACS3-HMAC-SHA256";
private final String accessKeyId;
private final String accessKeySecret;
private final String signName;
private final String templateCode;
public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
if (config != null) {
this.accessKeyId = config.getAccessKeyId();
this.accessKeySecret = config.getAccessKeySecret();
this.signName = config.getSignName();
this.templateCode = config.getTemplateCode();
} else {
this.accessKeyId = "";
this.accessKeySecret = "";
this.signName = "";
this.templateCode = "";
}
}
@Override
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// Extract alert info
String instance = null;
String priority = null;
String content = null;
if (alert.getCommonLabels() != null) {
instance = alert.getCommonLabels().get("instance");
priority = alert.getCommonLabels().get("priority");
content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
}
// Build template parameters
Map<String, String> templateParam = new HashMap<>();
templateParam.put("instance", instance == null ? alert.getGroupKey() : instance);
templateParam.put("priority", priority == null ? "unknown" : priority);
templateParam.put("content", content);
sendSms(receiver.getPhone(), JsonUtil.toJson(templateParam));
}
private void sendSms(String phoneNumber, String templateParam) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// Build query parameters
Map<String, String> queryParams = new TreeMap<>();
queryParams.put("PhoneNumbers", phoneNumber);
queryParams.put("SignName", signName);
queryParams.put("TemplateCode", templateCode);
queryParams.put("TemplateParam", templateParam);
// Build canonical query string
StringBuilder canonicalQueryString = new StringBuilder();
queryParams.forEach((key, value) -> {
if (canonicalQueryString.length() > 0) {
canonicalQueryString.append("&");
}
canonicalQueryString.append(percentEncode(key))
.append("=")
.append(percentEncode(value));
});
// Generate timestamp and nonce
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
sdf.setTimeZone(new SimpleTimeZone(0, "GMT"));
String timestamp = sdf.format(new Date());
String nonce = UUID.randomUUID().toString();
// Calculate signature
String authorization = calculateAuthorization(
canonicalQueryString.toString(),
timestamp,
nonce
);
// Build URL
String url = "https://" + HOST + "/?" + canonicalQueryString;
// Build HTTP request
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Host", HOST);
httpPost.setHeader("Authorization", authorization);
httpPost.setHeader("x-acs-action", ACTION);
httpPost.setHeader("x-acs-version", API_VERSION);
httpPost.setHeader("x-acs-date", timestamp);
httpPost.setHeader("x-acs-signature-nonce", nonce);
httpPost.setHeader("x-acs-content-sha256",
CryptoUtils.sha256Hex(""));
log.info("Sending Alibaba SMS request to {}", url + ", params: " + templateParam + "headers: " + Arrays.toString(httpPost.getAllHeaders()));
// Send request and handle response
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
log.info("SMS response status: {}, body: {}", statusCode, responseBody);
if (statusCode != 200) {
throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
String code = jsonResponse.get("Code").asText();
if (!"OK".equals(code)) {
String message = jsonResponse.get("Message").asText();
throw new SendMessageException(code + ":" + message);
}
log.info("Successfully sent SMS to phone: {}", phoneNumber);
}
} catch (Exception e) {
log.warn("Failed to send SMS: {}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
private String calculateAuthorization(String canonicalQueryString, String timestamp, String nonce) {
try {
// Step 1: Build canonical request
String canonicalRequest = buildCanonicalRequest(canonicalQueryString, timestamp, nonce);
// Step 2: Build string to sign
String stringToSign = ALGORITHM + "\n" + CryptoUtils.sha256Hex(canonicalRequest);
// Step 3: Calculate signature
String signature = CryptoUtils.hmacSha256Hex(accessKeySecret, stringToSign);
// Step 4: Build authorization header
return ALGORITHM + " Credential=" + accessKeyId + ",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + "x-acs-signature-nonce;x-acs-version,Signature=" + signature;
} catch (Exception e) {
throw new RuntimeException("Failed to calculate authorization", e);
}
}
private String buildCanonicalRequest(String canonicalQueryString, String timestamp, String nonce) {
return "POST\n"
+ "/\n"
+ canonicalQueryString + "\n"
+ "host:" + HOST + "\n"
+ "x-acs-action:" + ACTION + "\n"
+ "x-acs-content-sha256:" + CryptoUtils.sha256Hex("") + "\n"
+ "x-acs-date:" + timestamp + "\n"
+ "x-acs-signature-nonce:" + nonce + "\n"
+ "x-acs-version:" + API_VERSION + "\n\n"
+ "host;x-acs-action;x-acs-content-sha256;x-acs-date;"
+ "x-acs-signature-nonce;x-acs-version\n"
+ CryptoUtils.sha256Hex("");
}
private String percentEncode(String value) {
try {
return java.net.URLEncoder.encode(value, StandardCharsets.UTF_8)
.replace("+", "%20")
.replace("*", "%2A")
.replace("%7E", "~");
} catch (Exception e) {
throw new RuntimeException("Failed to encode value", e);
}
}
@Override
public String getType() {
return ALIBABA;
}
@Override
public boolean checkConfig() {
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
}
}
@@ -17,13 +17,21 @@
package org.apache.hertzbeat.alert.service.impl;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Stack;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.service.DataSourceService;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.StringUtils;
/**
* datasource service
@@ -33,21 +41,307 @@ import java.util.Map;
public class DataSourceServiceImpl implements DataSourceService {
@Autowired(required = false)
private Map<String, QueryExecutor> executors;
private List<QueryExecutor> executors;
private static final Pattern EXPR_TOKEN = Pattern.compile("\\(|\\)|[a-zA-Z_][a-zA-Z0-9_={}\\[\\]\".]*|\\d+(\\.\\d+)?|>=|<=|==|!=|>|<|and|or|unless");
private static final String THRESHOLD = "__threshold__";
private static final String VALUE = "__value__";
@Override
public List<Map<String, Object>> query(String datasource, String query) {
QueryExecutor executor = executors.get(datasource);
public List<Map<String, Object>> calculate(String datasource, String expr) {
if (!StringUtils.hasText(expr)) {
throw new IllegalArgumentException("Empty expression");
}
if (executors == null || executors.isEmpty()) {
throw new IllegalArgumentException("No query executor found");
}
QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null);
if (executor == null) {
throw new IllegalArgumentException("Unsupported datasource: " + datasource);
}
return executor.execute(query);
// replace all white space
expr = expr.replaceAll("\\s+", " ");
try {
return evaluate(expr, executor);
} catch (Exception e) {
log.error("Error executing query on datasource {}: {}", datasource, e.getMessage());
throw new RuntimeException("Query execution failed", e);
}
}
/**
*
*/
public interface QueryExecutor {
List<Map<String, Object>> execute(String query);
private List<Map<String, Object>> evaluate(String expr, QueryExecutor executor) {
Stack<List<Map<String, Object>>> values = new Stack<>();
Stack<String> operators = new Stack<>();
Matcher matcher = EXPR_TOKEN.matcher(expr);
List<String> tokens = new ArrayList<>();
while (matcher.find()) {
tokens.add(matcher.group());
}
for (String token : tokens) {
if (token.equals("(")) {
operators.push(token);
} else if (token.equals(")")) {
while (!operators.isEmpty() && !operators.peek().equals("(")) {
applyOperator(values, operators.pop());
}
// remove the left parenthesis
operators.pop();
} else if (token.matches(">=|<=|==|!=|>|<")) {
operators.push(token);
} else if (token.equals("and") || token.equals("or") || token.equals("unless")) {
while (!operators.isEmpty() && precedence(operators.peek()) >= precedence(token)) {
applyOperator(values, operators.pop());
}
operators.push(token);
} else if (token.matches("\\d+(\\.\\d+)?")) {
double value = Double.parseDouble(token);
List<Map<String, Object>> numAsList = new ArrayList<>();
numAsList.add(Map.of(THRESHOLD, value));
values.push(numAsList);
} else if (token.matches("[a-zA-Z_][a-zA-Z0-9_={}\\[\\]\".]*")) {
List<Map<String, Object>> results = executor.execute(token);
values.push(results);
}
}
while (!operators.isEmpty()) {
applyOperator(values, operators.pop());
}
return values.isEmpty() ? new LinkedList<>() : values.pop();
}
private int precedence(String op) {
return switch (op) {
case "or" -> 1;
case "unless" -> 2;
case "and" -> 3;
case ">", "<", ">=", "<=", "==", "!=" -> 4;
default -> 0;
};
}
private void applyOperator(Stack<List<Map<String, Object>>> values, String op) {
if (values.size() < 2) {
return;
};
List<Map<String, Object>> rightOperand = values.pop();
List<Map<String, Object>> leftOperand = values.pop();
if (rightOperand.size() == 1 && rightOperand.get(0).containsKey(THRESHOLD)) {
double threshold = (double) rightOperand.get(0).get(THRESHOLD);
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> item : leftOperand) {
Object queryValues = item.get(VALUE);
if (queryValues == null) {
// ignore the query result data is empty
continue;
}
// queryValues may be a list of values, or a single value
Object matchValue = evaluateCondition(queryValues, op, threshold);
item.put(VALUE, matchValue);
// if matchValue is null, mean not match the threshold
// if not null, mean match the threshold
result.add(new HashMap<>(item));
}
if (!result.isEmpty()) {
values.push(result);
}
return;
}
Map<String, Object> leftMap = null;
boolean leftMatch = false;
Map<String, Object> rightMap = null;
boolean rightMatch = false;
switch (op) {
case "and" -> {
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) {
rightMap.putAll(leftMap);
values.push(new LinkedList<>(List.of(rightMap)));
} else if (leftMap != null) {
leftMap.put(VALUE, null);
values.push(new LinkedList<>(List.of(leftMap)));
} else if (rightMap != null) {
rightMap.put(VALUE, null);
values.push(new LinkedList<>(List.of(rightMap)));
}
}
case "or" -> {
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) {
rightMap.putAll(leftMap);
values.push(new LinkedList<>(List.of(rightMap)));
} else if (leftMatch) {
values.push(new LinkedList<>(List.of(leftMap)));
} else if (rightMatch) {
values.push(new LinkedList<>(List.of(rightMap)));
} else {
if (leftMap != null && rightMap != null) {
rightMap.putAll(leftMap);
values.push(new LinkedList<>(List.of(rightMap)));
} else if (leftMap != null) {
values.push(new LinkedList<>(List.of(leftMap)));
} else if (rightMap != null){
values.push(new LinkedList<>(List.of(rightMap)));
}
}
}
case "unless" -> {
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) {
values.push(new LinkedList<>(List.of(leftMap)));
} else {
if (leftMap != null) {
leftMap.put(VALUE, null);
values.push(new LinkedList<>(List.of(leftMap)));
} else {
if (rightMap != null) {
rightMap.put(VALUE, null);
values.push(new LinkedList<>(List.of(rightMap)));
}
}
}
}
default -> throw new IllegalArgumentException("Unsupported operator: " + op);
}
}
private Object evaluateCondition(Object value, String operator, Double threshold) {
// value may be a list of values, or a single value
switch (operator) {
case ">":
// if value is list, return the max value
if (value instanceof List<?> values) {
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString()))
.max(Double::compareTo).orElse(null);
if (doubleValue != null) {
return doubleValue > threshold ? doubleValue : null;
} else {
return null;
}
} else {
return Double.parseDouble(value.toString()) > threshold ? value : null;
}
case ">=":
if (value instanceof List<?> values) {
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString()))
.max(Double::compareTo).orElse(null);
if (doubleValue != null) {
return doubleValue >= threshold ? doubleValue : null;
} else {
return null;
}
} else {
return Double.parseDouble(value.toString()) >= threshold ? value : null;
}
case "<":
if (value instanceof List<?> values) {
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString()))
.min(Double::compareTo).orElse(null);
if (doubleValue != null) {
return doubleValue < threshold ? doubleValue : null;
} else {
return null;
}
} else {
return Double.parseDouble(value.toString()) < threshold ? value : null;
}
case "<=":
if (value instanceof List<?> values) {
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString()))
.min(Double::compareTo).orElse(null);
if (doubleValue != null) {
return doubleValue <= threshold ? doubleValue : null;
} else {
return null;
}
} else {
return Double.parseDouble(value.toString()) <= threshold ? value : null;
}
case "==":
if (value instanceof List<?> values) {
for (Object v : values) {
if (v.equals(threshold)) {
return v;
}
}
return null;
} else {
return value.equals(threshold) ? value : null;
}
case "!=":
if (value instanceof List<?> values) {
for (Object v : values) {
if (v.equals(threshold)) {
return null;
}
}
return value;
} else {
return value.equals(threshold) ? null : value;
}
default:
// unsupported operator todo add more operator
return null;
}
}
public void setExecutors(List<QueryExecutor> mockExecutor) {
this.executors = mockExecutor;
}
}
@@ -18,18 +18,6 @@
package org.apache.hertzbeat.alert.service.impl;
import jakarta.persistence.criteria.Predicate;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.common.cache.CacheFactory;
@@ -51,10 +39,28 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Message notification configuration implementation
*/
@@ -80,44 +86,87 @@ public class NoticeConfigServiceImpl implements NoticeConfigService, CommandLine
private AlertNoticeDispatch dispatcherAlarm;
@Override
public List<NoticeReceiver> getNoticeReceivers(String name) {
public Page<NoticeReceiver> getNoticeReceivers(String name, int pageIndex, int pageSize) {
Specification<NoticeReceiver> specification = (root, query, criteriaBuilder) -> {
Predicate predicate = criteriaBuilder.conjunction();
if (StringUtils.isNotBlank(name)) {
Predicate predicateName = criteriaBuilder.like(root.get("name"), "%" + name + "%");
Predicate predicateName = criteriaBuilder.like(
criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase() + "%"
);
predicate = criteriaBuilder.and(predicateName);
}
return predicate;
};
return noticeReceiverDao.findAll(specification);
return noticeReceiverDao.findAll(specification, PageRequest.of(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "id")));
}
@Override
public List<NoticeTemplate> getNoticeTemplates(String name) {
Specification<NoticeTemplate> specification = (root, query, criteriaBuilder) -> {
Predicate predicate = criteriaBuilder.conjunction();
if (StringUtils.isNotBlank(name)) {
Predicate predicateName = criteriaBuilder.like(root.get("name"), "%" + name + "%");
predicate = criteriaBuilder.and(predicateName);
public List<NoticeReceiver> getAllNoticeReceivers() {
return noticeReceiverDao.findAll();
}
@Override
public Page<NoticeTemplate> getNoticeTemplates(String name, boolean preset, int pageIndex, int pageSize) {
if (preset) {
// Query preset templates
List<NoticeTemplate> defaultTemplates = new LinkedList<>(PRESET_TEMPLATE.values());
// Filter by name (case-insensitive)
List<NoticeTemplate> filteredDefaultTemplates = defaultTemplates.stream()
.filter(template -> StringUtils.isBlank(name)
|| template.getName().toLowerCase().contains(name.toLowerCase()))
.collect(Collectors.toList());
// Pagination logic
int totalItems = filteredDefaultTemplates.size();
int fromIndex = Math.min(pageIndex * pageSize, totalItems);
int toIndex = Math.min(fromIndex + pageSize, totalItems);
if (fromIndex >= totalItems) {
return new PageImpl<>(Collections.emptyList(), PageRequest.of(pageIndex, pageSize), totalItems);
}
return predicate;
};
List<NoticeTemplate> paginatedTemplates = filteredDefaultTemplates.subList(fromIndex, toIndex);
return new PageImpl<>(paginatedTemplates, PageRequest.of(pageIndex, pageSize), totalItems);
} else {
// Query custom templates
Specification<NoticeTemplate> specification = (root, query, criteriaBuilder) -> {
Predicate predicate = criteriaBuilder.conjunction();
if (StringUtils.isNotBlank(name)) {
Predicate predicateName = criteriaBuilder.like(
criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase() + "%"
);
predicate = criteriaBuilder.and(predicateName);
}
return predicate;
};
PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "id"));
return noticeTemplateDao.findAll(specification, pageRequest);
}
}
@Override
public List<NoticeTemplate> getAllNoticeTemplates() {
List<NoticeTemplate> defaultTemplates = new LinkedList<>(PRESET_TEMPLATE.values());
defaultTemplates.addAll(noticeTemplateDao.findAll(specification));
defaultTemplates.addAll(noticeTemplateDao.findAll());
return defaultTemplates;
}
@Override
public List<NoticeRule> getNoticeRules(String name) {
public Page<NoticeRule> getNoticeRules(String name, int pageIndex, int pageSize) {
Specification<NoticeRule> specification = (root, query, criteriaBuilder) -> {
Predicate predicate = criteriaBuilder.conjunction();
if (StringUtils.isNotBlank(name)) {
Predicate predicateName = criteriaBuilder.like(root.get("name"), "%" + name + "%");
Predicate predicateName = criteriaBuilder.like(
criteriaBuilder.lower(root.get("name")), "%" + name.toLowerCase() + "%"
);
predicate = criteriaBuilder.and(predicateName);
}
return predicate;
};
return noticeRuleDao.findAll(specification);
return noticeRuleDao.findAll(specification, PageRequest.of(pageIndex, pageSize, Sort.by(Sort.Direction.DESC, "id")));
}
@Override
@@ -0,0 +1,223 @@
/*
* 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.hertzbeat.alert.config.TencentSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.hertzbeat.common.util.JsonUtil;
import com.fasterxml.jackson.databind.JsonNode;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT;
/**
* sms service client for tencent cloud <br>
* doc: <a href="https://cloud.tencent.com/document/api/382/55981">https://cloud.tencent.com/document/api/382/55981</a>
*/
@Slf4j
public class TencentSmsClientImpl implements SmsClient {
private static final String RESPONSE_OK = "Ok";
private static final String REGION = "ap-guangzhou";
private static final String API_VERSION = "2021-01-11";
private static final String ACTION = "SendSms";
private static final String HOST = "sms.tencentcloudapi.com";
private static final Charset UTF8 = StandardCharsets.UTF_8;
private String appId;
private String signName;
private String templateId;
private String secretId;
private String secretKey;
public TencentSmsClientImpl(TencentSmsProperties config) {
if (config != null) {
this.appId = config.getAppId();
this.signName = config.getSignName();
this.templateId = config.getTemplateId();
this.secretId = config.getSecretId();
this.secretKey = config.getSecretKey();
}
}
@Override
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
// todo limit the number of words
String instance = null;
String priority = null;
String content = null;
if (alert.getCommonLabels() != null) {
instance = alert.getCommonLabels().get("instance");
priority = alert.getCommonLabels().get("priority");
content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
}
String[] templateValues = new String[3];
templateValues[0] = instance == null ? alert.getGroupKey() : instance;
templateValues[1] = priority == null ? "unknown" : priority;
templateValues[2] = content;
String[] phones = new String[1];
phones[0] = receiver.getPhone();
sendSms(this.appId, this.signName, this.templateId, templateValues, phones);
}
public void sendSms(String appId, String signName, String templateId,
String[] templateValues, String[] phones) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
// build request payload
Map<String, Object> params = new HashMap<>();
params.put("SmsSdkAppId", appId);
params.put("SignName", signName);
params.put("TemplateId", templateId);
params.put("TemplateParamSet", templateValues);
params.put("PhoneNumberSet", phones);
String payload = JsonUtil.toJson(params);
// calculate request signature
String authorization = calculateAuthorization(
secretId, secretKey, "sms", HOST, REGION,
ACTION, API_VERSION, payload);
// build http request
HttpPost httpPost = new HttpPost("https://" + HOST);
httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
httpPost.setHeader("Host", HOST);
httpPost.setHeader("X-TC-Action", ACTION);
httpPost.setHeader("X-TC-Timestamp", timestamp);
httpPost.setHeader("X-TC-Version", API_VERSION);
httpPost.setHeader("X-TC-Region", REGION);
httpPost.setHeader("Authorization", authorization);
httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8));
log.debug("Sending SMS request to {}, payload: {}", httpPost.getURI(), payload);
// send http request and handle response
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
log.debug("SMS response status: {}, body: {}", statusCode, responseBody);
if (statusCode != 200) {
throw new SendMessageException("HTTP request failed with status code: " + statusCode);
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
JsonNode responseNode = jsonResponse.get("Response");
JsonNode error = responseNode.get("Error");
if (error != null) {
String code = error.get("Code").asText();
String message = error.get("Message").asText();
throw new SendMessageException(code + ":" + message);
}
JsonNode sendStatusSet = responseNode.get("SendStatusSet");
if (sendStatusSet != null && sendStatusSet.isArray() && sendStatusSet.size() > 0) {
JsonNode firstStatus = sendStatusSet.get(0);
String code = firstStatus.get("Code").asText();
String message = firstStatus.get("Message").asText();
if (!RESPONSE_OK.equals(code)) {
throw new SendMessageException(code + ":" + message);
}
}
log.info("Successfully sent SMS to phones: {}", String.join(",", phones));
}
} catch (Exception e) {
log.warn("Failed to send SMS: {}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
@Override
public String getType() {
return TENCENT;
}
@Override
public boolean checkConfig() {
if (appId.isBlank() || templateId.isBlank() || secretId.isBlank() || secretKey.isBlank()) {
return false;
}
return true;
}
public static String calculateAuthorization(String secretId, String secretKey,
String service, String host, String region,
String action, String version, String payload) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
// Step 1: Construct the canonical request string
String httpRequestMethod = "POST";
String canonicalUri = "/";
String canonicalQueryString = "";
String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n";
String signedHeaders = "content-type;host";
String hashedRequestPayload = CryptoUtils.sha256Hex(payload);
String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
+ canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
// Step 2: Construct the string to sign
String algorithm = "TC3-HMAC-SHA256";
String credentialScope = date + "/" + service + "/" + "tc3_request";
String hashedCanonicalRequest = CryptoUtils.sha256Hex(canonicalRequest);
String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
// Step 3: Calculate the signature
byte[] secretDate = CryptoUtils.hmac256(("TC3" + secretKey).getBytes(UTF8), date);
byte[] secretService = CryptoUtils.hmac256(secretDate, service);
byte[] secretSigning = CryptoUtils.hmac256(secretService, "tc3_request");
String signature = DatatypeConverter.printHexBinary(
CryptoUtils.hmac256(secretSigning, stringToSign)).toLowerCase();
// Step 4: Construct the Authorization header
return algorithm + " " + "Credential=" + secretId + "/" + credentialScope + ", "
+ "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
}
}
@@ -0,0 +1,193 @@
/*
* 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.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS;
/**
* UniSMS client implementation <br/>
* doc:<a href="https://unisms.apistd.com/docs/api/send">https://unisms.apistd.com/docs/api/send</a>
*/
@Slf4j
public class UniSmsClientImpl implements SmsClient {
private static final String API_URL = "https://uni.apistd.com";
private static final String ACTION = "sms.message.send";
private static final String SUCCESS_CODE = "0";
private static final String HMAC_ALGORITHM = "hmac-sha256";
private final UniSmsProperties config;
public UniSmsClientImpl(UniSmsProperties config) {
this.config = config;
}
@Override
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// build request parameters
Map<String, Object> params = new HashMap<>();
params.put("to", receiver.getPhone());
params.put("signature", config.getSignature());
params.put("templateId", config.getTemplateId());
// build template data
Map<String, String> templateData = new HashMap<>();
String instance = alert.getCommonLabels().getOrDefault("instance", alert.getGroupKey());
String priority = alert.getCommonLabels().getOrDefault("priority", "unknown");
String content = alert.getCommonAnnotations().get("summary");
content = content == null ? alert.getCommonAnnotations().get("description") : content;
if (content == null) {
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
}
templateData.put("instance", instance);
templateData.put("priority", priority);
templateData.put("content", content);
params.put("templateData", templateData);
// build URL and request headers
String url;
if ("hmac".equalsIgnoreCase(config.getAuthMode())) {
url = buildHmacUrl();
} else {
url = buildSimpleUrl();
}
// send HTTP request
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Accept", "application/json");
String payload = JsonUtil.toJson(params);
httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8));
log.info("Sending SMS request to UniSMS, payload: {}, url: {}", payload, url);
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
handleResponse(response, receiver.getPhone());
}
} catch (Exception e) {
log.error("Failed to send SMS via UniSMS: {}", e.getMessage());
throw new SendMessageException(e.getMessage());
}
}
private String buildSimpleUrl() {
return String.format("%s/?action=%s&accessKeyId=%s",
API_URL, ACTION, config.getAccessKeyId());
}
private String buildHmacUrl() {
long timestamp = System.currentTimeMillis();
String nonce = generateNonce();
// build query parameters
Map<String, String> params = new TreeMap<>();
params.put("accessKeyId", config.getAccessKeyId());
params.put("action", ACTION);
params.put("algorithm", HMAC_ALGORITHM);
params.put("nonce", nonce);
params.put("timestamp", String.valueOf(timestamp));
// build sign text
String signText = params.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
// calculate signature
String signature = CryptoUtils.hmacSha256Base64(config.getAccessKeySecret(), signText);
return String.format("%s/?action=%s&accessKeyId=%s&algorithm=%s&timestamp=%d&nonce=%s&signature=%s",
API_URL, ACTION, config.getAccessKeyId(), HMAC_ALGORITHM, timestamp, nonce, signature);
}
private String generateNonce() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
private void handleResponse(CloseableHttpResponse response, String phone) throws IOException {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity());
log.info("UniSMS response status: {}, body: {}", statusCode, responseBody);
if (statusCode != 200) {
throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
}
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
String code = jsonResponse.get("code").asText();
if (!SUCCESS_CODE.equals(code)) {
String message = jsonResponse.get("message").asText();
throw new SendMessageException(code + ":" + message);
}
log.info("Successfully sent SMS to phone: {}", phone);
}
@Override
public String getType() {
return UNISMS;
}
@Override
public boolean checkConfig() {
if (config == null
|| config.getAccessKeyId() == null
|| config.getAccessKeyId().isBlank()
|| config.getSignature() == null
|| config.getSignature().isBlank()
|| config.getTemplateId() == null
|| config.getTemplateId().isBlank()) {
return false;
}
// HMAC mode requires additional check for accessKeySecret
if ("hmac".equalsIgnoreCase(config.getAuthMode())
&& (config.getAccessKeySecret() == null || config.getAccessKeySecret().isBlank())) {
return false;
}
return true;
}
}
@@ -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.alert.util;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
/**
* General encryption utility class
*/
@Slf4j
public class CryptoUtils {
private static final Charset UTF8 = StandardCharsets.UTF_8;
private CryptoUtils() {}
/**
* Calculate HMAC-SHA256 signature
* @param key secret key
* @param msg message to be signed
* @return signed byte array
*/
public static byte[] hmac256(byte[] key, String msg) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
mac.init(secretKeySpec);
return mac.doFinal(msg.getBytes(UTF8));
} catch (Exception e) {
log.error("Failed to calculate HMAC-SHA256: {}", e.getMessage());
throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
}
}
/**
* Calculate SHA256 hash and convert to lowercase hexadecimal string
* @param data data to be hashed
* @return lowercase hexadecimal string
*/
public static String sha256Hex(String data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(data.getBytes(UTF8));
return DatatypeConverter.printHexBinary(digest).toLowerCase();
} catch (Exception e) {
log.error("Failed to calculate SHA256: {}", e.getMessage());
throw new RuntimeException("Failed to calculate SHA256", e);
}
}
/**
* Calculate HMAC-SHA256 signature and convert to Base64 string
* @param key secret key
* @param data data to be signed
* @return Base64 encoded signature string
*/
public static String hmacSha256Base64(String key, String data) {
byte[] hmacResult = hmac256(key.getBytes(UTF8), data);
return Base64.getEncoder().encodeToString(hmacResult);
}
/**
* Calculate HMAC-SHA256 signature and convert to lowercase hexadecimal string
* @param key secret key
* @param data data to be signed
* @return lowercase hexadecimal string
*/
public static String hmacSha256Hex(String key, String data) {
byte[] hmacResult = hmac256(key.getBytes(UTF8), data);
return DatatypeConverter.printHexBinary(hmacResult).toLowerCase();
}
}
@@ -0,0 +1,32 @@
# 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.
alerter.availability.recover = 可用性警報已解決,監視狀態現在正常
alerter.alarm.recover = 警報解決通知
alerter.notify.title = HertzBeat 警報通知
alerter.notify.target = 監視目標
alerter.notify.monitorId = 監視 ID
alerter.notify.monitorName = 監視名稱
alerter.notify.monitorHost = 監視主機
alerter.notify.priority = 警報優先級
alerter.notify.triggerTime = 警報觸發時間
alerter.notify.restoreTime = 警報恢復時間
alerter.notify.times = 警報觸發次數
alerter.notify.tags = 警報標籤
alerter.notify.content = 警報內容
alerter.notify.console = 控制台登錄
alerter.priority.0 = 緊急警報
alerter.priority.1 = 嚴重警報
alerter.priority.2 = 警告警報
@@ -28,7 +28,6 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefineMonitorBind;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -43,7 +42,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
/**
* Test case for {@link AlertDefineController}
*/
@Disabled
@ExtendWith(MockitoExtension.class)
class AlertDefineControllerTest {
@@ -66,6 +64,7 @@ class AlertDefineControllerTest {
this.alertDefine = AlertDefine.builder()
.id(1L)
.name("alertDefine")
.expr("1 > 0")
.times(1)
.template("template")
@@ -138,14 +137,4 @@ class AlertDefineControllerTest {
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andReturn();
}
@Test
void applyAlertDefineMonitorsBind() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/api/alert/define/" + this.alertDefine.getId() + "/monitors")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.toJson(this.alertDefineMonitorBinds)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andReturn();
}
}
@@ -19,7 +19,6 @@ package org.apache.hertzbeat.alert.controller;
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;
@@ -31,7 +30,6 @@ import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -50,7 +48,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
* Test case for {@link AlertDefinesController}
* Test whether the data mocked at the mock is correct, and test whether the format of the returned data is correct
*/
@Disabled
@ExtendWith(MockitoExtension.class)
class AlertDefinesControllerTest {
@@ -93,8 +90,7 @@ class AlertDefinesControllerTest {
pageRequest = PageRequest.of((Integer) content.get("pageIndex"), (Integer) content.get("pageSize"), sortExp);
}
// @Test
// todo: fix this test
@Test
void getAlertDefines() throws Exception {
// Test the correctness of the mock
@@ -112,31 +108,27 @@ class AlertDefinesControllerTest {
// }
// }))).thenReturn(new PageImpl<AlertDefine>(new ArrayList<AlertDefine>()));
AlertDefine define = AlertDefine.builder().id(9L).expr("x").times(1).build();
Mockito.when(alertDefineService.getAlertDefines(null, null, "id", "desc", 1, 10)).thenReturn(new PageImpl<>(Collections.singletonList(define)));
PageRequest pageRequest = PageRequest.of(pageIndex, pageSize, Sort.by(Sort.Order.asc(sort)));
Mockito.when(alertDefineService.getAlertDefines(
Mockito.eq(ids),
Mockito.isNull(),
Mockito.eq(sort),
Mockito.eq(order),
Mockito.eq(pageIndex),
Mockito.eq(pageSize)
)).thenReturn(new PageImpl<>(Collections.singletonList(define), pageRequest, 1));
mockMvc.perform(MockMvcRequestBuilders.get(
"/api/alert/defines")
mockMvc.perform(MockMvcRequestBuilders.get("/api/alert/defines")
.param("ids", ids.toString().substring(1, ids.toString().length() - 1))
.param("priority", priority.toString())
.param("sort", sort)
.param("order", order)
.param("pageIndex", pageIndex.toString())
.param("pageSize", pageSize.toString()))
.param("pageIndex", String.valueOf(pageIndex))
.param("pageSize", String.valueOf(pageSize)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andExpect(jsonPath("$.data.content").value(new ArrayList<>()))
.andExpect(jsonPath("$.data.pageable").value("INSTANCE"))
.andExpect(jsonPath("$.data.totalPages").value(1))
.andExpect(jsonPath("$.data.totalElements").value(0))
.andExpect(jsonPath("$.data.last").value(true))
.andExpect(jsonPath("$.data.number").value(0))
.andExpect(jsonPath("$.data.size").value(0))
.andExpect(jsonPath("$.data.first").value(true))
.andExpect(jsonPath("$.data.numberOfElements").value(0))
.andExpect(jsonPath("$.data.empty").value(true))
.andExpect(jsonPath("$.data.sort.empty").value(true))
.andExpect(jsonPath("$.data.sort.sorted").value(false))
.andExpect(jsonPath("$.data.sort.unsorted").value(true))
.andExpect(jsonPath("$.data.content[0].id").value(9))
.andExpect(jsonPath("$.data.content[0].expr").value("x"))
.andExpect(jsonPath("$.data.content[0].times").value(1))
.andReturn();
}
@@ -27,12 +27,12 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import org.apache.hertzbeat.alert.service.AlertGroupConvergeService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -44,7 +44,6 @@ import org.springframework.test.web.servlet.MockMvc;
/**
* test case for {@link AlertGroupConvergeController}
*/
@Disabled
@ExtendWith(MockitoExtension.class)
public class AlertGroupConvergeControllerTest {
@@ -79,8 +78,8 @@ public class AlertGroupConvergeControllerTest {
mockMvc.perform(post("/api/alert/group")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.toJson(alertGroupConverge))
).andExpect(status().isOk())
.content(JsonUtil.toJson(alertGroupConverge)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andExpect(jsonPath("$.msg").value("Add success"));
}
@@ -93,8 +92,8 @@ public class AlertGroupConvergeControllerTest {
mockMvc.perform(put("/api/alert/group")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.toJson(alertGroupConverge))
).andExpect(status().isOk())
.content(JsonUtil.toJson(alertGroupConverge)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andExpect(jsonPath("$.msg").value("Modify success"));
}
@@ -119,7 +118,6 @@ public class AlertGroupConvergeControllerTest {
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.MONITOR_NOT_EXIST_CODE))
.andExpect(jsonPath("$.msg").value("AlertGroupConverge not exist."));
.andExpect(jsonPath("$.msg").value("Alert Group Converge not exist."));
}
}
@@ -33,7 +33,6 @@ import org.apache.hertzbeat.alert.service.AlertGroupConvergeService;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -52,7 +51,6 @@ import org.springframework.test.web.servlet.MockMvc;
*/
@ExtendWith(MockitoExtension.class)
@Disabled
class AlertGroupConvergesControllerTest {
private MockMvc mockMvc;
@@ -101,6 +99,7 @@ class AlertGroupConvergesControllerTest {
.param("order", "desc")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andExpect(jsonPath("$.data.content[0].id").value(1))
.andExpect(jsonPath("$.data.content[0].name").value("Converge1"))
.andExpect(jsonPath("$.data.content[1].id").value(2))
@@ -118,5 +117,4 @@ class AlertGroupConvergesControllerTest {
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE));
}
}
}
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.alert.controller;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -44,6 +43,10 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@@ -168,12 +171,38 @@ class NoticeConfigControllerTest {
@Test
void getReceivers() throws Exception {
NoticeReceiver receiver1 = new NoticeReceiver();
receiver1.setId(1L);
receiver1.setName("Receiver1");
//Mockito.when(noticeConfigService.getNoticeReceivers())
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receivers?name={name}", "tom"))
NoticeReceiver receiver2 = new NoticeReceiver();
receiver2.setId(2L);
receiver2.setName("Receiver2");
Page<NoticeReceiver> receiverPage = new PageImpl<>(
Arrays.asList(receiver1, receiver2),
PageRequest.of(0, 8, Sort.by("id").descending()),
2
);
when(noticeConfigService.getNoticeReceivers("Receiver", 0, 8)).thenReturn(receiverPage);
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receivers")
.param("name", "Receiver")
.param("pageIndex", "0")
.param("pageSize", "8")
.param("sort", "id")
.param("order", "desc")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andReturn();
.andExpect(jsonPath("$.data.content[0].id").value(1))
.andExpect(jsonPath("$.data.content[0].name").value("Receiver1"))
.andExpect(jsonPath("$.data.content[1].id").value(2))
.andExpect(jsonPath("$.data.content[1].name").value("Receiver2"))
.andExpect(jsonPath("$.data.totalElements").value(2))
.andExpect(jsonPath("$.data.totalPages").value(1))
.andExpect(jsonPath("$.data.size").value(8))
.andExpect(jsonPath("$.data.number").value(0));
}
@Test
@@ -245,15 +274,38 @@ class NoticeConfigControllerTest {
@Test
void getRules() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/rules"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andReturn();
NoticeRule rule1 = new NoticeRule();
rule1.setId(1L);
rule1.setName("Rule1");
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/rules?name={name}", "tom"))
NoticeRule rule2 = new NoticeRule();
rule2.setId(2L);
rule2.setName("Rule2");
Page<NoticeRule> rulePage = new PageImpl<>(
Arrays.asList(rule1, rule2),
PageRequest.of(0, 8, Sort.by("id").descending()),
2
);
when(noticeConfigService.getNoticeRules("Rule", 0, 8)).thenReturn(rulePage);
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/rules")
.param("name", "Rule")
.param("pageIndex", "0")
.param("pageSize", "8")
.param("sort", "id")
.param("order", "desc")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andReturn();
.andExpect(jsonPath("$.data.content[0].id").value(1))
.andExpect(jsonPath("$.data.content[0].name").value("Rule1"))
.andExpect(jsonPath("$.data.content[1].id").value(2))
.andExpect(jsonPath("$.data.content[1].name").value("Rule2"))
.andExpect(jsonPath("$.data.totalElements").value(2))
.andExpect(jsonPath("$.data.totalPages").value(1))
.andExpect(jsonPath("$.data.size").value(8))
.andExpect(jsonPath("$.data.number").value(0));
}
@Test
@@ -362,22 +414,40 @@ class NoticeConfigControllerTest {
}
@Test
void testGetTemplates() throws Exception {
// Mock the service response
void getTemplates() throws Exception {
NoticeTemplate template1 = new NoticeTemplate();
template1.setId(1L);
template1.setName("Template1");
NoticeTemplate template2 = new NoticeTemplate();
template2.setName("Template2");
List<NoticeTemplate> templates = Arrays.asList(template1, template2);
when(noticeConfigService.getNoticeTemplates(any())).thenReturn(templates);
// Perform the GET request and verify the response
this.mockMvc.perform(get("/api/notice/templates")
.param("name", "Template"))
NoticeTemplate template2 = new NoticeTemplate();
template2.setId(2L);
template2.setName("Template2");
Page<NoticeTemplate> templatePage = new PageImpl<>(
Arrays.asList(template1, template2),
PageRequest.of(0, 8, Sort.by("id").descending()),
2
);
when(noticeConfigService.getNoticeTemplates("Template", true, 0, 8)).thenReturn(templatePage);
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/templates")
.param("name", "Template")
.param("preset", "true")
.param("pageIndex", "0")
.param("pageSize", "8")
.param("sort", "id")
.param("order", "desc")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andExpect(jsonPath("$.data[0].name").value("Template1"))
.andExpect(jsonPath("$.data[1].name").value("Template2"));
.andExpect(jsonPath("$.data.content[0].id").value(1))
.andExpect(jsonPath("$.data.content[0].name").value("Template1"))
.andExpect(jsonPath("$.data.content[1].id").value(2))
.andExpect(jsonPath("$.data.content[1].name").value("Template2"))
.andExpect(jsonPath("$.data.totalElements").value(2))
.andExpect(jsonPath("$.data.totalPages").value(1))
.andExpect(jsonPath("$.data.size").value(8))
.andExpect(jsonPath("$.data.number").value(0));
}
@Test
@@ -414,5 +484,25 @@ class NoticeConfigControllerTest {
verify(noticeConfigService, times(1)).sendTestMsg(noticeReceiver);
}
@Test
void getAllTemplates() throws Exception {
List<NoticeTemplate> templates = Arrays.asList(new NoticeTemplate(), new NoticeTemplate());
when(noticeConfigService.getAllNoticeTemplates()).thenReturn(templates);
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/templates/all"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andReturn();
}
@Test
void getAllReceivers() throws Exception {
List<NoticeReceiver> receivers = Arrays.asList(new NoticeReceiver(), new NoticeReceiver());
when(noticeConfigService.getAllNoticeReceivers()).thenReturn(receivers);
this.mockMvc.perform(MockMvcRequestBuilders.get("/api/notice/receivers/all"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value((int) CommonConstants.SUCCESS_CODE))
.andReturn();
}
}
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
import org.apache.hertzbeat.alert.config.AlertSseManager;
import org.apache.hertzbeat.alert.service.NoticeConfigService;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -60,6 +61,9 @@ class AlertNoticeDispatchTest {
@Mock
private AlertNotifyHandler alertNotifyHandler;
@Mock
private AlertSseManager emitterManager;
private AlertNoticeDispatch alertNoticeDispatch;
private static final int DISPATCH_THREADS = 3;
@@ -77,7 +81,8 @@ class AlertNoticeDispatchTest {
noticeConfigService,
alertStoreHandler,
alertNotifyHandlerList,
pluginRunner
pluginRunner,
emitterManager
);
receiver = NoticeReceiver.builder()
@@ -27,7 +27,6 @@ import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -39,7 +38,6 @@ import java.util.List;
/**
* Test case for {@link DbAlertStoreHandlerImpl}
*/
@Disabled
@ExtendWith(MockitoExtension.class)
class DbAlertStoreHandlerImplTest {
@@ -75,27 +73,34 @@ class DbAlertStoreHandlerImplTest {
public void testStoreNewAlert() {
String groupKey = "test-group";
groupAlert.setGroupKey(groupKey);
when(groupAlertDao.findByGroupKey(groupKey)).thenReturn(null);
SingleAlert savedSingleAlert = new SingleAlert();
when(singleAlertDao.save(any(SingleAlert.class))).thenReturn(savedSingleAlert);
GroupAlert savedGroupAlert = new GroupAlert();
when(groupAlertDao.save(any(GroupAlert.class))).thenReturn(savedGroupAlert);
dbAlertStoreHandler.store(groupAlert);
verify(singleAlertDao).save(any(SingleAlert.class));
verify(groupAlertDao).save(groupAlert);
}
@Test
public void testStoreExistingAlert() {
String groupKey = "test-group";
String fingerprint = "test-fingerprint";
groupAlert.setGroupKey(groupKey);
singleAlert.setFingerprint(fingerprint);
GroupAlert existingGroup = new GroupAlert();
existingGroup.setId(1L);
when(groupAlertDao.findByGroupKey(groupKey)).thenReturn(existingGroup);
SingleAlert existingAlert = new SingleAlert();
existingAlert.setId(1L);
existingAlert.setStatus("firing");
@@ -103,11 +108,15 @@ class DbAlertStoreHandlerImplTest {
existingAlert.setActiveAt(2000L);
existingAlert.setTriggerTimes(1);
when(singleAlertDao.findByFingerprint(fingerprint)).thenReturn(existingAlert);
when(singleAlertDao.save(any(SingleAlert.class))).thenReturn(existingAlert);
when(groupAlertDao.save(any(GroupAlert.class))).thenReturn(existingGroup);
dbAlertStoreHandler.store(groupAlert);
verify(singleAlertDao).save(any(SingleAlert.class));
verify(groupAlertDao).save(groupAlert);
assertEquals(1L, groupAlert.getId());
}
}
@@ -99,7 +99,7 @@ class EmailAlertNotifyHandlerImplTest {
template.setName("test-template");
template.setContent("test content");
// 设置邮件服务器配置
// Set up email server configuration
MailServerConfig mailServerConfig = new MailServerConfig();
mailServerConfig.setEmailHost("smtp.example.com");
mailServerConfig.setEmailPort(587);
@@ -83,8 +83,8 @@ class AlarmInhibitReduceTest {
MockitoAnnotations.openMocks(this);
when(alertInhibitDao.findAlertInhibitsByEnableIsTrue())
.thenReturn(Collections.emptyList());
// 正确设置 AlerterProperties mock
// Correctly set up AlerterProperties mock
AlerterProperties.InhibitProperties inhibitProperties = new AlerterProperties.InhibitProperties();
inhibitProperties.setTtl(60000);
when(alerterProperties.getInhibit()).thenReturn(inhibitProperties);
@@ -30,18 +30,17 @@ import java.util.Map;
import org.apache.hertzbeat.alert.dao.AlertSilenceDao;
import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch;
import org.apache.hertzbeat.common.cache.CacheFactory;
import org.apache.hertzbeat.common.entity.alerter.AlertSilence;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Test for AlarmSilenceReduce
* Test for {@link AlarmSilenceReduce}
*/
@Disabled
class AlarmSilenceReduceTest {
@Mock
@@ -55,21 +54,23 @@ class AlarmSilenceReduceTest {
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
when(alertSilenceDao.findAll()).thenReturn(Collections.emptyList());
CacheFactory.clearAlertSilenceCache();
alarmSilenceReduce = new AlarmSilenceReduce(alertSilenceDao, alertNoticeDispatch);
}
@Test
void whenNoSilenceRules_shouldForwardAlert() {
when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.emptyList());
GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
alarmSilenceReduce.silenceAlarm(alert);
verify(alertNoticeDispatch).dispatchAlarm(alert);
}
@Test
void whenMatchingSilenceRule_shouldNotForwardAlert() {
// Create silence rule
AlertSilence silenceRule = AlertSilence.builder()
.enable(true)
@@ -81,7 +82,7 @@ class AlarmSilenceReduceTest {
.times(0)
.build();
when(alertSilenceDao.findAll()).thenReturn(Collections.singletonList(silenceRule));
when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule));
when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule);
GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
@@ -107,9 +108,9 @@ class AlarmSilenceReduceTest {
.times(0)
.build();
when(alertSilenceDao.findAll()).thenReturn(Collections.singletonList(silenceRule));
when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule));
when(alertSilenceDao.save(any(AlertSilence.class))).thenReturn(silenceRule);
GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
alarmSilenceReduce.silenceAlarm(alert);
@@ -130,29 +131,7 @@ class AlarmSilenceReduceTest {
.times(0)
.build();
when(alertSilenceDao.findAll()).thenReturn(Collections.singletonList(silenceRule));
GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
alarmSilenceReduce.silenceAlarm(alert);
verify(alertNoticeDispatch).dispatchAlarm(alert);
verify(alertSilenceDao, never()).save(any());
}
@Test
void whenSilenceRuleDisabled_shouldForwardAlert() {
AlertSilence silenceRule = AlertSilence.builder()
.enable(false)
.matchAll(false)
.type((byte) 0)
.labels(createLabels("service", "web"))
.periodStart(LocalDateTime.now().minusHours(1).atZone(ZoneId.systemDefault()))
.periodEnd(LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault()))
.times(0)
.build();
when(alertSilenceDao.findAll()).thenReturn(Collections.singletonList(silenceRule));
when(alertSilenceDao.findAlertSilencesByEnableTrue()).thenReturn(Collections.singletonList(silenceRule));
GroupAlert alert = createGroupAlert("firing", createLabels("service", "web"));
@@ -24,16 +24,18 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.alert.dto.AlertDefineDTO;
import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO;
import org.apache.hertzbeat.alert.service.impl.AlertDefineExcelImExportServiceImpl;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.common.util.export.ExcelExportUtils;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -42,7 +44,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
/**
* test case for {@link AlertDefineExcelImExportServiceImpl}
*/
@Disabled
@ExtendWith(MockitoExtension.class)
public class AlertDefineExcelImExportServiceTest {
@@ -61,15 +62,13 @@ public class AlertDefineExcelImExportServiceTest {
Row row = initialSheet.createRow(1);
row.createCell(0).setCellValue("app1");
row.createCell(1).setCellValue("metric1");
row.createCell(2).setCellValue("field1");
row.createCell(3).setCellValue(true);
row.createCell(4).setCellValue("expr1");
row.createCell(5).setCellValue(1);
row.createCell(6).setCellValue(10);
row.createCell(7).setCellValue("[{\"name\":\"tag1\",\"value\":\"value1\"}]");
row.createCell(2).setCellValue("expr1");
row.createCell(3).setCellValue(10);
row.createCell(4).setCellValue(1);
row.createCell(5).setCellValue(JsonUtil.toJson(Map.of("key", "value")));
row.createCell(6).setCellValue(JsonUtil.toJson(Map.of("key", "value")));
row.createCell(7).setCellValue("template1");
row.createCell(8).setCellValue(true);
row.createCell(9).setCellValue(true);
row.createCell(10).setCellValue("template1");
ByteArrayInputStream inputStream = new ByteArrayInputStream(toByteArray(initialWorkbook));
@@ -88,9 +87,12 @@ public class AlertDefineExcelImExportServiceTest {
assertEquals("app1", alertDefineDTO.getName());
assertEquals("metric1", alertDefineDTO.getType());
assertEquals("expr1", alertDefineDTO.getExpr());
assertEquals(10, alertDefineDTO.getTimes());
assertTrue(alertDefineDTO.getEnable());
assertEquals(10, alertDefineDTO.getPeriod());
assertEquals(1, alertDefineDTO.getTimes());
assertEquals(Map.of("key", "value"), alertDefineDTO.getLabels());
assertEquals(Map.of("key", "value"), alertDefineDTO.getAnnotations());
assertEquals("template1", alertDefineDTO.getTemplate());
assertTrue(alertDefineDTO.getEnable());
}
}
@@ -103,9 +105,12 @@ public class AlertDefineExcelImExportServiceTest {
alertDefineDTO.setName("app1");
alertDefineDTO.setType("metric1");
alertDefineDTO.setExpr("expr1");
alertDefineDTO.setTimes(10);
alertDefineDTO.setEnable(true);
alertDefineDTO.setPeriod(10);
alertDefineDTO.setTimes(1);
alertDefineDTO.setLabels(Map.of("key", "value"));
alertDefineDTO.setAnnotations(Map.of("key", "value"));
alertDefineDTO.setTemplate("template1");
alertDefineDTO.setEnable(true);
exportAlertDefineDTO.setAlertDefine(alertDefineDTO);
exportAlertDefineList.add(exportAlertDefineDTO);
@@ -115,21 +120,26 @@ public class AlertDefineExcelImExportServiceTest {
try (Workbook resultWorkbook = WorkbookFactory.create(new ByteArrayInputStream(outputStream.toByteArray()))) {
Sheet resultSheet = resultWorkbook.getSheetAt(0);
Row headerRow = resultSheet.getRow(0);
assertEquals("app", headerRow.getCell(0).getStringCellValue());
assertEquals("metric", headerRow.getCell(1).getStringCellValue());
assertEquals("Name", headerRow.getCell(0).getStringCellValue());
assertEquals("Type", headerRow.getCell(1).getStringCellValue());
assertEquals("Expr", headerRow.getCell(2).getStringCellValue());
assertEquals("Period", headerRow.getCell(3).getStringCellValue());
assertEquals("Times", headerRow.getCell(4).getStringCellValue());
assertEquals("Labels", headerRow.getCell(5).getStringCellValue());
assertEquals("Annotations", headerRow.getCell(6).getStringCellValue());
assertEquals("Template", headerRow.getCell(7).getStringCellValue());
assertEquals("Enable", headerRow.getCell(8).getStringCellValue());
Row dataRow = resultSheet.getRow(1);
assertEquals("app1", dataRow.getCell(0).getStringCellValue());
assertEquals("metric1", dataRow.getCell(1).getStringCellValue());
assertEquals("field1", dataRow.getCell(2).getStringCellValue());
assertTrue(dataRow.getCell(3).getBooleanCellValue());
assertEquals("expr1", dataRow.getCell(4).getStringCellValue());
assertEquals(1, (int) dataRow.getCell(5).getNumericCellValue());
assertEquals(10, (int) dataRow.getCell(6).getNumericCellValue());
assertEquals("[{\"name\":\"tag1\",\"value\":\"value1\"}]", dataRow.getCell(7).getStringCellValue());
assertEquals("expr1", dataRow.getCell(2).getStringCellValue());
assertEquals(10, (int) dataRow.getCell(3).getNumericCellValue());
assertEquals(1, (int) dataRow.getCell(4).getNumericCellValue());
assertEquals(JsonUtil.toJson(Map.of("key", "value")), dataRow.getCell(5).getStringCellValue());
assertEquals(JsonUtil.toJson(Map.of("key", "value")), dataRow.getCell(6).getStringCellValue());
assertEquals("template1", dataRow.getCell(7).getStringCellValue());
assertTrue(dataRow.getCell(8).getBooleanCellValue());
assertTrue(dataRow.getCell(9).getBooleanCellValue());
assertEquals("template1", dataRow.getCell(10).getStringCellValue());
}
}
}
@@ -142,4 +152,4 @@ public class AlertDefineExcelImExportServiceTest {
}
}
}
}
@@ -33,6 +33,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.apache.hertzbeat.alert.dto.AlertDefineDTO;
import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO;
@@ -68,7 +69,7 @@ class AlertDefineJsonImExportServiceTest {
@BeforeEach
public void setup() {
inputStream = new ByteArrayInputStream(JSON_DATA.getBytes());
inputStream = new ByteArrayInputStream(JSON_DATA.getBytes(StandardCharsets.UTF_8));
AlertDefineDTO alertDefine = new AlertDefineDTO();
alertDefine.setName("App1");
@@ -30,6 +30,7 @@ import static org.mockito.Mockito.when;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import org.apache.hertzbeat.alert.calculate.PeriodicAlertRuleScheduler;
import org.apache.hertzbeat.alert.dao.AlertDefineDao;
import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
@@ -54,6 +55,9 @@ class AlertDefineServiceTest {
@Mock
private AlertDefineDao alertDefineDao;
@Mock
private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
@Mock
private List<AlertDefineImExportService> alertDefineImExportServiceList;
@@ -64,6 +68,7 @@ class AlertDefineServiceTest {
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(this.alertDefineService, "alertDefineDao", alertDefineDao);
ReflectionTestUtils.setField(this.alertDefineService, "periodicAlertRuleScheduler", periodicAlertRuleScheduler);
this.alertDefine = AlertDefine.builder()
.id(1L)
@@ -39,7 +39,6 @@ import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO;
import org.apache.hertzbeat.alert.service.impl.AlertDefineYamlImExportServiceImpl;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -48,8 +47,7 @@ import org.yaml.snakeyaml.Yaml;
/**
* test case for {@link AlertDefineYamlImExportServiceImpl}
*/
@Disabled
*/
@ExtendWith(MockitoExtension.class)
class AlertDefineYamlImExportServiceTest {
@@ -17,22 +17,27 @@
package org.apache.hertzbeat.alert.service;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao;
import org.apache.hertzbeat.alert.reduce.AlarmGroupReduce;
import org.apache.hertzbeat.alert.service.impl.AlertGroupConvergeServiceImpl;
import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anySet;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import org.apache.hertzbeat.alert.dao.AlertGroupConvergeDao;
import org.apache.hertzbeat.alert.service.impl.AlertGroupConvergeServiceImpl;
import org.apache.hertzbeat.common.entity.alerter.AlertGroupConverge;
import org.junit.jupiter.api.Disabled;
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 org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
@@ -42,10 +47,9 @@ import org.springframework.data.jpa.domain.Specification;
/**
* test case for {@link AlertGroupConvergeServiceImpl}
*/
*/
@ExtendWith(MockitoExtension.class)
@Disabled
class AlertGroupConvergeServiceTest {
@Mock
@@ -54,22 +58,28 @@ class AlertGroupConvergeServiceTest {
@InjectMocks
private AlertGroupConvergeServiceImpl alertGroupConvergeService;
@Mock
private AlarmGroupReduce alarmGroupReduce;
@Test
public void testAddAlertGroupConverge() {
AlertGroupConverge alertGroupConverge = new AlertGroupConverge();
doNothing().when(alarmGroupReduce).refreshGroupDefines(anyList());
when(alertGroupConvergeDao.save(alertGroupConverge)).thenReturn(alertGroupConverge);
alertGroupConvergeService.addAlertGroupConverge(alertGroupConverge);
verify(alertGroupConvergeDao, times(1)).save(alertGroupConverge);
verify(alarmGroupReduce, times(1)).refreshGroupDefines(anyList());
}
@Test
public void testModifyAlertGroupConverge() {
AlertGroupConverge alertGroupConverge = new AlertGroupConverge();
doNothing().when(alarmGroupReduce).refreshGroupDefines(anyList());
when(alertGroupConvergeDao.save(alertGroupConverge)).thenReturn(alertGroupConverge);
alertGroupConvergeService.modifyAlertGroupConverge(alertGroupConverge);
verify(alertGroupConvergeDao, times(1)).save(alertGroupConverge);
verify(alarmGroupReduce, times(1)).refreshGroupDefines(anyList());
}
@Test
@@ -79,7 +89,6 @@ class AlertGroupConvergeServiceTest {
AlertGroupConverge alertGroupConverge = new AlertGroupConverge();
when(alertGroupConvergeDao.findById(convergeId)).thenReturn(Optional.of(alertGroupConverge));
AlertGroupConverge result = alertGroupConvergeService.getAlertGroupConverge(convergeId);
verify(alertGroupConvergeDao, times(1)).findById(convergeId);
assertEquals(alertGroupConverge, result);
}
@@ -87,10 +96,11 @@ class AlertGroupConvergeServiceTest {
@Test
public void testDeleteAlertGroupConverges() {
Set<Long> convergeIds = Set.of(1L, 2L, 3L);
alertGroupConvergeService.deleteAlertGroupConverges(convergeIds);
verify(alertGroupConvergeDao, times(1)).deleteAlertGroupConvergesByIdIn(convergeIds);
doNothing().when(alertGroupConvergeDao).deleteAlertGroupConvergesByIdIn(anySet());
doNothing().when(alarmGroupReduce).refreshGroupDefines(anyList());
alertGroupConvergeService.deleteAlertGroupConverges(new HashSet<>(Arrays.asList(1L, 2L)));
verify(alertGroupConvergeDao, times(1)).deleteAlertGroupConvergesByIdIn(anySet());
verify(alarmGroupReduce, times(1)).refreshGroupDefines(anyList());
}
@Test
@@ -17,25 +17,23 @@
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.mockito.Mockito.any;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.alert.dao.GroupAlertDao;
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
import org.apache.hertzbeat.alert.service.impl.AlertServiceImpl;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -44,9 +42,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
/**
* Test case for {@link AlertService}
*/
*/
@ExtendWith(MockitoExtension.class)
@Disabled
class AlertServiceTest {
@Mock
private GroupAlertDao groupAlertDao;
@@ -83,14 +80,23 @@ class AlertServiceTest {
}
@Test
void getAlertsSummary() {
List<SingleAlert> singleAlerts = new ArrayList<>();
singleAlerts.add(SingleAlert.builder().status("firing")
.labels(Map.of(CommonConstants.LABEL_ALERT_SEVERITY, "warning")).build());
when(singleAlertDao.querySingleAlertsByStatus(any())).thenReturn(singleAlerts);
when(singleAlertDao.count()).thenReturn(1L);
void testGetAlertsSummary() {
SingleAlert alert = new SingleAlert();
alert.setLabels(Collections.singletonMap(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL));
assertDoesNotThrow(() -> alertService.getAlertsSummary());
assertNotNull(alertService.getAlertsSummary());
when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert));
when(singleAlertDao.count()).thenReturn(10L);
AlertSummary summary = alertService.getAlertsSummary();
assertNotNull(summary);
assertEquals(1, summary.getPriorityCriticalNum());
assertEquals(0, summary.getPriorityEmergencyNum());
assertEquals(0, summary.getPriorityWarningNum());
assertEquals(10L, summary.getTotal());
assertEquals(90.0f, summary.getRate());
verify(singleAlertDao, times(1)).querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING);
verify(singleAlertDao, times(1)).count();
}
}
@@ -0,0 +1,547 @@
/*
* 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.
*/
/*
* 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 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 org.apache.hertzbeat.alert.service.impl.DataSourceServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import org.mockito.Mockito;
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
/**
* test case for {@link DataSourceService}
*/
class DataSourceServiceTest {
private DataSourceServiceImpl dataSourceService;
@BeforeEach
void setUp() {
dataSourceService = new DataSourceServiceImpl();
}
@Test
void calculate1() {
List<Map<String, Object>> prometheusData = 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(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__"));
assertEquals(200.0, result.get(1).get("__value__"));
}
@Test
void calculate2() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total <= 100");
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate3() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total >= 200");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(200.0, result.get(1).get("__value__"));
}
@Test
void calculate4() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 250");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate5() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total < 100");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate6() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total > 200");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate7() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "(node_cpu_seconds_total <= 100)");
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate8() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"}[4m] <= 100");
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate9() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} == 100");
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
assertNull(result.get(1).get("__value__"));
}
@Test
void calculate10() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} != 100");
assertEquals(2, result.size());
assertNull(result.get(0).get("__value__"));
assertEquals(200.0, result.get(1).get("__value__"));
}
@Test
void calculate11() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
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\"} < 120");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
void calculate12() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
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\"} < 120)");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
void calculate13() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 150 and node_cpu_seconds_total{mode=\"idle\"} < 220");
assertEquals(1, result.size());
assertEquals(200.0, result.get(0).get("__value__"));
}
@Test
void calculate14() {
List<Map<String, Object>> prometheusData = 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(Mockito.anyString())).thenReturn(prometheusData);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "(node_cpu_seconds_total{mode=\"user\"} > 150) and (node_cpu_seconds_total{mode=\"idle\"} < 220)");
assertEquals(1, result.size());
assertEquals(200.0, result.get(0).get("__value__"));
}
@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"))
);
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);
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__"));
}
@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"))
);
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);
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");
assertEquals(1, result.size());
assertNull(result.get(0).get("__value__"));
}
@Test
void calculate17() {
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"))
);
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);
dataSourceService.setExecutors(List.of(mockExecutor));
List<Map<String, Object>> result = dataSourceService.calculate("promql", "node_cpu_seconds_total{mode=\"user\"} > 150 or node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(1, result.size());
assertEquals(200.0, result.get(0).get("__value__"));
}
@Test
void calculate18() {
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"))
);
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);
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\"} < 120");
assertEquals(1, result.size());
assertEquals(100.0, result.get(0).get("__value__"));
}
@Test
void calculate19() {
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"))
);
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);
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__"));
}
@Test
void calculate20() {
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"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "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);
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__"));
}
@Test
void calculate21() {
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"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "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);
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__"));
}
@Test
void calculate22() {
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"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "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);
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__"));
}
@Test
void calculate23() {
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"))
);
List<Map<String, Object>> prometheusData2 = List.of(
new HashMap<>(Map.of("__value__", 100.0, "timestamp", 1343554, "key", "node1")),
new HashMap<>(Map.of("__value__", 200.0, "timestamp", 1343555, "book", "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);
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__"));
}
@Test
void calculate24() {
List<Map<String, Object>> prometheusData = List.of();
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(0, result.size());
}
@Test
void calculate25() {
List<Map<String, Object>> prometheusData = List.of();
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{mode=\"user\"} > 250 unless node_cpu_seconds_total{mode=\"idle\"} < 20");
assertEquals(0, result.size());
}
@Test
void calculate26() {
List<Map<String, Object>> prometheusData = List.of(
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(1, result.size());
assertEquals(200.0, result.get(0).get("__value__"));
}
}
@@ -17,33 +17,42 @@
package org.apache.hertzbeat.alert.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.apache.hertzbeat.alert.dao.NoticeReceiverDao;
import org.apache.hertzbeat.alert.dao.NoticeRuleDao;
import org.apache.hertzbeat.alert.dao.NoticeTemplateDao;
import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch;
import org.apache.hertzbeat.alert.service.impl.NoticeConfigServiceImpl;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.NoticeRule;
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
import org.apache.hertzbeat.alert.notice.AlertNoticeDispatch;
import org.apache.hertzbeat.alert.dao.NoticeReceiverDao;
import org.apache.hertzbeat.alert.dao.NoticeRuleDao;
import org.apache.hertzbeat.alert.dao.NoticeTemplateDao;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test case for {@link NoticeConfigService}
*/
@Disabled
@ExtendWith(MockitoExtension.class)
class NoticeConfigServiceTest {
@@ -58,26 +67,130 @@ class NoticeConfigServiceTest {
@InjectMocks
private NoticeConfigServiceImpl noticeConfigService;
private NoticeReceiver receiver1;
private NoticeReceiver receiver2;
private NoticeTemplate template1;
private NoticeTemplate template2;
private NoticeRule rule1;
private NoticeRule rule2;
@BeforeEach
void setUp() {
receiver1 = new NoticeReceiver();
receiver1.setId(1L);
receiver1.setName("Receiver1");
receiver2 = new NoticeReceiver();
receiver2.setId(2L);
receiver2.setName("Receiver2");
template1 = new NoticeTemplate();
template1.setId(1L);
template1.setName("Template1");
template2 = new NoticeTemplate();
template2.setId(2L);
template2.setName("Template2");
rule1 = new NoticeRule();
rule1.setId(1L);
rule1.setName("Rule1");
rule2 = new NoticeRule();
rule2.setId(2L);
rule2.setName("Rule2");
}
@Test
void getNoticeReceivers() {
noticeConfigService.getNoticeReceivers(null);
verify(noticeReceiverDao, times(1)).findAll(any(Specification.class));
Page<NoticeReceiver> receiverPage = new PageImpl<>(
Arrays.asList(receiver1, receiver2),
PageRequest.of(0, 8, Sort.by(Sort.Direction.DESC, "id")),
2
);
when(noticeReceiverDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(receiverPage);
Page<NoticeReceiver> result = noticeConfigService.getNoticeReceivers("Receiver", 0, 8);
assertEquals(2, result.getTotalElements());
assertEquals(1, result.getTotalPages());
assertEquals(8, result.getSize());
assertEquals(0, result.getNumber());
assertEquals(receiver1, result.getContent().get(0));
assertEquals(receiver2, result.getContent().get(1));
verify(noticeReceiverDao, times(1)).findAll(any(Specification.class), any(PageRequest.class));
}
@Test
void getAllNoticeReceivers() {
when(noticeReceiverDao.findAll()).thenReturn(Arrays.asList(receiver1, receiver2));
List<NoticeReceiver> result = noticeConfigService.getAllNoticeReceivers();
assertEquals(2, result.size());
assertEquals(receiver1, result.get(0));
assertEquals(receiver2, result.get(1));
verify(noticeReceiverDao, times(1)).findAll();
}
@Test
void getNoticeTemplates() {
noticeConfigService.getNoticeTemplates(null);
verify(noticeTemplateDao, times(1)).findAll(any(Specification.class));
Page<NoticeTemplate> templatePage = new PageImpl<>(
Arrays.asList(template1, template2),
PageRequest.of(0, 8, Sort.by(Sort.Direction.DESC, "id")),
2
);
when(noticeTemplateDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(templatePage);
Page<NoticeTemplate> result = noticeConfigService.getNoticeTemplates("Template", false, 0, 8);
assertEquals(2, result.getTotalElements());
assertEquals(1, result.getTotalPages());
assertEquals(8, result.getSize());
assertEquals(0, result.getNumber());
assertEquals(template1, result.getContent().get(0));
assertEquals(template2, result.getContent().get(1));
verify(noticeTemplateDao, times(1)).findAll(any(Specification.class), any(PageRequest.class));
}
@Test
void getAllNoticeTemplates() {
when(noticeTemplateDao.findAll()).thenReturn(Arrays.asList(template1, template2));
List<NoticeTemplate> result = noticeConfigService.getAllNoticeTemplates();
assert result.size() >= 2;
assertEquals(template1, result.get(result.size() - 2));
assertEquals(template2, result.get(result.size() - 1));
verify(noticeTemplateDao, times(1)).findAll();
}
@Test
void getNoticeRules() {
noticeConfigService.getNoticeRules(null);
verify(noticeRuleDao, times(1)).findAll(any(Specification.class));
Page<NoticeRule> rulePage = new PageImpl<>(
Arrays.asList(rule1, rule2),
PageRequest.of(0, 8, Sort.by(Sort.Direction.DESC, "id")),
2
);
when(noticeRuleDao.findAll(any(Specification.class), any(PageRequest.class))).thenReturn(rulePage);
Page<NoticeRule> result = noticeConfigService.getNoticeRules("Rule", 0, 8);
assertEquals(2, result.getTotalElements());
assertEquals(1, result.getTotalPages());
assertEquals(8, result.getSize());
assertEquals(0, result.getNumber());
assertEquals(rule1, result.getContent().get(0));
assertEquals(rule2, result.getContent().get(1));
verify(noticeRuleDao, times(1)).findAll(any(Specification.class), any(PageRequest.class));
}
@Test
@@ -155,5 +155,9 @@
<artifactId>plc4j-driver-modbus</artifactId>
<version>0.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
</dependency>
</dependencies>
</project>
@@ -26,20 +26,25 @@ import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
import org.apache.hertzbeat.collector.collect.common.cache.JdbcConnect;
import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
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.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.SshTunnel;
import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
import org.postgresql.util.PSQLException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
@@ -55,7 +60,7 @@ public class JdbcCommonCollect extends AbstractCollect {
private static final String QUERY_TYPE_MULTI_ROW = "multiRow";
private static final String QUERY_TYPE_COLUMNS = "columns";
private static final String RUN_SCRIPT = "runScript";
private static final String[] VULNERABLE_KEYWORDS = {"allowLoadLocalInfile", "allowLoadLocalInfileInPath", "useLocalInfile"};
private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance();
@@ -73,16 +78,26 @@ public class JdbcCommonCollect extends AbstractCollect {
}
}
}
SshTunnelHelper.checkTunnelParam(metrics.getJdbc().getSshTunnel());
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
long startTime = System.currentTimeMillis();
JdbcProtocol jdbcProtocol = metrics.getJdbc();
String databaseUrl = constructDatabaseUrl(jdbcProtocol);
SshTunnel sshTunnel = jdbcProtocol.getSshTunnel();
int timeout = CollectUtil.getTimeout(jdbcProtocol.getTimeout());
Statement statement = null;
String databaseUrl;
try {
if (sshTunnel != null && Boolean.parseBoolean(sshTunnel.getEnable())) {
int localPort = SshTunnelHelper.localPortForward(sshTunnel, jdbcProtocol.getHost(), jdbcProtocol.getPort());
databaseUrl = constructDatabaseUrl(jdbcProtocol, "localhost", String.valueOf(localPort));
} else {
databaseUrl = constructDatabaseUrl(jdbcProtocol, jdbcProtocol.getHost(), jdbcProtocol.getPort());
}
statement = getConnection(jdbcProtocol.getUsername(),
jdbcProtocol.getPassword(), databaseUrl, timeout);
switch (jdbcProtocol.getQueryType()) {
@@ -112,6 +127,14 @@ public class JdbcCommonCollect extends AbstractCollect {
log.warn("Jdbc sql error: {}, code: {}.", sqlException.getMessage(), sqlException.getErrorCode());
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Query Error: " + sqlException.getMessage() + " Code: " + sqlException.getErrorCode());
} catch (SshException sshException) {
Throwable throwable = sshException.getCause();
if (throwable instanceof SshChannelOpenException) {
log.warn("[Jdbc collect] Remote ssh server no more session channel, please increase sshd_config MaxSessions.");
}
String errorMsg = CommonUtil.getMessageFromThrowable(sshException);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg("Peer ssh connection failed: " + errorMsg);
} catch (Exception e) {
String errorMessage = CommonUtil.getMessageFromThrowable(e);
log.error("Jdbc error: {}.", errorMessage, e);
@@ -185,13 +208,14 @@ public class JdbcCommonCollect extends AbstractCollect {
* eg:
* query metricsone tow three four
* query sqlselect one, tow, three, four from book limit 1;
*
* @param statement statement
* @param sql sql
* @param columns query metrics field list
* @param sql sql
* @param columns query metrics field list
* @throws Exception when error happen
*/
private void queryOneRow(Statement statement, String sql, List<String> columns,
CollectRep.MetricsData.Builder builder, long startTime) throws Exception {
CollectRep.MetricsData.Builder builder, long startTime) throws Exception {
statement.setMaxRows(1);
try (ResultSet resultSet = statement.executeQuery(sql)) {
if (resultSet.next()) {
@@ -216,14 +240,15 @@ public class JdbcCommonCollect extends AbstractCollect {
* eg:
* query metricsone two three four
* query sqlselect key, value from book; the key is the query metrics fields
* select key, value from book;
* select key, value from book;
* one - value1
* two - value2
* three - value3
* four - value4
*
* @param statement statement
* @param sql sql
* @param columns query metrics field list
* @param sql sql
* @param columns query metrics field list
* @throws Exception when error happen
*/
private void queryOneRowByMatchTwoColumns(Statement statement, String sql, List<String> columns,
@@ -256,9 +281,10 @@ public class JdbcCommonCollect extends AbstractCollect {
* query metricsone tow three four
* query sqlselect one, tow, three, four from book;
* and return multi row record mapping with the metrics
*
* @param statement statement
* @param sql sql
* @param columns query metrics field list
* @param sql sql
* @param columns query metrics field list
* @throws Exception when error happen
*/
private void queryMultiRow(Statement statement, String sql, List<String> columns,
@@ -283,14 +309,16 @@ public class JdbcCommonCollect extends AbstractCollect {
/**
* construct jdbc url due the jdbc protocol
*
* @param jdbcProtocol jdbc
* @return URL
*/
private String constructDatabaseUrl(JdbcProtocol jdbcProtocol) {
private String constructDatabaseUrl(JdbcProtocol jdbcProtocol, String host, String port) {
if (Objects.nonNull(jdbcProtocol.getUrl())
&& !Objects.equals("", jdbcProtocol.getUrl())
&& jdbcProtocol.getUrl().startsWith("jdbc")) {
String url = jdbcProtocol.getUrl().toLowerCase(); // convert the URL to lowercase for case-insensitive checking
// convert the URL to lowercase for case-insensitive checking
String url = jdbcProtocol.getUrl().toLowerCase();
// check whether the parameter is valid
if (url.contains("create trigger") || url.contains("create alias") || url.contains("runscript from")
|| url.contains("allowloadlocalinfile") || url.contains("allowloadlocalinfileinpath")
@@ -302,25 +330,19 @@ public class JdbcCommonCollect extends AbstractCollect {
return jdbcProtocol.getUrl();
}
return switch (jdbcProtocol.getPlatform()) {
case "mysql", "mariadb" ->
"jdbc:mysql://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase())
+ "?useUnicode=true&characterEncoding=utf-8&useSSL=false";
case "postgresql" ->
"jdbc:postgresql://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "postgresql" -> "jdbc:postgresql://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "clickhouse" ->
"jdbc:clickhouse://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "clickhouse" -> "jdbc:clickhouse://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "sqlserver" ->
"jdbc:sqlserver://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "sqlserver" -> "jdbc:sqlserver://" + host + ":" + port
+ ";" + (jdbcProtocol.getDatabase() == null ? "" : "DatabaseName=" + jdbcProtocol.getDatabase())
+ ";trustServerCertificate=true;";
case "oracle" ->
"jdbc:oracle:thin:@" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
case "oracle" -> "jdbc:oracle:thin:@" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "dm" ->
"jdbc:dm://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort();
case "dm" -> "jdbc:dm://" + host + ":" + port;
default -> throw new IllegalArgumentException("Not support database platform: " + jdbcProtocol.getPlatform());
};
}
@@ -17,9 +17,11 @@
package org.apache.hertzbeat.collector.collect.ftp;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
@@ -29,6 +31,10 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.FtpProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.sftp.client.SftpClient;
import org.apache.sshd.sftp.client.SftpClientFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -56,33 +62,14 @@ public class FtpCollectImpl extends AbstractCollect {
Assert.hasText(ftpProtocol.getTimeout(), "Ftp Protocol timeout is required.");
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
FTPClient ftpClient = new FTPClient();
FtpProtocol ftpProtocol = metrics.getFtp();
// Set timeout
ftpClient.setControlKeepAliveReplyTimeout(Integer.parseInt(ftpProtocol.getTimeout()));
// Collect data to load in CollectRep.ValueRow.Builder's object
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
Map<String, String> valueMap;
try {
valueMap = collectValue(ftpClient, ftpProtocol);
metrics.getAliasFields().forEach(it -> {
if (valueMap.containsKey(it)) {
String fieldValue = valueMap.get(it);
valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
}
});
} catch (Exception e) {
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(e.getMessage());
return;
boolean ssl = Boolean.parseBoolean(metrics.getFtp().getSsl());
if (ssl){
handleSftpCollect(builder, metrics);
} else {
handleFtpCollect(builder, metrics);
}
builder.addValueRow(valueRowBuilder.build());
}
/**
@@ -113,8 +100,23 @@ public class FtpCollectImpl extends AbstractCollect {
};
}
private Map<String, String> collectValue(SftpClient sftpClient, FtpProtocol ftpProtocol) {
boolean isActive;
String responseTime;
try {
long startTime = System.currentTimeMillis();
sftpClient.stat(ftpProtocol.getDirection());
isActive = true;
long endTime = System.currentTimeMillis();
responseTime = String.valueOf(endTime - startTime);
} catch (IOException e) {
throw new IllegalArgumentException("[SFTPClient] error: {}" + CommonUtil.getMessageFromThrowable(e), e);
}
return Map.of("isActive", Boolean.toString(isActive), "responseTime", responseTime);
}
/**
* login
* ftp login
*/
private void login(FTPClient ftpClient, FtpProtocol ftpProtocol) {
try {
@@ -147,8 +149,84 @@ public class FtpCollectImpl extends AbstractCollect {
}
}
private ClientSession connect(SshClient client, FtpProtocol ftpProtocol) {
client.start();
try {
ClientSession session = client.connect(ftpProtocol.getUsername(), ftpProtocol.getHost(), Integer.parseInt(ftpProtocol.getPort()))
.verify(Integer.parseInt(ftpProtocol.getTimeout()))
.getSession();
session.addPasswordIdentity(ftpProtocol.getPassword());
session.auth().verify(Integer.parseInt(ftpProtocol.getTimeout()));
return session;
} catch (Exception e) {
throw new IllegalArgumentException("[sftp connection] error: {}" + CommonUtil.getMessageFromThrowable(e), e);
}
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_FTP;
}
}
private void handleFtpCollect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
FTPClient ftpClient = new FTPClient();
FtpProtocol ftpProtocol = metrics.getFtp();
// Set timeout
ftpClient.setControlKeepAliveReplyTimeout(Integer.parseInt(ftpProtocol.getTimeout()));
// Collect data to load in CollectRep.ValueRow.Builder's object
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
Map<String, String> valueMap;
try {
valueMap = collectValue(ftpClient, ftpProtocol);
metrics.getAliasFields().forEach(it -> {
if (valueMap.containsKey(it)) {
String fieldValue = valueMap.get(it);
valueRowBuilder.addColumn(Objects.requireNonNullElse(fieldValue, CommonConstants.NULL_VALUE));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
}
});
} catch (Exception e) {
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(e.getMessage());
return;
}
builder.addValueRow(valueRowBuilder.build());
}
private void handleSftpCollect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
FtpProtocol ftpProtocol = metrics.getFtp();
ClientSession session = null;
SftpClient sftpClient = null;
SshClient client = null;
try {
client = SshClient.setUpDefaultClient();
session = connect(client, ftpProtocol);
sftpClient = SftpClientFactory.instance().createSftpClient(session);
Map<String, String> valueMap = collectValue(sftpClient, ftpProtocol);
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
metrics.getAliasFields().forEach(it ->
valueRowBuilder.addColumn(valueMap.getOrDefault(it, CommonConstants.NULL_VALUE))
);
builder.addValueRow(valueRowBuilder.build());
} catch (Exception e) {
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(e.getMessage());
} finally {
try {
if (sftpClient != null && sftpClient.isOpen()){
sftpClient.close();
}
if (session != null && session.isOpen()){
session.close();
}
if (client != null && client.isOpen()){
client.close();
}
} catch (Exception e){
log.error("[SFTPClient] error while closing: {}", CommonUtil.getMessageFromThrowable(e), e);
}
}
}
}
@@ -41,7 +41,6 @@ import javax.net.ssl.SSLException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.util.Base64;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.collect.http.promethus.AbstractPrometheusParse;
@@ -59,6 +58,7 @@ 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.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.Header;
@@ -153,7 +153,7 @@ public class HttpCollectImpl extends AbstractCollect {
case DispatchConstants.PARSE_XML_PATH ->
parseResponseByXmlPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder);
case DispatchConstants.PARSE_WEBSITE ->
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime, response);
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime);
case DispatchConstants.PARSE_SITE_MAP ->
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
case DispatchConstants.PARSE_HEADER ->
@@ -222,8 +222,7 @@ public class HttpCollectImpl extends AbstractCollect {
}
private void parseResponseByWebsite(String resp, Metrics metrics, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime,
CloseableHttpResponse response) {
CollectRep.MetricsData.Builder builder, Long responseTime) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
for (String alias : metrics.getAliasFields()) {
@@ -290,9 +289,10 @@ public class HttpCollectImpl extends AbstractCollect {
long startTime = System.currentTimeMillis();
try {
HttpGet httpGet = new HttpGet(siteUrl);
CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet);
statusCode = response.getStatusLine().getStatusCode();
EntityUtils.consume(response.getEntity());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet)) {
statusCode = response.getStatusLine().getStatusCode();
EntityUtils.consume(response.getEntity());
}
} catch (ClientProtocolException e1) {
if (e1.getCause() != null) {
errorMsg = e1.getCause().getMessage();
@@ -549,7 +549,7 @@ public class HttpCollectImpl extends AbstractCollect {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + SignConstants.DOUBLE_MARK + authorization.getBasicAuthPassword();
String encodedAuth = new String(Base64.encodeBase64(authStr.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth);
}
}
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.collector.collect.http.promethus.exporter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -419,7 +418,7 @@ public class ExporterParser {
* @return true/false
*/
private boolean isValidLabelValue(String s) {
return s != null && s.equals(new String(s.getBytes(StandardCharsets.UTF_8)));
return s != null;
}
private boolean isSum(String s) {
@@ -32,7 +32,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.SSLException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.util.Base64;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily;
import org.apache.hertzbeat.collector.collect.prometheus.parser.TextParser;
@@ -44,6 +43,7 @@ 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;
@@ -89,9 +89,8 @@ public class PrometheusAutoCollectImpl {
}
HttpContext httpContext = createHttpContext(metrics.getPrometheus());
HttpUriRequest request = createHttpRequest(metrics.getPrometheus());
try {
CloseableHttpResponse response = CommonHttpClient.getHttpClient()
.execute(request, httpContext);
try (CloseableHttpResponse response =
CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
boolean isSuccessInvoke = defaultSuccessStatusCodes.contains(statusCode);
log.debug("http response status: {}", statusCode);
@@ -278,7 +277,7 @@ public class PrometheusAutoCollectImpl {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword();
String encodedAuth = new String(Base64.encodeBase64(authStr.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth);
}
}
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.collector.collect.prometheus.parser;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -300,6 +299,6 @@ public class TextParser {
* @return true/false
*/
private static boolean isValidLabelValue(String s) {
return s != null && s.equals(new String(s.getBytes(StandardCharsets.UTF_8)));
return s != null;
}
}
@@ -86,8 +86,7 @@ public class PushCollectImpl extends AbstractCollect {
HttpContext httpContext = createHttpContext(pushProtocol);
HttpUriRequest request = createHttpRequest(pushProtocol, monitorId, time);
try {
CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext);
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != SUCCESS_CODE) {
builder.setCode(CollectRep.Code.FAIL);
@@ -28,6 +28,8 @@ import io.lettuce.core.cluster.models.partitions.Partitions;
import io.lettuce.core.cluster.models.partitions.RedisClusterNode;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@@ -42,6 +44,7 @@ import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
import org.apache.hertzbeat.collector.collect.common.cache.RedisConnect;
import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
@@ -51,6 +54,8 @@ import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.MapCapUtil;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -79,6 +84,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
RedisProtocol redisProtocol = metrics.getRedis();
Assert.hasText(redisProtocol.getHost(), "Redis Protocol host is required.");
Assert.hasText(redisProtocol.getPort(), "Redis Protocol port is required.");
SshTunnelHelper.checkTunnelParam(metrics.getRedis().getSshTunnel());
}
@Override
@@ -96,6 +102,14 @@ public class RedisCommonCollectImpl extends AbstractCollect {
log.info("[redis connection] error: {}", errorMsg);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg(errorMsg);
} catch (SshException sshException) {
Throwable throwable = sshException.getCause();
if (throwable instanceof SshChannelOpenException) {
log.warn("[redis collect] Remote ssh server no more session channel, please increase sshd_config MaxSessions.");
}
String errorMsg = CommonUtil.getMessageFromThrowable(sshException);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder.setMsg("Peer ssh connection failed: " + errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("[redis collect] error: {}", e.getMessage(), e);
@@ -109,7 +123,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param metrics metrics config
* @return data
*/
private Map<String, String> getSingleRedisInfo(Metrics metrics) {
private Map<String, String> getSingleRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException {
StatefulRedisConnection<String, String> connection = getSingleConnection(metrics.getRedis());
String info = connection.sync().info(metrics.getName());
Map<String, String> valueMap = parseInfo(info, metrics);
@@ -125,7 +139,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param metrics metrics config
* @return data
*/
private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) {
private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException {
Map<String, StatefulRedisClusterConnection<String, String>> connectionMap = getConnectionList(metrics.getRedis());
List<Map<String, String>> list = new ArrayList<>(connectionMap.size());
connectionMap.forEach((identity, connection) ->{
@@ -179,12 +193,16 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol protocol
* @return connection
*/
private StatefulRedisConnection<String, String> getSingleConnection(RedisProtocol redisProtocol) {
CacheIdentifier identifier = doIdentifier(redisProtocol);
private StatefulRedisConnection<String, String> getSingleConnection(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
String[] resolvedArr = resolveHostAndPort(redisProtocol);
String host = resolvedArr[0];
String port = resolvedArr[1];
CacheIdentifier identifier = doIdentifier(redisProtocol, host, port);
StatefulRedisConnection<String, String> connection = (StatefulRedisConnection<String, String>) getStatefulConnection(identifier);
if (Objects.isNull(connection)) {
// reuse connection failed, new one
RedisClient redisClient = buildSingleClient(redisProtocol);
RedisClient redisClient = buildSingleClient(redisProtocol, host, port);
connection = redisClient.connect();
connectionCache.addCache(identifier, new RedisConnect(connection));
}
@@ -196,7 +214,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol protocol
* @return connection map
*/
private Map<String, StatefulRedisClusterConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) {
private Map<String, StatefulRedisClusterConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
// first connection
StatefulRedisClusterConnection<String, String> connection = getClusterConnection(redisProtocol);
Partitions partitions = connection.getPartitions();
@@ -217,12 +235,16 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol redis protocol
* @return cluster connection
*/
private StatefulRedisClusterConnection<String, String> getClusterConnection(RedisProtocol redisProtocol) {
CacheIdentifier identifier = doIdentifier(redisProtocol);
private StatefulRedisClusterConnection<String, String> getClusterConnection(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
String[] resolvedArr = resolveHostAndPort(redisProtocol);
String host = resolvedArr[0];
String port = resolvedArr[1];
CacheIdentifier identifier = doIdentifier(redisProtocol, host, port);
StatefulRedisClusterConnection<String, String> connection = (StatefulRedisClusterConnection<String, String>) getStatefulConnection(identifier);
if (connection == null) {
// reuse connection failed, new one
RedisClusterClient redisClusterClient = buildClusterClient(redisProtocol);
RedisClusterClient redisClusterClient = buildClusterClient(redisProtocol, host, port);
connection = redisClusterClient.connect();
connectionCache.addCache(identifier, new RedisConnect(connection));
}
@@ -260,8 +282,8 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol redis protocol config
* @return redis cluster client
*/
private RedisClusterClient buildClusterClient(RedisProtocol redisProtocol) {
return RedisClusterClient.create(defaultClientResources, redisUri(redisProtocol));
private RedisClusterClient buildClusterClient(RedisProtocol redisProtocol, String host, String port) {
return RedisClusterClient.create(defaultClientResources, redisUri(redisProtocol, host, port));
}
/**
@@ -270,12 +292,12 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol redis protocol config
* @return redis single client
*/
private RedisClient buildSingleClient(RedisProtocol redisProtocol) {
return RedisClient.create(defaultClientResources, redisUri(redisProtocol));
private RedisClient buildSingleClient(RedisProtocol redisProtocol, String host, String port) {
return RedisClient.create(defaultClientResources, redisUri(redisProtocol, host, port));
}
private RedisURI redisUri(RedisProtocol redisProtocol) {
RedisURI.Builder redisUriBuilder = RedisURI.builder().withHost(redisProtocol.getHost()).withPort(Integer.parseInt(redisProtocol.getPort()));
private RedisURI redisUri(RedisProtocol redisProtocol, String host, String port) {
RedisURI.Builder redisUriBuilder = RedisURI.builder().withHost(host).withPort(Integer.parseInt(port));
if (StringUtils.hasText(redisProtocol.getUsername())) {
redisUriBuilder.withClientName(redisProtocol.getUsername());
}
@@ -295,10 +317,10 @@ public class RedisCommonCollectImpl extends AbstractCollect {
return ip + SignConstants.DOUBLE_MARK + port;
}
private CacheIdentifier doIdentifier(RedisProtocol redisProtocol) {
private CacheIdentifier doIdentifier(RedisProtocol redisProtocol, String host, String port) {
return CacheIdentifier.builder()
.ip(redisProtocol.getHost())
.port(redisProtocol.getPort())
.ip(host)
.port(port)
.username(redisProtocol.getUsername())
.password(redisProtocol.getPassword())
.customArg(redisProtocol.getPattern())
@@ -328,6 +350,22 @@ public class RedisCommonCollectImpl extends AbstractCollect {
return result;
}
private String[] resolveHostAndPort(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
boolean enableSshTunnel = Optional.ofNullable(redisProtocol.getSshTunnel())
.map(ssh -> Boolean.parseBoolean(ssh.getEnable()))
.orElse(false);
String host;
String port;
if (enableSshTunnel){
host = "localhost";
port = String.valueOf(SshTunnelHelper.localPortForward(redisProtocol.getSshTunnel(), redisProtocol.getHost(), redisProtocol.getPort()));
} else {
host = redisProtocol.getHost();
port = redisProtocol.getPort();
}
return new String[]{host, port};
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_REDIS;
@@ -18,7 +18,6 @@
package org.apache.hertzbeat.collector.collect.ssh;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
@@ -30,35 +29,27 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
import org.apache.hertzbeat.collector.collect.common.cache.SshConnect;
import org.apache.hertzbeat.collector.collect.common.ssh.CommonSshBlacklist;
import org.apache.hertzbeat.collector.collect.common.ssh.CommonSshClient;
import org.apache.hertzbeat.collector.collect.common.ssh.SshHelper;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.collector.util.PrivateKeyUtils;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.util.io.output.NoCloseOutputStream;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.springframework.util.StringUtils;
/**
@@ -291,57 +282,9 @@ public class SshCollectImpl extends AbstractCollect {
private ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection)
throws IOException, GeneralSecurityException {
CacheIdentifier identifier = CacheIdentifier.builder()
.ip(sshProtocol.getHost()).port(sshProtocol.getPort())
.username(sshProtocol.getUsername()).password(sshProtocol.getPassword())
.build();
ClientSession clientSession = null;
if (reuseConnection) {
Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true);
if (cacheOption.isPresent()) {
SshConnect sshConnect = (SshConnect) cacheOption.get();
clientSession = sshConnect.getConnection();
try {
if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) {
clientSession = null;
connectionCommonCache.removeCache(identifier);
}
} catch (Exception e) {
log.warn(e.getMessage());
clientSession = null;
connectionCommonCache.removeCache(identifier);
}
}
if (clientSession != null) {
return clientSession;
}
}
SshClient sshClient = CommonSshClient.getSshClient();
clientSession = sshClient.connect(sshProtocol.getUsername(), sshProtocol.getHost(), Integer.parseInt(sshProtocol.getPort()))
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
if (StringUtils.hasText(sshProtocol.getPassword())) {
clientSession.addPasswordIdentity(sshProtocol.getPassword());
} else if (StringUtils.hasText(sshProtocol.getPrivateKey())) {
var resourceKey = PrivateKeyUtils.writePrivateKey(sshProtocol.getHost(), sshProtocol.getPrivateKey());
FilePasswordProvider passwordProvider = (session, resource, index) -> {
if (StringUtils.hasText(sshProtocol.getPrivateKeyPassphrase())) {
return sshProtocol.getPrivateKeyPassphrase();
}
return null;
};
SecurityUtils.loadKeyPairIdentities(null, () -> resourceKey, new FileInputStream(resourceKey), passwordProvider)
.forEach(clientSession::addPublicKeyIdentity);
} // else auth with localhost private public key certificates
// auth
if (!clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS).isSuccess()) {
clientSession.close();
throw new IllegalArgumentException("ssh auth failed.");
}
if (reuseConnection) {
SshConnect sshConnect = new SshConnect(clientSession);
connectionCommonCache.addCache(identifier, sshConnect);
}
return clientSession;
return SshHelper.getConnectSession(
sshProtocol.getHost(), sshProtocol.getPort(), sshProtocol.getUsername(), sshProtocol.getPassword(),
sshProtocol.getPrivateKey(), sshProtocol.getPrivateKeyPassphrase(), timeout, reuseConnection
);
}
}
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.collector.collect.telnet;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ConnectException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -122,7 +123,7 @@ public class TelnetCollectImpl extends AbstractCollect {
return new HashMap<>(16);
}
OutputStream outputStream = telnetClient.getOutputStream();
outputStream.write(cmd.getBytes());
outputStream.write(cmd.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
String result = new String(telnetClient.getInputStream().readAllBytes());
String[] lines = result.split("\n");
@@ -28,6 +28,7 @@ import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
@@ -128,16 +129,16 @@ public class WebsocketCollectImpl extends AbstractCollect {
byte[] key = generateRandomKey();
String base64Key = base64Encode(key);
String requestLine = "GET " + websocketProtocol.getPath() + " HTTP/1.1\r\n";
out.write(requestLine.getBytes());
out.write(requestLine.getBytes(StandardCharsets.UTF_8));
String hostName = InetAddress.getLocalHost().getHostAddress();
out.write(("Host:" + hostName + "\r\n").getBytes());
out.write("Upgrade: websocket\r\n".getBytes());
out.write("Connection: Upgrade\r\n".getBytes());
out.write("Sec-WebSocket-Version: 13\r\n".getBytes());
out.write("Sec-WebSocket-Extensions: chat, superchat\r\n".getBytes());
out.write(("Sec-WebSocket-Key: " + base64Key + "\r\n").getBytes());
out.write("Content-Length: 0\r\n".getBytes());
out.write("\r\n".getBytes());
out.write(("Host:" + hostName + "\r\n").getBytes(StandardCharsets.UTF_8));
out.write("Upgrade: websocket\r\n".getBytes(StandardCharsets.UTF_8));
out.write("Connection: Upgrade\r\n".getBytes(StandardCharsets.UTF_8));
out.write("Sec-WebSocket-Version: 13\r\n".getBytes(StandardCharsets.UTF_8));
out.write("Sec-WebSocket-Extensions: chat, superchat\r\n".getBytes(StandardCharsets.UTF_8));
out.write(("Sec-WebSocket-Key: " + base64Key + "\r\n").getBytes(StandardCharsets.UTF_8));
out.write("Content-Length: 0\r\n".getBytes(StandardCharsets.UTF_8));
out.write("\r\n".getBytes(StandardCharsets.UTF_8));
out.flush();
}
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.collector.collect.database;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
@@ -99,28 +100,27 @@ class JdbcCommonCollectTest {
"dm"
};
for (String platform : platforms) {
assertDoesNotThrow(() -> {
JdbcProtocol jdbc = new JdbcProtocol();
jdbc.setPlatform(platform);
Metrics metrics = new Metrics();
metrics.setJdbc(jdbc);
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
jdbcCommonCollect.collect(builder, metrics);
});
}
// invalid platform
assertThrows(IllegalArgumentException.class, () -> {
JdbcProtocol jdbc = new JdbcProtocol();
jdbc.setPlatform("invalid");
jdbc.setPlatform(platform);
Metrics metrics = new Metrics();
metrics.setJdbc(jdbc);
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
jdbcCommonCollect.collect(builder, metrics);
});
assertNotEquals(builder.getMsg(), "Query Error: Not support database platform: " + platform);
}
// invalid platform
JdbcProtocol jdbc = new JdbcProtocol();
jdbc.setPlatform("invalid");
Metrics metrics = new Metrics();
metrics.setJdbc(jdbc);
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
jdbcCommonCollect.collect(builder, metrics);
assertEquals(builder.getCode(), CollectRep.Code.FAIL);
assertEquals(builder.getMsg(), "Query Error: Not support database platform: invalid");
}
@Test
@@ -17,68 +17,63 @@
package org.apache.hertzbeat.collector.collect.prometheus.parser;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class OnlineParserTest {
@Disabled // Disabled due to the fact that the URL is not reachable unless you have the Prometheus server running
@Test
void parseMetrics() {
try {
URL url = new URL("http://localhost:9090/metrics");
InputStream inputStream = url.openStream();
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
System.out.println(1);
}
catch (Exception e) {
e.printStackTrace();
}
void parseMetrics() throws Exception {
URL url = new URL("http://localhost:9090/metrics");
InputStream inputStream = url.openStream();
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
assertNotNull(metricFamilyMap);
}
@Test
void parseMetrics2() {
try {
String str = """
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds { quantile="0"} 2.0209e-05 321312
go_gc_duration_seconds{ quantile = "0.25" } 6.6917e-05
go_gc_duration_seconds{quantile="0.5"} -Inf
go_gc_duration_seconds{ quantile = "0.75"} +Inf
go_gc_duration_seconds{quantile="1"} NaN
go_gc_duration_seconds_sum 0.001134793 321314
go_gc_duration_seconds_count 5 43
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 32
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.21.6"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 1.5716224e+07
# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.
# TYPE go_memstats_alloc_bytes_total counter
go_memstats_alloc_bytes_total 2.0707544e+07
# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.
# TYPE go_memstats_buck_hash_sys_bytes gauge
go_memstats_buck_hash_sys_bytes 1.457881e+06
# HELP go_memstats_frees_total Total number of frees.
# TYPE go_memstats_frees_total counter
go_memstats_frees_total 50438
# HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.
# TYPE go_memstats_gc_sys_bytes gauge
go_memstats_gc_sys_bytes 4.614808e+06""";
InputStream inputStream = new ByteArrayInputStream(str.getBytes());
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
System.out.println(1);
}
catch (Exception e) {
e.printStackTrace();
}
void parseMetrics2() throws Exception {
String str = """
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds { quantile="0"} 2.0209e-05 321312
go_gc_duration_seconds{ quantile = "0.25" } 6.6917e-05
go_gc_duration_seconds{quantile="0.5"} -Inf
go_gc_duration_seconds{ quantile = "0.75"} +Inf
go_gc_duration_seconds{quantile="1"} NaN
go_gc_duration_seconds_sum 0.001134793 321314
go_gc_duration_seconds_count 5 43
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 32
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.21.6"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 1.5716224e+07
# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.
# TYPE go_memstats_alloc_bytes_total counter
go_memstats_alloc_bytes_total 2.0707544e+07
# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.
# TYPE go_memstats_buck_hash_sys_bytes gauge
go_memstats_buck_hash_sys_bytes 1.457881e+06
# HELP go_memstats_frees_total Total number of frees.
# TYPE go_memstats_frees_total counter
go_memstats_frees_total 50438
# HELP go_memstats_gc_sys_bytes Number of bytes used for garbage collection system metadata.
# TYPE go_memstats_gc_sys_bytes gauge
go_memstats_gc_sys_bytes 4.614808e+06""";
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
assertNotNull(metricFamilyMap);
}
}
@@ -144,12 +144,25 @@ public class CommonHttpClient {
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1, threadFactory);
scheduledExecutor.scheduleWithFixedDelay(() -> {
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(100, TimeUnit.SECONDS);
connectionManager.closeIdleConnections(40, TimeUnit.SECONDS);
}, 40L, 40L, TimeUnit.SECONDS);
// shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(CommonHttpClient::close));
} catch (Exception ignored) {}
}
public static CloseableHttpClient getHttpClient() {
return httpClient;
}
public static void close() {
try {
httpClient.close();
} catch (Exception e) {
log.error("close http client error", e);
}
}
}
@@ -25,6 +25,7 @@ import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.common.kex.BuiltinDHFactories;
import org.apache.sshd.core.CoreModuleProperties;
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
/**
* common ssh pool client
@@ -52,6 +53,7 @@ public class CommonSshClient {
BuiltinDHFactories.VALUES,
ClientBuilder.DH2KEX
));
SSH_CLIENT.setForwardingFilter(new AcceptAllForwardingFilter());
// todo when connect AlibabaCloud ubuntu server, custom signature factories will cause error, why?
// SSH_CLIENT.setSignatureFactories(new ArrayList<>(BuiltinSignatures.VALUES));
SSH_CLIENT.start();
@@ -0,0 +1,104 @@
/*
* 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.common.ssh;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
import org.apache.hertzbeat.collector.collect.common.cache.SshConnect;
import org.apache.hertzbeat.collector.util.PrivateKeyUtils;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.springframework.util.StringUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* ssh helper
*/
@Slf4j
public class SshHelper {
private static final GlobalConnectionCache CONNECTION_COMMON_CACHE = GlobalConnectionCache.getInstance();
public static ClientSession getConnectSession(String host, String port, String username, String password, String privateKey,
String privateKeyPassphrase, int timeout, boolean reuseConnection)
throws IOException, GeneralSecurityException {
CacheIdentifier identifier = CacheIdentifier.builder()
.ip(host).port(port)
.username(username).password(password)
.build();
ClientSession clientSession = null;
if (reuseConnection) {
Optional<AbstractConnection<?>> cacheOption = CONNECTION_COMMON_CACHE.getCache(identifier, true);
if (cacheOption.isPresent()) {
SshConnect sshConnect = (SshConnect) cacheOption.get();
clientSession = sshConnect.getConnection();
try {
if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) {
clientSession = null;
CONNECTION_COMMON_CACHE.removeCache(identifier);
}
} catch (Exception e) {
log.warn(e.getMessage());
clientSession = null;
CONNECTION_COMMON_CACHE.removeCache(identifier);
}
}
if (clientSession != null) {
return clientSession;
}
}
SshClient sshClient = CommonSshClient.getSshClient();
clientSession = sshClient.connect(username, host, Integer.parseInt(port))
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
if (StringUtils.hasText(password)) {
clientSession.addPasswordIdentity(password);
} else if (StringUtils.hasText(privateKey)) {
var resourceKey = PrivateKeyUtils.writePrivateKey(host, privateKey);
FilePasswordProvider passwordProvider = (session, resource, index) -> {
if (StringUtils.hasText(privateKeyPassphrase)) {
return privateKeyPassphrase;
}
return null;
};
SecurityUtils.loadKeyPairIdentities(null, () -> resourceKey, new FileInputStream(resourceKey), passwordProvider)
.forEach(clientSession::addPublicKeyIdentity);
} // else auth with localhost private public key certificates
// auth
if (!clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS).isSuccess()) {
clientSession.close();
throw new IllegalArgumentException("ssh auth failed.");
}
if (reuseConnection) {
SshConnect sshConnect = new SshConnect(clientSession);
CONNECTION_COMMON_CACHE.addCache(identifier, sshConnect);
}
return clientSession;
}
}
@@ -0,0 +1,313 @@
/*
* 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.common.ssh;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.Scheduler;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.job.SshTunnel;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.net.ServerSocket;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.util.Comparator;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
/**
* Ssh Tunnel Helper
*/
@Slf4j
public class SshTunnelHelper {
private static final long DEFAULT_CACHE_TIMEOUT = 500 * 1000;
private static final Cache<SshClientSessionWrapper, LocalPortForwardingWrapper> TRACKER_CACHE =
Caffeine.newBuilder()
.initialCapacity(1)
.maximumSize(1000)
.expireAfterAccess(Duration.ofMillis(DEFAULT_CACHE_TIMEOUT))
.scheduler(Scheduler.systemScheduler())
.removalListener((key, value, cause) -> {
if (cause == RemovalCause.REPLACED) {
return;
}
if (key != null && value != null) {
// 1. try close tunnel
SshClientSessionWrapper clientSessionWrapper = (SshClientSessionWrapper) key;
LocalPortForwardingWrapper wrapper = (LocalPortForwardingWrapper) value;
wrapper.remove(clientSessionWrapper.getClientSession());
// 2. try close session
if (!clientSessionWrapper.isShareConnection()) {
try {
clientSessionWrapper.close();
log.info("[SSH Tunnel] close unshared ssh connection, {}", clientSessionWrapper);
} catch (IOException e) {
log.error("[SSH Tunnel] close unshared ssh connection error", e);
}
}
}
})
.build();
/**
* check ssh tunnel param
*
* @param sshTunnel ssh tunnel param
*/
public static void checkTunnelParam(SshTunnel sshTunnel) {
if (sshTunnel == null || !Boolean.parseBoolean(sshTunnel.getEnable())) {
return;
}
if (!StringUtils.hasText(sshTunnel.getHost())) {
throw new IllegalArgumentException("ssh tunnel must has ssh host param");
}
if (!StringUtils.hasText(sshTunnel.getPort())) {
throw new IllegalArgumentException("ssh tunnel must has ssh port param");
}
if (!StringUtils.hasText(sshTunnel.getUsername())) {
throw new IllegalArgumentException("ssh tunnel must has ssh username param");
}
}
/**
* create ssh tunnel
*
* @param sshTunnel ssh tunnel param
* @param remoteHost remote host
* @param remotePort remote port
* @return local port
*/
public static int localPortForward(SshTunnel sshTunnel, String remoteHost, String remotePort) throws GeneralSecurityException, IOException {
boolean shareConnection = Boolean.parseBoolean(sshTunnel.getShareConnection());
// 1. get ssh session
ClientSession session = SshHelper.getConnectSession(sshTunnel.getHost(), sshTunnel.getPort(),
sshTunnel.getUsername(), sshTunnel.getPassword(), sshTunnel.getPrivateKey(), sshTunnel.getPrivateKeyPassphrase(),
Integer.parseInt(sshTunnel.getTimeout()), shareConnection);
SshClientSessionWrapper sessionWrapper = new SshClientSessionWrapper(session, shareConnection);
// 2. get tunnel
LocalPortForwardingWrapper forwardingWrapper = selectWrapper(
TRACKER_CACHE.getIfPresent(sessionWrapper), sessionWrapper, remoteHost, remotePort);
int localPort;
if (forwardingWrapper == null) {
localPort = getRandomPort();
LocalPortForwardingWrapper newForwardingWrapper = sessionWrapper
.createLocalPortForwardingTracker(localPort, remoteHost, Integer.parseInt(remotePort));
if (TRACKER_CACHE.getIfPresent(sessionWrapper) == null) {
TRACKER_CACHE.put(sessionWrapper, newForwardingWrapper);
}
log.info("[SSH Tunnel] created ssh forwarding tracker ssh:{}, remote:{}, localPort:{}",
sshTunnel.getHost() + ":" + sshTunnel.getPort(), remoteHost + ":" + remotePort, localPort);
} else {
localPort = forwardingWrapper.getTracker().getLocalAddress().getPort();
}
return localPort;
}
/**
* get tunnel
*
* @param wrapper LocalPortForwardingWrapper
* @param sessionWrapper SshClientSessionWrapper
* @param remoteHost remote host
* @param remotePort remote port
* @return LocalPortForwardingWrapper
*/
private static LocalPortForwardingWrapper selectWrapper(LocalPortForwardingWrapper wrapper, SshClientSessionWrapper sessionWrapper,
String remoteHost, String remotePort) {
if (wrapper == null) {
return null;
}
List<LocalPortForwardingWrapper> selectList = wrapper.select(sessionWrapper.getClientSession(), localPortForwardWrapper -> {
if (!localPortForwardWrapper.isOpen()) {
return false;
}
ExplicitPortForwardingTracker tracker = localPortForwardWrapper.getTracker();
SshdSocketAddress remoteAddress = tracker.getRemoteAddress();
return Objects.equals(remoteAddress.getHostName(), remoteHost)
&& Objects.equals(remoteAddress.getPort(), Integer.parseInt(remotePort));
});
if (selectList.isEmpty()) {
return null;
}
LocalPortForwardingWrapper selected;
if (selectList.size() == 1) {
selected = selectList.get(0);
} else {
selected = selectList.stream().min(Comparator.comparing(LocalPortForwardingWrapper::getLastAccessTime)).get();
}
selected.setLastAccessTime(System.currentTimeMillis());
return selected;
}
private static int getRandomPort() throws IOException {
try (ServerSocket serverSocket = new ServerSocket(0)) {
return serverSocket.getLocalPort();
}
}
@Getter
@Setter
@EqualsAndHashCode
private static class SshClientSessionWrapper {
private ClientSession clientSession;
private boolean shareConnection;
public SshClientSessionWrapper(ClientSession clientSession, boolean shareConnection) {
this.clientSession = clientSession;
this.shareConnection = shareConnection;
}
/**
* Starts a local port forwarding
* @param localPort local port
* @param remoteHost remove host
* @param remotePort remote port
* @return LocalPortForwardingWrapper
*/
public LocalPortForwardingWrapper createLocalPortForwardingTracker(Integer localPort, String remoteHost, Integer remotePort) throws IOException {
SshdSocketAddress remoteAddress = new SshdSocketAddress(remoteHost, remotePort);
SshdSocketAddress localAddress = new SshdSocketAddress("localhost", localPort);
ExplicitPortForwardingTracker tracker = clientSession.createLocalPortForwardingTracker(localAddress, remoteAddress);
return new LocalPortForwardingWrapper(tracker);
}
/**
* close client session
*/
public void close() throws IOException {
clientSession.close();
}
@Override
public String toString() {
return "{ ssh:%s, shareConnection:%b }".formatted(clientSession, shareConnection);
}
}
@Getter
@Setter
@EqualsAndHashCode
private static class LocalPortForwardingWrapper {
private static Map<ClientSession, List<LocalPortForwardingWrapper>> map = new ConcurrentHashMap<>();
private ExplicitPortForwardingTracker tracker;
private Long lastAccessTime;
public LocalPortForwardingWrapper(ExplicitPortForwardingTracker tracker) {
this.tracker = tracker;
this.lastAccessTime = System.currentTimeMillis();
map.computeIfAbsent(tracker.getClientSession(), (key) -> new ArrayList<>()).add(this);
}
/**
* select ClientSession LocalPortForwardingWrapper List
* @param session ssh client session
* @param predicate condition
* @return LocalPortForwardWrapper
*/
public List<LocalPortForwardingWrapper> select(ClientSession session, Predicate<LocalPortForwardingWrapper> predicate) {
List<LocalPortForwardingWrapper> trackerList = map.get(session);
if (CollectionUtils.isEmpty(trackerList)) {
return trackerList;
}
List<LocalPortForwardingWrapper> list = new ArrayList<>();
long currentTimeMillis = System.currentTimeMillis();
Iterator<LocalPortForwardingWrapper> iterator = trackerList.iterator();
while (iterator.hasNext()) {
LocalPortForwardingWrapper wrapper = iterator.next();
// lazy remove
if (currentTimeMillis - wrapper.getLastAccessTime() > DEFAULT_CACHE_TIMEOUT) {
try {
wrapper.getTracker().close();
iterator.remove();
log.info("[SSH Tunnel] Lazy Remove ssh local port forwarding {}", wrapper);
} catch (IOException e) {
log.warn("[SSH Tunnel] Lazy Remove ssh local port forwarding Error", e);
}
} else if (predicate == null || predicate.test(wrapper)) {
list.add(wrapper);
}
}
return list;
}
/**
* remove session local port forwarding
* @param session ssh client session
*/
public void remove(ClientSession session) {
List<LocalPortForwardingWrapper> trackerList = map.get(session);
if (CollectionUtils.isEmpty(trackerList)) {
return;
}
Iterator<LocalPortForwardingWrapper> iterator = trackerList.iterator();
while (iterator.hasNext()){
try {
LocalPortForwardingWrapper next = iterator.next();
next.close();
iterator.remove();
log.info("[SSH Tunnel] Remove ssh local port forwarding, {}", next);
} catch (IOException e) {
log.error("[SSH Tunnel] Remove ssh session local port forwarding error", e);
}
}
}
public void close() throws IOException {
tracker.close();
}
public boolean isOpen() {
return tracker.isOpen();
}
@Override
public String toString() {
return "{ ssh:%s, remote:%s, localPort:%d }".formatted(
tracker.getSession().getConnectAddress(),
tracker.getRemoteAddress(),
tracker.getLocalAddress().getPort()
);
}
}
}
@@ -154,13 +154,11 @@ public class DispatchProperties {
/**
* Schedule Data Export Configuration Properties
* 调度数据出口配置属性
*/
public static class ExportProperties {
/**
* kafka configuration information
* kafka配置信息
*/
private KafkaProperties kafka;
@@ -178,18 +176,15 @@ public class DispatchProperties {
public static class KafkaProperties {
/**
* Whether the kafka data export is started
* kafka数据出口是否启动
*/
private boolean enabled = true;
/**
* kafka's connection server url
* kafka的连接服务器url
*/
private String servers = "http://127.0.0.1:2379";
/**
* Topic name to send data to
* 发送数据的topic名称
*/
private String topic;
@@ -223,7 +223,9 @@ public class KafkaCollectImpl extends AbstractCollect {
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
Assert.isTrue(metrics != null, "Metrics cannot be null");
KafkaProtocol kafkaProtocol = metrics.getKclient();
// Ensure that metrics and kafkaProtocol are not null
Assert.isTrue(metrics != null && kafkaProtocol != null, "Kafka collect must have kafkaProtocol params");
// Ensure that host and port are not empty
@@ -42,7 +42,7 @@ public class KafkaCollectTest {
@Test
void preCheck() {
// metrics is null
assertThrows(NullPointerException.class, () -> {
assertThrows(IllegalArgumentException.class, () -> {
collect.preCheck(null);
});
@@ -51,22 +51,21 @@ public class KafkaCollectTest {
collect.preCheck(Metrics.builder().build());
});
KafkaProtocol kafka = new KafkaProtocol();
Metrics metric = Metrics.builder().kclient(kafka).build();
// kafka srv host is null
assertThrows(IllegalArgumentException.class, () -> {
KafkaProtocol kafka = new KafkaProtocol();
collect.preCheck(Metrics.builder().kclient(kafka).build());
collect.preCheck(metric);
});
// kafka port is null
assertThrows(IllegalArgumentException.class, () -> {
KafkaProtocol kafka = KafkaProtocol.builder().host("127.0.0.1").build();
collect.preCheck(Metrics.builder().kclient(kafka).build());
kafka.setHost("127.0.0.1");
collect.preCheck(metric);
});
// no exception throw
assertDoesNotThrow(() -> {
KafkaProtocol kafka = KafkaProtocol.builder().host("127.0.0.1").port("9092").build();
collect.preCheck(Metrics.builder().kclient(kafka).build());
kafka.setPort("9092");
collect.preCheck(metric);
});
}
@@ -94,13 +94,11 @@ public class NebulaGraphCollectImpl extends AbstractCollect {
String resp;
long responseTime;
HashMap<String, String> resultMap = new HashMap<>(64);
CloseableHttpResponse response;
HttpContext httpContext = createHttpContext(nebulaGraph.getHost(), nebulaGraph.getPort());
HttpUriRequest request = createHttpRequest(nebulaGraph.getHost(), nebulaGraph.getPort(),
nebulaGraph.getUrl(), nebulaGraph.getTimeout());
try {
// Send an HTTP request to obtain response data
response = CommonHttpClient.getHttpClient().execute(request, httpContext);
// Send an HTTP request to obtain response data
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != SUCCESS_CODE) {
builder.setCode(CollectRep.Code.FAIL);
+6
View File
@@ -173,6 +173,12 @@
<artifactId>snappy-java</artifactId>
<version>${snappy-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>${javaparser.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -42,11 +42,6 @@ public class CommonProperties {
*/
private DataQueueProperties queue;
/**
* sms impl properties
*/
private SmsProperties sms;
/**
* data queue properties
*/
@@ -146,83 +141,4 @@ public class CommonProperties {
*/
private String alertsDataTopic;
}
/**
* sms properties
*/
@Getter
@Setter
public static class SmsProperties {
//Tencent cloud SMS configuration
private TencentSmsProperties tencent;
//Ali cloud SMS configuration
private AliYunSmsProperties aliYun;
}
/**
* tencent sms properties
*/
@Getter
@Setter
public static class TencentSmsProperties {
/**
* Tencent cloud account secret id
*/
private String secretId;
/**
* Tencent cloud account secret key
*/
private String secretKey;
/**
* SMS app id
*/
private String appId;
/**
* SMS signature
*/
private String signName;
/**
* SMS template ID
*/
private String templateId;
}
/**
* aliYun sms properties
*/
@Getter
@Setter
public static class AliYunSmsProperties {
/**
* Aliyun account access key id
*/
private String secretId;
/**
* Ali Cloud account access key
*/
private String secretKey;
/**
* SMS app id
*/
private String appId;
/**
* SMS signature
*/
private String signName;
/**
* ID of the SMS template
*/
private String templateId;
}
}
@@ -25,13 +25,12 @@ import java.util.Arrays;
public enum AiTypeEnum {
/**
* 智普
* ZhiPu
*/
zhiPu,
/**
* sparkDesk
* 科大讯飞
*/
sparkDesk,
@@ -25,7 +25,7 @@ public interface NetworkConstants {
String KEEP_ALIVE = "Keep-Alive";
String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36\")";
String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36";
String IPV6 = "ipv6";
@@ -59,14 +59,8 @@ public interface NetworkConstants {
int READ_TIME_OUT = 6 * 1000;
int WRITE_TIME_OUT = 6 * 1000;
int CONNECT_TIME_OUT = 6 * 1000;
int CHUNK_SIZE = 8196;
int MAX_IDLE_CONNECTIONS = 20;
int KEEP_ALIVE_TIMEOUT = 30 * 1000;
int HTTP_CLIENT_CONNECTION_MANAGER_MAX_PER_ROUTE = 20;
int HTTP_CLIENT_CONNECTION_MANAGER_MAX_TOTAL = 20;
int HTTPCLIENT_KEEP_ALIVE_DURATION = 30 * 1000;
int HTTP_CLIENT_CONNECTION_MANAGER_CLOSE_WAIT_TIME_MS = 1000;
int HTTP_CLIENT_CONNECTION_MANAGER_CLOSE_IDLE_TIME_S = 30;
}
}
@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.constants;
/**
* SMS provider constants
*/
public interface SmsConstants {
// Tencent cloud SMS
String TENCENT = "tencent";
// Alibaba Cloud SMS
String ALIBABA = "alibaba";
// UniSMS
String UNISMS = "unisms";
}
@@ -18,6 +18,7 @@
package org.apache.hertzbeat.common.entity.alerter;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
@@ -97,10 +98,12 @@ public class GroupAlert {
@Schema(title = "This record creation time (millisecond timestamp)")
@CreatedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime gmtCreate;
@Schema(title = "Record the latest modification time (timestamp in milliseconds)")
@LastModifiedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime gmtUpdate;
@Transient
@@ -18,6 +18,7 @@
package org.apache.hertzbeat.common.entity.alerter;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
@@ -100,10 +101,12 @@ public class SingleAlert {
@Schema(title = "This record creation time (millisecond timestamp)")
@CreatedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime gmtCreate;
@Schema(title = "Record the latest modification time (timestamp in milliseconds)")
@LastModifiedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime gmtUpdate;
@Override
@@ -41,7 +41,7 @@ public class Configmap implements Serializable {
private String key;
/**
* parameter value 参数value
* parameter value
*/
private Object value;
@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.entity.job;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.job.protocol.CommonRequestProtocol;
import org.apache.hertzbeat.common.entity.job.protocol.Protocol;
/**
* ssh tunnel
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SshTunnel implements CommonRequestProtocol, Protocol {
/**
* enable ssh tunnel
*/
private String enable = "false";
/**
* IP ADDRESS OR DOMAIN NAME OF THE PEER HOST
*/
private String host;
/**
* Peer host port
*/
private String port = "22";
/**
* TIME OUT PERIOD
*/
private String timeout = "6000";
/**
* UserName
*/
private String username;
/**
* Password (optional)
*/
private String password;
/**
* Private key (optional)
*/
private String privateKey;
/**
* private key passphrase (optional)
*/
private String privateKeyPassphrase;
/**
* share connection session
*/
private String shareConnection = "true";
}
@@ -59,4 +59,10 @@ public class FtpProtocol implements CommonRequestProtocol, Protocol {
* Timeout
*/
private String timeout;
/**
* Whether ftp uses link encryption ssl/tls, i.e. ftp or sftp
*
*/
private String ssl = "false";
}
@@ -21,6 +21,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.job.SshTunnel;
/**
* Database configuration information implemented by the common jdbc specification
@@ -70,4 +71,9 @@ public class JdbcProtocol implements CommonRequestProtocol, Protocol {
* DATABASE LINK URL eg: jdbc:mysql://localhost:3306/usthe
*/
private String url;
/**
* ssh tunnel
*/
private SshTunnel sshTunnel;
}
@@ -21,6 +21,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hertzbeat.common.entity.job.SshTunnel;
/**
* Redis Protocol
@@ -61,4 +62,9 @@ public class RedisProtocol implements CommonRequestProtocol, Protocol {
*/
private String timeout;
/**
* SSH TUNNEL
*/
private SshTunnel sshTunnel;
}
@@ -52,7 +52,7 @@ public class RocketmqProtocol implements CommonRequestProtocol, Protocol {
private String secretKey;
/**
* jsonpath解析脚本
* jsonpath parsing script
*/
private String parseScript;
@@ -22,6 +22,7 @@ package org.apache.hertzbeat.common.entity.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
@@ -406,7 +407,7 @@ public final class CollectRep {
fieldIndex < row.getColumnsList().size()) {
String value = row.getColumns(fieldIndex);
if (value != null) {
vector.set(rowIndex, value.getBytes());
vector.set(rowIndex, value.getBytes(StandardCharsets.UTF_8));
}
}
}
@@ -0,0 +1,30 @@
/*
* 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.event;
import org.springframework.context.ApplicationEvent;
/**
* the event for sms config change
*/
public class SmsConfigChangeEvent extends ApplicationEvent {
public SmsConfigChangeEvent(Object source) {
super(source);
}
}
@@ -90,7 +90,7 @@ public final class AesUtil {
// encode content to byte array
byte[] byteAes = cipher.doFinal(byteEncode);
// base64 encode content
return new String(Base64.getEncoder().encode(byteAes), StandardCharsets.UTF_8);
return Base64.getEncoder().encodeToString(byteAes);
} catch (Exception e) {
log.error("aes encode content error: {}", e.getMessage(), e);
return content;
@@ -142,7 +142,7 @@ public final class AesUtil {
/**
* Determine whether it is encrypted
* @param text text
* @return true-是 false-否
* @return true false
*/
public static boolean isCiphertext(String text, String decryptKey) {
// First use whether it is base64 to determine whether it has been encrypted
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.common.util;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
@@ -39,4 +40,8 @@ public final class Base64Util {
return false;
}
}
public static String encode(String str) {
return Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8));
}
}
@@ -68,19 +68,19 @@ class KafkaCommonDataQueueTest {
when(commonProperties.getQueue()).thenReturn(dataQueueProperties);
when(dataQueueProperties.getKafka()).thenReturn(kafkaProperties);
// 设置所有必需的 topic
// Set all required topics
when(kafkaProperties.getMetricsDataTopic()).thenReturn("metricsDataTopic");
when(kafkaProperties.getAlertsDataTopic()).thenReturn("alertsDataTopic");
when(kafkaProperties.getMetricsDataToStorageTopic()).thenReturn("metricsDataToStorageTopic");
when(kafkaProperties.getServiceDiscoveryDataTopic()).thenReturn("serviceDiscoveryDataTopic");
when(kafkaProperties.getServers()).thenReturn("localhost:9092");
// 模拟 consumer 的 subscribe 方法
// Simulate the subscribe method for consumers
doNothing().when(metricsDataToAlertConsumer).subscribe(anyCollection());
kafkaCommonDataQueue = new KafkaCommonDataQueue(commonProperties);
// 使用反射设置私有字段
// Use reflection to set private fields
setPrivateField(kafkaCommonDataQueue, "metricsDataProducer", metricsDataProducer);
setPrivateField(kafkaCommonDataQueue, "metricsDataToAlertConsumer", metricsDataToAlertConsumer);
}
@@ -98,16 +98,16 @@ class KafkaCommonDataQueueTest {
@Test
void testPollMetricsDataToAlerter() throws InterruptedException {
// 创建一个测试数据
// Create a test data
CollectRep.MetricsData expectedData = CollectRep.MetricsData.newBuilder()
.setMetrics("test metrics")
.build();
// 创建一个包含测试数据的 ConsumerRecord
// Create a ConsumerRecord containing test data
ConsumerRecord<Long, CollectRep.MetricsData> record =
new ConsumerRecord<>("metricsDataTopic", 0, 0L, 1L, expectedData);
// 创建一个包含单个记录的 ConsumerRecords
// Create a ConsumerRecords containing a single record.
Map<TopicPartition, List<ConsumerRecord<Long, CollectRep.MetricsData>>> recordsMap =
Collections.singletonMap(
new TopicPartition("metricsDataTopic", 0),
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.ipc.ArrowStreamWriter;
@@ -86,7 +87,7 @@ class KafkaMetricsDataDeserializerTest {
@Test
void testDeserializeWithInvalidBytes() {
byte[] invalidBytes = "invalid data".getBytes();
byte[] invalidBytes = "invalid data".getBytes(StandardCharsets.UTF_8);
assertThrows(RuntimeException.class, () -> deserializer.deserialize("", invalidBytes));
}
@@ -0,0 +1,146 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.util;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* Test case for checking Chinese characters in Java files
*/
@Slf4j
public class ChineseCharacterCheckTest {
private static final Pattern CHINESE_CHAR_PATTERN = Pattern.compile("[\u4e00-\u9fa5]");
private static final Set<String> EXCLUDED_FILES = new HashSet<>(Collections.singletonList("Metrics"));
private static final String MAIN_SOURCE_DIR = "src/main/java";
private static final String TEST_SOURCE_DIR = "src/test/java";
private final JavaParser javaParser = new JavaParser();
private final String sourceDir;
private final String testDir;
public ChineseCharacterCheckTest() {
boolean isWindowsOs = System.getProperty("os.name").toLowerCase().startsWith("win");
String separator = isWindowsOs ? "\\" : "/";
this.sourceDir = MAIN_SOURCE_DIR.replace("/", separator);
this.testDir = TEST_SOURCE_DIR.replace("/", separator);
}
@Test
void shouldNotContainChineseInComments() {
List<String> violations = scanForChineseCharacters(ScanTarget.COMMENTS);
assertNoChineseCharacters(violations);
}
private List<String> scanForChineseCharacters(ScanTarget target) {
List<String> violations = new ArrayList<>();
try (Stream<Path> paths = Files.walk(Paths.get(".."), FileVisitOption.FOLLOW_LINKS)) {
paths.filter(this::isValidJavaFile)
.forEach(path -> processFile(path, target, violations));
} catch (IOException e) {
throw new RuntimeException("Failed to scan Java files", e);
}
return violations;
}
private boolean isValidJavaFile(Path path) {
String pathStr = path.toString();
return pathStr.endsWith(".java")
&& (pathStr.contains(sourceDir) || pathStr.contains(testDir))
&& EXCLUDED_FILES.stream().noneMatch(pathStr::contains);
}
private void processFile(Path path, ScanTarget target, List<String> violations) {
try {
ParseResult<CompilationUnit> parseResult = javaParser.parse(Files.newInputStream(path));
parseResult.getResult().ifPresent(cu -> {
if (target.includeComments()) {
checkComments(cu, path, violations);
}
if (target.includeCode()) {
checkCode(cu, path, violations);
}
});
} catch (Exception e) {
log.error("Error processing file: {}", path, e);
}
}
private void checkComments(CompilationUnit cu, Path path, List<String> violations) {
cu.getAllContainedComments().stream()
.filter(comment -> CHINESE_CHAR_PATTERN.matcher(comment.getContent()).find())
.forEach(comment -> violations.add(formatViolation(path, "comment", comment.getContent().trim())));
}
private void checkCode(CompilationUnit cu, Path path, List<String> violations) {
cu.findAll(com.github.javaparser.ast.expr.StringLiteralExpr.class).stream()
.filter(str -> CHINESE_CHAR_PATTERN.matcher(str.getValue()).find())
.forEach(str -> violations.add(formatViolation(path, "code", str.getValue())));
}
private String formatViolation(Path path, String location, String content) {
return String.format("Chinese characters found in %s at %s: %s",
location, path.toAbsolutePath(), content);
}
private void assertNoChineseCharacters(List<String> violations) {
Assertions.assertEquals(0, violations.size(),
() -> String.format("Found Chinese characters in files:%n%s",
String.join(System.lineSeparator(), violations)));
}
private enum ScanTarget {
COMMENTS(true, false),
CODE(false, true),
ALL(true, true);
private final boolean checkComments;
private final boolean checkCode;
ScanTarget(boolean checkComments, boolean checkCode) {
this.checkComments = checkComments;
this.checkCode = checkCode;
}
public boolean includeComments() {
return checkComments;
}
public boolean includeCode() {
return checkCode;
}
}
}
@@ -24,6 +24,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import java.nio.charset.StandardCharsets;
/**
* test case for {@link FileUtil}.
*/
@@ -41,9 +43,12 @@ class FileUtilTest {
@BeforeEach
void setUp() {
jsonFile = new MockMultipartFile("file", "test.json", MediaType.APPLICATION_JSON_VALUE, "test content".getBytes());
excelFile = new MockMultipartFile("file", "test.xlsx", EXCEL_TYPE, "test content".getBytes());
yamlFile = new MockMultipartFile("file", "test.yaml", YAML_TYPE, "test content".getBytes());
jsonFile = new MockMultipartFile("file", "test.json", MediaType.APPLICATION_JSON_VALUE,
"test content".getBytes(StandardCharsets.UTF_8));
excelFile = new MockMultipartFile("file", "test.xlsx", EXCEL_TYPE,
"test content".getBytes(StandardCharsets.UTF_8));
yamlFile = new MockMultipartFile("file", "test.yaml", YAML_TYPE,
"test content".getBytes(StandardCharsets.UTF_8));
emptyFile = new MockMultipartFile("file", "", null, (byte[]) null);
}
@@ -53,5 +53,10 @@
<version>${hertzbeat.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

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