Compare commits

...
Author SHA1 Message Date
Tomsun28 d0c6933a88 Merge branch 'master' into feat/refactor_monitor 2025-12-05 22:03:13 +08:00
Duansg 3e567e5be6 [improve]Improve issues related to tag conflicts (#3889) 2025-12-05 22:02:43 +08:00
Tomsun28 a995dd7daf Merge branch 'master' into feat/refactor_monitor 2025-12-04 23:56:29 +08:00
Tomsun28andaias00 4ae6ba9a3c feat: update add monitors pop selected menus modal (#3878)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-12-04 23:55:10 +08:00
360abcc61d [feature] Using duckdb instead of jpa to store history data (#3883)
Signed-off-by: Logic <zqr10159@126.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 23:51:04 +08:00
liuhy c8739f530e [add] Add parameter validators and their corresponding tests 2025-12-04 17:02:17 +08:00
aias00andCopilot 7b2c822802 Update hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidator.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 17:01:28 +08:00
aias00andCopilot 339c9775e3 Update hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/ParamValidatorManager.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 17:01:19 +08:00
aias00andCopilot 8efbba1693 Update hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidator.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 17:01:10 +08:00
aias00andCopilot 20e016dff7 Update hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 17:01:00 +08:00
aias00andCopilot f5bd371114 Update hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/component/validator/impl/BooleanParamValidator.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 17:00:49 +08:00
aias00andgithub-actions[bot] 172a279082 Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/TextParamValidatorTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 17:00:21 +08:00
aias00andCopilot f4d19a2f7f Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/ArrayParamValidatorTest.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 17:00:11 +08:00
aias00andCopilot 47f2d5be6c Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorTest.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 17:00:01 +08:00
aias00andgithub-actions[bot] c48abad4cf Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/helper/MonitorImExportHelperTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:59:34 +08:00
aias00andgithub-actions[bot] 6c6b253f00 Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/PasswordParamValidatorTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:59:17 +08:00
aias00andgithub-actions[bot] 6878bf84ae Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/OptionParamValidatorTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:59:04 +08:00
aias00andgithub-actions[bot] 926a399105 Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/NumberParamValidatorTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:58:50 +08:00
aias00andgithub-actions[bot] 29e1e9cb40 Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/JsonParamValidatorTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:58:39 +08:00
aias00andgithub-actions[bot] 1397bb15ec Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:58:31 +08:00
aias00andgithub-actions[bot] 8a9765c99a Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/HostParamValidatorAdapterTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:58:23 +08:00
aias00andgithub-actions[bot] 6f935357b8 Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/impl/BooleanParamValidatorTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:58:11 +08:00
aias00andgithub-actions[bot] 9faead5aee Update hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/component/validator/ParamValidatorManagerTest.java
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: aias00 <liuhongyu@apache.org>
2025-12-04 16:58:01 +08:00
liuhy a56aa09ed6 [add] Add parameter validators and their corresponding tests 2025-12-04 16:52:12 +08:00
P_PeacefulandTomsun28 41e57db032 [bugfix] notice policy lost Gotify receiver type (#3886)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-03 22:44:20 +08:00
DuansgandTomsun28 b62137d185 [improve]Improve performance issues caused by too many metrics (#3879)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-03 22:30:26 +08:00
DuansgandTomsun28 2bd635e1d4 [fix]Fix some systemic issues (#3884)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-12-03 22:25:02 +08:00
Yang Chen 8e8ab80aa0 Revert "[refractor] Using Spring jdbc to query Greptime log" (#3885) 2025-12-03 12:32:45 +08:00
LogicandYang Chen 7f4c2e0487 [refractor] Using Spring jdbc to query Greptime log (#3880)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
2025-12-03 12:11:08 +08:00
e52e715a53 improvement: optimize log stream for high TPS scenarios (#3876)
Signed-off-by: Yang Chen <1597081640@qq.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-12-03 11:18:49 +08:00
e3e7377d86 [refactor] Refactor Monitor host field to instance and update related logic (#3863)
Signed-off-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-01 21:45:59 +08:00
P_PeacefulandTomsun28 625dbe9624 [bugfix] redis cluster monitor master-slave relationship is inconsistent (#3874)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-11-30 19:39:40 +08:00
Duansgandaias00 42f0a74edf [fix] adding trusted domain verification (#3866)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-11-27 20:38:00 +08:00
Tomsun28 cc9f53d71a chore: update sureness yml (#3870) 2025-11-27 20:10:46 +08:00
DeleiGuo 2f122dfb05 [docs] fix the missing documents references in the sidebar (#3867) 2025-11-26 16:52:43 +08:00
f7b0010180 fix: Remove redundant metrics data send in KafkaCommonDataQueue (#3854)
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-24 22:11:49 +08:00
2c0fecaa62 [improve] Added simple exponential backoff strategy (#3860)
Signed-off-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Yang Chen <1597081640@qq.com>
2025-11-21 16:28:21 +08:00
548966906c [bugfix] align tooltip position for mute icon with other icons (#3862)
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-11-20 22:22:56 +08:00
6473546f56 [feat] Add DB2 database monitoring support (#3857)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-20 22:16:52 +08:00
035d63c880 [feature] Support FeiShu/Lark App Alert Notification (#3856)
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-11-20 00:14:57 +08:00
Yang ChenandCalvin d56176dfa5 feat: add new docker-compose deployment solution (#3859)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-11-19 23:49:13 +08:00
Yang ChenandCalvin c1815f5107 fix: allow access to /log/** path to resolve unauthorized error on refresh (#3858)
Co-authored-by: Calvin <zhengqiwei@apache.org>
2025-11-18 01:33:29 +08:00
7ac175e726 [Improve]Unify UI border-radius to 6px for a more professional look (#3853)
Co-authored-by: Duansg <siguoduan@gmail.com>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
2025-11-17 23:02:21 +08:00
eb5e843251 [feat] Add webhook authentication support for alert notifications (#3797)
Co-authored-by: Sherlock Yin <sherlock.yin1994@gmail.com>
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: kangli <likang@apache.org>
Co-authored-by: 刘俊辉 <liu.junhui3@iwhalecloud.com>
2025-11-17 22:59:35 +08:00
starryCoderandDuansg bc098aa4d2 [bugfix]manager: Avoid NPE when metrics data queue is empty (#3848)
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-14 16:09:49 +08:00
Tomsun28 bcd394b47f [docs] update doc fix wrong manager (#3850)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-08 20:33:42 +08:00
Duansg fd2202fcaf [doc] add new committer blog (#3847) 2025-11-06 21:37:17 +08:00
a7629e317a Modify the issue of incorrect signature in Alibaba's SMS sending (#3788)
Co-authored-by: Logic <zqr10159@dromara.org>
Co-authored-by: Tomsun28 <tomsun28@outlook.com>
Co-authored-by: shown <yuluo08290126@gmail.com>
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: Calvin <zhengqiwei@apache.org>
Co-authored-by: aias00 <liuhongyu@apache.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-06 10:38:48 +08:00
DeleiGuoandshown 3a378f8710 [feature] support the security settings for robot signatures in DingTalk (#3841)
Co-authored-by: shown <yuluo08290126@gmail.com>
2025-11-03 11:43:08 +08:00
Tomsun28 cf59907fcd fix the labels limit error (#3842)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-11-02 20:56:21 +08:00
223 changed files with 7272 additions and 1601 deletions
+3 -4
View File
@@ -20,9 +20,11 @@ github:
homepage: https://hertzbeat.apache.org/
labels:
- ai
- agent
- llm
- monitoring
- logs
- monitor
- monitoring
- notifications
- alerting
- self-hosted
@@ -34,12 +36,9 @@ github:
- uptime
- status
- status-page
- devops
- server
- linux
- database
- mysql
- cloud
enabled_merge_buttons:
squash: true
merge: false
+10 -10
View File
@@ -90,24 +90,24 @@ e2e:
backend:
- changed-files:
- any-glob-to-any-file:
- "alerter/**/*"
- "collector/**/*"
- "common/**/*"
- "grafana/**/*"
- "manager/**/*"
- "push/**/*"
- "remoting/**/*"
- "warehouse/**/*"
- "hertzbeat-alerter/**/*"
- "hertzbeat-collector/**/*"
- "hertzbeat-common/**/*"
- "hertzbeat-grafana/**/*"
- "hertzbeat-manager/**/*"
- "hertzbeat-push/**/*"
- "hertzbeat-remoting/**/*"
- "hertzbeat-warehouse/**/*"
collector:
- changed-files:
- any-glob-to-any-file:
- "collector/**/*"
- "hertzbeat-collector/**/*"
monitoring-template:
- changed-files:
- any-glob-to-any-file:
- "manager/src/main/resources/define/**/*"
- "hertzbeat-manager/src/main/resources/define/**/*"
HIP:
- changed-files:
+3 -3
View File
@@ -4,7 +4,7 @@ image:
tasks:
- name: Run backend
before: cd hertzbeat-manager
before: cd hertzbeat-startup
command: |
gp sync-await setup-backend
mvn spring-boot:run
@@ -13,7 +13,7 @@ tasks:
before: cd web-app
command: |
gp sync-await setup-frontend
yarn start --public-host "`gp url 4200`"
pnpm start --public-host "`gp url 4200`"
openMode: split-right
- name: Setup backend
@@ -25,7 +25,7 @@ tasks:
- name: Setup frontend
init: |
cd web-app && yarn install
cd web-app && pnpm install
command: |
gp sync-done setup-frontend
exit 0
+8 -30
View File
@@ -20,27 +20,27 @@
- 💬**Suggestion**: You can also make some suggestions to the project or community to promote the healthy development of the community;
More see [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
More see [Contribution Types](https://allcontributors.org/emoji-key)
Even small corrections to typos are very welcome :)
### Getting HertzBeat up and running
> To get HertzBeat code running on your development tools, and able to debug with breakpoints.
> This is a front-end and back-end separation project. To start the local code, the back-end [manager](manager) and the front-end [web-app](web-app) must be started separately.
> This is a front-end and back-end separation project. To start the local code, the back-end [startup](hertzbeat-startup) and the front-end [web-app](web-app) must be started separately.
#### Backend start
1. Requires `maven3+`, `java17` and `lombok` environments
2. (Optional) Modify the configuration file: `manager/src/main/resources/application.yml`
2. (Optional) Modify the configuration file: `hertzbeat-startup/src/main/resources/application.yml`
3. Execute under the project root directory: `mvn clean install -DskipTests`
4. Add VM Options: `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
5. Start `springboot manager` service: `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
5. Start `springboot startup` service: `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
#### Frontend start
@@ -131,17 +131,6 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
## 🥐 Architecture
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** Provide monitoring management, system management basic services.
> Provides monitoring management, monitoring configuration management, system user management, etc.
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** Provide metrics data collection services.
> Use common protocols to remotely collect and obtain peer-to-peer metrics data.
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** Provide monitoring data warehousing services.
> Metrics data management, data query, calculation and statistics.
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** Provide alert service.
> Alarm calculation trigger, monitoring status linkage, alarm configuration, and alarm notification.
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** Provide web ui.
> Angular Web UI.
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
<br>
@@ -172,26 +161,26 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
- 💬**建议**:也可以对项目或者社区提出一些建议,促进社区的良性发展;
更多贡献方式参见 [Contribution Types](https://allcontributors.org/docs/en/emoji-key)
更多贡献方式参见 [Contribution Types](https://allcontributors.org/emoji-key)
即便是小到错别字的修正我们也都非常欢迎 :)
### 让 HertzBeat 运行起来
> 让 HertzBeat 的代码在您的开发工具上运行起来,并且能够断点调试。
> 此为前后端分离项目,本地代码启动需将后端[manager](manager)和前端[web-app](web-app)分别启动生效。
> 此为前后端分离项目,本地代码启动需将后端[hertzbeat-startup](hertzbeat-startup)和前端[web-app](web-app)分别启动生效。
#### 后端启动
1. 需要 `maven3+`, `java17` 和 `lombok` 环境
2. (可选)修改配置文件配置信息-`manager/src/main/resources/application.yml`
2. (可选)修改配置文件配置信息-`hertzbeat-startup/src/main/resources/application.yml`
3. 在项目根目录运行编译: `mvn clean install -DskipTests`
4. 在 `jvm` 加入参数 `--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`
5. 启动`springboot manager`服务 `manager/src/main/java/org/apache/hertzbeat/hertzbeat-manager/Manager.java`
5. 启动`springboot startup`服务 `hertzbeat-startup/src/main/java/org/apache/hertzbeat/startup/HertzBeatApplication.java`
#### 前端启动
@@ -282,15 +271,4 @@ Add WeChat account `ahertzbeat` to pull you into the WeChat group.
### 模块
- **[manager](https://github.com/apache/hertzbeat/tree/master/hertzbeat-manager)** 提供监控管理,系统管理基础服务
> 提供对监控的管理,监控应用配置的管理,系统用户租户后台管理等。
- **[collector](https://github.com/apache/hertzbeat/tree/master/collector)** 提供监控数据采集服务
> 使用通用协议远程采集获取对端指标数据。
- **[warehouse](https://github.com/apache/hertzbeat/tree/master/warehouse)** 提供监控数据仓储服务
> 采集指标结果数据管理,数据落盘,查询,计算统计。
- **[alerter](https://github.com/apache/hertzbeat/tree/master/hertzbeat-alerter)** 提供告警服务
> 告警计算触发,任务状态联动,告警配置,告警通知。
- **[web-app](https://github.com/apache/hertzbeat/tree/master/web-app)** 提供可视化控制台页面
> 监控告警系统可视化控制台前端
![hertzBeat](home/static/img/docs/hertzbeat-arch.png)
+2 -2
View File
@@ -155,8 +155,8 @@ Detailed config refer to [Install HertzBeat via Package](https://hertzbeat.apach
##### 3Start via source code
1. Local source code debugging needs to start the back-end project `manager` and the front-end project `web-app`.
2. Backendneed `maven3+`, `java17`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `manager` service.
1. Local source code debugging needs to start the back-end project `hertzbeat-startup` and the front-end project `web-app`.
2. Backendneed `maven3+`, `java17`, `lombok`, add VM options in IDE: ` --add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED `, then start the `hertzbeat-startup` service.
3. Webneed `nodejs npm angular-cli` environment, Run `ng serve --open` in `web-app` directory after backend startup.
4. Access `http://localhost:4200` to start, default account: `admin/hertzbeat`
+2 -2
View File
@@ -152,8 +152,8 @@
##### 方式三:本地代码启动
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `manager` 和前端工程 `web-app`
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `manager` 服务即可。
1. 此为前后端分离项目,本地代码调试需要分别启动后端工程 `hertzbeat-startup` 和前端工程 `web-app`
2. 后端:需要 `maven3+`, `java17` 和 `lombok` 环境,修改 `YML` 配置信息,添加JVM参数`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED`后启动 `hertzbeat-startup` 服务即可。
3. 前端:需要 `nodejs npm angular-cli`环境,待本地后端启动后,在 `web-app` 目录下启动 `ng serve --open`
4. 浏览器访问 `http://localhost:4200` 即可开始,默认账号密码 `admin/hertzbeat`
+2 -2
View File
@@ -155,8 +155,8 @@
##### 方式3:ローカルの実行
1. ローカルの実行には、バックエンドのプロジェクト`manager`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
2. バックエンド:`maven3+`、`java17`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`manager` を起動します。
1. ローカルの実行には、バックエンドのプロジェクト`hertzbeat-startup`とフロントエンドのプロジェクト`web-app`を起動する必要があります。
2. バックエンド:`maven3+`、`java17`と`lombok`の環境は必要です。`YML` 設定を修正し、Java仮想マシンパラメータに`--add-opens=java.base/java.nio=org.apache.arrow.memory.core,ALL-UNNAMED` を追加し、`hertzbeat-startup` を起動します。
3. フロントエンド:`nodejs npm angular-cli`の環境は必要です。ローカルのバックエンドが立ち上がったら、`web-app` ディレクトリで `ng serve --open` というコマンドを実行します。
4. スタート:`http://localhost:4200`にアクセスします。デフォルトのアカウントとパスワード:`admin/hertzbeat`。
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ftp",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorFTP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "api",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorHTTP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ping",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorPing}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "port",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorPort}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "fullsite",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorSiteMap}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "ssl_cert",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorSSL}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "udp_port",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorUDP}}"
},
"params": [
+1 -1
View File
@@ -5,7 +5,7 @@
"intervals": 60,
"tags": [],
"app": "website",
"host": "127.0.0.1",
"instance": "127.0.0.1",
"name": "{{.param.monitorWebsite}}"
},
"params": [
+1 -1
View File
@@ -4,7 +4,7 @@
"intervals": 10,
"tags": [],
"app": "kubernetes",
"host": "172.29.0.11",
"instance": "172.29.0.11",
"name": "Brave_Stingray_55yR"
},
"collector": "",
@@ -33,16 +33,17 @@ public interface MetricsTools {
/**
* Get historical metrics data for a monitor
* @param monitorId Monitor ID
* @param app Monitor type (e.g., "linux", "mysql", "http")
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @param metric Specific metric field (e.g., "usage", "used", "available")
* @param label Label filter for specific instances
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
*
* @param instance Instance identifier (e.g., "ip:port", "ip", or "domain")
* @param app Monitor type (e.g., "linux", "mysql", "http")
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
* @param metric Specific metric field (e.g., "usage", "used", "available")
* @param label Label filter for specific instances
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
* @param interval Whether to aggregate data with intervals
* @return Historical metrics data formatted for display
*/
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
String getHistoricalMetrics(String instance, String app, String metrics, String metric, String label, String history, Boolean interval);
/**
* Check warehouse storage server status
@@ -152,7 +152,7 @@ public class MetricsToolsImpl implements MetricsTools {
Ask user to provide the filters for labels, history and interval aggregation
""")
public String getHistoricalMetrics(
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
@ToolParam(description = "Instance identifier (e.g., 'ip:port', 'ip', or 'domain')") String instance,
@ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app,
@ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics,
@ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter,
@@ -161,7 +161,7 @@ public class MetricsToolsImpl implements MetricsTools {
@ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) {
try {
log.info("Getting historical metrics for monitor {} and metrics {}", monitorId, metrics);
log.info("Getting historical metrics for monitor instance {} and metrics {}", instance, metrics);
if (history == null || history.trim().isEmpty()) {
history = "24h";
@@ -170,15 +170,15 @@ public class MetricsToolsImpl implements MetricsTools {
interval = true;
}
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(
monitorId, app, metrics, fieldParameter, label, history, interval);
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(instance,
app, metrics, fieldParameter, history, interval);
if (historyData == null) {
return String.format("No historical metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
return String.format("No historical metrics data found for monitor %s and metrics '%s'", instance, metrics);
}
StringBuilder response = new StringBuilder();
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(monitorId).append(")\n");
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(instance).append(")\n");
response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n");
response.append("=".repeat(60)).append("\n\n");
@@ -36,6 +36,7 @@ import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Implementation of Monitoring Tools functionality
@@ -171,7 +172,7 @@ public class MonitorToolsImpl implements MonitorTools {
response.append("ID: ").append(monitor.getId())
.append(" | Name: ").append(monitor.getName())
.append(" | Type: ").append(monitor.getApp())
.append(" | Host: ").append(monitor.getHost())
.append(" | Instance: ").append(monitor.getInstance())
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
// Add creation date for better context
@@ -257,12 +258,14 @@ public class MonitorToolsImpl implements MonitorTools {
if (intervals == null || intervals < 10) {
intervals = 600;
}
String instance = Objects.nonNull(port) ? host.trim() + ":" + port : host.trim();
// Create Monitor entity
Monitor monitor = Monitor.builder()
.name(name.trim())
.app(app.toLowerCase().trim())
.host(host.trim())
.instance(instance)
.intervals(intervals)
.status((byte) 1)
.type((byte) 0)
@@ -17,15 +17,6 @@
package org.apache.hertzbeat.alert.calculate.realtime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.AlerterWorkerPool;
@@ -41,11 +32,24 @@ import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
import org.apache.hertzbeat.common.util.BackoffUtils;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.ExponentialBackoff;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Calculate alarms based on the alarm definition rules and collected data
*/
@@ -122,13 +126,22 @@ public class MetricsRealTimeAlertCalculator {
*/
public void startCalculate() {
Runnable runnable = () -> {
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
while (!Thread.currentThread().isInterrupted()) {
try {
CollectRep.MetricsData metricsData = dataQueue.pollMetricsDataToAlerter();
if (metricsData == null) {
continue;
}
backoff.reset();
calculate(metricsData);
dataQueue.sendMetricsDataToStorage(metricsData);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} catch (CommonDataQueueUnknownException ue) {
if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) {
break;
}
} catch (Exception e) {
log.error("calculate alarm error: {}.", e.getMessage(), e);
}
@@ -143,7 +156,7 @@ public class MetricsRealTimeAlertCalculator {
long currentTimeMilli = System.currentTimeMillis();
String instance = String.valueOf(metricsData.getId());
String instanceName = metricsData.getInstanceName();
String instanceHost = metricsData.getInstanceHost();
String instanceHost = metricsData.getInstance();
String app = metricsData.getApp();
String metrics = metricsData.getMetrics();
if ((CommonConstants.PROMETHEUS_APP_PREFIX + instanceName).equals(metricsData.getApp())) {
@@ -192,12 +205,11 @@ public class MetricsRealTimeAlertCalculator {
}
Long defineId = define.getId();
Map<String, String> commonFingerPrints = new HashMap<>(8);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
// here use the alert name as finger, not care the alert name may be changed
commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instanceHost);
commonFingerPrints.putAll(define.getLabels());
if (labels != null) {
commonFingerPrints.putAll(labels);
@@ -17,13 +17,17 @@
package org.apache.hertzbeat.alert.calculate.realtime;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.calculate.realtime.window.LogWorker;
import org.apache.hertzbeat.alert.calculate.realtime.window.TimeService;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
import org.apache.hertzbeat.common.util.BackoffUtils;
import org.apache.hertzbeat.common.util.ExponentialBackoff;
import org.springframework.stereotype.Component;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.LinkedBlockingQueue;
@@ -58,16 +62,22 @@ public class WindowedLogRealTimeAlertCalculator implements Runnable {
@Override
public void run() {
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
while (!Thread.currentThread().isInterrupted()) {
try {
LogEntry logEntry = dataQueue.pollLogEntry();
if (logEntry != null) {
processLogEntry(logEntry);
dataQueue.sendLogEntryToStorage(logEntry);
if (logEntry == null) {
continue;
}
backoff.reset();
processLogEntry(logEntry);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (CommonDataQueueUnknownException ue) {
if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) {
break;
}
} catch (Exception e) {
log.error("Error in log dispatch loop: {}", e.getMessage(), e);
}
@@ -18,12 +18,17 @@
package org.apache.hertzbeat.alert.notice.impl;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
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;
@@ -54,7 +59,13 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<DingTalkWebHookDto> httpEntity = new HttpEntity<>(dingTalkWebHookDto, headers);
String webHookUrl = alerterProperties.getDingTalkWebhookUrl() + receiver.getAccessToken();
StringBuilder webHookUrlBuilder = new StringBuilder()
.append(alerterProperties.getDingTalkWebhookUrl())
.append(receiver.getAccessToken());
if (StringUtils.isNotBlank(receiver.getAppSecret())) {
webHookUrlBuilder.append(signSecret(receiver.getAppSecret()));
}
String webHookUrl = webHookUrlBuilder.toString();
ResponseEntity<CommonRobotNotifyResp> responseEntity = restTemplate.postForEntity(webHookUrl,
httpEntity, CommonRobotNotifyResp.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
@@ -102,6 +113,13 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
return dingTalkWebHookDto;
}
private String signSecret(String secret) throws Exception {
Long timestamp = System.currentTimeMillis();
String sign = URLEncoder.encode(CryptoUtils.hmacSha256Base64(secret, timestamp + "\n" + secret),
StandardCharsets.UTF_8);
return String.format("&timestamp=%s&sign=%s", timestamp, sign);
}
@Override
public byte type() {
return 5;
@@ -109,6 +127,7 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
/**
* DingTalk robot request body
*
* @version 1.0
*/
@Data
@@ -187,5 +206,4 @@ final class DingTalkRobotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandl
}
}
@@ -0,0 +1,487 @@
/*
* 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 com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.AlerterProperties;
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.JsonUtil;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* FeiShu app alert notify impl
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class FeiShuAppAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
/**
* get tenant access_token url
*/
private static final String TENANT_ACCESS_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
/**
* get FeiShu app employee url
*/
private static final String EMPLOYEE_URL = "https://open.feishu.cn/open-apis/ehr/v1/employees?status=2&status=4&user_id_type=user_id&page_size=100";
/**
* send FeiShu app message url
*/
private static final String APP_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
/**
* send FeiShu app batch message url
*/
private static final String APP_BATCH_MESSAGE_URL = "https://open.feishu.cn/open-apis/message/v4/batch_send/";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final byte USER_RECEIVE_TYPE = 0;
private static final byte CHAT_RECEIVE_TYPE = 1;
private static final byte PART_RECEIVE_TYPE = 2;
private static final byte ALL_RECEIVE_TYPE = 3;
private final RestTemplate restTemplate;
private final AlerterProperties alerterProperties;
@Override
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) throws AlertNoticeException {
var appId = receiver.getAppId();
var appSecret = receiver.getAppSecret();
var larkReceiveIdType = receiver.getLarkReceiveType();
try {
var accessToken = getAccessToken(appId, appSecret);
var notificationContent = JsonUtil.toJson(renderContent(noticeTemplate, alert));
JsonNode messageContent = createLarkMessage(receiver, notificationContent);
switch (larkReceiveIdType) {
case USER_RECEIVE_TYPE -> {
String[] userIds = receiver.getUserId().split(",");
if (userIds.length == 1) {
sendLarkMessage(accessToken, "user_id", userIds[0], messageContent);
} else {
sendLarkUserBatchMessage(accessToken, userIds, messageContent);
}
}
case CHAT_RECEIVE_TYPE -> sendLarkMessage(accessToken, "chat_id", receiver.getChatId(), messageContent);
case PART_RECEIVE_TYPE ->
sendLarkDepartmentBatchMessage(accessToken, receiver.getPartyId().split(","), messageContent);
case ALL_RECEIVE_TYPE -> {
List<String> userIds = new ArrayList<>();
getLarkEmployeeUserIds(accessToken, null, userIds);
sendLarkUserBatchMessage(accessToken, userIds.toArray(new String[0]), messageContent);
}
default -> throw new AlertNoticeException("Invalid larkReceiveIdType: " + larkReceiveIdType);
}
} catch (Exception e) {
throw new AlertNoticeException("[FeiShu App Notify Error] " + e.getMessage());
}
}
@Override
public byte type() {
return 14;
}
/**
* Send FeiShu app message to chat or designated personnel
*
* @param accessToken Tenant access token
* @param receiverIdType FeiShu app send message receiver id type: user_id, chat_id
* @param receiverId FeiShu app user id or chat id
* @param messageContent Message content
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send message</a>
*/
private void sendLarkMessage(String accessToken, String receiverIdType, String receiverId, JsonNode messageContent) throws JsonProcessingException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
FeiShuAppMessageDto messageDto = FeiShuAppMessageDto.builder()
.receiveId(receiverId)
.content(escapedCompactJson(messageContent))
.build();
HttpEntity<FeiShuAppMessageDto> request = new HttpEntity<>(messageDto, headers);
call(APP_MESSAGE_URL + "?receive_id_type=" + receiverIdType, request, HttpMethod.POST, FeiShuAppResponse.class);
}
/**
* Send FeiShu app department batch message
*
* @param accessToken Tenant access token
* @param partyIds FeiShu app department ids
* @param messageContent Message content
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send batch message</a>
*/
private void sendLarkDepartmentBatchMessage(String accessToken, String[] partyIds, JsonNode messageContent) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
FeiShuAppBatchMessageDto batchMessageDto = FeiShuAppBatchMessageDto.builder()
.departmentIds(partyIds)
.card(messageContent)
.build();
HttpEntity<FeiShuAppBatchMessageDto> request = new HttpEntity<>(batchMessageDto, headers);
call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST, FeiShuAppResponse.class);
}
/**
* Send FeiShu app batch message to user
*
* @param accessToken Tenant access token
* @param userIds FeiShu app user ids
* @param messageContent Message content
* @see <a href="https://open.feishu.cn/document/server-docs/im-v1/batch_message/send-messages-in-batches">send batch message</a>
*/
private void sendLarkUserBatchMessage(String accessToken, String[] userIds, JsonNode messageContent) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
FeiShuAppBatchMessageDto batchMessageDto = FeiShuAppBatchMessageDto.builder()
.userIds(userIds)
.card(messageContent)
.build();
HttpEntity<FeiShuAppBatchMessageDto> request = new HttpEntity<>(batchMessageDto, headers);
FeiShuAppResponse call = call(APP_BATCH_MESSAGE_URL, request, HttpMethod.POST, FeiShuAppResponse.class);
}
/**
* Get FeiShu app tenant access token
*
* @param appId Unique identifier for the application, obtained after creating the application
* @param appSecret Application key, obtained after creating the application
* @return Tenant access token
* @see <a href="https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal">tenant_access_token</a>
*/
private String getAccessToken(String appId, String appSecret) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
FeiShuAppAccessTokenDto accessTokenRequest = FeiShuAppAccessTokenDto.builder()
.appId(appId)
.appSecret(appSecret)
.build();
HttpEntity<FeiShuAppAccessTokenDto> request = new HttpEntity<>(accessTokenRequest, headers);
FeiShuAppAccessTokenResponse data = call(TENANT_ACCESS_TOKEN_URL, request, HttpMethod.POST, FeiShuAppAccessTokenResponse.class);
return data.getTenantAccessToken();
}
/**
* Get FeiShu app employee user ids
*
* @param accessToken FeiShu app tenant access token
* @param pageToken Paging marker, left blank for the first request, indicating traversal from scratch;
* When there are more items in the pagination query result, a new page_token will be returned at the same time.
* The next iteration can use this page_token to obtain the query result
* @param userIds Collection for recursive padding
* @see <a href="https://open.feishu.cn/document/server-docs/ehr-v1/list?appId=cli_a999532b1f52900b">https://open.feishu.cn/open-apis/ehr/v1/employees</a>
*/
private void getLarkEmployeeUserIds(String accessToken, String pageToken, List<String> userIds) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
HttpEntity<String> request = new HttpEntity<>(headers);
var url = StringUtils.isNotBlank(pageToken) ? EMPLOYEE_URL + "&page_token=" + pageToken : EMPLOYEE_URL;
FeiShuAppEmployeeResponse employeeResponse = call(url,
request,
HttpMethod.GET,
FeiShuAppEmployeeResponse.class);
if (Objects.equals(employeeResponse.getCode(), 0)) {
userIds.addAll(employeeResponse.getData().getItems().stream().map(FeiShuAppEmployeeResponse.Employee::getUserId).toList());
var hasMore = employeeResponse.getData().getHasMore();
if (Boolean.TRUE.equals(hasMore)) {
getLarkEmployeeUserIds(accessToken, employeeResponse.getData().getPageToken(), userIds);
}
}
}
private <R extends FeiShuAppResponse, E> R call(String url, HttpEntity<E> request, HttpMethod httpMethod, Class<R> responseType) {
ResponseEntity<R> response = restTemplate.exchange(url, httpMethod, request, responseType);
if (Objects.nonNull(response.getBody()) && !Objects.equals(response.getBody().getCode(), 0)) {
log.warn("Send FeiShu App Error: {}", response.getBody().getMsg());
throw new AlertNoticeException("Http StatusCode " + response.getStatusCode() + " Error: " + response.getBody().getMsg());
}
return response.getBody();
}
private JsonNode createLarkMessage(NoticeReceiver receiver, String notificationContent) throws JsonProcessingException {
String larkCardMessage = """
{
"schema": "2.0",
"config": {
"update_multi": true,
"locales": [
"en_us",
"zh_cn"
],
"style": {
"text_size": {
"normal_v2": {
"default": "normal",
"pc": "normal",
"mobile": "heading"
}
}
}
},
"body": {
"direction": "vertical",
"padding": "12px 12px 12px 12px",
"elements": [
{
"tag": "markdown",
"content": "%s",
"i18n_content": {
"en_us": ""
},
"text_align": "left",
"text_size": "normal_v2",
"margin": "0px 0px 0px 0px"
},
{
"tag": "hr",
"margin": "0px 0px 0px 0px"
},
{
"tag": "column_set",
"horizontal_align": "left",
"columns": [
{
"tag": "column",
"width": "weighted",
"elements": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "登入控制台",
"i18n_content": {
"en_us": "Login In"
}
},
"type": "default",
"width": "default",
"size": "medium",
"behaviors": [
{
"type": "open_url",
"default_url": "%s",
"pc_url": "",
"ios_url": "",
"android_url": ""
}
]
}
],
"direction": "horizontal",
"vertical_spacing": "8px",
"horizontal_align": "left",
"vertical_align": "top",
"weight": 1
}
],
"margin": "0px 0px 0px 0px"
}
]
},
"header": {
"title": {
"tag": "plain_text",
"content": "HertzBeat 告警",
"i18n_content": {
"en_us": "HertzBeat Alarm"
}
},
"subtitle": {
"tag": "plain_text",
"content": ""
},
"template": "red",
"padding": "12px 12px 12px 12px"
}
}
""";
Byte larkReceiveIdType = receiver.getLarkReceiveType();
String userId = receiver.getUserId();
String atUserElement = "";
if (Objects.equals(larkReceiveIdType, CHAT_RECEIVE_TYPE) && StringUtils.isNotBlank(userId)) {
atUserElement = "\\n" + Arrays.stream(userId.split(","))
.map(id -> "<at id=" + id + "></at>")
.collect(Collectors.joining(" "));
}
if (notificationContent.startsWith("\"") && notificationContent.endsWith("\"")) {
notificationContent = StringUtils.removeStart(notificationContent, "\"");
notificationContent = StringUtils.removeEnd(notificationContent, "\"");
}
String jsonStr = String.format(larkCardMessage,
notificationContent.replace("\"", "\\\"") + atUserElement,
alerterProperties.getConsoleUrl());
return OBJECT_MAPPER.readTree(jsonStr);
}
private String escapedCompactJson(JsonNode json) throws JsonProcessingException {
return OBJECT_MAPPER.writeValueAsString(json);
}
/**
* feiShu app response
*/
@Data
protected static class FeiShuAppResponse {
private Integer code;
private String msg;
}
/**
* FeiShu app message get tenant access token request
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
protected static class FeiShuAppAccessTokenDto {
@JsonProperty("app_id")
private String appId;
@JsonProperty("app_secret")
private String appSecret;
}
/**
* FeiShu app message get tenant access token response
*/
@EqualsAndHashCode(callSuper = true)
@Data
protected static class FeiShuAppAccessTokenResponse extends FeiShuAppResponse {
@JsonProperty("tenant_access_token")
private String tenantAccessToken;
}
/**
* FeiShu app employee response
*/
@EqualsAndHashCode(callSuper = true)
@Data
protected static class FeiShuAppEmployeeResponse extends FeiShuAppResponse {
private EmployeeResponseData data;
@Data
private static class EmployeeResponseData {
@JsonProperty("page_token")
private String pageToken;
@JsonProperty("has_more")
private Boolean hasMore;
private List<Employee> items;
}
@Data
private static class Employee {
@JsonProperty("user_id")
private String userId;
}
}
/**
* FeiShu app message request
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
protected static class FeiShuAppMessageDto {
@JsonProperty("receive_id")
private String receiveId;
@Builder.Default
@JsonProperty("msg_type")
private String msgType = "interactive";
@JsonProperty("content")
private String content;
@Builder.Default
private final String uuid = UUID.randomUUID().toString();
}
/**
* FeiShu app batch message request
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
protected static class FeiShuAppBatchMessageDto {
@Builder.Default
@JsonProperty("msg_type")
private final String msgType = "interactive";
private JsonNode card;
@JsonProperty("department_ids")
private String[] departmentIds;
@JsonProperty("user_ids")
private String[] userIds;
}
}
@@ -48,6 +48,11 @@ final class WebHookAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
}
HttpHeaders headers = new HttpHeaders();
if ("Basic".equalsIgnoreCase(receiver.getHookAuthType())) {
headers.setBasicAuth(receiver.getHookAuthToken());
} else if ("Bearer".equalsIgnoreCase(receiver.getHookAuthType())) {
headers.setBearerAuth(receiver.getHookAuthToken());
}
headers.setContentType(MediaType.APPLICATION_JSON);
// alert.setContent(escapeJsonStr(alert.getContent()));
String webhookJson = renderContent(noticeTemplate, alert);
@@ -18,6 +18,7 @@
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
@@ -43,6 +44,7 @@ import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
@@ -70,6 +72,8 @@ public class HuaweiCloudExternAlertService implements ExternAlertService {
private static final String CHARSET_UTF8 = StandardCharsets.UTF_8.name();
private static final String SUBSCRIBE_URL_PREFIX = "https://console.huaweicloud.com/smn/subscription/confirm";
private final AlarmCommonReduce alarmCommonReduce;
public HuaweiCloudExternAlertService(AlarmCommonReduce alarmCommonReduce) {
@@ -197,6 +201,9 @@ public class HuaweiCloudExternAlertService implements ExternAlertService {
if (StringUtils.isBlank(subscribeUrl)) {
return;
}
if (!subscribeUrl.startsWith(SUBSCRIBE_URL_PREFIX)) {
throw new SecurityException("Untrusted domain: " + subscribeUrl);
}
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(subscribeUrl);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
@@ -303,6 +310,13 @@ public class HuaweiCloudExternAlertService implements ExternAlertService {
*/
private X509Certificate getCertificate(String signCertUrl) throws Exception {
URL url = new URL(signCertUrl);
if (!"https".equalsIgnoreCase(url.getProtocol())) {
throw new SecurityException("Only HTTPS is allowed");
}
boolean trusted = Arrays.stream(Region.values()).anyMatch(ep -> ep.getEndpoint().equals(url.getHost()));
if (!trusted) {
throw new SecurityException("Untrusted domain: " + url.getHost());
}
try (InputStream in = url.openStream()) {
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
return (X509Certificate) cf.generateCertificate(in);
@@ -340,4 +354,51 @@ public class HuaweiCloudExternAlertService implements ExternAlertService {
return "huaweicloud-ces";
}
/**
* doc: <a href="https://console.huaweicloud.com/apiexplorer/#/endpoint/SMN">SMN API</a>
*/
@Getter
private enum Region {
AE_AD_1("ae-ad-1", "smn.ae-ad-1.myhuaweicloud.com"),
AF_SOUTH_1("af-south-1", "smn.af-south-1.myhuaweicloud.com"),
AP_SOUTHEAST_1("ap-southeast-1", "smn.ap-southeast-1.myhuaweicloud.com"),
AP_SOUTHEAST_2("ap-southeast-2", "smn.ap-southeast-2.myhuaweicloud.com"),
AP_SOUTHEAST_3("ap-southeast-3", "smn.ap-southeast-3.myhuaweicloud.com"),
AP_SOUTHEAST_4("ap-southeast-4", "smn.ap-southeast-4.myhuaweicloud.com"),
CN_EAST_2("cn-east-2", "smn.cn-east-2.myhuaweicloud.com"),
CN_EAST_3("cn-east-3", "smn.cn-east-3.myhuaweicloud.com"),
CN_EAST_4("cn-east-4", "smn.cn-east-4.myhuaweicloud.com"),
CN_EAST_5("cn-east-5", "smn.cn-east-5.myhuaweicloud.com"),
CN_NORTH_1("cn-north-1", "smn.cn-north-1.myhuaweicloud.com"),
CN_NORTH_11("cn-north-11", "smn.cn-north-11.myhuaweicloud.com"),
CN_NORTH_12("cn-north-12", "smn.cn-north-12.myhuaweicloud.com"),
CN_NORTH_2("cn-north-2", "smn.cn-north-2.myhuaweicloud.cn"),
CN_NORTH_4("cn-north-4", "smn.cn-north-4.myhuaweicloud.com"),
CN_NORTH_9("cn-north-9", "smn.cn-north-9.myhuaweicloud.com"),
CN_SOUTH_1("cn-south-1", "smn.cn-south-1.myhuaweicloud.com"),
CN_SOUTH_2("cn-south-2", "smn.cn-south-2.myhuaweicloud.com"),
CN_SOUTH_4("cn-south-4", "smn.cn-south-4.myhuaweicloud.com"),
CN_SOUTHWEST_2("cn-southwest-2", "smn.cn-southwest-2.myhuaweicloud.com"),
CN_SOUTHWEST_3("cn-southwest-3", "smn.cn-southwest-3.myhuaweicloud.com"),
EU_WEST_0("eu-west-0", "smn.eu-west-0.myhuaweicloud.com"),
LA_NORTH_2("la-north-2", "smn.la-north-2.myhuaweicloud.com"),
LA_SOUTH_2("la-south-2", "smn.la-south-2.myhuaweicloud.com"),
MY_KUALALUMPUR_1("my-kualalumpur-1", "smn.my-kualalumpur-1.myhuaweicloud.com"),
NA_MEXICO_1("na-mexico-1", "smn.na-mexico-1.myhuaweicloud.com"),
RU_MOSCOW_1("ru-moscow-1", "smn.ru-moscow-1.myhuaweicloud.com"),
SA_BRAZIL_1("sa-brazil-1", "smn.sa-brazil-1.myhuaweicloud.com"),
TR_WEST_1("tr-west-1", "smn.tr-west-1.myhuaweicloud.com"),
EU_WEST_101("eu-west-101", "smn.eu-west-101.myhuaweicloud.eu");
private final String id;
private final String endpoint;
Region(String id, String endpoint) {
this.id = id;
this.endpoint = endpoint;
}
}
}
@@ -303,9 +303,8 @@ public class NoticeConfigServiceImpl implements NoticeConfigService, CommandLine
@Override
public boolean sendTestMsg(NoticeReceiver noticeReceiver) {
Map<String, String> labels = new HashMap<>(8);
labels.put(CommonConstants.LABEL_INSTANCE, "1000000");
labels.put(CommonConstants.LABEL_INSTANCE, "127.0.0.1");
labels.put(CommonConstants.LABEL_ALERT_NAME, "CPU Usage Alert");
labels.put(CommonConstants.LABEL_INSTANCE_HOST, "127.0.0.1");
Map<String, String> annotations = new HashMap<>(8);
annotations.put("suggest", "Please check the CPU usage of the server");
SingleAlert singleAlert1 = SingleAlert.builder()
@@ -88,7 +88,7 @@ public class CryptoUtils {
* @return lowercase hexadecimal string
*/
public static String hmacSha256Hex(String key, String data) {
return hmacSha256Hex(hmac256(key.getBytes(UTF8), data), data);
return hmacSha256Hex(key.getBytes(UTF8), data);
}
/**
@@ -125,7 +125,7 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj_copy");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
meta.put(MetricDataConstants.INSTANCE, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(destination, mode, metricValue));
@@ -175,7 +175,7 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Cool_Stingray_34Nj");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
meta.put(MetricDataConstants.INSTANCE, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(destination, mode, metricValue));
@@ -225,7 +225,7 @@ public class MetricsRealTimeAlertCalculatorMatchTest {
Map<String, String> meta = new HashMap<>();
meta.put(MetricDataConstants.INSTANCE_NAME, "Vibrant_Gazelle_83vJ");
meta.put(MetricDataConstants.INSTANCE_HOST, "127.0.0.1");
meta.put(MetricDataConstants.INSTANCE, "127.0.0.1");
builder.addMetadataAll(meta);
builder.addAllFields(Lists.newArrayList(responseTime));
@@ -108,7 +108,6 @@ class WindowedLogRealTimeAlertCalculatorTest {
verify(timeService).isLateData(anyLong());
verify(timeService).updateMaxTimestamp(anyLong());
verify(logWorker).reduceAndSendLogTask(validLogEntry);
verify(dataQueue).sendLogEntryToStorage(validLogEntry);
}
@Test
@@ -134,7 +133,6 @@ class WindowedLogRealTimeAlertCalculatorTest {
verify(timeService, never()).isLateData(anyLong());
verify(timeService, never()).updateMaxTimestamp(anyLong());
verify(logWorker, never()).reduceAndSendLogTask(any());
verify(dataQueue).sendLogEntryToStorage(invalidTimestampLogEntry);
}
@Test
@@ -162,7 +160,6 @@ class WindowedLogRealTimeAlertCalculatorTest {
verify(timeService).isLateData(anyLong());
verify(timeService, never()).updateMaxTimestamp(anyLong());
verify(logWorker, never()).reduceAndSendLogTask(any());
verify(dataQueue).sendLogEntryToStorage(lateDataLogEntry);
}
@Test
@@ -88,7 +88,7 @@ class AlertDefineControllerTest {
Monitor.builder()
.id(1L)
.app("app")
.host("localhost")
.instance("localhost")
.name("monitor")
.build()
)
@@ -0,0 +1,333 @@
/*
* 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 org.apache.hertzbeat.alert.AlerterProperties;
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.entity.alerter.SingleAlert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ResourceBundle;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
/**
* Test case for FeiShu App Alert Notify
*/
@ExtendWith(MockitoExtension.class)
class FeiShuAppAlertNotifyHandlerImplTest {
@Mock
private RestTemplate restTemplate;
@Mock
private ResourceBundle bundle;
@Mock
private AlerterProperties alerterProperties;
@InjectMocks
private FeiShuAppAlertNotifyHandlerImpl feiShuAppAlertNotifyHandler;
private NoticeReceiver receiver;
private GroupAlert groupAlert;
private NoticeTemplate template;
@BeforeEach
public void setUp() {
receiver = new NoticeReceiver();
receiver.setId(1L);
receiver.setName("test-receiver");
receiver.setType((byte) 14);
receiver.setAppId("cli-test-app-id");
receiver.setAppSecret("test-app-secret");
groupAlert = new GroupAlert();
SingleAlert singleAlert = new SingleAlert();
singleAlert.setLabels(new HashMap<>());
singleAlert.getLabels().put("severity", "critical");
singleAlert.getLabels().put("alertname", "Test Alert");
List<SingleAlert> alerts = new ArrayList<>();
alerts.add(singleAlert);
groupAlert.setAlerts(alerts);
template = new NoticeTemplate();
template.setId(1L);
template.setName("test-template");
template.setContent("test content");
lenient().when(bundle.getString("alerter.notify.title")).thenReturn("Alert Notification");
lenient().when(alerterProperties.getConsoleUrl()).thenReturn("https://console.hertzbeat.com");
}
/**
* Test successful notification to single user
*/
@Test
public void testNotifyAlertSuccessSingleUser() {
// Setup receiver for single user
receiver.setLarkReceiveType((byte) 0);
receiver.setUserId("user-001");
// Mock access token response
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
accessTokenResp.setCode(0);
accessTokenResp.setMsg("success");
accessTokenResp.setTenantAccessToken("test-access-token");
// Mock message send response
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
messageResp.setCode(0);
messageResp.setMsg("success");
// Mock restTemplate calls
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
.thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK));
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
.thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
}
/**
* Test successful notification to multiple users
*/
@Test
public void testNotifyAlertSuccessMultipleUsers() {
receiver.setLarkReceiveType((byte) 0);
receiver.setUserId("user-001,user-002,user-003");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
accessTokenResp.setCode(0);
accessTokenResp.setMsg("success");
accessTokenResp.setTenantAccessToken("test-access-token");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
messageResp.setCode(0);
messageResp.setMsg("success");
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
.thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK));
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
.thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
}
/**
* Test successful notification to chat
*/
@Test
public void testNotifyAlertSuccessChat() {
receiver.setLarkReceiveType((byte) 1);
receiver.setChatId("chat-001");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
accessTokenResp.setCode(0);
accessTokenResp.setMsg("success");
accessTokenResp.setTenantAccessToken("test-access-token");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
messageResp.setCode(0);
messageResp.setMsg("success");
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
.thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK));
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
.thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
}
/**
* Test successful notification to departments
*/
@Test
public void testNotifyAlertSuccessDepartments() {
receiver.setLarkReceiveType((byte) 2);
receiver.setPartyId("dept-001,dept-002");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
accessTokenResp.setCode(0);
accessTokenResp.setMsg("success");
accessTokenResp.setTenantAccessToken("test-access-token");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
messageResp.setCode(0);
messageResp.setMsg("success");
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
.thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK));
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
.thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
}
/**
* Test notification failure due to access token error
*/
@Test
public void testNotifyAlertFailureAccessToken() {
receiver.setLarkReceiveType((byte) 0);
receiver.setUserId("user-001");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
accessTokenResp.setCode(999);
accessTokenResp.setMsg("Invalid app credentials");
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
.thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK));
assertThrows(AlertNoticeException.class, () -> {
feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
});
}
/**
* Test notification failure due to message send error
*/
@Test
public void testNotifyAlertFailureMessageSend() {
receiver.setLarkReceiveType((byte) 0);
receiver.setUserId("user-001");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
accessTokenResp.setCode(0);
accessTokenResp.setMsg("success");
accessTokenResp.setTenantAccessToken("test-access-token");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse messageResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse();
messageResp.setCode(999);
messageResp.setMsg("User not found");
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
.thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK));
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppResponse.class)))
.thenReturn(new ResponseEntity<>(messageResp, HttpStatus.OK));
assertThrows(AlertNoticeException.class, () -> {
feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
});
}
/**
* Test invalid larkReceiveType
*/
@Test
public void testInvalidLarkReceiveType() {
receiver.setLarkReceiveType((byte) 99);
receiver.setUserId("user-001");
FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse accessTokenResp =
new FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse();
accessTokenResp.setCode(0);
accessTokenResp.setMsg("success");
accessTokenResp.setTenantAccessToken("test-access-token");
when(restTemplate.exchange(
anyString(),
eq(org.springframework.http.HttpMethod.POST),
any(),
eq(FeiShuAppAlertNotifyHandlerImpl.FeiShuAppAccessTokenResponse.class)))
.thenReturn(new ResponseEntity<>(accessTokenResp, HttpStatus.OK));
assertThrows(AlertNoticeException.class, () -> {
feiShuAppAlertNotifyHandler.send(receiver, template, groupAlert);
});
}
}
@@ -146,7 +146,42 @@ public class HuaweiCloudExternAlertServiceTest {
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testSubscriptionUrlFail() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSubscribeUrl("https://console.huaweicloud.com/smn/subscription/confirm?token=477a784601d744e4ab9ab83986502d31c4b938"
+ "0ec0b64392b134e517c3aa17eb7b3a12dc9f3b4ab495e61c4dee654b435d7223ea934345bf8ae8901cef912b1d&topic_urn=urn:smn:cn-north-4"
+ ":477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX&region=cn-north-4");
externAlert.setSignature("ottf37C/2RdDgqimRQMIBU6i7XjUfPPMU760jJn71wwP3825YPoIT22uw2A9399rkm9Jrt1qUEFrDLuA5yHFLd5n/XoM4FghIgyFn7VIfgpuVM31a+co78s"
+ "YBiZ1egOCE/AwFm2oygRhfIceUj9Kw9vmc06el9TXY6RtE5tAEF6qEmICtTh45KwtCO/WRs3DY72dQi5hm0w7/tktS4WFZ1iP4LHt5eCwFvnH0u29Y96cJNI0fLUQxI5MkhgjK"
+ "77JkFK7UT6ZYJZhzgSp/B7OQGStOQx+3Duvx4T4CzccZQM3sca81Z0B0GFGWeVXuEHyCPLsayY/Iz+5Tco51elT8w==");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("242fac183d3a4936b5ead6c725a32ed0");
externAlert.setType("SubscriptionConfirmation");
externAlert.setMessage("You are invited to subscribe to topic: urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:"
+ "CES_notification_group_bngJ2aMpX. To confirm this subscription, please visit the subscribe_url included in this message. The subscribe_url is valid only within 48 hours.");
externAlert.setSigningCertUrl("https://xxxx.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
externAlert.setTimestamp("2025-06-07T15:07:14Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testCertUrlFailed() {
HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLb"
+ "vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX"
+ "w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E"
+ "0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
externAlert.setSubject("DCS Redis “dcs-h4tv” ");
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
externAlert.setType("Notification");
externAlert.setMessage("{}");
externAlert.setSigningCertUrl("https://xxx.myhuaweicloud.com/failedUrl");
externAlert.setTimestamp("2025-06-07T15:12:09Z");
externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
verify(alarmCommonReduce, never()).reduceAndSendAlarm(any(SingleAlert.class));
}
}
@@ -42,7 +42,7 @@ public class CryptoUtilsTest {
@Test
void testHmacSha256Hex() {
String signature = CryptoUtils.hmacSha256Hex("your-real-key", "your-real-data");;
assertEquals("41878ccd7ecd795a2dd7ec39be7f33fed4be3ec75f5307689e39dd6f41fdbaac", signature);
assertEquals("f09adf5f4bf94eddecf0f7c8f39a3a8dc7f95ccdc2faf2e5330bc5a78e4bba95", signature);
}
}
@@ -109,6 +109,12 @@
<artifactId>orai18n</artifactId>
<scope>provided</scope>
</dependency>
<!-- db2 -->
<dependency>
<groupId>com.ibm.db2</groupId>
<artifactId>jcc</artifactId>
<scope>provided</scope>
</dependency>
<!--redis-->
<dependency>
<groupId>io.lettuce</groupId>
@@ -152,7 +158,6 @@
</dependency>
<!--plc-->
<dependency>
<groupId>org.apache.plc4x</groupId>
@@ -172,6 +172,14 @@ public class JdbcCommonCollect extends AbstractCollect {
".*en\\s*([/\\\\]|\\\\n|/n|\\n)\\s*crypt.*",
".*ci\\s*([/\\\\]|\\\\n|/n|\\n)\\s*pher.*"
});
// DB2 bypass mode
PLATFORM_BYPASS_PATTERNS.put("db2", new String[]{
".*security\\s*([/\\\\]|\\\\n|/n|\\n)\\s*mechanism.*",
".*client\\s*([/\\\\]|\\\\n|/n|\\n)\\s*accounting.*",
".*ssl\\s*([/\\\\]|\\\\n|/n|\\n)\\s*connection.*",
".*use\\s*([/\\\\]|\\\\n|/n|\\n)\\s*client\\s*([/\\\\]|\\\\n|/n|\\n)\\s*information.*"
});
}
private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance();
@@ -531,6 +539,8 @@ public class JdbcCommonCollect extends AbstractCollect {
case "oracle" -> "jdbc:oracle:thin:@" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "dm" -> "jdbc:dm://" + host + ":" + port;
case "db2" -> "jdbc:db2://" + host + ":" + port
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
case "testcontainers" -> "jdbc:tc:" + host + ":" + port
+ ":///" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase()) + "?user=root&password=root";
default -> throw new IllegalArgumentException("Not support database platform: " + jdbcProtocol.getPlatform());
@@ -67,6 +67,8 @@ public class RedisCommonCollectImpl extends AbstractCollect {
private static final String CLUSTER = "3";
private static final String SINGLE = "1";
private static final String CLUSTER_INFO = "cluster";
private static final String UNIQUE_IDENTITY = "identity";
@@ -140,7 +142,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @return data
*/
private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException {
Map<String, StatefulRedisClusterConnection<String, String>> connectionMap = getConnectionList(metrics.getRedis());
Map<String, StatefulRedisConnection<String, String>> connectionMap = getConnectionList(metrics.getRedis());
List<Map<String, String>> list = new ArrayList<>(connectionMap.size());
connectionMap.forEach((identity, connection) ->{
String info = connection.sync().info(metrics.getName());
@@ -214,16 +216,23 @@ public class RedisCommonCollectImpl extends AbstractCollect {
* @param redisProtocol protocol
* @return connection map
*/
private Map<String, StatefulRedisClusterConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
private Map<String, StatefulRedisConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
// first connection
StatefulRedisClusterConnection<String, String> connection = getClusterConnection(redisProtocol);
Partitions partitions = connection.getPartitions();
Map<String, StatefulRedisClusterConnection<String, String>> clusterConnectionMap = new HashMap<>(partitions.size());
Map<String, StatefulRedisConnection<String, String>> clusterConnectionMap = new HashMap<>(partitions.size());
for (RedisClusterNode partition : partitions) {
RedisURI uri = partition.getUri();
redisProtocol.setHost(uri.getHost());
redisProtocol.setPort(String.valueOf(uri.getPort()));
StatefulRedisClusterConnection<String, String> clusterConnection = getClusterConnection(redisProtocol);
RedisProtocol singleRedisProtocol = RedisProtocol.builder()
.host(uri.getHost())
.port(String.valueOf(uri.getPort()))
.username(redisProtocol.getUsername())
.password(redisProtocol.getPassword())
.pattern(SINGLE)
.timeout(redisProtocol.getTimeout())
.sshTunnel(redisProtocol.getSshTunnel())
.build();
StatefulRedisConnection<String, String> clusterConnection = getSingleConnection(singleRedisProtocol);
clusterConnectionMap.put(doUri(uri.getHost(), uri.getPort()), clusterConnection);
}
return clusterConnectionMap;
@@ -19,10 +19,13 @@ package org.apache.hertzbeat.collector.collect.redis;
import static org.apache.hertzbeat.common.constants.CommonConstants.TYPE_STRING;
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import io.lettuce.core.cluster.models.partitions.Partitions;
import io.lettuce.core.cluster.models.partitions.RedisClusterNode;
import io.lettuce.core.resource.ClientResources;
@@ -37,6 +40,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -51,13 +55,19 @@ public class RedisClusterCollectImplTest {
@Mock
private StatefulRedisClusterConnection<String, String> connection;
private StatefulRedisClusterConnection<String, String> clusterConnection;
@Mock
private RedisAdvancedClusterCommands<String, String> cmd;
private StatefulRedisConnection<String, String> singleConnection;
@Mock
private RedisClusterClient client;
private RedisCommands<String, String> cmd;
@Mock
private RedisClusterClient clusterClient;
@Mock
private RedisClient singleClient;
@BeforeEach
void setUp() {
@@ -65,8 +75,10 @@ public class RedisClusterCollectImplTest {
@AfterEach
void setDown() {
connection.close();
client.shutdown();
clusterConnection.close();
singleConnection.close();
clusterClient.shutdown();
singleClient.shutdown();
}
@Test
@@ -110,10 +122,14 @@ public class RedisClusterCollectImplTest {
metrics.setAliasFields(aliasField);
metrics.setFields(fields);
Mockito.mockStatic(RedisClusterClient.class).when(() -> RedisClusterClient.create(Mockito.any(ClientResources.class),
Mockito.any(RedisURI.class))).thenReturn(client);
Mockito.when(client.connect()).thenReturn(connection);
MockedStatic<RedisClusterClient> redisClusterClientMockedStatic = Mockito.mockStatic(RedisClusterClient.class);
redisClusterClientMockedStatic.when(() -> RedisClusterClient.create(Mockito.any(ClientResources.class),
Mockito.any(RedisURI.class))).thenReturn(clusterClient);
Mockito.when(clusterClient.connect()).thenReturn(clusterConnection);
MockedStatic<RedisClient> redisClientMockedStatic = Mockito.mockStatic(RedisClient.class);
redisClientMockedStatic.when(() -> RedisClient.create(Mockito.any(ClientResources.class),
Mockito.any(RedisURI.class))).thenReturn(singleClient);
Mockito.when(singleClient.connect()).thenReturn(singleConnection);
Partitions partitions = new Partitions();
RedisClusterNode node = new RedisClusterNode();
@@ -125,9 +141,9 @@ public class RedisClusterCollectImplTest {
node2.setUri(RedisURI.create("redis://" + uri2));
partitions.add(node2);
Mockito.when(connection.getPartitions()).thenReturn(partitions);
Mockito.when(clusterConnection.getPartitions()).thenReturn(partitions);
Mockito.when(connection.sync()).thenReturn(cmd);
Mockito.when(singleConnection.sync()).thenReturn(cmd);
Mockito.when(cmd.info(metrics.getName())).thenReturn(info);
Mockito.when(cmd.clusterInfo()).thenReturn(clusterInfo);
@@ -147,6 +163,8 @@ public class RedisClusterCollectImplTest {
assertEquals(row.getColumns(2), uri2);
}
}
redisClusterClientMockedStatic.close();
redisClientMockedStatic.close();
}
}
@@ -25,6 +25,8 @@ import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* for collector instance
* send collect response data by netty
@@ -108,4 +110,24 @@ public class NettyDataQueue implements CommonDataQueue {
public LogEntry pollLogEntryToStorage() throws InterruptedException {
return null;
}
@Override
public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) {
}
@Override
public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException {
return List.of();
}
@Override
public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) {
}
@Override
public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException {
return List.of();
}
}
@@ -125,6 +125,11 @@ public class CommonProperties {
*/
private String logEntryToStorageQueueName;
/**
* Timeout for blocking wait in seconds (defaults to 1 second if not configured)
*/
private Long waitTimeout;
}
/**
@@ -97,11 +97,6 @@ public interface CommonConstants {
*/
String LABEL_ALERT_NAME = "alertname";
/**
* label key: instance host
*/
String LABEL_INSTANCE_HOST = "instancehost";
/**
* label key: instance name
*/
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.constants;
/**
* HertzBeat Reserved Keywords Enum
*/
public enum HertzBeatKeywordsEnum {
INSTANCE("instance", "metric_instance");
private final String keyword;
private final String alias;
HertzBeatKeywordsEnum(String keyword, String alias) {
this.keyword = keyword;
this.alias = alias;
}
public String getKeyword() {
return keyword;
}
public String getAlias() {
return alias;
}
}
@@ -38,5 +38,5 @@ public interface MetricDataConstants {
String CODE = "code";
String MSG = "msg";
String INSTANCE_NAME = "instancename";
String INSTANCE_HOST = "instancehost";
String INSTANCE = "instance";
}
@@ -68,12 +68,13 @@ public class NoticeReceiver {
private String name;
@Schema(title = "Notification information method: 0-SMS 1-Email 2-webhook 3-WeChat Official Account 4-Enterprise WeChat Robot "
+ "5-DingTalk Robot 6-FeiShu Robot 7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise WeChat app message",
+ "5-DingTalk Robot 6-FeiShu Robot 7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise WeChat app message "
+ "11-Slack 12-Discord 13-Gotify 14-FeiShu app message",
description = "Notification information method: "
+ "0-SMS 1-Email 2-webhook 3-WeChat Official Account "
+ "4-Enterprise WeChat Robot 5-DingTalk Robot 6-FeiShu Robot "
+ "7-Telegram Bot 8-SlackWebHook 9-Discord Bot 10-Enterprise "
+ "WeChat app message",
+ "WeChat app message 11-Slack 12-Discord 13-Gotify 14-FeiShu app message",
accessMode = READ_WRITE)
@Min(0)
@NotNull(message = "type can not null")
@@ -98,6 +99,20 @@ public class NoticeReceiver {
@Column(length = 1000)
private String hookUrl;
@Schema(title = "Auth Type: WebHook Authorization Type",
description = "Auth Type: WebHook Authorization Type, one of the 'None', 'Basic' and 'Bearer'",
example = "None", accessMode = READ_ONLY)
@Size(max = 300)
@Column(length = 300)
private String hookAuthType;
@Schema(title = "Auth Token: WebHook Authorization Token",
description = "Auth Token: WebHook Authorization Token",
example = "YWxpY2U6c3VwZXJtYW4", accessMode = READ_WRITE)
@Size(max = 300)
@Column(length = 300)
private String hookAuthToken;
@Schema(title = "openId : The notification method is valid for WeChat official account, enterprise WeChat robot or FlyBook robot",
description = "openId : The notification method is valid for WeChat official account, enterprise WeChat robot or FlyBook robot",
example = "343432", accessMode = READ_WRITE)
@@ -105,6 +120,12 @@ public class NoticeReceiver {
@Column(length = 300)
private String wechatId;
@Schema(title = "FeiShu app id : The notification method is valid for FeiShu app message",
description = "FeiShu app id : The notification method is valid for FeiShu app message",
example = "34823984635647", accessMode = READ_WRITE)
@Size(max = 255)
private String appId;
@Schema(title = "Access token : The notification method is valid for DingTalk robot",
description = "Access token : The notification method is valid for DingTalk robot",
example = "34823984635647", accessMode = READ_WRITE)
@@ -127,11 +148,21 @@ public class NoticeReceiver {
example = "779294123", accessMode = READ_WRITE)
private String tgMessageThreadId;
@Schema(title = "FeiShu app message receiveType: 0-user 1-chat 2-party 3-all",
description = "FeiShu app message receiveType: 0-user 1-chat 2-party 3-all",
example = "1", accessMode = READ_WRITE)
private Byte larkReceiveType;
@Schema(title = "DingTalk,FeiShu,WeWork user id: The notification method is valid for DingTalk,FeiShu,WeWork Bot",
description = "DingTalk,FeiShu,WeWork user id: The notification method is valid for DingTalk,FeiShu,WeWork Bot",
example = "779294123", accessMode = READ_WRITE)
private String userId;
@Schema(title = "FeiShu app message chatId: The notification method is valid for FeiShu app message",
description = "FeiShu app message chatId: The notification method is valid for FeiShu app message",
example = "779294123", accessMode = READ_WRITE)
private String chatId;
@Schema(title = "URL address: The notification method is valid for Slack",
description = "URL address: The notification method is valid for Slack",
example = "https://hooks.slack.com/services/XXXX/XXXX/XXXX", accessMode = READ_WRITE)
@@ -18,13 +18,14 @@
package org.apache.hertzbeat.common.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* Metric History Range Query Data
*/
@@ -35,8 +36,8 @@ import lombok.NoArgsConstructor;
@Schema(description = "Metric History Range Query Data")
public class MetricsHistoryData {
@Schema(title = "Monitoring Task ID")
private Long id;
@Schema(title = "Monitor Instance (e.g., ip:port or domain)")
private String instance;
@Schema(title = "Monitoring Type")
private String app;
@@ -52,7 +52,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Entity
@Table(name = "hzb_monitor", indexes = {
@Index(name = "monitor_query_index", columnList = "app"),
@Index(name = "monitor_query_index", columnList = "host"),
@Index(name = "monitor_query_index", columnList = "instance"),
@Index(name = "monitor_query_index", columnList = "name")
})
@Data
@@ -82,10 +82,10 @@ public class Monitor {
@Size(max = 100)
private String scrape;
@Schema(title = "peer host: ipv4, ipv6, domain name", example = "192.167.25.11", accessMode = READ_WRITE)
@Schema(title = "the monitor target: ip/domain+port or ip/domain", example = "192.167.25.11:8081", accessMode = READ_WRITE)
@Size(max = 100)
@HostValid
private String host;
private String instance;
@Schema(title = "Monitoring of the acquisition interval time in seconds", example = "600", accessMode = READ_WRITE)
@Min(10)
@@ -225,9 +225,9 @@ public final class CollectRep {
return metadata.getOrDefault(MetricDataConstants.INSTANCE_NAME, null);
}
public String getInstanceHost() {
public String getInstance() {
Map<String, String> metadata = getMetadata();
return metadata.getOrDefault(MetricDataConstants.INSTANCE_HOST, null);
return metadata.getOrDefault(MetricDataConstants.INSTANCE, null);
}
public Map<String, String> getLabels() {
@@ -501,8 +501,8 @@ public final class CollectRep {
return metadata.getOrDefault(MetricDataConstants.INSTANCE_NAME, null);
}
public String getInstanceHost() {
return metadata.getOrDefault(MetricDataConstants.INSTANCE_HOST, null);
public String getInstance() {
return metadata.getOrDefault(MetricDataConstants.INSTANCE, null);
}
public Map<String, String> getLabels() {
@@ -37,7 +37,7 @@ import lombok.NoArgsConstructor;
*/
@Entity
@Table(name = "hzb_history", indexes = {
@Index(name = "history_query_index", columnList = "monitorId"),
@Index(name = "history_query_index", columnList = "instance"),
@Index(name = "history_query_index", columnList = "app"),
@Index(name = "history_query_index", columnList = "metrics"),
@Index(name = "history_query_index", columnList = "metric")
@@ -54,8 +54,8 @@ public class History {
@Schema(description = "Metric data history entity primary key index ID", example = "87584674384", accessMode = READ_ONLY)
private Long id;
@Schema(title = "Monitoring Id", example = "87432674336", accessMode = READ_WRITE)
private Long monitorId;
@Schema(title = "Monitoring instance", example = "127.0.0.1:8080", accessMode = READ_WRITE)
private String instance;
@Schema(title = "Monitoring Type mysql oracle db2")
private String app;
@@ -67,7 +67,7 @@ public class History {
private String metric;
@Column(length = 5000)
private String instance;
private String metricLabels;
@Schema(title = "Metric Type 0: Number 1String")
private Byte metricType;
@@ -17,6 +17,7 @@
package org.apache.hertzbeat.common.queue;
import java.util.List;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.apache.hertzbeat.common.entity.message.CollectRep;
@@ -91,4 +92,32 @@ public interface CommonDataQueue {
* @throws InterruptedException when poll timeout
*/
LogEntry pollLogEntryToStorage() throws InterruptedException;
/**
* send batch log entries to alert queue
* @param logEntries list of log entry data
*/
void sendLogEntryToAlertBatch(List<LogEntry> logEntries);
/**
* poll batch log entries from alert queue
* @param maxBatchSize maximum number of entries to poll
* @return list of log entry data
* @throws InterruptedException when poll timeout
*/
List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException;
/**
* send batch log entries to storage queue
* @param logEntries list of log entry data
*/
void sendLogEntryToStorageBatch(List<LogEntry> logEntries);
/**
* poll batch log entries from storage queue
* @param maxBatchSize maximum number of entries to poll
* @return list of log entry data
* @throws InterruptedException when poll timeout
*/
List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException;
}
@@ -17,9 +17,12 @@
package org.apache.hertzbeat.common.queue.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.DataQueueConstants;
import org.apache.hertzbeat.common.entity.log.LogEntry;
@@ -117,6 +120,48 @@ public class InMemoryCommonDataQueue implements CommonDataQueue, DisposableBean
return logEntryToStorageQueue.take();
}
@Override
public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) {
if (logEntries == null || logEntries.isEmpty()) {
return;
}
for (LogEntry logEntry : logEntries) {
logEntryQueue.offer(logEntry);
}
}
@Override
public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException {
List<LogEntry> batch = new ArrayList<>(maxBatchSize);
LogEntry first = logEntryQueue.poll(1, TimeUnit.SECONDS);
if (first != null) {
batch.add(first);
logEntryQueue.drainTo(batch, maxBatchSize - 1);
}
return batch;
}
@Override
public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) {
if (logEntries == null || logEntries.isEmpty()) {
return;
}
for (LogEntry logEntry : logEntries) {
logEntryToStorageQueue.offer(logEntry);
}
}
@Override
public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException {
List<LogEntry> batch = new ArrayList<>(maxBatchSize);
LogEntry first = logEntryToStorageQueue.poll(1, TimeUnit.SECONDS);
if (first != null) {
batch.add(first);
logEntryToStorageQueue.drainTo(batch, maxBatchSize - 1);
}
return batch;
}
@Override
public void destroy() {
metricsDataToAlertQueue.clear();
@@ -18,8 +18,10 @@
package org.apache.hertzbeat.common.queue.impl;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;
@@ -33,6 +35,7 @@ import org.apache.hertzbeat.common.serialize.KafkaLogEntryDeserializer;
import org.apache.hertzbeat.common.serialize.KafkaLogEntrySerializer;
import org.apache.hertzbeat.common.serialize.KafkaMetricsDataDeserializer;
import org.apache.hertzbeat.common.serialize.KafkaMetricsDataSerializer;
import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
@@ -117,7 +120,7 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
Map<String, Object> metricsToStorageConsumerConfig = new HashMap<>(consumerConfig);
metricsToStorageConsumerConfig.put("group.id", "metrics-persistent-consumer");
metricsDataToStorageConsumer = new KafkaConsumer<>(metricsToStorageConsumerConfig, new LongDeserializer(), new KafkaMetricsDataDeserializer());
metricsDataToStorageConsumer.subscribe(Collections.singletonList(kafka.getMetricsDataTopic()));
metricsDataToStorageConsumer.subscribe(Collections.singletonList(kafka.getMetricsDataToStorageTopic()));
Map<String, Object> serviceDiscoveryDataConsumerConfig = new HashMap<>(consumerConfig);
serviceDiscoveryDataConsumerConfig.put("group.id", "service-discovery-data-consumer");
@@ -176,6 +179,7 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
dataConsumer.commitAsync();
} catch (Exception e) {
log.error(e.getMessage());
throw new CommonDataQueueUnknownException(e.getMessage(), e);
} finally {
lock.unlock();
}
@@ -259,6 +263,92 @@ public class KafkaCommonDataQueue implements CommonDataQueue, DisposableBean {
return genericPollDataFunction(logEntryToStorageQueue, logEntryToStorageConsumer, logEntryToStorageLock);
}
@Override
public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) {
if (logEntries == null || logEntries.isEmpty()) {
return;
}
if (logEntryProducer != null) {
try {
for (LogEntry logEntry : logEntries) {
ProducerRecord<Long, LogEntry> record = new ProducerRecord<>(kafka.getLogEntryDataTopic(), logEntry);
logEntryProducer.send(record);
}
} catch (Exception e) {
log.error("Failed to send LogEntry batch to Kafka: {}", e.getMessage());
for (LogEntry logEntry : logEntries) {
logEntryQueue.offer(logEntry);
}
}
} else {
log.warn("logEntryProducer is not enabled, using memory queue");
for (LogEntry logEntry : logEntries) {
logEntryQueue.offer(logEntry);
}
}
}
@Override
public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException {
return genericBatchPollDataFunction(logEntryQueue, logEntryConsumer, logEntryLock, maxBatchSize);
}
@Override
public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) {
if (logEntries == null || logEntries.isEmpty()) {
return;
}
if (logEntryProducer != null) {
try {
for (LogEntry logEntry : logEntries) {
ProducerRecord<Long, LogEntry> record = new ProducerRecord<>(kafka.getLogEntryDataToStorageTopic(), logEntry);
logEntryProducer.send(record);
}
} catch (Exception e) {
log.error("Failed to send LogEntry batch to storage via Kafka: {}", e.getMessage());
for (LogEntry logEntry : logEntries) {
logEntryToStorageQueue.offer(logEntry);
}
}
} else {
log.warn("logEntryProducer is not enabled, using memory queue for storage");
for (LogEntry logEntry : logEntries) {
logEntryToStorageQueue.offer(logEntry);
}
}
}
@Override
public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException {
return genericBatchPollDataFunction(logEntryToStorageQueue, logEntryToStorageConsumer, logEntryToStorageLock, maxBatchSize);
}
public <T> List<T> genericBatchPollDataFunction(LinkedBlockingQueue<T> dataQueue, KafkaConsumer<Long, T> dataConsumer,
ReentrantLock lock, int maxBatchSize) throws InterruptedException {
List<T> batch = new ArrayList<>(maxBatchSize);
lock.lockInterruptibly();
try {
dataQueue.drainTo(batch, maxBatchSize);
if (batch.size() >= maxBatchSize) {
return batch;
}
ConsumerRecords<Long, T> records = dataConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<Long, T> record : records) {
if (batch.size() < maxBatchSize) {
batch.add(record.value());
} else {
dataQueue.offer(record.value());
}
}
dataConsumer.commitAsync();
} catch (Exception e) {
log.error(e.getMessage());
} finally {
lock.unlock();
}
return batch;
}
@Override
public void destroy() throws Exception {
if (metricsDataProducer != null) {
@@ -17,6 +17,9 @@
package org.apache.hertzbeat.common.queue.impl;
import java.util.ArrayList;
import java.util.List;
import io.lettuce.core.KeyValue;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
@@ -29,10 +32,13 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.serialize.RedisLogEntryCodec;
import org.apache.hertzbeat.common.serialize.RedisMetricsDataCodec;
import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.Objects;
/**
* common data queue implement redis.
*/
@@ -56,6 +62,7 @@ public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean {
private final String logEntryQueueName;
private final String logEntryToStorageQueueName;
private final CommonProperties.RedisProperties redisProperties;
private final Long waitTimeout;
public RedisCommonDataQueue(CommonProperties properties) {
@@ -83,36 +90,23 @@ public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean {
this.metricsDataQueueNameToAlerter = redisProperties.getMetricsDataQueueNameToAlerter();
this.logEntryQueueName = redisProperties.getLogEntryQueueName();
this.logEntryToStorageQueueName = redisProperties.getLogEntryToStorageQueueName();
this.waitTimeout = Objects.requireNonNullElse(redisProperties.getWaitTimeout(), 1L);
}
@Override
public CollectRep.MetricsData pollMetricsDataToAlerter() {
try {
return syncCommands.rpop(metricsDataQueueNameToAlerter);
} catch (Exception e) {
log.error(e.getMessage());
return null;
}
public CollectRep.MetricsData pollMetricsDataToAlerter() throws InterruptedException {
return genericBlockingPollFunction(metricsDataQueueNameToAlerter, syncCommands);
}
@Override
public CollectRep.MetricsData pollMetricsDataToStorage() throws InterruptedException {
try {
return syncCommands.rpop(metricsDataQueueNameToStorage);
} catch (Exception e) {
log.error(e.getMessage());
return null;
}
return genericBlockingPollFunction(metricsDataQueueNameToStorage, syncCommands);
}
@Override
public CollectRep.MetricsData pollServiceDiscoveryData() throws InterruptedException {
try {
return syncCommands.rpop(metricsDataQueueNameForServiceDiscovery);
} catch (Exception e) {
log.error(e.getMessage());
return null;
}
return genericBlockingPollFunction(metricsDataQueueNameForServiceDiscovery, syncCommands);
}
@Override
@@ -153,12 +147,7 @@ public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean {
@Override
public LogEntry pollLogEntry() throws InterruptedException {
try {
return logEntrySyncCommands.rpop(logEntryQueueName);
} catch (Exception e) {
log.error("Failed to poll LogEntry from Redis: {}", e.getMessage());
throw new InterruptedException("Failed to poll LogEntry from Redis");
}
return genericBlockingPollFunction(logEntryQueueName, logEntrySyncCommands);
}
@Override
@@ -172,12 +161,43 @@ public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean {
@Override
public LogEntry pollLogEntryToStorage() throws InterruptedException {
try {
return logEntrySyncCommands.rpop(logEntryToStorageQueueName);
} catch (Exception e) {
log.error("Failed to poll LogEntry from storage via Redis: {}", e.getMessage());
throw new InterruptedException("Failed to poll LogEntry from storage via Redis");
return genericBlockingPollFunction(logEntryToStorageQueueName, logEntrySyncCommands);
}
@Override
@SuppressWarnings("unchecked")
public void sendLogEntryToAlertBatch(List<LogEntry> logEntries) {
if (logEntries == null || logEntries.isEmpty()) {
return;
}
try {
logEntrySyncCommands.lpush(logEntryQueueName, logEntries.toArray(new LogEntry[0]));
} catch (Exception e) {
log.error("Failed to send LogEntry batch to Redis: {}", e.getMessage());
}
}
@Override
public List<LogEntry> pollLogEntryToAlertBatch(int maxBatchSize) throws InterruptedException {
return genericBatchPollFunction(logEntryQueueName, logEntrySyncCommands, maxBatchSize);
}
@Override
@SuppressWarnings("unchecked")
public void sendLogEntryToStorageBatch(List<LogEntry> logEntries) {
if (logEntries == null || logEntries.isEmpty()) {
return;
}
try {
logEntrySyncCommands.lpush(logEntryToStorageQueueName, logEntries.toArray(new LogEntry[0]));
} catch (Exception e) {
log.error("Failed to send LogEntry batch to storage via Redis: {}", e.getMessage());
}
}
@Override
public List<LogEntry> pollLogEntryToStorageBatch(int maxBatchSize) throws InterruptedException {
return genericBatchPollFunction(logEntryToStorageQueueName, logEntrySyncCommands, maxBatchSize);
}
@Override
@@ -187,4 +207,36 @@ public class RedisCommonDataQueue implements CommonDataQueue, DisposableBean {
redisClient.shutdown();
}
private <T> T genericBlockingPollFunction(String key, RedisCommands<String, T> commands) throws InterruptedException {
try {
// Use BRPOP for blocking pop with the configured timeout.
// If data arrives, it returns immediately; if it times out, it returns null.
KeyValue<String, T> keyData = commands.brpop(waitTimeout, key);
if (keyData != null) {
return keyData.getValue();
} else {
// Returns null on timeout
return null;
}
} catch (Exception e) {
log.error("Redis BRPOP failed: {}", e.getMessage());
throw new CommonDataQueueUnknownException(e.getMessage(), e);
}
}
private List<LogEntry> genericBatchPollFunction(String key, RedisCommands<String, LogEntry> commands, int maxBatchSize) {
List<LogEntry> batch = new ArrayList<>(maxBatchSize);
try {
List<LogEntry> elements = commands.rpop(key, maxBatchSize);
if (elements != null) {
batch.addAll(elements);
}
} catch (Exception e) {
log.error("Redis batch poll failed: {}", e.getMessage());
}
return batch;
}
}
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.support.exception;
/**
* Common data queue unknown exception
*/
public class CommonDataQueueUnknownException extends RuntimeException {
public CommonDataQueueUnknownException() {
}
public CommonDataQueueUnknownException(String message) {
super(message);
}
public CommonDataQueueUnknownException(String message, Throwable cause) {
super(message, cause);
}
public CommonDataQueueUnknownException(Throwable cause) {
super(cause);
}
}
@@ -44,7 +44,21 @@ public class HostParamValidator implements ConstraintValidator<HostValid, String
value = value.replaceFirst(PATTERN_HTTPS, BLANK);
}
return IpDomainUtil.validateIpDomain(value);
String hostPart = value;
if (value.contains(":")) {
// if contains multiple ":", it may be IPv6 with port
if (value.lastIndexOf(":") > value.indexOf(":") && value.contains("[")) {
int portIndex = value.lastIndexOf(":");
hostPart = value.substring(0, portIndex);
} else if (value.split(":").length == 2) {
// it is IPv4 or domain with port
String[] parts = value.split(":");
hostPart = parts[0];
}
}
return IpDomainUtil.validateIpDomain(hostPart);
}
}
@@ -0,0 +1,50 @@
/*
* 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 java.util.concurrent.TimeUnit;
/**
* Backoff utility class.
*/
public final class BackoffUtils {
/** Private constructor to prevent instantiation */
private BackoffUtils() {}
/**
* Sleeps for the next delay specified by the ExponentialBackoff instance.
* If the thread is interrupted during sleep, it resets the interrupt status
* and returns false to indicate that the operation should not continue.
*
* @param backoff the ExponentialBackoff instance to get the next delay from
* @return true if the sleep completed without interruption, false otherwise
*/
public static boolean shouldContinueAfterBackoff(ExponentialBackoff backoff) {
if (Thread.currentThread().isInterrupted()) {
return false;
}
try {
TimeUnit.MILLISECONDS.sleep(backoff.nextDelay());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
return true;
}
}
@@ -0,0 +1,66 @@
/*
* 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;
/**
* Exponential backoff utility class.
*
* <p>Provides exponentially increasing delays starting from an initial value,
* doubling with each call to {@link #nextDelay()} until reaching the maximum value.
* Call {@link #reset()} to restart from the initial delay.</p>
*
* <p>Note: This class is <b>not</b> thread-safe. Each thread should use its own instance.</p>
*/
public final class ExponentialBackoff {
private final long initial;
private final long max;
private long current;
public ExponentialBackoff(long initial, long max) {
if (initial <= 0 || max < initial) {
throw new IllegalArgumentException("Invalid exponential backoff params");
}
this.initial = initial;
this.max = max;
this.current = initial;
}
/**
* Returns the current delay value and advances to the next delay.
* The delay doubles on each call until it reaches the maximum value.
*
* @return the delay in milliseconds to wait before the next retry
*/
public long nextDelay() {
long delay = this.current;
if (this.current <= this.max / 2) {
this.current = this.current * 2;
} else {
this.current = this.max;
}
return delay;
}
/**
* Resets the backoff to the initial delay value.
* Should be called when an operation succeeds after previous failures.
*/
public void reset() {
this.current = this.initial;
}
}
@@ -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.common.util;
import org.apache.hertzbeat.common.constants.HertzBeatKeywordsEnum;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Validate JEXL rules
*/
public class HertzBeatKeywordsUtil {
private static final Map<String, HertzBeatKeywordsEnum> KEYWORDS =
Arrays.stream(HertzBeatKeywordsEnum.values())
.collect(Collectors.toMap(HertzBeatKeywordsEnum::getKeyword, e -> e));
/**
* Verify if the field matches any reserved keywords
*
* @param field Field name to verify
*/
public static void verifyKeywords(String field) {
HertzBeatKeywordsEnum keyword = KEYWORDS.get(field);
if (null == keyword) {
return;
}
throw new IllegalArgumentException(
String.format("Field matches keyword `%s`, please set alias `%s`.",
keyword.getKeyword(), keyword.getAlias()));
}
}
@@ -27,6 +27,7 @@ import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.KeyValue;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@@ -78,7 +79,7 @@ class RedisCommonDataQueueTest {
when(redisProperties.getLogEntryToStorageQueueName()).thenReturn("logEntryToStorageQueue");
when(redisProperties.getRedisHost()).thenReturn("localhost");
when(redisProperties.getRedisPort()).thenReturn(6379);
when(redisProperties.getWaitTimeout()).thenReturn(1L);
try (MockedStatic<RedisClient> mockedRedisClient = mockStatic(RedisClient.class)) {
mockedRedisClient.when(() -> RedisClient.create(any(RedisURI.class))).thenReturn(redisClient);
when(redisClient.connect(any(RedisMetricsDataCodec.class))).thenReturn(connection);
@@ -95,11 +96,13 @@ class RedisCommonDataQueueTest {
CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder()
.setMetrics("test metrics")
.build();
String queueName = "metricsDataQueueToAlerter";
when(syncCommands.rpop("metricsDataQueueToAlerter")).thenReturn(metricsData);
when(syncCommands.brpop(1L, queueName)).thenReturn(KeyValue.just(queueName, metricsData));
CollectRep.MetricsData actualMetricsData = redisCommonDataQueue.pollMetricsDataToAlerter();
assertEquals(metricsData, actualMetricsData);
verify(syncCommands).brpop(1L, queueName);
}
@Test
@@ -163,13 +166,14 @@ class RedisCommonDataQueueTest {
.severityText("WARN")
.body("Test warning log message")
.build();
String queueName = "logEntryQueue";
when(logEntrySyncCommands.rpop("logEntryQueue")).thenReturn(expectedLogEntry);
when(logEntrySyncCommands.brpop(1L, queueName)).thenReturn(KeyValue.just(queueName, expectedLogEntry));
LogEntry result = redisCommonDataQueue.pollLogEntry();
assertEquals(expectedLogEntry, result);
verify(logEntrySyncCommands).rpop("logEntryQueue");
verify(logEntrySyncCommands).brpop(1L, "logEntryQueue");
}
@Test
@@ -181,13 +185,13 @@ class RedisCommonDataQueueTest {
.severityText("FATAL")
.body("Critical error log for storage")
.build();
String queueName = "logEntryToStorageQueue";
when(logEntrySyncCommands.rpop("logEntryToStorageQueue")).thenReturn(expectedLogEntry);
when(logEntrySyncCommands.brpop(1L, queueName)).thenReturn(KeyValue.just(queueName, expectedLogEntry));
LogEntry result = redisCommonDataQueue.pollLogEntryToStorage();
assertEquals(expectedLogEntry, result);
verify(logEntrySyncCommands).rpop("logEntryToStorageQueue");
verify(logEntrySyncCommands).brpop(1L, "logEntryToStorageQueue");
}
@Test
@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test case for {@link BackoffUtils}
*/
class BackoffUtilsTest {
@Test
void shouldContinueAfterBackoff() {
ExponentialBackoff backoff = new ExponentialBackoff(10L, 100L);
boolean shouldContinue = BackoffUtils.shouldContinueAfterBackoff(backoff);
assertTrue(shouldContinue);
}
@Test
void shouldNotContinueWhenInterrupted() {
Thread.currentThread().interrupt();
ExponentialBackoff backoff = new ExponentialBackoff(10L, 100L);
boolean shouldContinue = BackoffUtils.shouldContinueAfterBackoff(backoff);
assertFalse(shouldContinue);
}
@Test
void shouldHandleInterruptedException() throws InterruptedException {
final Thread mainThread = Thread.currentThread();
ExponentialBackoff backoff = new ExponentialBackoff(1000L, 2000L);
Thread interruptingThread = new Thread(() -> {
try {
// Give the main thread some time to enter the sleep
Thread.sleep(200);
mainThread.interrupt();
} catch (InterruptedException ignored) {
}
});
interruptingThread.start();
boolean shouldContinue = BackoffUtils.shouldContinueAfterBackoff(backoff);
interruptingThread.join();
assertFalse(shouldContinue);
assertTrue(Thread.currentThread().isInterrupted());
}
}
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.util;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* Test case for {@link ExponentialBackoff}
*/
class ExponentialBackoffTest {
@Test
void testProgressionAndCap() {
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
Assertions.assertEquals(50L, backoff.nextDelay());
Assertions.assertEquals(100L, backoff.nextDelay());
Assertions.assertEquals(200L, backoff.nextDelay());
Assertions.assertEquals(400L, backoff.nextDelay());
Assertions.assertEquals(800L, backoff.nextDelay());
Assertions.assertEquals(1000L, backoff.nextDelay());
Assertions.assertEquals(1000L, backoff.nextDelay());
}
@Test
void testReset() {
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
Assertions.assertEquals(50L, backoff.nextDelay());
Assertions.assertEquals(100L, backoff.nextDelay());
backoff.reset();
Assertions.assertEquals(50L, backoff.nextDelay());
}
@Test
void testInvalidParams() {
Assertions.assertThrows(IllegalArgumentException.class, () -> new ExponentialBackoff(0L, 1000L));
Assertions.assertThrows(IllegalArgumentException.class, () -> new ExponentialBackoff(50L, 10L));
}
}
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.common.util;
import org.apache.hertzbeat.common.constants.HertzBeatKeywordsEnum;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Test case for {@link HertzBeatKeywordsUtil}
*/
class HertzBeatKeywordsUtilTest {
@Test
void testVerifyKeywords_WithReservedKeyword_Instance() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> HertzBeatKeywordsUtil.verifyKeywords("instance")
);
String expectedMessage = String.format(
"Field matches keyword `%s`, please set alias `%s`.",
HertzBeatKeywordsEnum.INSTANCE.getKeyword(),
HertzBeatKeywordsEnum.INSTANCE.getAlias()
);
assertEquals(expectedMessage, exception.getMessage());
}
@Test
void testVerifyKeywords_WithValidField() {
assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords("cpu_usage"));
assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords("memory_used"));
assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords("response_time"));
assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords("metric_instance"));
assertDoesNotThrow(() -> HertzBeatKeywordsUtil.verifyKeywords(HertzBeatKeywordsEnum.INSTANCE.getAlias()));
}
}
@@ -102,7 +102,7 @@ public class LogPeriodicAlertE2eTest {
@DynamicPropertySource
static void greptimeProps(DynamicPropertyRegistry r) {
// Configure GreptimeDB storage
r.add("warehouse.store.jpa.enabled", () -> "false");
r.add("warehouse.store.duckdb.enabled", () -> "false");
r.add("warehouse.store.greptime.enabled", () -> "true");
r.add("warehouse.store.greptime.http-endpoint", () -> "http://localhost:" + greptimedb.getMappedPort(GREPTIME_HTTP_PORT));
r.add("warehouse.store.greptime.grpc-endpoints", () -> "localhost:" + greptimedb.getMappedPort(GREPTIME_GRPC_PORT));
@@ -86,7 +86,7 @@ public class GreptimeLogStorageE2eTest {
@DynamicPropertySource
static void greptimeProps(DynamicPropertyRegistry r) {
r.add("warehouse.store.jpa.enabled", () -> "false");
r.add("warehouse.store.duckdb.enabled", () -> "false");
r.add("warehouse.store.greptime.enabled", () -> "true");
r.add("warehouse.store.greptime.http-endpoint", () -> "http://localhost:" + greptimedb.getMappedPort(GREPTIME_HTTP_PORT));
r.add("warehouse.store.greptime.grpc-endpoints", () -> "localhost:" + greptimedb.getMappedPort(GREPTIME_GRPC_PORT));
+11
View File
@@ -28,6 +28,10 @@
<artifactId>hertzbeat-log</artifactId>
<name>${project.artifactId}</name>
<properties>
<awaitility.version>4.2.0</awaitility.version>
</properties>
<dependencies>
<!-- common -->
<dependency>
@@ -67,6 +71,13 @@
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
</dependency>
<!-- awaitility for async testing -->
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -60,7 +60,7 @@ public class LogIngestionController {
@PostMapping("/ingest/{protocol}")
public ResponseEntity<Message<Void>> ingestExternLog(@PathVariable("protocol") String protocol,
@RequestBody String content) {
log.info("Receive extern log from protocol: {}, content length: {}", protocol, content == null ? 0 : content.length());
log.debug("Receive extern log from protocol: {}, content length: {}", protocol, content == null ? 0 : content.length());
if (!StringUtils.hasText(protocol)) {
protocol = DEFAULT_PROTOCOL; // Default to OTLP if no protocol specified
}
@@ -19,34 +19,75 @@
package org.apache.hertzbeat.log.notice;
import jakarta.annotation.PreDestroy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.entity.log.LogEntry;
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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* SSE manager for log
* SSE manager for log with batch processing support for high TPS scenarios
*/
@Component
@Slf4j
@Getter
public class LogSseManager {
private static final long BATCH_INTERVAL_MS = 200;
private static final int MAX_BATCH_SIZE = 1000;
private static final int MAX_QUEUE_SIZE = 10000;
private final Map<Long, SseSubscriber> emitters = new ConcurrentHashMap<>();
private final Queue<LogEntry> logQueue = new ConcurrentLinkedQueue<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "sse-batch-scheduler");
t.setDaemon(true);
return t;
});
private final ExecutorService senderPool = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r, "sse-sender");
t.setDaemon(true);
return t;
});
private final AtomicLong queueSize = new AtomicLong(0);
public LogSseManager() {
scheduler.scheduleAtFixedRate(this::flushBatch, BATCH_INTERVAL_MS, BATCH_INTERVAL_MS, TimeUnit.MILLISECONDS);
}
@PreDestroy
public void shutdown() {
scheduler.shutdown();
senderPool.shutdown();
try {
scheduler.awaitTermination(2, TimeUnit.SECONDS);
senderPool.awaitTermination(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
scheduler.shutdownNow();
senderPool.shutdownNow();
}
/**
* Create a new SSE emitter for a client with specified filters
* @param clientId The unique identifier for the client
* @param filters The filters to apply to the log data
* @return The SSE emitter
*/
public SseEmitter createEmitter(Long clientId, LogSseFilterCriteria filters) {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
@@ -54,55 +95,115 @@ public class LogSseManager {
emitter.onTimeout(() -> removeEmitter(clientId));
emitter.onError((ex) -> removeEmitter(clientId));
SseSubscriber subscriber = new SseSubscriber(emitter, filters);
emitters.put(clientId, subscriber);
emitters.put(clientId, new SseSubscriber(emitter, filters));
return emitter;
}
/**
* Broadcast log data to all subscribers
* @param logEntry The log data to broadcast
* Queue log entry for batch processing
*/
@Async
public void broadcast(LogEntry logEntry) {
emitters.forEach((clientId, subscriber) -> {
try {
// Check if the log entry matches the subscriber's filter criteria
if (subscriber.filters == null || subscriber.filters.matches(logEntry)) {
subscriber.emitter.send(SseEmitter.event()
.id(String.valueOf(System.currentTimeMillis()))
.name("LOG_EVENT")
.data(logEntry));
}
} catch (IOException | IllegalStateException e) {
subscriber.emitter.complete();
removeEmitter(clientId);
} catch (Exception exception) {
log.error("Failed to broadcast log to client: {}", exception.getMessage());
subscriber.emitter.complete();
removeEmitter(clientId);
if (queueSize.incrementAndGet() > MAX_QUEUE_SIZE) {
queueSize.decrementAndGet();
return;
}
boolean offered = logQueue.offer(logEntry);
if (!offered) {
queueSize.decrementAndGet();
log.warn("Failed to enqueue log entry: {}", logEntry);
}
}
/**
* Flush queued logs to all subscribers in batch
*/
private void flushBatch() {
try {
if (logQueue.isEmpty() || emitters.isEmpty()) {
return;
}
});
List<LogEntry> batch = new ArrayList<>(MAX_BATCH_SIZE);
LogEntry entry;
while (batch.size() < MAX_BATCH_SIZE && (entry = logQueue.poll()) != null) {
batch.add(entry);
queueSize.decrementAndGet();
}
if (batch.isEmpty()) {
return;
}
// Send to each subscriber in parallel
for (Map.Entry<Long, SseSubscriber> e : emitters.entrySet()) {
Long clientId = e.getKey();
SseSubscriber subscriber = e.getValue();
List<LogEntry> filtered = filterLogs(batch, subscriber.filters);
if (!filtered.isEmpty()) {
senderPool.submit(() -> sendToSubscriber(clientId, subscriber.emitter, filtered));
}
}
} catch (Exception e) {
log.error("Error in flushBatch: {}", e.getMessage(), e);
}
}
private void sendToSubscriber(Long clientId, SseEmitter emitter, List<LogEntry> logs) {
try {
long batchTimestamp = System.currentTimeMillis();
int sequenceNumber = 0;
for (LogEntry logEntry : logs) {
String eventId = batchTimestamp + "-" + sequenceNumber++;
emitter.send(SseEmitter.event()
.id(eventId)
.name("LOG_EVENT")
.data(logEntry));
}
} catch (IOException | IllegalStateException e) {
safeComplete(clientId, emitter);
} catch (Exception e) {
log.error("Failed to send to client {}: {}", clientId, e.getMessage());
safeComplete(clientId, emitter);
}
}
private void safeComplete(Long clientId, SseEmitter emitter) {
try {
emitter.complete();
} catch (Exception ignored) {
}
removeEmitter(clientId);
}
private List<LogEntry> filterLogs(List<LogEntry> logs, LogSseFilterCriteria filters) {
if (filters == null) {
return logs;
}
List<LogEntry> filtered = new ArrayList<>();
for (LogEntry log : logs) {
if (filters.matches(log)) {
filtered.add(log);
}
}
return filtered;
}
private void removeEmitter(Long clientId) {
emitters.remove(clientId);
}
public long getQueueSize() {
return queueSize.get();
}
/**
* SSE subscriber
* SseSubscriber for SseEmitter and LogSseFilterCriteria
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class SseSubscriber {
/**
* The SSE emitter for streaming log events
*/
private SseEmitter emitter;
/**
* The filters for streaming log events
*/
private LogSseFilterCriteria filters;
}
}
@@ -69,13 +69,9 @@ public class OtlpLogProtocolAdapter implements LogProtocolAdapter {
// Extract LogEntry instances from the request
List<LogEntry> logEntries = extractLogEntries(request);
log.debug("Successfully extracted {} log entries from OTLP payload {}", logEntries.size(), content);
logEntries.forEach(entry -> {
commonDataQueue.sendLogEntry(entry);
logSseManager.broadcast(entry);
log.info("Log entry sent to queue: {}", entry);
});
commonDataQueue.sendLogEntryToStorageBatch(logEntries);
commonDataQueue.sendLogEntryToAlertBatch(logEntries);
logEntries.forEach(logSseManager::broadcast);
} catch (InvalidProtocolBufferException e) {
log.error("Failed to parse OTLP log payload: {}", e.getMessage());
throw new IllegalArgumentException("Invalid OTLP log content", e);
@@ -20,19 +20,21 @@
package org.apache.hertzbeat.log.notice;
import org.apache.hertzbeat.common.entity.log.LogEntry;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -41,14 +43,21 @@ import static org.mockito.Mockito.verify;
/**
* Unit tests for {@link LogSseManager}.
*/
@ExtendWith(MockitoExtension.class)
class LogSseManagerTest {
@Spy
private LogSseManager logSseManager;
private static final Long CLIENT_ID = 1L;
@BeforeEach
void setUp() {
logSseManager = new LogSseManager();
}
@AfterEach
void tearDown() {
logSseManager.shutdown();
}
@Test
void shouldCreateAndStoreEmitter() {
// When: Creating a new emitter for a client
@@ -73,12 +82,14 @@ class LogSseManagerTest {
// When: An "INFO" log is broadcast
logSseManager.broadcast(infoLog);
// Then: The log should be sent to the client
verify(mockEmitter).send(any(SseEmitter.SseEventBuilder.class));
// Then: The log should be sent to the client (wait for batch processing)
await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() ->
verify(mockEmitter, atLeastOnce()).send(any(SseEmitter.SseEventBuilder.class))
);
}
@Test
void shouldNotBroadcastLogWhenFilterDoesNotMatch() throws IOException {
void shouldNotBroadcastLogWhenFilterDoesNotMatch() throws IOException, InterruptedException {
// Given: A client with a filter for "ERROR" logs
LogSseFilterCriteria filters = new LogSseFilterCriteria();
filters.setSeverityText("ERROR");
@@ -90,6 +101,9 @@ class LogSseManagerTest {
// When: An "INFO" log is broadcast
logSseManager.broadcast(infoLog);
// Wait for batch processing
Thread.sleep(300);
// Then: The log should NOT be sent to the client
verify(mockEmitter, never()).send(any(SseEmitter.SseEventBuilder.class));
}
@@ -105,12 +119,14 @@ class LogSseManagerTest {
// When: Any log is broadcast
logSseManager.broadcast(anyLog);
// Then: The log should be sent to the client
verify(mockEmitter).send(any(SseEmitter.SseEventBuilder.class));
// Then: The log should be sent to the client (wait for batch processing)
await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() ->
verify(mockEmitter, atLeastOnce()).send(any(SseEmitter.SseEventBuilder.class))
);
}
@Test
void shouldBroadcastOnlyToMatchingSubscribers() throws IOException {
void shouldBroadcastOnlyToMatchingSubscribers() throws IOException, InterruptedException {
// Given: Two clients with different filters
LogSseFilterCriteria infoFilter = new LogSseFilterCriteria();
infoFilter.setSeverityText("INFO");
@@ -127,8 +143,12 @@ class LogSseManagerTest {
// When: An "INFO" log is broadcast
logSseManager.broadcast(infoLog);
// Wait for batch processing
await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() ->
verify(infoEmitter, atLeastOnce()).send(any(SseEmitter.SseEventBuilder.class))
);
// Then: The log is sent only to the client subscribed to "INFO" logs
verify(infoEmitter).send(any(SseEmitter.SseEventBuilder.class));
verify(errorEmitter, never()).send(any(SseEmitter.SseEventBuilder.class));
}
@@ -146,8 +166,10 @@ class LogSseManagerTest {
logSseManager.broadcast(log);
// Then: The failing emitter should be completed and removed
verify(mockEmitter).complete();
assertFalse(logSseManager.getEmitters().containsKey(CLIENT_ID));
await().atMost(500, TimeUnit.MILLISECONDS).untilAsserted(() -> {
verify(mockEmitter).complete();
assertFalse(logSseManager.getEmitters().containsKey(CLIENT_ID));
});
}
/**
@@ -44,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@@ -85,12 +86,16 @@ class OtlpLogProtocolAdapterTest {
adapter.ingest(otlpPayload);
ArgumentCaptor<LogEntry> logEntryCaptor = ArgumentCaptor.forClass(LogEntry.class);
verify(commonDataQueue, times(1)).sendLogEntry(logEntryCaptor.capture());
verify(logSseManager, times(1)).broadcast(logEntryCaptor.capture());
ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture());
verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList());
verify(logSseManager, times(1)).broadcast(any(LogEntry.class));
LogEntry capturedEntry = logEntryCaptor.getValue();
assertNotNull(capturedEntry);
List<LogEntry> capturedList = listCaptor.getValue();
assertNotNull(capturedList);
assertEquals(1, capturedList.size());
LogEntry capturedEntry = capturedList.get(0);
assertEquals("test-service", capturedEntry.getResource().get("service_name"));
assertEquals("test-version", capturedEntry.getResource().get("service_version"));
assertEquals("test-scope", capturedEntry.getInstrumentationScope().getName());
@@ -106,8 +111,13 @@ class OtlpLogProtocolAdapterTest {
adapter.ingest(otlpPayload);
verify(commonDataQueue, times(2)).sendLogEntry(any(LogEntry.class));
ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture());
verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList());
verify(logSseManager, times(2)).broadcast(any(LogEntry.class));
List<LogEntry> capturedList = listCaptor.getValue();
assertEquals(2, capturedList.size());
}
@Test
@@ -116,15 +126,16 @@ class OtlpLogProtocolAdapterTest {
adapter.ingest(otlpPayload);
verify(commonDataQueue, times(1)).sendLogEntry(any(LogEntry.class));
ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture());
verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList());
verify(logSseManager, times(1)).broadcast(any(LogEntry.class));
ArgumentCaptor<LogEntry> logEntryCaptor = ArgumentCaptor.forClass(LogEntry.class);
verify(commonDataQueue).sendLogEntry(logEntryCaptor.capture());
LogEntry capturedEntry = logEntryCaptor.getValue();
assertNotNull(capturedEntry);
List<LogEntry> capturedList = listCaptor.getValue();
assertNotNull(capturedList);
assertEquals(1, capturedList.size());
LogEntry capturedEntry = capturedList.get(0);
Map<String, Object> attributes = capturedEntry.getAttributes();
assertEquals("string_value", attributes.get("string_attr"));
assertEquals(true, attributes.get("bool_attr"));
@@ -145,12 +156,15 @@ class OtlpLogProtocolAdapterTest {
adapter.ingest(otlpPayload);
verify(commonDataQueue, times(1)).sendLogEntry(any(LogEntry.class));
ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture());
verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList());
verify(logSseManager, times(1)).broadcast(any(LogEntry.class));
ArgumentCaptor<LogEntry> logEntryCaptor = ArgumentCaptor.forClass(LogEntry.class);
verify(commonDataQueue).sendLogEntry(logEntryCaptor.capture());
List<LogEntry> capturedList = listCaptor.getValue();
assertEquals(1, capturedList.size());
LogEntry capturedEntry = logEntryCaptor.getValue();
LogEntry capturedEntry = capturedList.get(0);
assertEquals("1234567890abcdef1234567890abcdef", capturedEntry.getTraceId());
assertEquals("1234567890abcdef", capturedEntry.getSpanId());
assertEquals(1, capturedEntry.getTraceFlags());
@@ -170,7 +184,15 @@ class OtlpLogProtocolAdapterTest {
adapter.ingest(otlpPayload);
verifyNoInteractions(commonDataQueue, logSseManager);
ArgumentCaptor<List<LogEntry>> listCaptor = ArgumentCaptor.forClass(List.class);
verify(commonDataQueue, times(1)).sendLogEntryToStorageBatch(listCaptor.capture());
verify(commonDataQueue, times(1)).sendLogEntryToAlertBatch(anyList());
List<LogEntry> capturedList = listCaptor.getValue();
assertNotNull(capturedList);
assertEquals(0, capturedList.size());
verifyNoInteractions(logSseManager);
}
private String createValidOtlpLogPayload() throws Exception {
@@ -18,13 +18,6 @@
package org.apache.hertzbeat.manager.component.sd;
import com.google.common.collect.Maps;
import java.time.LocalDateTime;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.arrow.RowWrapper;
@@ -34,6 +27,9 @@ import org.apache.hertzbeat.common.entity.manager.MonitorBind;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.queue.CommonDataQueue;
import org.apache.hertzbeat.common.support.exception.CommonDataQueueUnknownException;
import org.apache.hertzbeat.common.util.BackoffUtils;
import org.apache.hertzbeat.common.util.ExponentialBackoff;
import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorDao;
@@ -43,6 +39,14 @@ import org.apache.hertzbeat.manager.service.MonitorService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Service Discovery Worker
*/
@@ -80,8 +84,13 @@ public class ServiceDiscoveryWorker implements InitializingBean {
private class SdUpdateTask implements Runnable {
@Override
public void run() {
ExponentialBackoff backoff = new ExponentialBackoff(50L, 1000L);
while (!Thread.currentThread().isInterrupted()) {
try (final CollectRep.MetricsData metricsData = dataQueue.pollServiceDiscoveryData()) {
if (metricsData == null) {
continue;
}
backoff.reset();
Long monitorId = metricsData.getId();
final Monitor mainMonitor = monitorDao.findById(monitorId).orElse(null);
if (mainMonitor == null) {
@@ -114,13 +123,14 @@ public class ServiceDiscoveryWorker implements InitializingBean {
.filter(p -> !p.isEmpty())
.orElse(defaultPort);
final String keyStr = host + ":" + port;
final String instance = port.isEmpty() ? host : host + ":" + port;
if (subMonitorBindMap.containsKey(keyStr)) {
subMonitorBindMap.remove(keyStr);
continue;
}
Monitor newMonitor = mainMonitor.clone();
newMonitor.setId(null);
newMonitor.setHost(host);
newMonitor.setInstance(instance);
newMonitor.setName(newMonitor.getName() + "-" + host + ":" + port);
newMonitor.setScrape(CommonConstants.SCRAPE_STATIC);
newMonitor.setGmtCreate(LocalDateTime.now());
@@ -153,6 +163,10 @@ public class ServiceDiscoveryWorker implements InitializingBean {
final Set<Long> needCancelMonitorIdSet = subMonitorBindMap.values().stream()
.map(MonitorBind::getMonitorId).collect(Collectors.toSet());
monitorService.deleteMonitors(needCancelMonitorIdSet);
} catch (CommonDataQueueUnknownException ue) {
if (!BackoffUtils.shouldContinueAfterBackoff(backoff)) {
break;
}
} catch (Exception exception) {
log.error(exception.getMessage(), exception);
}
@@ -0,0 +1,44 @@
/*
* 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.manager.component.validator;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
/**
* Parameter validator interface
*/
public interface ParamValidator {
/**
* Check if the validator supports the given parameter type
*
* @param type parameter type
* @return true if supported
*/
boolean support(String type);
/**
* Validate the parameter
*
* @param paramDefine parameter definition
* @param param parameter actual value
* @throws IllegalArgumentException if validation fails
*/
void validate(ParamDefine paramDefine, Param param) throws IllegalArgumentException;
}
@@ -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.manager.component.validator;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Parameter validator manager
*/
@Component
public class ParamValidatorManager {
private final List<ParamValidator> validators;
public ParamValidatorManager(List<ParamValidator> validators) {
this.validators = validators;
}
public void validate(ParamDefine paramDefine, Param param) {
for (ParamValidator validator : validators) {
if (validator.support(paramDefine.getType())) {
validator.validate(paramDefine, param);
return;
}
}
// No validator found for the given type.
throw new IllegalArgumentException("ParamDefine type " + paramDefine.getType() + " is invalid.");
}
}
@@ -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.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.springframework.stereotype.Component;
/**
* Array parameter validator
*/
@Component
public class ArrayParamValidator implements ParamValidator {
@Override
public boolean support(String type) {
return "array".equals(type);
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
String[] arrays = param.getParamValue().split(",");
if (arrays.length == 0) {
throw new IllegalArgumentException("Param field " + paramDefine.getField() + " value "
+ param.getParamValue() + " is invalid arrays value");
}
if (param.getParamValue().startsWith("[") && param.getParamValue().endsWith("]")) {
param.setParamValue(param.getParamValue().substring(1, param.getParamValue().length() - 1));
}
}
}
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.springframework.stereotype.Component;
/**
* Boolean parameter validator
*/
@Component
public class BooleanParamValidator implements ParamValidator {
@Override
public boolean support(String type) {
return "boolean".equals(type);
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
String booleanValue = param.getParamValue();
if (!"true".equalsIgnoreCase(booleanValue) && !"false".equalsIgnoreCase(booleanValue)) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value "
+ booleanValue + " is invalid boolean value.");
}
}
}
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.springframework.stereotype.Component;
/**
* Host parameter validator that delegates to the common HostParamValidator
*/
@Component
public class HostParamValidatorAdapter implements ParamValidator {
private final org.apache.hertzbeat.common.support.valid.HostParamValidator hostValidator;
public HostParamValidatorAdapter() {
this.hostValidator = new org.apache.hertzbeat.common.support.valid.HostParamValidator();
}
@Override
public boolean support(String type) {
return "host".equals(type);
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
if (!hostValidator.isValid(param.getParamValue(), null)) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value "
+ param.getParamValue() + " is invalid host value.");
}
}
}
@@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.component.validator.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.springframework.stereotype.Component;
/**
* JSON parameter validator
*/
@Component
public class JsonParamValidator implements ParamValidator {
@Override
public boolean support(String type) {
return "metrics-field".equals(type) || "key-value".equals(type);
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
if (JsonUtil.fromJson(param.getParamValue(), new TypeReference<>() {
}) == null) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value "
+ param.getParamValue() + " is invalid key-value value");
}
}
}
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.IntervalExpressionUtil;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.springframework.stereotype.Component;
/**
* Number parameter validator
*/
@Component
public class NumberParamValidator implements ParamValidator {
@Override
public boolean support(String type) {
return "number".equals(type);
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
Double doubleValue = org.apache.hertzbeat.common.util.CommonUtil.parseStrDouble(param.getParamValue());
if (doubleValue == null) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " type "
+ paramDefine.getType() + " is invalid.");
}
if (paramDefine.getRange() != null) {
if (!IntervalExpressionUtil.validNumberIntervalExpress(doubleValue,
paramDefine.getRange())) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " type "
+ paramDefine.getType() + " over range " + paramDefine.getRange());
}
}
param.setType(CommonConstants.PARAM_TYPE_NUMBER);
}
}
@@ -0,0 +1,54 @@
/*
* 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.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Option parameter validator for radio and checkbox types
*/
@Component
public class OptionParamValidator implements ParamValidator {
@Override
public boolean support(String type) {
return "radio".equals(type) || "checkbox".equals(type);
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
List<ParamDefine.Option> options = paramDefine.getOptions();
boolean invalid = true;
if (options != null) {
for (ParamDefine.Option option : options) {
if (param.getParamValue().equalsIgnoreCase(option.getValue())) {
invalid = false;
break;
}
}
}
if (invalid) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " value "
+ param.getParamValue() + " is invalid option value");
}
}
}
@@ -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.manager.component.validator.impl;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.springframework.stereotype.Component;
/**
* Password parameter validator
*/
@Component
public class PasswordParamValidator implements ParamValidator {
@Override
public boolean support(String type) {
return "password".equals(type);
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
String passwordValue = param.getParamValue();
if (!AesUtil.isCiphertext(passwordValue)) {
passwordValue = AesUtil.aesEncode(passwordValue);
param.setParamValue(passwordValue);
}
param.setType(CommonConstants.PARAM_TYPE_PASSWORD);
}
}
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.manager.component.validator.ParamValidator;
import org.springframework.stereotype.Component;
/**
* Text parameter validator
*/
@Component
public class TextParamValidator implements ParamValidator {
@Override
public boolean support(String type) {
return "text".equals(type) || "textarea".equals(type);
}
@Override
public void validate(ParamDefine paramDefine, Param param) {
Short limit = paramDefine.getLimit();
if (limit != null && param.getParamValue().length() > limit) {
throw new IllegalArgumentException("Params field " + paramDefine.getField() + " type "
+ paramDefine.getType() + " over limit " + limit);
}
}
}
@@ -150,7 +150,7 @@ public class CollectorJobScheduler implements CollectorScheduling, CollectJobSch
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
CommonConstants.LABEL_INSTANCE, monitor.getInstance());
appDefine.setMetadata(metadata);
List<Param> params = paramDao.findParamsByMonitorId(monitor.getId());
List<Configmap> configmaps = params.stream()
@@ -50,7 +50,7 @@ import org.springframework.util.StringUtils;
@Order(value = Ordered.LOWEST_PRECEDENCE - 1)
@Slf4j
public class SchedulerInit implements CommandLineRunner {
@Autowired
private CollectorScheduling collectorScheduling;
@@ -59,6 +59,7 @@ public class SchedulerInit implements CommandLineRunner {
private static final String MAIN_COLLECTOR_NODE_IP = "127.0.0.1";
private static final String DEFAULT_COLLECTOR_VERSION = "DEBUG";
public static final String PARAM_FIELD_PORT = "port";
@Autowired
private AppService appService;
@@ -117,8 +118,11 @@ public class SchedulerInit implements CommandLineRunner {
appDefine.setDefaultInterval(monitor.getIntervals());
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
String instance = monitor.getInstance();
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
CommonConstants.LABEL_INSTANCE, instance);
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
@@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.service.helper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.constants.ExportFileConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.util.FileUtil;
import org.apache.hertzbeat.manager.config.ManagerSseManager;
import org.apache.hertzbeat.manager.service.ImExportService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Helper class for monitor import and export operations
*/
@Component
@Slf4j
public class MonitorImExportHelper {
private static final String CONTENT_VALUE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK
+ "charset=" + StandardCharsets.UTF_8;
private final Map<String, ImExportService> imExportServiceMap = new HashMap<>();
private final ManagerSseManager managerSseManager;
public MonitorImExportHelper(List<ImExportService> imExportServiceList, ManagerSseManager managerSseManager) {
imExportServiceList.forEach(it -> imExportServiceMap.put(it.type(), it));
this.managerSseManager = managerSseManager;
}
public void export(List<Long> ids, String type, HttpServletResponse res) throws Exception {
var imExportService = imExportServiceMap.get(type);
if (imExportService == null) {
throw new IllegalArgumentException("not support export type: " + type);
}
var fileName = imExportService.getFileName();
res.setHeader(HttpHeaders.CONTENT_DISPOSITION, CONTENT_VALUE);
res.setContentType(CONTENT_VALUE);
res.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
res.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION);
imExportService.exportConfig(res.getOutputStream(), ids);
}
public void importConfig(MultipartFile file) throws Exception {
var fileName = FileUtil.getFileName(file);
var type = FileUtil.getFileType(file);
try {
if (!imExportServiceMap.containsKey(type)) {
String errMsg = ExportFileConstants.FILE + " " + fileName + " is not supported.";
throw new RuntimeException(errMsg);
}
var imExportService = imExportServiceMap.get(type);
imExportService.importConfig(fileName, file.getInputStream());
} catch (Exception e) {
managerSseManager.broadcastImportTaskFail(fileName, e.getMessage());
throw e;
}
}
}
@@ -34,6 +34,7 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.common.support.exception.CommonException;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.HertzBeatKeywordsUtil;
import org.apache.hertzbeat.common.util.JexlCheckerUtil;
import org.apache.hertzbeat.manager.dao.DefineDao;
import org.apache.hertzbeat.manager.dao.MonitorDao;
@@ -439,24 +440,18 @@ public class AppServiceImpl implements AppService, InitializingBean {
for (ParamDefine param : app.getParams()) {
CommonUtil.validDefineI18n(param.getName(), param.getField() + " param");
}
for (Metrics metric : app.getMetrics()) {
CommonUtil.validDefineI18n(metric.getI18n(), metric.getName() + " metric");
if (metric.getFields() == null){
continue;
}
for (Metrics.Field field : metric.getFields()) {
CommonUtil.validDefineI18n(field.getI18n(), metric.getName() + " metric " + field.getField() + " field");
}
}
if (!isModify) {
Assert.isNull(appDefines.get(app.getApp().toLowerCase()),
"monitoring template name " + app.getApp() + " already exists.");
}
Set<String> fieldsSet = new HashSet<>(16);
for (Metrics metrics : app.getMetrics()) {
CommonUtil.validDefineI18n(metrics.getI18n(), metrics.getName() + " metric");
Assert.notEmpty(metrics.getFields(), "monitoring template metrics fields can not null");
fieldsSet.clear();
for (Metrics.Field field : metrics.getFields()) {
CommonUtil.validDefineI18n(field.getI18n(), metrics.getName() + " metric " + field.getField() + " field");
HertzBeatKeywordsUtil.verifyKeywords(field.getField());
if (fieldsSet.contains(field.getField())) {
throw new IllegalArgumentException(app.getApp() + " " + metrics.getName() + " "
+ field.getField() + " can not duplicated.");
@@ -123,10 +123,11 @@ public class BulletinServiceImpl implements BulletinService {
if (null == monitor) {
continue;
}
BulletinMetricsData.Data.DataBuilder dataBuilder = BulletinMetricsData.Data.builder()
.monitorId(monitorId)
.monitorName(monitor.getName())
.host(monitor.getHost());
.host(monitor.getInstance());
List<BulletinMetricsData.Metric> metrics = new ArrayList<>();
Map<String, List<String>> fieldMap = bulletin.getFields();
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.manager.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Sets;
import com.google.common.primitives.Longs;
import com.usthe.sureness.subject.SubjectSum;
@@ -29,8 +28,6 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dao.AlertDefineBindDao;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.ExportFileConstants;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard;
import org.apache.hertzbeat.common.entity.job.Configmap;
@@ -45,15 +42,11 @@ import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.support.event.MonitorDeletedEvent;
import org.apache.hertzbeat.common.util.AesUtil;
import org.apache.hertzbeat.common.util.FileUtil;
import org.apache.hertzbeat.common.util.IntervalExpressionUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.common.util.JexlCheckerUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
import org.apache.hertzbeat.grafana.service.DashboardService;
import org.apache.hertzbeat.manager.config.ManagerSseManager;
import org.apache.hertzbeat.manager.component.validator.ParamValidatorManager;
import org.apache.hertzbeat.manager.dao.CollectorDao;
import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao;
import org.apache.hertzbeat.manager.dao.LabelDao;
@@ -65,10 +58,10 @@ import org.apache.hertzbeat.manager.pojo.dto.MetricsInfo;
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
import org.apache.hertzbeat.manager.scheduler.CollectJobScheduling;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.manager.service.ImExportService;
import org.apache.hertzbeat.manager.service.LabelService;
import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.helper.MonitorImExportHelper;
import org.apache.hertzbeat.manager.support.exception.MonitorDatabaseException;
import org.apache.hertzbeat.manager.support.exception.MonitorDetectException;
import org.apache.hertzbeat.warehouse.service.WarehouseService;
@@ -79,16 +72,12 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
@@ -111,9 +100,12 @@ public class MonitorServiceImpl implements MonitorService {
public static final String PATTERN_HTTPS = "(?i)https://";
private static final Long MONITOR_ID_TMP = 1000000000L;
private static final byte ALL_MONITOR_STATUS = 9;
public static final String PARAM_FIELD_PORT = "port";
private static final String CONTENT_VALUE = MediaType.APPLICATION_OCTET_STREAM_VALUE + SignConstants.SINGLE_MARK + "charset=" + StandardCharsets.UTF_8;
private final Map<String, ImExportService> imExportServiceMap = new HashMap<>();
@Autowired
private ParamValidatorManager paramValidatorManager;
@Autowired
private MonitorImExportHelper monitorImExportHelper;
@Autowired
private AppService appService;
@Autowired
@@ -137,18 +129,12 @@ public class MonitorServiceImpl implements MonitorService {
@Autowired
private DashboardService dashboardService;
@Autowired
private ManagerSseManager managerSseManager;
@Autowired
private LabelDao labelDao;
@Autowired
private LabelService labelService;
@Autowired
private MetricsFavoriteService metricsFavoriteService;
public MonitorServiceImpl(List<ImExportService> imExportServiceList) {
imExportServiceList.forEach(it -> imExportServiceMap.put(it.type(), it));
}
@Override
@Transactional(readOnly = true)
public void detectMonitor(Monitor monitor, List<Param> params, String collector) throws MonitorDetectException {
@@ -161,7 +147,8 @@ public class MonitorServiceImpl implements MonitorService {
@Override
@Transactional(rollbackFor = Exception.class)
public void addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard grafanaDashboard) throws RuntimeException {
public void addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard grafanaDashboard)
throws RuntimeException {
// Apply for monitor id
long monitorId = SnowFlakeIdGenerator.generateId();
Map<String, String> labels = monitor.getLabels();
@@ -176,12 +163,13 @@ public class MonitorServiceImpl implements MonitorService {
}
// Construct the collection task Job entity
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape())
|| !StringUtils.hasText(monitor.getScrape());
String app = isStatic ? monitor.getApp() : monitor.getScrape();
Job appDefine = appService.getAppDefine(app);
if (!isStatic) {
appDefine.setSd(true);
monitor.setHost("unknow");
monitor.setInstance("unknow");
}
if (CommonConstants.PROMETHEUS.equals(monitor.getApp())) {
appDefine.setApp(CommonConstants.PROMETHEUS_APP_PREFIX + monitor.getName());
@@ -190,8 +178,23 @@ public class MonitorServiceImpl implements MonitorService {
appDefine.setDefaultInterval(monitor.getIntervals());
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
String instance = monitor.getInstance();
// The port field may be null
Param portParam = params.stream()
.filter(param -> PARAM_FIELD_PORT.equals(param.getField()))
.findFirst()
.orElse(null);
String portWithMark = (Objects.isNull(portParam) || !StringUtils.hasText(portParam.getParamValue()))
? ""
: SignConstants.DOUBLE_MARK + portParam.getParamValue();
if (Objects.nonNull(instance)) {
instance = instance + portWithMark;
}
monitor.setInstance(instance);
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
CommonConstants.LABEL_INSTANCE, instance);
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
@@ -200,11 +203,12 @@ public class MonitorServiceImpl implements MonitorService {
return new Configmap(param.getField(), param.getParamValue(), param.getType());
}).collect(Collectors.toList());
appDefine.setConfigmap(configmaps);
long jobId = collector == null ? collectJobScheduling.addAsyncCollectJob(appDefine, null) :
collectJobScheduling.addAsyncCollectJob(appDefine, collector);
long jobId = collector == null ? collectJobScheduling.addAsyncCollectJob(appDefine, null)
: collectJobScheduling.addAsyncCollectJob(appDefine, collector);
try {
detectMonitor(monitor, params, collector);
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
try {
if (collector != null) {
@@ -217,7 +221,8 @@ public class MonitorServiceImpl implements MonitorService {
monitor.setId(monitorId);
monitor.setJobId(jobId);
// create grafana dashboard
if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && grafanaDashboard != null && grafanaDashboard.isEnabled()) {
if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && grafanaDashboard != null
&& grafanaDashboard.isEnabled()) {
dashboardService.createOrUpdateDashboard(grafanaDashboard.getTemplate(), monitorId);
}
monitorDao.save(monitor);
@@ -231,16 +236,7 @@ public class MonitorServiceImpl implements MonitorService {
@Override
public void export(List<Long> ids, String type, HttpServletResponse res) throws Exception {
var imExportService = imExportServiceMap.get(type);
if (imExportService == null) {
throw new IllegalArgumentException("not support export type: " + type);
}
var fileName = imExportService.getFileName();
res.setHeader(HttpHeaders.CONTENT_DISPOSITION, CONTENT_VALUE);
res.setContentType(CONTENT_VALUE);
res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
res.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION);
imExportService.exportConfig(res.getOutputStream(), ids);
monitorImExportHelper.export(ids, type, res);
}
@Override
@@ -250,35 +246,24 @@ public class MonitorServiceImpl implements MonitorService {
.stream()
.map(Monitor::getId)
.collect(Collectors.toList());
// Use the existing export method to export all monitors
export(allMonitorIds, type, res);
}
@Override
public void importConfig(MultipartFile file) throws Exception {
var fileName = FileUtil.getFileName(file);
var type = FileUtil.getFileType(file);
try {
if (!imExportServiceMap.containsKey(type)) {
String errMsg = ExportFileConstants.FILE + " " + fileName + " is not supported.";
throw new RuntimeException(errMsg);
}
var imExportService = imExportServiceMap.get(type);
imExportService.importConfig(fileName, file.getInputStream());
} catch (Exception e){
managerSseManager.broadcastImportTaskFail(fileName, e.getMessage());
throw e;
}
monitorImExportHelper.importConfig(file);
}
@Override
@Transactional(readOnly = true)
public void validate(MonitorDto monitorDto, Boolean isModify) throws IllegalArgumentException {
// The request monitoring parameter matches the monitoring parameter definition mapping check
// The request monitoring parameter matches the monitoring parameter definition
// mapping check
Monitor monitor = monitorDto.getMonitor();
// The Service Discovery host field may be null
monitor.setHost(StringUtils.hasText(monitor.getHost()) ? monitor.getHost().trim() : null);
monitor.setInstance(StringUtils.hasText(monitor.getInstance()) ? monitor.getInstance().trim() : null);
monitor.setName(monitor.getName().trim());
Map<String, Param> paramMap = monitorDto.getParams()
.stream()
@@ -288,7 +273,7 @@ public class MonitorServiceImpl implements MonitorService {
param.setParamValue(value);
})
.collect(Collectors.toMap(Param::getField, param -> param));
// Check name uniqueness and can not equal app type
// Check name uniqueness and can not equal app type
if (isModify != null) {
Optional<Job> defineOptional = appService.getAppDefineOption(monitor.getName());
if (defineOptional.isPresent()) {
@@ -318,7 +303,8 @@ public class MonitorServiceImpl implements MonitorService {
// Parameter definition structure verification
List<ParamDefine> paramDefines = appService.getAppParamDefines(monitorDto.getMonitor().getApp());
if (!CollectionUtils.isEmpty(paramDefines)) {
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape())
|| !StringUtils.hasText(monitor.getScrape());
for (ParamDefine paramDefine : paramDefines) {
String field = paramDefine.getField();
Param param = paramMap.get(field);
@@ -330,123 +316,7 @@ public class MonitorServiceImpl implements MonitorService {
throw new IllegalArgumentException("Params field " + field + " is required.");
}
if (param != null && StringUtils.hasText(param.getParamValue())) {
switch (paramDefine.getType()) {
case "number":
double doubleValue;
try {
doubleValue = Double.parseDouble(param.getParamValue());
} catch (Exception e) {
throw new IllegalArgumentException("Params field " + field + " type "
+ paramDefine.getType() + " is invalid.");
}
if (paramDefine.getRange() != null) {
if (!IntervalExpressionUtil.validNumberIntervalExpress(doubleValue,
paramDefine.getRange())) {
throw new IllegalArgumentException("Params field " + field + " type "
+ paramDefine.getType() + " over range " + paramDefine.getRange());
}
}
param.setType(CommonConstants.PARAM_TYPE_NUMBER);
break;
case "textarea":
Short textareaLimit = paramDefine.getLimit();
if (textareaLimit != null && param.getParamValue().length() > textareaLimit) {
throw new IllegalArgumentException("Params field " + field + " type "
+ paramDefine.getType() + " over limit " + param.getParamValue());
}
break;
case "text":
Short textLimit = paramDefine.getLimit();
if (textLimit != null && param.getParamValue().length() > textLimit) {
throw new IllegalArgumentException("Params field " + field + " type "
+ paramDefine.getType() + " over limit " + textLimit);
}
break;
case "host":
String hostValue = param.getParamValue();
if (hostValue.toLowerCase().contains(NetworkConstants.HTTP_HEADER)) {
hostValue = hostValue.replaceAll(PATTERN_HTTP, SignConstants.BLANK);
}
if (hostValue.toLowerCase().contains(NetworkConstants.HTTPS_HEADER)) {
hostValue = hostValue.replace(PATTERN_HTTPS, SignConstants.BLANK);
}
if (!IpDomainUtil.validateIpDomain(hostValue)) {
throw new IllegalArgumentException("Params field " + field + " value "
+ hostValue + " is invalid host value.");
}
break;
case "password":
// The plaintext password needs to be encrypted for transmission and storage
String passwordValue = param.getParamValue();
if (!AesUtil.isCiphertext(passwordValue)) {
passwordValue = AesUtil.aesEncode(passwordValue);
param.setParamValue(passwordValue);
}
param.setType(CommonConstants.PARAM_TYPE_PASSWORD);
break;
case "boolean":
// boolean check
String booleanValue = param.getParamValue();
if (!"true".equalsIgnoreCase(booleanValue) && !"false".equalsIgnoreCase(booleanValue)) {
throw new IllegalArgumentException("Params field " + field + " value "
+ booleanValue + " is invalid boolean value.");
}
break;
case "radio":
// radio single value check
List<ParamDefine.Option> options = paramDefine.getOptions();
boolean invalid = true;
if (options != null) {
for (ParamDefine.Option option : options) {
if (param.getParamValue().equalsIgnoreCase(option.getValue())) {
invalid = false;
break;
}
}
}
if (invalid) {
throw new IllegalArgumentException("Params field " + field + " value "
+ param.getParamValue() + " is invalid option value");
}
break;
case "checkbox":
List<ParamDefine.Option> checkboxOptions = paramDefine.getOptions();
boolean checkboxInvalid = true;
if (checkboxOptions != null) {
for (ParamDefine.Option option : checkboxOptions) {
if (param.getParamValue().equalsIgnoreCase(option.getValue())) {
checkboxInvalid = false;
break;
}
}
}
if (checkboxInvalid) {
throw new IllegalArgumentException("Params field " + field + " value "
+ param.getParamValue() + " is invalid checkbox value");
}
break;
case "metrics-field":
case "key-value":
if (JsonUtil.fromJson(param.getParamValue(), new TypeReference<>() {
}) == null) {
throw new IllegalArgumentException("Params field " + field + " value "
+ param.getParamValue() + " is invalid key-value value");
}
break;
case "array":
String[] arrays = param.getParamValue().split(",");
if (arrays.length == 0) {
throw new IllegalArgumentException("Param field" + field + " value "
+ param.getParamValue() + " is invalid arrays value");
}
if (param.getParamValue().startsWith("[") && param.getParamValue().endsWith("]")) {
param.setParamValue(param.getParamValue().substring(1, param.getParamValue().length() - 1));
}
break;
// todo More parameter definitions and actual value format verification
default:
throw new IllegalArgumentException("ParamDefine type " + paramDefine.getType() + " is invalid.");
}
paramValidatorManager.validate(paramDefine, param);
}
}
}
@@ -470,7 +340,8 @@ public class MonitorServiceImpl implements MonitorService {
}
if (JexlCheckerUtil.verifyStartCharacter(field.getField())) {
throw new IllegalArgumentException(job.getApp() + " " + metrics.getName() + " "
+ field.getField() + " illegal start character, please modify the template information.");
+ field.getField()
+ " illegal start character, please modify the template information.");
}
if (JexlCheckerUtil.verifySpaces(field.getField())) {
throw new IllegalArgumentException(job.getApp() + " " + metrics.getName() + " "
@@ -483,7 +354,8 @@ public class MonitorServiceImpl implements MonitorService {
@Override
@Transactional(rollbackFor = Exception.class)
public void modifyMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard grafanaDashboard) throws RuntimeException {
public void modifyMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard grafanaDashboard)
throws RuntimeException {
long monitorId = monitor.getId();
// Check to determine whether the monitor corresponding to the monitor id exists
Optional<Monitor> queryOption = monitorDao.findById(monitorId);
@@ -507,7 +379,22 @@ public class MonitorServiceImpl implements MonitorService {
labelDao.saveAll(addLabels);
}
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
String instance = monitor.getInstance();
// The port field may be null
Param portParam = params.stream()
.filter(param -> PARAM_FIELD_PORT.equals(param.getField()))
.findFirst()
.orElse(null);
String portWithMark = (Objects.isNull(portParam) || !StringUtils.hasText(portParam.getParamValue()))
? ""
: SignConstants.DOUBLE_MARK + portParam.getParamValue();
if (Objects.nonNull(instance)) {
instance = instance + portWithMark;
}
monitor.setInstance(instance);
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape())
|| !StringUtils.hasText(monitor.getScrape());
if (preMonitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE) {
// Construct the collection task Job entity
String app = isStatic ? monitor.getApp() : monitor.getScrape();
@@ -526,12 +413,13 @@ public class MonitorServiceImpl implements MonitorService {
appDefine.setScheduleType(monitor.getScheduleType());
appDefine.setCronExpression(monitor.getCronExpression());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
CommonConstants.LABEL_INSTANCE, monitor.getInstance());
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
List<Configmap> configmaps = params.stream().map(param ->
new Configmap(param.getField(), param.getParamValue(), param.getType())).collect(Collectors.toList());
List<Configmap> configmaps = params.stream()
.map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType()))
.collect(Collectors.toList());
appDefine.setConfigmap(configmaps);
long newJobId;
if (collector == null) {
@@ -544,7 +432,8 @@ public class MonitorServiceImpl implements MonitorService {
// execute only in non paused status
try {
detectMonitor(monitor, params, collector);
} catch (Exception ignored) {}
} catch (Exception ignored) {
}
}
// After the update is successfully released, refresh the database
@@ -556,7 +445,8 @@ public class MonitorServiceImpl implements MonitorService {
.build();
collectorMonitorBindDao.save(collectorMonitorBind);
}
// force update gmtUpdate time, due the case: monitor not change, param change. we also think monitor change
// force update gmtUpdate time, due the case: monitor not change, param change.
// we also think monitor change
monitor.setGmtUpdate(LocalDateTime.now());
// update or open grafana dashboard
if (monitor.getApp().equals(CommonConstants.PROMETHEUS) && grafanaDashboard != null) {
@@ -588,7 +478,8 @@ public class MonitorServiceImpl implements MonitorService {
if (CollectionUtils.isEmpty(ids)) {
return;
}
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId)
.collect(Collectors.toSet());
Set<Long> allMonitorIds = new HashSet<>(ids);
allMonitorIds.addAll(subMonitorIds);
List<Monitor> monitors = monitorDao.findMonitorsByIdIn(allMonitorIds);
@@ -628,24 +519,29 @@ public class MonitorServiceImpl implements MonitorService {
List<Param> params = paramDao.findParamsByMonitorId(id);
monitorDto.setParams(params);
List<MetricsInfo> metricsInfos;
if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(monitor.getApp()) || monitor.getType() == CommonConstants.MONITOR_TYPE_PUSH_AUTO_CREATE) {
if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(monitor.getApp())
|| monitor.getType() == CommonConstants.MONITOR_TYPE_PUSH_AUTO_CREATE) {
List<CollectRep.MetricsData> metricsDataList = warehouseService.queryMonitorMetricsData(id);
metricsInfos = metricsDataList.stream()
.map(t -> MetricsInfo.builder().name(t.getMetrics()).favorited(favoritedMetrics.contains(t.getMetrics())).build())
.map(t -> MetricsInfo.builder().name(t.getMetrics())
.favorited(favoritedMetrics.contains(t.getMetrics())).build())
.collect(Collectors.toList());
monitorDto.setGrafanaDashboard(dashboardService.getDashboardByMonitorId(id));
} else {
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape())
|| !StringUtils.hasText(monitor.getScrape());
String type = isStatic ? monitor.getApp() : monitor.getScrape();
Job job = appService.getAppDefine(type);
metricsInfos = job.getMetrics().stream()
.filter(Metrics::isVisible)
.map(t -> MetricsInfo.builder().name(t.getName()).favorited(favoritedMetrics.contains(t.getName())).build())
.map(t -> MetricsInfo.builder().name(t.getName())
.favorited(favoritedMetrics.contains(t.getName())).build())
.collect(Collectors.toList());
}
monitorDto.setMetrics(metricsInfos);
monitorDto.setMonitor(monitor);
Optional<CollectorMonitorBind> bindOptional = collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(monitor.getId());
Optional<CollectorMonitorBind> bindOptional = collectorMonitorBindDao
.findCollectorMonitorBindByMonitorId(monitor.getId());
bindOptional.ifPresent(bind -> monitorDto.setCollector(bind.getCollector()));
return monitorDto;
} else {
@@ -654,7 +550,8 @@ public class MonitorServiceImpl implements MonitorService {
}
@Override
public Page<Monitor> getMonitors(List<Long> monitorIds, String app, String search, Byte status, String sort, String order, int pageIndex, int pageSize, String labels) {
public Page<Monitor> getMonitors(List<Long> monitorIds, String app, String search, Byte status, String sort,
String order, int pageIndex, int pageSize, String labels) {
Specification<Monitor> specification = (root, query, criteriaBuilder) -> {
List<Predicate> andList = new ArrayList<>();
if (!CollectionUtils.isEmpty(monitorIds)) {
@@ -678,7 +575,8 @@ public class MonitorServiceImpl implements MonitorService {
List<Predicate> orList = new ArrayList<>();
if (StringUtils.hasText(search)) {
Predicate predicateHost = criteriaBuilder.like(root.get("host"), "%" + search + "%");
Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")), "%" + search.toLowerCase() + "%");
Predicate predicateName = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")),
"%" + search.toLowerCase() + "%");
Long id = Longs.tryParse(search);
if (id != null) {
orList.add(criteriaBuilder.equal(root.get("id"), id));
@@ -726,12 +624,13 @@ public class MonitorServiceImpl implements MonitorService {
return;
}
// Update monitoring status Delete corresponding monitoring periodic task
// The jobId is not deleted, and the jobId is reused again after the management is started.
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
// The jobId is not deleted, and the jobId is reused again after the management
// is started.
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId)
.collect(Collectors.toSet());
ids.addAll(subMonitorIds);
List<Monitor> managedMonitors = monitorDao.findMonitorsByIdIn(ids)
.stream().filter(monitor ->
monitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE)
.stream().filter(monitor -> monitor.getStatus() != CommonConstants.MONITOR_PAUSED_CODE)
.peek(monitor -> monitor.setStatus(CommonConstants.MONITOR_PAUSED_CODE))
.collect(Collectors.toList());
if (!CollectionUtils.isEmpty(managedMonitors)) {
@@ -745,11 +644,11 @@ public class MonitorServiceImpl implements MonitorService {
@Override
public void enableManageMonitors(Set<Long> ids) {
// Update monitoring status Add corresponding monitoring periodic task
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId).collect(Collectors.toSet());
Set<Long> subMonitorIds = monitorBindDao.findMonitorBindsByBizIdIn(ids).stream().map(MonitorBind::getMonitorId)
.collect(Collectors.toSet());
ids.addAll(subMonitorIds);
List<Monitor> unManagedMonitors = monitorDao.findMonitorsByIdIn(ids)
.stream().filter(monitor ->
monitor.getStatus() == CommonConstants.MONITOR_PAUSED_CODE)
.stream().filter(monitor -> monitor.getStatus() == CommonConstants.MONITOR_PAUSED_CODE)
.peek(monitor -> monitor.setStatus(CommonConstants.MONITOR_UP_CODE))
.collect(Collectors.toList());
if (unManagedMonitors.isEmpty()) {
@@ -759,7 +658,8 @@ public class MonitorServiceImpl implements MonitorService {
for (Monitor monitor : unManagedMonitors) {
// Construct the collection task Job entity
List<Param> params = paramDao.findParamsByMonitorId(monitor.getId());
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) || !StringUtils.hasText(monitor.getScrape());
boolean isStatic = CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape())
|| !StringUtils.hasText(monitor.getScrape());
String app = isStatic ? monitor.getApp() : monitor.getScrape();
Job appDefine = appService.getAppDefine(app);
if (!isStatic) {
@@ -775,26 +675,28 @@ public class MonitorServiceImpl implements MonitorService {
appDefine.setScheduleType(monitor.getScheduleType());
appDefine.setCronExpression(monitor.getCronExpression());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
CommonConstants.LABEL_INSTANCE, monitor.getInstance());
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
List<Configmap> configmaps = params.stream().map(param ->
new Configmap(param.getField(), param.getParamValue(), param.getType())).collect(Collectors.toList());
List<Configmap> configmaps = params.stream()
.map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType()))
.collect(Collectors.toList());
List<ParamDefine> paramDefaultValue = appDefine.getParams().stream()
.filter(item -> StringUtils.hasText(item.getDefaultValue()))
.toList();
paramDefaultValue.forEach(defaultVar -> {
if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) {
Configmap configmap = new Configmap(defaultVar.getField(), defaultVar.getDefaultValue(), CommonConstants.TYPE_STRING);
Configmap configmap = new Configmap(defaultVar.getField(), defaultVar.getDefaultValue(),
CommonConstants.TYPE_STRING);
configmaps.add(configmap);
}
});
appDefine.setConfigmap(configmaps);
// Issue collection tasks
Optional<CollectorMonitorBind> bindOptional =
collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(monitor.getId());
Optional<CollectorMonitorBind> bindOptional = collectorMonitorBindDao
.findCollectorMonitorBindByMonitorId(monitor.getId());
String collector = bindOptional.map(CollectorMonitorBind::getCollector).orElse(null);
long newJobId = collectJobScheduling.addAsyncCollectJob(appDefine, collector);
monitor.setJobId(newJobId);
@@ -813,24 +715,26 @@ public class MonitorServiceImpl implements MonitorService {
if (CollectionUtils.isEmpty(appCounts)) {
return null;
}
//Statistical category information, calculate the number of corresponding states for each monitor
// Statistical category information, calculate the number of corresponding
// states for each monitor
Map<String, AppCount> appCountMap = new HashMap<>(appCounts.size());
for (AppCount item : appCounts) {
AppCount appCount = appCountMap.getOrDefault(item.getApp(), new AppCount());
appCount.setApp(item.getApp());
switch (item.getStatus()) {
case CommonConstants.MONITOR_UP_CODE ->
appCount.setAvailableSize(appCount.getAvailableSize() + item.getSize());
appCount.setAvailableSize(appCount.getAvailableSize() + item.getSize());
case CommonConstants.MONITOR_DOWN_CODE ->
appCount.setUnAvailableSize(appCount.getUnAvailableSize() + item.getSize());
appCount.setUnAvailableSize(appCount.getUnAvailableSize() + item.getSize());
case CommonConstants.MONITOR_PAUSED_CODE ->
appCount.setUnManageSize(appCount.getUnManageSize() + item.getSize());
appCount.setUnManageSize(appCount.getUnManageSize() + item.getSize());
default -> {
}
}
appCountMap.put(item.getApp(), appCount);
}
//Traverse the map obtained by statistics and convert it into a List<App Count> result set
// Traverse the map obtained by statistics and convert it into a List<App Count>
// result set
return appCountMap.values().stream().map(item -> {
item.setSize(item.getAvailableSize() + item.getUnManageSize() + item.getUnAvailableSize());
try {
@@ -871,7 +775,7 @@ public class MonitorServiceImpl implements MonitorService {
appDefine.setCyclic(true);
appDefine.setTimestamp(System.currentTimeMillis());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
CommonConstants.LABEL_INSTANCE, monitor.getInstance());
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
@@ -883,7 +787,8 @@ public class MonitorServiceImpl implements MonitorService {
.toList();
paramDefaultValue.forEach(defaultVar -> {
if (configmaps.stream().noneMatch(item -> item.getKey().equals(defaultVar.getField()))) {
Configmap configmap = new Configmap(defaultVar.getField(), defaultVar.getDefaultValue(), (byte) 1);
Configmap configmap = new Configmap(defaultVar.getField(), defaultVar.getDefaultValue(),
(byte) 1);
configmaps.add(configmap);
}
});
@@ -899,7 +804,7 @@ public class MonitorServiceImpl implements MonitorService {
}
}
}
@Override
public Monitor getMonitor(Long monitorId) {
return monitorDao.findById(monitorId).orElse(null);
@@ -950,7 +855,6 @@ public class MonitorServiceImpl implements MonitorService {
addMonitor(newMonitor, newParams, null, null);
}
private void detectSdMonitor(Monitor monitor, List<Param> params, String collector) {
Long monitorId = monitor.getId();
if (monitorId == null || monitorId == 0) {
@@ -967,8 +871,9 @@ public class MonitorServiceImpl implements MonitorService {
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
List<Configmap> configmaps = params.stream().map(param ->
new Configmap(param.getField(), param.getParamValue(), param.getType())).collect(Collectors.toList());
List<Configmap> configmaps = params.stream()
.map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType()))
.collect(Collectors.toList());
appDefine.setConfigmap(configmaps);
appDefine.setSd(true);
List<CollectRep.MetricsData> collectRep;
@@ -1003,14 +908,16 @@ public class MonitorServiceImpl implements MonitorService {
appDefine.setCyclic(false);
appDefine.setTimestamp(System.currentTimeMillis());
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
CommonConstants.LABEL_INSTANCE, monitor.getInstance());
appDefine.setMetadata(metadata);
appDefine.setLabels(monitor.getLabels());
appDefine.setAnnotations(monitor.getAnnotations());
List<Configmap> configmaps = params.stream().map(param ->
new Configmap(param.getField(), param.getParamValue(), param.getType())).collect(Collectors.toList());
List<Configmap> configmaps = params.stream()
.map(param -> new Configmap(param.getField(), param.getParamValue(), param.getType()))
.collect(Collectors.toList());
appDefine.setConfigmap(configmaps);
// To detect availability, you only need to collect the set of availability metrics with a priority of 0.
// To detect availability, you only need to collect the set of availability
// metrics with a priority of 0.
List<Metrics> availableMetrics = appDefine.getMetrics().stream()
.filter(item -> item.getPriority() == 0).collect(Collectors.toList());
appDefine.setMetrics(availableMetrics);
@@ -0,0 +1,381 @@
# 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.
# The monitoring type categoryservice-application service monitoring db-database monitoring custom-custom monitoring os-operating system monitoring
category: db
# The monitoring type eg: linux windows tomcat mysql aws...
app: db2
# The monitoring i18n name
name:
zh-CN: DB2数据库
en-US: DB2 DB
ja-JP: DB2 データベース
# The description and help of this monitoring type
help:
zh-CN: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 协议</a> 通过配置 SQL 对 DB2 数据库的通用性能指标(表空间、性能状态、用户连接、缓存、慢SQL等)进行采集监控。<br>您可以点击“<i>新建 DB2 数据库</i>”并进行配置,或者选择“<i>更多操作</i>”,导入已有配置。
en-US: HertzBeat uses <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC Protocol</a> to configure SQL for collecting general metrics of DB2 database (tablespace、user connect、performance、slow SQL etc.). <br>You can click "<i>New DB2 Database</i>" and configure it, or select "<i>More Operations"</i> to import the existing configuration.
zh-TW: HertzBeat 使用 <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBC 協議</a> 通過配置 SQL 對 DB2 數據庫的通用性能指標(表空間、性能狀態、用戶連接、緩存、慢SQL等)進行采集監控。<br>您可以點擊“<i>新建 DB2 數據庫</i>”並進行配置,或者選擇“<i>更多操作</i>”,導入已有配置。
ja-JP: Hertzbeat は <a class='help_module_content' href='https://hertzbeat.apache.org/docs/advanced/extend-jdbc'> JDBCプロトコルを介して</a> DB2 データベースの一般的なパフォーマンスのメトリクスを監視します。<br>「<i>新規 DB2 データベース</i>」をクリックしてパラメタを設定した後、新規することができます。
helpLink:
zh-CN: https://hertzbeat.apache.org/zh-cn/docs/help/db2/
en-US: https://hertzbeat.apache.org/docs/help/db2/
# Input params define for monitoring(render web ui by the definition)
params:
# field-param field key
- field: host
# name-param field display i18n name
name:
zh-CN: 目标Host
en-US: Target Host
ja-JP: 目標ホスト
# type-param field type(most mapping the html input type)
type: host
# required-true or false
required: true
# field-param field key
- field: port
# name-param field display i18n name
name:
zh-CN: 端口
en-US: Port
ja-JP: ポート
# type-param field type(most mapping the html input type)
type: number
# when type is number, range is required
range: '[0,65535]'
# required-true or false
required: true
# default value
defaultValue: 50000 # DB2 default port
- field: timeout
name:
zh-CN: 查询超时时间(ms)
en-US: Query Timeout(ms)
ja-JP: クエリタイムアウト(ms)
type: number
range: '[400,200000]'
required: false
hide: true
defaultValue: 6000
- field: reuseConnection
name:
zh-CN: 复用连接
en-US: Reuse Connection
ja-JP: 接続再利用
type: boolean
required: false
defaultValue: true
- field: database
name:
zh-CN: 数据库名
en-US: Database Name
ja-JP: データベース名
type: text
required: true # Database Name is typically mandatory for DB2 JDBC connection
- field: username
name:
zh-CN: 用户名
en-US: Username
ja-JP: ユーザー名
type: text
limit: 50
required: false
- field: password
name:
zh-CN: 密码
en-US: Password
ja-JP: パスワード
type: password
required: false
- field: url
name:
zh-CN: URL
en-US: URL
ja-JP: URL
type: text
required: false
hide: true
# collect metrics config list
metrics:
# metrics - basic
- name: basic
i18n:
zh-CN: 基本信息
en-US: Basic Info
ja-JP: 基礎情報
priority: 0
fields:
- field: database_version
type: 1
label: true
i18n:
zh-CN: 数据库版本
en-US: Database Version
ja-JP: データベースバージョン
- field: instance_name
type: 1
i18n:
zh-CN: 实例名
en-US: Instance Name
ja-JP: インスタンス名
- field: status
type: 1
i18n:
zh-CN: 实例状态
en-US: Instance Status
ja-JP: インスタンス的状态
- field: num_db
type: 0
i18n:
zh-CN: 数据库数量
en-US: Number of Databases
ja-JP: データベースの数
aliasFields:
- INSTANCE_NAME
- INSTANCE_STATUS
- NUM_DB
- CURRENT_USER
calculates:
- database_version=INSTANCE_NAME
- instance_name=INSTANCE_NAME
- status=INSTANCE_STATUS
- num_db=NUM_DB
protocol: jdbc
jdbc:
host: ^_^host^_^
port: ^_^port^_^
platform: db2
username: ^_^username^_^
password: ^_^password^_^
database: ^_^database^_^
timeout: ^_^timeout^_^
reuseConnection: ^_^reuseConnection^_^
queryType: oneRow
sql: "SELECT CURRENT SERVER AS INSTANCE_NAME, 'ACTIVE' AS INSTANCE_STATUS, 1 AS NUM_DB, CURRENT USER AS CURRENT_USER FROM SYSIBM.SYSDUMMY1"
url: ^_^url^_^
# Combined tablespace capacity and usage
- name: tablespace_usage
i18n:
zh-CN: 表空间使用率
en-US: Tablespace Usage
ja-JP: 表領域使用率
priority: 1
fields:
- field: tablespace_name
type: 1
i18n:
zh-CN: 表空间名
en-US: Tablespace Name
ja-JP: 表領域名
label: true
- field: status
type: 1
i18n:
zh-CN: 类型
en-US: Type
ja-JP: タイプ
- field: total
type: 0
i18n:
zh-CN: 总容量
en-US: Total Size
ja-JP: 総容量
unit: MB
- field: used
type: 0
i18n:
zh-CN: 已使用
en-US: Used Size
ja-JP: 使用済み
unit: MB
- field: free
type: 0
i18n:
zh-CN: 空闲
en-US: Free Size
ja-JP: 利用可能
unit: MB
- field: used_percentage
type: 0
i18n:
zh-CN: 已用百分比
en-US: Used Percentage
ja-JP: 使用した比率
unit: '%'
aliasFields:
- TBSP_NAME
- TBSP_TYPE
- TBSP_TOTAL_SIZE_KB
- TBSP_USED_SIZE_KB
- TBSP_FREE_SIZE_KB
calculates:
- tablespace_name=TBSP_NAME
- status=TBSP_TYPE
- total=TBSP_TOTAL_SIZE_KB / 1024
- used=TBSP_USED_SIZE_KB / 1024
- free=TBSP_FREE_SIZE_KB / 1024
- used_percentage=TBSP_USED_SIZE_KB / TBSP_TOTAL_SIZE_KB * 100
protocol: jdbc
jdbc:
host: ^_^host^_^
port: ^_^port^_^
platform: db2
username: ^_^username^_^
password: ^_^password^_^
database: ^_^database^_^
timeout: ^_^timeout^_^
reuseConnection: ^_^reuseConnection^_^
queryType: multiRow
sql: "SELECT TBSP_NAME, TBSP_TYPE, TBSP_TOTAL_SIZE_KB, TBSP_USED_SIZE_KB, TBSP_FREE_SIZE_KB FROM SYSIBMADM.TBSP_UTILIZATION"
url: ^_^url^_^
# Sessions/Applications status distribution
- name: sessions_status
i18n:
zh-CN: 会话状态
en-US: Session Status
ja-JP: セッション状態
priority: 2
fields:
- field: status_type
type: 1
i18n:
zh-CN: 状态类型
en-US: Status Type
ja-JP: 状態タイプ
label: true
- field: count
type: 0
i18n:
zh-CN: 数量
en-US: Count
ja-JP:
protocol: jdbc
jdbc:
host: ^_^host^_^
port: ^_^port^_^
platform: db2
username: ^_^username^_^
password: ^_^password^_^
database: ^_^database^_^
timeout: ^_^timeout^_^
reuseConnection: ^_^reuseConnection^_^
queryType: multiRow
sql: "SELECT APPL_STATUS AS STATUS_TYPE, COUNT(*) AS COUNT FROM SYSIBMADM.APPLICATIONS GROUP BY APPL_STATUS"
url: ^_^url^_^
# Total number of applications/sessions
- name: process
i18n:
zh-CN: 应用进程数
en-US: Application Process Count
ja-JP: アプリケーションプロセス数
priority: 5
fields:
- field: process_count
type: 0
i18n:
zh-CN: 进程数
en-US: Process Count
ja-JP: プロセス数
protocol: jdbc
jdbc:
host: ^_^host^_^
port: ^_^port^_^
platform: db2
username: ^_^username^_^
password: ^_^password^_^
database: ^_^database^_^
timeout: ^_^timeout^_^
reuseConnection: ^_^reuseConnection^_^
queryType: oneRow
# Count of all running applications
sql: "select count(*) as process_count from SYSIBMADM.APPLICATIONS"
url: ^_^url^_^
- name: locks
i18n:
zh-CN:
en-US: Locks
ja-JP: ロック
priority: 10
fields:
- field: waiting_locks
type: 0
i18n:
zh-CN: 当前等待锁数
en-US: Current Waiting Locks
ja-JP: 現在の待機中ロック数
aliasFields:
- WAITING_LOCKS
calculates:
- waiting_locks=WAITING_LOCKS
protocol: jdbc
jdbc:
host: ^_^host^_^
port: ^_^port^_^
platform: db2
username: ^_^username^_^
password: ^_^password^_^
database: ^_^database^_^
timeout: ^_^timeout^_^
reuseConnection: ^_^reuseConnection^_^
queryType: oneRow
sql: "SELECT COUNT(*) AS WAITING_LOCKS FROM SYSIBMADM.MON_LOCKWAITS"
url: ^_^url^_^
# Slow Query / Top Statements (by Total CPU Time)
- name: slow_query
i18n:
zh-CN: 慢查询
en-US: Slow Query
ja-JP: スロークエリ
priority: 14
fields:
- field: avg_exe_time
type: 0
i18n:
zh-CN: 平均执行时间
en-US: Avg Execution Time
ja-JP: 平均実行時間
unit: ms
- field: sql_text
type: 1
i18n:
zh-CN: SQL文本
en-US: SQL Text
ja-JP: SQLテキスト
aliasFields:
- AVG_EXE_TIME
- SQL_TEXT
calculates:
- avg_exe_time=AVG_EXE_TIME
- sql_text=SQL_TEXT
protocol: jdbc
jdbc:
host: ^_^host^_^
port: ^_^port^_^
platform: db2
username: ^_^username^_^
password: ^_^password^_^
database: ^_^database^_^
timeout: ^_^timeout^_^
reuseConnection: ^_^reuseConnection^_^
queryType: multiRow
sql: "select total_act_time/num_exec_with_metrics as AVG_EXE_TIME, substr(stmt_text,1,4000) as SQL_TEXT from table(mon_get_pkg_cache_stmt(null, null, null, -2)) as T where num_exec_with_metrics>=1 order by 1 desc fetch first 100 rows only"
url: ^_^url^_^
@@ -162,7 +162,7 @@ metrics:
# metrics fields list.
fields:
# Metric information includes the following: Field name, Type: 0-number, 1-string, instance: indicates whether the metric is the primary key, unit: the unit of the metric
- field: instance
- field: metric_instance
type: 1
i18n:
zh-CN: 实例
@@ -180,6 +180,12 @@ metrics:
zh-CN:
en-US: Value
ja-JP:
aliasFields:
- instance
- timestamp
- value
calculates:
- metric_instance=instance
# Monitoring protocol used for data collection, e.g. sql, ssh, http, telnet, wmi, snmp, sdk.
protocol: http
# When the protocol is HTTP, the specific collection configuration is as follows
@@ -201,7 +207,7 @@ metrics:
^_^headers^_^: ^_^headers^_^
# Request parameter content
params:
^_^params^_^: ^_^params^_^
query: basic_influxdb_memstats_alloc
# authorization
authorization:
# Authentication method: Basic Auth, Digest Auth, Bearer Token
@@ -231,7 +237,7 @@ metrics:
zh-CN: 任务
en-US: Job
ja-JP: タスク
- field: instance
- field: metric_instance
type: 1
i18n:
zh-CN: 实例
@@ -255,6 +261,14 @@ metrics:
zh-CN:
en-US: Value
ja-JP:
aliasFields:
- job
- instance
- database
- timestamp
- value
calculates:
- metric_instance=instance
# Monitoring protocol used for data collection, e.g. sql, ssh, http, telnet, wmi, snmp, sdk.
protocol: http
# When the protocol is HTTP, the specific collection configuration is as follows
@@ -300,7 +314,7 @@ metrics:
# metrics fields list.
fields:
# Metric information includes the following: Field name, Type: 0-number, 1-string, instance: indicates whether the metric is the primary key, unit: the unit of the metric
- field: instance
- field: metric_instance
type: 1
i18n:
zh-CN: 实例
@@ -318,6 +332,12 @@ metrics:
zh-CN:
en-US: Value
ja-JP:
aliasFields:
- instance
- timestamp
- value
calculates:
- metric_instance=instance
# Monitoring protocol used for data collection, e.g. sql, ssh, http, telnet, wmi, snmp, sdk.
protocol: http
# When the protocol is HTTP, the specific collection configuration is as follows
@@ -364,7 +384,7 @@ metrics:
# metrics fields list.
fields:
# Metric information includes the following: Field name, Type: 0-number, 1-string, instance: indicates whether the metric is the primary key, unit: the unit of the metric
- field: instance
- field: metric_instance
type: 1
i18n:
zh-CN: 实例
@@ -382,6 +402,12 @@ metrics:
zh-CN:
en-US: Value
ja-JP:
aliasFields:
- instance
- timestamp
- value
calculates:
- metric_instance=instance
# Monitoring protocol used for data collection, e.g. sql, ssh, http, telnet, wmi, snmp, sdk.
protocol: http
# When the protocol is HTTP, the specific collection configuration is as follows
@@ -166,7 +166,7 @@ metrics:
zh-CN: 名称
en-US: Name
ja-JP: 名前
- field: instance
- field: metric_instance
type: 1
i18n:
zh-CN: 实例
@@ -184,6 +184,13 @@ metrics:
zh-CN: 数值
en-US: Value
ja-JP:
aliasFields:
- __name__
- instance
- timestamp
- value
calculates:
- metric_instance=instance
# The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk
protocol: http
# The config content when protocol is http
@@ -295,7 +302,7 @@ metrics:
# collect metrics content
fields:
# field-metric name, type-metric type(0-number,1-string), unit-metric unit('%','ms','MB'), label-whether it is a metrics label field
- field: instance
- field: metric_instance
type: 1
i18n:
zh-CN: 实例
@@ -325,6 +332,14 @@ metrics:
zh-CN: 数值
en-US: Value
ja-JP:
aliasFields:
- instance
- job
- topic
- timestamp
- value
calculates:
- metric_instance=instance
# The protocol used for monitoring, eg: sql, ssh, http, telnet, wmi, snmp, sdk
protocol: http
# The specific collection configuration when the protocol is http
@@ -0,0 +1,47 @@
## 🔔 HertzBeat Alert Notification
### Alert Summary
> - Status: ${status}
<#if commonLabels??>
<#if commonLabels.severity??>
> - Severity: ${commonLabels.severity?switch("critical", "❤️ Critical", "warning", "💛 Warning", "info", "💚 Info", "Unknown")}
</#if>
<#if commonLabels.alertname??>
> - Alert Name: ${commonLabels.alertname}
</#if>
</#if>
### Alert Details
<#list alerts as alert>
#### Alert ${alert?index + 1}
<#if alert.labels??>
<#list alert.labels?keys as key>
> - ${key}: ${alert.labels[key]}
</#list>
</#if>
<#if alert.content?? && alert.content != "">
> - Content: ${alert.content}
</#if>
> - Trigger Count: ${alert.triggerTimes}
> - Start Time: ${(alert.startAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}
<#if alert.activeAt??>
> - Active Time: ${(alert.activeAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}
</#if>
<#if alert.endAt??>
> - End Time: ${(alert.endAt?number_to_datetime)?string('yyyy-MM-dd HH:mm:ss')}
</#if>
<#if alert.annotations?? && alert.annotations?size gt 0>
##### Additional Information
<#list alert.annotations?keys as key>
> - ${key}: ${alert.annotations[key]}
</#list>
</#if>
</#list>
<#if commonAnnotations?? && commonAnnotations?size gt 0>
### Common Information
<#list commonAnnotations?keys as key>
> - ${key}: ${commonAnnotations[key]}
</#list>
</#if>
@@ -0,0 +1,72 @@
/*
* 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.manager.component.validator;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ParamValidatorManagerTest {
private ParamValidatorManager paramValidatorManager;
@Mock
private ParamValidator paramValidator;
@BeforeEach
void setUp() {
paramValidatorManager = new ParamValidatorManager(List.of(paramValidator));
}
@Test
void validate_Success() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("text");
Param param = new Param();
when(paramValidator.support("text")).thenReturn(true);
assertDoesNotThrow(() -> paramValidatorManager.validate(paramDefine, param));
verify(paramValidator).validate(paramDefine, param);
}
@Test
void validate_NoValidatorFound() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("unknown");
Param param = new Param();
when(paramValidator.support("unknown")).thenReturn(false);
assertThrows(IllegalArgumentException.class, () -> paramValidatorManager.validate(paramDefine, param));
}
}
@@ -0,0 +1,74 @@
/*
* 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.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ArrayParamValidatorTest {
private ArrayParamValidator validator;
@BeforeEach
void setUp() {
validator = new ArrayParamValidator();
}
@Test
void support() {
assertTrue(validator.support("array"));
}
@Test
void validate_ValidArray() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("array");
Param param = new Param();
param.setParamValue("val1,val2");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_ValidArrayWithBrackets() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("array");
Param param = new Param();
param.setParamValue("[val1,val2]");
validator.validate(paramDefine, param);
assertEquals("val1,val2", param.getParamValue());
}
@Test
void validate_EmptyArray() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("array");
paramDefine.setField("tags");
Param param = new Param();
param.setParamValue("");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
assertEquals("", param.getParamValue());
}
}
@@ -0,0 +1,85 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BooleanParamValidatorTest {
private BooleanParamValidator validator;
@BeforeEach
void setUp() {
validator = new BooleanParamValidator();
}
@Test
void support() {
assertTrue(validator.support("boolean"));
}
@Test
void validate_True() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("boolean");
Param param = new Param();
param.setParamValue("true");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_False() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("boolean");
Param param = new Param();
param.setParamValue("false");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_CaseInsensitive() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("boolean");
Param param = new Param();
param.setParamValue("True");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_Invalid() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("boolean");
paramDefine.setField("ssl");
Param param = new Param();
param.setParamValue("yes");
assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param));
}
}
@@ -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.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HostParamValidatorAdapterTest {
private HostParamValidatorAdapter validator;
@BeforeEach
void setUp() {
validator = new HostParamValidatorAdapter();
}
@Test
void support() {
assertTrue(validator.support("host"));
}
@Test
void validate_ValidHost() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("host");
Param param = new Param();
param.setParamValue("127.0.0.1");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_ValidDomain() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("host");
Param param = new Param();
param.setParamValue("localhost");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_HttpPrefix() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("host");
Param param = new Param();
param.setParamValue("http://127.0.0.1");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_InvalidHost() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("host");
paramDefine.setField("host");
Param param = new Param();
param.setParamValue("invalid host");
assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param));
}
@Test
void validate_HostWithPort() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("host");
Param param = new Param();
param.setParamValue("127.0.0.1:8080");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
}
@@ -0,0 +1,66 @@
/*
* 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.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JsonParamValidatorTest {
private JsonParamValidator validator;
@BeforeEach
void setUp() {
validator = new JsonParamValidator();
}
@Test
void support() {
assertTrue(validator.support("metrics-field"));
assertTrue(validator.support("key-value"));
}
@Test
void validate_ValidJson() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("key-value");
Param param = new Param();
param.setParamValue("{\"key\":\"value\"}");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_InvalidJson() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("key-value");
paramDefine.setField("headers");
Param param = new Param();
param.setParamValue("{key:value}"); // Invalid JSON
assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param));
}
}
@@ -0,0 +1,90 @@
/*
* 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.manager.component.validator.impl;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NumberParamValidatorTest {
private NumberParamValidator validator;
@BeforeEach
void setUp() {
validator = new NumberParamValidator();
}
@Test
void support() {
assertTrue(validator.support("number"));
}
@Test
void validate_ValidNumber() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("number");
Param param = new Param();
param.setParamValue("123");
validator.validate(paramDefine, param);
assertEquals(CommonConstants.PARAM_TYPE_NUMBER, param.getType());
}
@Test
void validate_InvalidNumber() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("number");
paramDefine.setField("port");
Param param = new Param();
param.setParamValue("abc");
assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param));
}
@Test
void validate_NumberInRange() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("number");
paramDefine.setRange("[0,100]");
Param param = new Param();
param.setParamValue("50");
validator.validate(paramDefine, param);
}
@Test
void validate_NumberOutOfRange() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("number");
paramDefine.setField("port");
paramDefine.setRange("[0,100]");
Param param = new Param();
param.setParamValue("150");
assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param));
}
}
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.hertzbeat.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class OptionParamValidatorTest {
private OptionParamValidator validator;
@BeforeEach
void setUp() {
validator = new OptionParamValidator();
}
@Test
void support() {
assertTrue(validator.support("radio"));
assertTrue(validator.support("checkbox"));
}
@Test
void validate_ValidOption() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("radio");
paramDefine.setOptions(List.of(new ParamDefine.Option("opt1", "val1"), new ParamDefine.Option("opt2", "val2")));
Param param = new Param();
param.setParamValue("val1");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_InvalidOption() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("radio");
paramDefine.setField("method");
paramDefine.setOptions(List.of(new ParamDefine.Option("opt1", "val1")));
Param param = new Param();
param.setParamValue("val2");
assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param));
}
@Test
void validate_NullOptions() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("radio");
paramDefine.setField("method");
Param param = new Param();
param.setParamValue("val1");
assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param));
}
}
@@ -0,0 +1,75 @@
/*
* 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.manager.component.validator.impl;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.util.AesUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PasswordParamValidatorTest {
private PasswordParamValidator validator;
@BeforeEach
void setUp() {
validator = new PasswordParamValidator();
}
@Test
void support() {
assertTrue(validator.support("password"));
}
@Test
void validate_EncryptsPlaintext() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("password");
Param param = new Param();
String plaintext = "password123";
param.setParamValue(plaintext);
validator.validate(paramDefine, param);
assertNotEquals(plaintext, param.getParamValue());
assertTrue(AesUtil.isCiphertext(param.getParamValue()));
assertEquals(CommonConstants.PARAM_TYPE_PASSWORD, param.getType());
}
@Test
void validate_IgnoresCiphertext() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("password");
Param param = new Param();
String ciphertext = AesUtil.aesEncode("password123");
param.setParamValue(ciphertext);
validator.validate(paramDefine, param);
assertEquals(ciphertext, param.getParamValue());
assertEquals(CommonConstants.PARAM_TYPE_PASSWORD, param.getType());
}
}
@@ -0,0 +1,68 @@
/*
* 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.manager.component.validator.impl;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TextParamValidatorTest {
private TextParamValidator validator;
@BeforeEach
void setUp() {
validator = new TextParamValidator();
}
@Test
void support() {
assertTrue(validator.support("text"));
assertTrue(validator.support("textarea"));
}
@Test
void validate_ValidLength() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("text");
paramDefine.setLimit((short) 10);
Param param = new Param();
param.setParamValue("12345");
assertDoesNotThrow(() -> validator.validate(paramDefine, param));
}
@Test
void validate_OverLimit() {
ParamDefine paramDefine = new ParamDefine();
paramDefine.setType("text");
paramDefine.setField("name");
paramDefine.setLimit((short) 3);
Param param = new Param();
param.setParamValue("12345");
assertThrows(IllegalArgumentException.class, () -> validator.validate(paramDefine, param));
}
}
@@ -60,7 +60,7 @@ class MonitorControllerTest {
monitor.setJobId(43243543543L);
monitor.setName("Api-TanCloud.cn");
monitor.setName("TanCloud");
monitor.setHost("192.167.25.11");
monitor.setInstance("192.167.25.11:8989");
monitor.setIntervals(600);
monitor.setDescription("对SAAS网站TanCloud的可用性监控");
monitor.setCreator("tom");
@@ -115,7 +115,7 @@ class MonitorControllerTest {
monitor.setJobId(43243543543L);
monitor.setName("Api-TanCloud.cn");
monitor.setName("TanCloud");
monitor.setHost("192.167.25.11");
monitor.setInstance("192.167.25.11:8989");
monitor.setIntervals(600);
monitor.setDescription("对SAAS网站TanCloud的可用性监控");
monitor.setCreator("tom");
@@ -142,7 +142,7 @@ class MonitorControllerTest {
monitor.setJobId(43243543543L);
monitor.setName("Api-TanCloud.cn");
monitor.setName("TanCloud");
monitor.setHost("192.167.25.11");
monitor.setInstance("192.167.25.11:8989");
monitor.setIntervals(600);
monitor.setDescription("对SAAS网站TanCloud的可用性监控");
monitor.setCreator("tom");
@@ -116,7 +116,7 @@ public class CollectorJobSchedulerTest {
when(collectorMonitorBindDao.findCollectorMonitorBindsByCollector(identity)).thenReturn(List.of(bind));
// mock monitor
Monitor monitor = Monitor.builder().id(1L).name("test-monitor").host("127.0.0.1").app("test-app").intervals(60).status((byte) 1).build();
Monitor monitor = Monitor.builder().id(1L).name("test-monitor").instance("127.0.0.1:8080").app("test-app").intervals(60).status((byte) 1).build();
when(monitorDao.findMonitorsByIdIn(any())).thenReturn(List.of(monitor));
// mock Params
@@ -145,7 +145,7 @@ public class CollectorJobSchedulerTest {
assertNotNull(job);
assertNotNull(job.getMetadata());
assertEquals("test-monitor", job.getMetadata().get(CommonConstants.LABEL_INSTANCE_NAME));
assertEquals("127.0.0.1", job.getMetadata().get(CommonConstants.LABEL_INSTANCE_HOST));
assertEquals("127.0.0.1:8080", job.getMetadata().get(CommonConstants.LABEL_INSTANCE));
}
}

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