Compare commits

...
26 Commits
Author SHA1 Message Date
LogicandDuansg b9f1d65cb5 [refactor] Apache Fory (#3873)
Signed-off-by: Logic <zqr10159@dromara.org>
Co-authored-by: Duansg <siguoduan@gmail.com>
2025-11-27 17:28:44 +08:00
Logic 1dea5467fc [refactor]: springboot4.0.0 (#3872) 2025-11-27 17:22:07 +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
Tomsun28 624ba8dc8d docs: update readme and doc (#3840) 2025-11-02 12:14:20 +08:00
Tomsun28andaias00 fb4b665c85 refactor ai agent and fix bug, update doc (#3836)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-11-01 21:22:42 +08:00
Calvin 5a9f371e9d [doc] add pmc blog of Calvin, add Duansg as committer and Calvin as PMC (#3838) 2025-11-01 09:38:45 +08:00
Duansg 5c3d1a04de [improve] SLS integrated access supports merged alarm resolution (#3835) 2025-10-31 21:00:34 +08:00
Cyantyandaias00 024b44f477 [ci] replace outdated link addresses in dead link check (#3834)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-10-31 10:16:10 +08:00
Cyanty 35046f8a06 [ci] fix markdown lint ci (#3833) 2025-10-31 09:36:28 +08:00
Duansgandaias00 23c2f85e8a [fix] Optimize the issue where missing tags cause alert statuses to become disordered (#3829)
Co-authored-by: aias00 <liuhongyu@apache.org>
2025-10-29 14:24:02 +08:00
Tomsun28 56226c1d52 refactor: split manager to startup module (#3830)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-10-29 13:49:04 +08:00
393 changed files with 8103 additions and 6868 deletions
+6 -6
View File
@@ -16,11 +16,15 @@
#
github:
description: Real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
description: An AI-powered next-generation open source real-time observability system.
homepage: https://hertzbeat.apache.org/
labels:
- monitoring
- ai
- agent
- llm
- logs
- monitor
- monitoring
- notifications
- alerting
- self-hosted
@@ -30,15 +34,11 @@ github:
- metrics
- observability
- uptime
- uptime-monitoring
- 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:
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
python-version: '3.8'
- name: Check Markdown
uses: DavidAnson/markdownlint-cli2-action@v16
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e
with:
globs: './home/**/*.md'
+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)
+15 -21
View File
@@ -9,8 +9,6 @@
<b><a href="README.md">English</a></b> | <a href="README_CN.md">中文</a> | <a href="README_JP.md">日本語</a>
</p>
> A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
@@ -21,6 +19,7 @@
[![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCri75zfWX0GHqJFPENEbLow?logo=youtube&label=YouTube%20Channel)](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/apache/hertzbeat)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/apache/hertzbeat)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/apache/hertzbeat)
**Home: [hertzbeat.apache.org](https://hertzbeat.apache.org)**
**Email: <a href="mailto:dev-subscribe@hertzbeat.apache.org">Mail to `dev-subscribe@hertzbeat.apache.org`</a> to subscribe mailing lists**
@@ -28,28 +27,23 @@
## 🎡 <font color="green">Introduction</font>
[Apache HertzBeat™](https://github.com/apache/hertzbeat) is an easy-to-use, open source, real-time observability system with agentless, high performance cluster, prometheus-compatible, offers powerful custom monitoring and status page building capabilities.
[Apache HertzBeat™](https://github.com/apache/hertzbeat) is an AI-powered next-generation open source real-time observability system. Unified metrics and logs collection, centralized alerting distribution, intelligent management and analysis. No Agent required, high performance cluster, provides powerful custom monitoring and status page building capabilities.
### Features
* Combines **monitoring, alarm, and notification** features into one platform, and supports monitoring for web service, program, database, cache, os, webserver, middleware, bigdata, cloud-native, network, custom and more.
* Easy to use and agentless, web-based and with one-click monitoring and alerting, zero learning curve.
* Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
* Compatible with the `Prometheus` ecosystem and more, can monitoring what `Prometheus` can monitoring with few clicks on webui.
* High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
* Provides flexible alarm threshold rules and timely notifications delivered via `Discord` `Slack` `Telegram` `Email` `Dingtalk` `WeChat` `FeiShu` `Webhook` `SMS` `ServerChan`.
* Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
- Integrates **collection + analysis + alerting + notification** into one platform, with new AI-powered interactions and features under HertzBeat AI, and built-in MCP Server capabilities.
- Unified metrics platform, agentless, Prometheus-compatible, supports application services, programs, databases, caches, operating systems, big data, middleware, web servers, cloud-native, networks, custom monitoring and more.
- Unified logging platform, seamlessly integrates multiple log sources through OTLP protocol for reporting.
- Unified alerting platform, integrates internal alerts with various external alert sources, unified alert processing and analysis, flexible real-time and periodic threshold rules, grouping convergence, silence, suppression, etc.
- Unified message distribution, alerts processed by the alerting platform are distributed via `Email` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` and other methods.
- Makes protocols such as `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` configurable, allowing you to collect any metrics by simply configuring the template `YML` file online. Imagine being able to quickly adapt to a new monitoring type like K8s or Docker simply by configuring online with HertzBeat.
- High performance, supports horizontal expansion of multi-collector clusters, multi-isolated network monitoring and cloud-edge collaboration.
- Provides powerful status page building capabilities, easily communicate the real-time status of your service to users.
> HertzBeat's powerful customization, multi-type support, high performance, easy expansion, and low coupling, aims to help users quickly build their own monitoring system.
> HertzBeat's unified platform, AI intelligence, powerful customization, multi-type support, high performance, and easy expansion, aims to help users quickly and conveniently achieve observability requirements.
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 Architecture
@@ -58,8 +52,8 @@
## ⛄ Supported
> We define all monitoring collection types such as `mysql`, `jvm`, and `k8s` as `YML` monitoring templates, allowing users to import them to support corresponding types of monitoring.
> Welcome everyone to contribute your customized general monitoring type YML template during use.
> We define all metrics collection types such as `mysql`, `jvm`, and `k8s` as `YML` templates, allowing users to import them to support corresponding types of monitoring.
> Welcome everyone to contribute your customized general metrics type YML template during use.
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
@@ -161,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`
+13 -20
View File
@@ -9,8 +9,6 @@
<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a>
</p>
> 实时观测系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
@@ -28,29 +26,24 @@
## 🎡 <font color="green">介绍</font>
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是 AI 驱动的下一代开源实时观测系统。指标日志统一收集,告警一站分发,智能管控分析。无需 Agent,高性能集群,提供强大的自定义监控和状态页构建能力。
### 特点
- **监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,无需学习成本。
- `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模板 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的
-```采集+分析+告警+通知```为一体,HertzBeat AI 驱动下的新交互与功能,也内置 MCP Server 对外能力
- 统一的指标平台,无需 Agent,兼容 Prometheus,支持应用服务,程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等。
- 统一的日志平台,通过 OTLP 协议多日志源无缝对接上报。
- 统一的告警平台,内部告警与外部多种告警源集成接入,统一告警处理分析,灵活的实时与周期阈值规则,分组收敛,静默,抑制等
- 统一的消息分发,告警平台处理后通过 `邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式分发通知。
-`Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需配置模板 `YML` 就能自定义采集指标。您相信只需简单配置即可快速适配一款 `K8s``Docker` 等新的监控类型吗?
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
- 灵活的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
> `HertzBeat`的强大自定义,多类型支持,高性能,易扩展,低耦合,希望能帮助用户快速搭建自有监控系统
> `HertzBeat`的统一平台,AI智能,强大自定义,多类型支持,高性能,易扩展,希望能帮助用户快速方便实现观测需求
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 模块
@@ -59,8 +52,8 @@
## ⛄ 已支持
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 监控模板,用户可以导入这些模板来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用监控类型监控模板。
> 我们将监控采集类型(mysql,jvm,k8s)都定义为 `yml` 模板,用户可以导入这些模板来支持对应类型的监控!
> 欢迎大家一起贡献你使用过程中自定义的通用指标模板。
- [Website](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-website.yml), [Port Telnet](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-port.yml),
[Http Api](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-api.yml), [Ping Connect](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-ping.yml),
@@ -98,7 +91,7 @@
- [CiscoSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-cisco_switch.yml), [HpeSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-hpe_switch.yml),
[HuaweiSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-huawei_switch.yml), [TpLinkSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-tplink_switch.yml),
[H3cSwitch](https://raw.githubusercontent.com/apache/hertzbeat/master/hertzbeat-manager/src/main/resources/define/app-h3c_switch.yml)
- 和更多自定义监控模板。
- 和更多自定义指标模板。
- 通知支持 `Discord` `Slack` `Telegram` `邮件` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱`
## 🐕 快速开始
@@ -159,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`
+12 -20
View File
@@ -9,8 +9,6 @@
<a href="README.md">English</a> | <a href="README_CN.md">中文</a> | <b><a href="README_JP.md">日本語</a></b>
</p>
> オープンソースのリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き。
[![Discord](https://img.shields.io/badge/Chat-Discord-7289DA?logo=discord)](https://discord.gg/Fb6M73htGr)
[![Reddit](https://img.shields.io/badge/Reddit-Community-7289DA?logo=reddit)](https://www.reddit.com/r/hertzbeat/)
[![Twitter](https://img.shields.io/twitter/follow/hertzbeat1024?logo=twitter)](https://x.com/hertzbeat1024)
@@ -28,29 +26,23 @@
## 🎡 <font color="green">紹介</font>
[Apache HertzBeat](https://github.com/apache/hertzbeat) incubating)はオープンソースリアルタイム監視システム、エージェントレス、クラスタ対応、Prometheus互換、カスタムおよびステータスページ付き
[Apache HertzBeat](https://github.com/apache/hertzbeat) は AI 駆動の次世代オープンソースリアルタイム観測システムです。メトリクスとログの統一収集、アラートの一元配信、インテリジェント管理分析。エージェント不要、高性能クラスタ、強力なカスタム監視とステータスページ構築機能を提供します
### 特性
- **モニター・アラーム・通知** を一体化し、Web、データベース、キャッシュ、OS、ミドルウェア、クラウドネイティブ、ネットワークなどをサポート
- 使いやすさを重視し、マウスのクリックだけで完全なWebベースの操作が可能
- `Http、Jmx、Ssh、Snmp、jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8S``Docker`のような新しい監視タイプに素早く対応できることを想像してみてください
- `Prometheus`のエコシステム互換、Prometheusが監視できるものを、全てオンライン設定によって監視することができます
- 高性能で、コレクタークラスタ、マルチアイソレートネットワーク、クラウドエッジをサポート
- 柔軟なアラーム閾値ルールで、メール、discord、slack、telegramなどを通じてタイムリーに通知
- 強力なステータスページを簡単に構築し、リアルタイムのステータスをユーザーと共有
- **収集+分析+アラート+通知**を一つのプラットフォームに統合し、HertzBeat AI 駆動の新しいインタラクションと機能、内蔵 MCP Server 機能を提供
- 統一メトリクスプラットフォーム、エージェントレス、Prometheus互換、アプリケーションサービス、プログラム、データベース、キャッシュ、オペレーティングシステム、ビッグデータ、ミドルウェア、Webサーバー、クラウドネイティブ、ネットワーク、カスタム監視などをサポート
- 統一ログプラットフォーム、OTLP プロトコルを通じて複数のログソースをシームレスに統合してレポート
- 統一アラートプラットフォーム、内部アラートと様々な外部アラートソースを統合接続、統一アラート処理分析、柔軟なリアルタイムと周期的閾値ルール、グループ収束、サイレンス、抑制など
- 統一メッセージ配信、アラートプラットフォームで処理後、`メール` `Discord` `Slack` `Telegram` `DingTalk` `WeChat` `FeiShu` `SMS` `Webhook` `ServerChan` などの方法で配信通知
- `Http、Jmx、Ssh、Snmp、Jdbc、Prometheus`などのプロトコルを設定可能にし、テンプレート`YML`ファイルをオンラインで設定するだけで、あらゆるメトリクスを収集できるようにします。HertzBeatでオンライン設定するだけで、`K8s``Docker`のような新しい監視タイプに素早く対応できることを想像してみてください
- 高性能で、コレクタークラスタの水平拡張、マルチ分離ネットワーク監視、クラウドエッジ協調をサポート
- 強力なステータスページ構築機能を提供し、サービスのリアルタイムステータスをユーザーに簡単に伝達。
> `HertzBeat`の強力なカスタマイズ、多種類のサポート、高性能および容易な拡張性は、ユーザーが迅速に独自の監視システムを構築するのを支援することを目的としています。
> `HertzBeat`の統一プラットフォーム、AI インテリジェンス、強力なカスタマイズ、多種類サポート、高性能、容易な拡張性は、ユーザーが迅速かつ便利に観測要件を実現することを支援することを目的としています。
----
![hertzbeat](home/static/img/home/1.png)
![hertzbeat](home/static/img/home/status.png)
![hertzbeat](home/static/img/home/9.png)
----
## 🥐 モジュール
@@ -163,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,81 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.springframework.data.domain.Page;
import java.util.List;
/**
* Interface that provides access to alert definition information by retrieving data
* through the underlying alert define service.
*/
public interface AlertDefineServiceAdapter {
/**
* Add a new alert rule definition
* @param alertDefine Alert definition to add
* @return Created alert definition with ID
*/
AlertDefine addAlertDefine(AlertDefine alertDefine);
/**
* Get alert definitions with filtering and pagination
* @param search Search term
* @param app Monitor type filter
* @param enabled Enabled status filter
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of alert definitions
*/
Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize);
/**
* Get alert definition by ID
* @param id Alert definition ID
* @return Alert definition if found
*/
AlertDefine getAlertDefine(Long id);
/**
* Enable or disable alert definition
* @param id Alert definition ID
* @param enabled Whether to enable
*/
void toggleAlertDefineStatus(Long id, boolean enabled);
/**
* Modify/update an existing alert definition
* @param alertDefine Alert definition to update
* @return Updated alert definition
*/
AlertDefine modifyAlertDefine(AlertDefine alertDefine);
/**
* Get specific app hierarchy structure
* @param app App type
* @param lang Language for localization
* @return List of hierarchy objects for specific app
*/
List<Hierarchy> getAppHierarchy(String app, String lang);
}
@@ -1,60 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.data.domain.Page;
/**
* Interface that provides access to alert information by retrieving alert data
* through the underlying alert service.
*/
public interface AlertServiceAdapter {
/**
* Get single alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of single alerts
*/
Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get group alerts with filtering and pagination
* @param status Alert status
* @param search Search term
* @param sort Sort field
* @param order Sort order
* @param pageIndex Page index
* @param pageSize Page size
* @return Page of group alerts
*/
Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize);
/**
* Get alerts summary statistics
* @return Alert summary information
*/
AlertSummary getAlertsSummary();
}
@@ -1,55 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
/**
* Interface that provides access to metrics information by retrieving metrics data
* through the underlying metrics service.
*/
public interface MetricsServiceAdapter {
/**
* Check warehouse storage server status
* @return true if warehouse is available, false otherwise
*/
Boolean getWarehouseStorageServerStatus();
/**
* Query real-time metrics data
* @param monitorId Monitor ID
* @param metrics Metrics name
* @return Real-time metrics data
*/
MetricsData getMetricsData(Long monitorId, String metrics);
/**
* Query historical metrics data
* @param monitorId Monitor ID
* @param app Monitor type
* @param metrics Metrics name
* @param metric Metric field name
* @param label Label filter
* @param history Query historical time period
* @param interval Whether to aggregate data
* @return Historical metrics data
*/
MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
}
@@ -1,71 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import java.util.List;
import java.util.Map;
/**
* Interface that provides access to monitor information by retrieving monitor data
* through the underlying monitor service.
*/
public interface MonitorServiceAdapter {
Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels
);
/**
* Add a new monitor
*
* @param monitor The monitor entity to create
* @param params List of parameters for the monitor
* @param collector Optional collector assignment
* @return The created monitor ID
*/
Long addMonitor(Monitor monitor, List<Param> params, String collector);
/**
* Get all available monitor types with their display names
*
* @param language Language code (e.g., "en-US", "zh-CN")
* @return Map of monitor type key to display name
*/
Map<String, String> getAvailableMonitorTypes(String language);
/**
* Get parameter definitions for a specific monitor type
*
* @param app Monitor type/application name (e.g., "linux", "mysql", "redis")
* @return List of parameter definitions for the monitor type
*/
List<ParamDefine> getMonitorParamDefines(String app);
}
@@ -1,257 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
/**
* Implementation of the AlertDefineServiceAdapter interface that provides access to alert definition information
* through reflection by invoking the underlying alert define service implementation.
*/
@Slf4j
@Component
public class AlertDefineServiceAdapterImpl implements AlertDefineServiceAdapter {
@Override
public AlertDefine addAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("addAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully added alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke addAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke addAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<AlertDefine> getAlertDefines(String search, String app, Boolean enabled, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod(
"getAlertDefines",
List.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<AlertDefine> result = (Page<AlertDefine>) method.invoke(
alertDefineService, null, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} alert defines", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefines via adapter", e);
throw new RuntimeException("Failed to invoke getAlertDefines via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine getAlertDefine(Long id) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine result = (AlertDefine) method.invoke(alertDefineService, id);
log.debug("Successfully retrieved alert define with ID: {}", id);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertDefine via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke getAlertDefine via adapter: " + e.getMessage(), e);
}
}
@Override
public void toggleAlertDefineStatus(Long id, boolean enabled) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
// First get the existing AlertDefine
Method getMethod = alertDefineService.getClass().getMethod("getAlertDefine", long.class);
AlertDefine alertDefine = (AlertDefine) getMethod.invoke(alertDefineService, id);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + id + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
Method modifyMethod = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
modifyMethod.invoke(alertDefineService, alertDefine);
log.debug("Successfully toggled alert define status for ID: {} to enabled: {}", id, enabled);
} catch (Exception e) {
log.error("Failed to invoke toggleAlertDefineStatus via adapter for ID: {}", id, e);
throw new RuntimeException("Failed to invoke toggleAlertDefineStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertDefine modifyAlertDefine(AlertDefine alertDefine) {
try {
Object alertDefineService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
try {
alertDefineService = SpringContextHolder.getBean("alertDefineServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertDefineServiceImpl'");
}
assert alertDefineService != null;
log.debug("AlertDefineService bean found: {}", alertDefineService.getClass().getSimpleName());
Method method = alertDefineService.getClass().getMethod("modifyAlertDefine", AlertDefine.class);
method.invoke(alertDefineService, alertDefine);
log.debug("Successfully modified alert define with ID: {}", alertDefine.getId());
return alertDefine;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: modifyAlertDefine", e);
} catch (Exception e) {
log.error("Failed to invoke modifyAlertDefine via adapter", e);
throw new RuntimeException("Failed to invoke modifyAlertDefine via adapter: " + e.getMessage(), e);
}
}
/**
* Retrieves the application hierarchy for a given app and language.
* Uses reflection to call the underlying app service method.
*
* @param app The application name
* @param lang The language code (optional, defaults to "en-US")
* @return List of Hierarchy objects representing the app hierarchy
*/
@Override
public List<Hierarchy> getAppHierarchy(String app, String lang) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAppHierarchy: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (lang == null || lang.trim().isEmpty()) {
lang = "en-US";
}
// Call getAppHierarchy method: getAppHierarchy(String app, String lang)
Method method = appService.getClass().getMethod("getAppHierarchy", String.class, String.class);
List<?> managerHierarchies = (List<?>) method.invoke(appService, app, lang);
// Convert manager DTOs to ai-agent DTOs
List<Hierarchy> result = UtilityClass.convertToAgentHierarchies(managerHierarchies);
log.debug("Successfully retrieved and converted {} hierarchies for app '{}'", result.size(), app);
return result;
} catch (Exception e) {
log.error("Failed to get app hierarchy for app '{}': {}", app, e.getMessage(), e);
throw new RuntimeException("Failed to get app hierarchy for " + app, e);
}
}
}
@@ -1,141 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the AlertServiceAdapter interface that provides access to alert information
* through reflection by invoking the underlying alert service implementation.
*/
@Slf4j
@Component
public class AlertServiceAdapterImpl implements AlertServiceAdapter {
@Override
public Page<SingleAlert> getSingleAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getSingleAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getSingleAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<SingleAlert> result = (Page<SingleAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} single alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getSingleAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getSingleAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getSingleAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public Page<GroupAlert> getGroupAlerts(String status, String search, String sort, String order, int pageIndex, int pageSize) {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getGroupAlerts: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod(
"getGroupAlerts",
String.class, String.class, String.class, String.class, int.class, int.class);
@SuppressWarnings("unchecked")
Page<GroupAlert> result = (Page<GroupAlert>) method.invoke(
alertService, status, search, sort, order, pageIndex, pageSize);
log.debug("Successfully retrieved {} group alerts", result.getContent().size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getGroupAlerts", e);
} catch (Exception e) {
log.error("Failed to invoke getGroupAlerts via adapter", e);
throw new RuntimeException("Failed to invoke getGroupAlerts via adapter: " + e.getMessage(), e);
}
}
@Override
public AlertSummary getAlertsSummary() {
try {
Object alertService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertsSummary: {}", subjectSum);
try {
alertService = SpringContextHolder.getBean("alertServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'alertServiceImpl'");
}
assert alertService != null;
log.debug("AlertService bean found: {}", alertService.getClass().getSimpleName());
Method method = alertService.getClass().getMethod("getAlertsSummary");
AlertSummary result = (AlertSummary) method.invoke(alertService);
log.debug("Successfully retrieved alerts summary");
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAlertsSummary", e);
} catch (Exception e) {
log.error("Failed to invoke getAlertsSummary via adapter", e);
throw new RuntimeException("Failed to invoke getAlertsSummary via adapter: " + e.getMessage(), e);
}
}
}
@@ -1,136 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* Implementation of the MetricsServiceAdapter interface that provides access to metrics information
* through reflection by invoking the underlying metrics service implementation.
*/
@Slf4j
@Component
public class MetricsServiceAdapterImpl implements MetricsServiceAdapter {
@Override
public Boolean getWarehouseStorageServerStatus() {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod("getWarehouseStorageServerStatus");
Boolean result = (Boolean) method.invoke(metricsDataService);
log.debug("Successfully retrieved warehouse storage server status: {}", result);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getWarehouseStorageServerStatus", e);
} catch (Exception e) {
log.error("Failed to invoke getWarehouseStorageServerStatus via adapter", e);
throw new RuntimeException("Failed to invoke getWarehouseStorageServerStatus via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsData getMetricsData(Long monitorId, String metrics) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricsData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricsData",
Long.class, String.class);
MetricsData result = (MetricsData) method.invoke(metricsDataService, monitorId, metrics);
log.debug("Successfully retrieved metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricsData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricsData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricsData via adapter: " + e.getMessage(), e);
}
}
@Override
public MetricsHistoryData getMetricHistoryData(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval) {
try {
Object metricsDataService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMetricHistoryData: {}", subjectSum);
try {
metricsDataService = SpringContextHolder.getBean("metricsDataServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'metricsDataServiceImpl'");
}
assert metricsDataService != null;
log.debug("MetricsDataService bean found: {}", metricsDataService.getClass().getSimpleName());
Method method = metricsDataService.getClass().getMethod(
"getMetricHistoryData",
Long.class, String.class, String.class, String.class, String.class, String.class, Boolean.class);
MetricsHistoryData result = (MetricsHistoryData) method.invoke(
metricsDataService, monitorId, app, metrics, metric, label, history, interval);
log.debug("Successfully retrieved historical metrics data for monitor {} and metrics {}", monitorId, metrics);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMetricHistoryData", e);
} catch (Exception e) {
log.error("Failed to invoke getMetricHistoryData via adapter for monitor {} and metrics {}", monitorId, metrics, e);
throw new RuntimeException("Failed to invoke getMetricHistoryData via adapter: " + e.getMessage(), e);
}
}
}
@@ -1,214 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.adapters.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.springframework.data.domain.Page;
import org.apache.hertzbeat.common.entity.manager.Monitor;
import org.apache.hertzbeat.common.entity.manager.Param;
import org.apache.hertzbeat.common.entity.manager.ParamDefine;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
/**
* Implementation of the MonitorServiceAdapter interface that provides access to monitor information
* through reflection by invoking the underlying monitor service implementation.
*/
@Slf4j
@Component
public class MonitorServiceAdapterImpl implements MonitorServiceAdapter {
@Override
public Page<Monitor> getMonitors(
List<Long> ids,
String app,
String search,
Byte status,
String sort,
String order,
Integer pageIndex,
Integer pageSize,
String labels) {
try {
// Provide default values for all nullable parameters
if (sort == null || sort.trim().isEmpty()) {
sort = "gmtCreate";
}
if (order == null || order.trim().isEmpty()) {
order = "desc";
}
if (pageIndex == null) {
pageIndex = 0;
}
if (pageSize == null) {
pageSize = 8;
}
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found: {}", monitorService.getClass().getSimpleName());
Method method = monitorService.getClass().getMethod(
"getMonitors",
List.class, String.class, String.class, Byte.class,
String.class, String.class, int.class, int.class, String.class);
@SuppressWarnings("unchecked")
Page<Monitor> result = (Page<Monitor>) method.invoke(
monitorService,
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result.getContent());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getMonitors", e);
} catch (Exception e) {
log.debug("Failed to invoke getMonitors via adapter", e);
throw new RuntimeException("Failed to invoke getMonitors via adapter", e);
}
}
@Override
public Long addMonitor(Monitor monitor, List<Param> params, String collector) {
try {
Object monitorService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
try {
monitorService = SpringContextHolder.getBean("monitorServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'monitorServiceImpl', trying by class name");
}
assert monitorService != null;
log.debug("MonitorService bean found for addMonitor: {}", monitorService.getClass().getSimpleName());
// Call addMonitor method: addMonitor(Monitor monitor, List<Param> params, String collector, GrafanaDashboard dashboard)
Method method = monitorService.getClass().getMethod(
"addMonitor",
Monitor.class, List.class, String.class,
Class.forName("org.apache.hertzbeat.common.entity.grafana.GrafanaDashboard"));
// Call the method with null dashboard
method.invoke(monitorService, monitor, params, collector, null);
log.debug("Successfully added monitor: {} with ID: {}", monitor.getName(), monitor.getId());
return monitor.getId();
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: addMonitor", e);
} catch (Exception e) {
log.error("Failed to invoke addMonitor via adapter", e);
throw new RuntimeException("Failed to invoke addMonitor via adapter: " + e.getMessage(), e);
}
}
@Override
public Map<String, String> getAvailableMonitorTypes(String language) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAvailableMonitorTypes: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getAvailableMonitorTypes: {}", appService.getClass().getSimpleName());
// Provide default language if not specified
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Call getI18nApps method: getI18nApps(String lang)
Method method = appService.getClass().getMethod("getI18nApps", String.class);
@SuppressWarnings("unchecked")
Map<String, String> result = (Map<String, String>) method.invoke(appService, language);
log.debug("Successfully retrieved {} monitor types", result.size());
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getI18nApps", e);
} catch (Exception e) {
log.error("Failed to invoke getI18nApps via adapter", e);
throw new RuntimeException("Failed to invoke getI18nApps via adapter: " + e.getMessage(), e);
}
}
@Override
public List<ParamDefine> getMonitorParamDefines(String app) {
try {
Object appService = null;
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
try {
appService = SpringContextHolder.getBean("appServiceImpl");
} catch (Exception e) {
log.debug("Could not find bean by name 'appServiceImpl', trying by class name");
}
assert appService != null;
log.debug("AppService bean found for getMonitorParamDefines: {}", appService.getClass().getSimpleName());
// Validate app parameter
if (app == null || app.trim().isEmpty()) {
throw new IllegalArgumentException("Monitor type/app parameter is required");
}
// Call getAppParamDefines method: getAppParamDefines(String app)
Method method = appService.getClass().getMethod("getAppParamDefines", String.class);
@SuppressWarnings("unchecked")
List<ParamDefine> result = (List<ParamDefine>) method.invoke(appService, app.toLowerCase().trim());
log.debug("Successfully retrieved {} parameter definitions for monitor type: {}", result.size(), app);
return result;
} catch (NoSuchMethodException e) {
throw new RuntimeException("Method not found: getAppParamDefines", e);
} catch (Exception e) {
log.error("Failed to invoke getAppParamDefines via adapter for app: {}", app, e);
throw new RuntimeException("Failed to invoke getAppParamDefines via adapter: " + e.getMessage(), e);
}
}
}
@@ -1,56 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.ai.model.ApiKey;
import org.springframework.stereotype.Component;
/**
* Dynamic LLM Provider API Key implementation that retrieves the API key
*/
@Slf4j
@Component
public class DynamicOpenAiApiKey implements ApiKey {
private final GeneralConfigDao generalConfigDao;
public DynamicOpenAiApiKey(GeneralConfigDao generalConfigDao) {
this.generalConfigDao = generalConfigDao;
}
@NotNull
@Override
public String getValue() {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
if (modelProviderConfig != null && modelProviderConfig.isEnable() && modelProviderConfig.isStatus()) {
log.debug("Retrieved {} API key from configuration service", modelProviderConfig.getCode());
return modelProviderConfig.getApiKey();
} else {
log.warn("No valid LLM Provider API key found in configuration");
return "";
}
}
}
@@ -1,53 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
/**
* Conversation DTO for AI chat conversations.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Conversation information")
public class ConversationDto {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Creation time")
private LocalDateTime createdAt;
@Schema(description = "Last updated time")
private LocalDateTime updatedAt;
@Schema(description = "Messages in this conversation")
private List<MessageDto> messages;
@Schema(description = "Message count")
private Integer messageCount;
}
@@ -1,87 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
/**
* Hierarchical structure
* eg: Monitoring Type metrics Information Hierarchy Relationship
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Schema(description = "Monitor Hierarchy")
public class Hierarchy {
/**
* Category value
*/
@Schema(description = "Category Value", example = "os", accessMode = READ_WRITE)
String category;
/**
* Attribute value
*/
@Schema(description = "Attribute value", example = "linux", accessMode = READ_WRITE)
String value;
/**
* Attribute internationalization tag
*/
@Schema(description = "Attribute internationalization tag", example = "Linux system", accessMode = READ_WRITE)
String label;
/**
* Is it a leaf node
*/
@Schema(description = "Is it a leaf node", example = "true", accessMode = READ_WRITE)
Boolean isLeaf = false;
/**
* Is hide this app type in main menus layout
*/
@Schema(description = "Is hide this app in main menus layout, only for app type, default true.", example = "true")
Boolean hide = true;
/**
* For leaf metric
* metric type 0-number: number 1-string: string
*/
@Schema(description = "metric type 0-number: number 1-string: string")
private Byte type;
/**
* metric unit
*/
@Schema(description = "metric unit")
private String unit;
/**
* Next level of association
*/
@Schema(description = "Next Hierarchy", accessMode = READ_WRITE)
private List<Hierarchy> children;
}
@@ -1,52 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Message DTO for chat messages.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat message")
public class MessageDto {
@Schema(description = "Message ID", example = "msg-123")
private String messageId;
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Message content", example = "List all monitors")
private String content;
@Schema(description = "Message role", example = "user", allowableValues = {"user", "assistant"})
private String role;
@Schema(description = "Message timestamp")
private LocalDateTime timestamp;
}
@@ -1,60 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service;
/**
* Ai Configuration Service
*/
public interface AiConfigService {
/**
* Validate OpenAI API key by calling the OpenAI API
* @param apiKey the API key to validate
* @return validation result with success status and message
*/
ValidationResult validateApiKey(String apiKey);
/**
* Validation result class
*/
class ValidationResult {
private final boolean valid;
private final String message;
private ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public static ValidationResult success(String message) {
return new ValidationResult(true, message);
}
public static ValidationResult failure(String message) {
return new ValidationResult(false, message);
}
public boolean isValid() {
return valid;
}
public String getMessage() {
return message;
}
}
}
@@ -1,100 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.service.AiConfigService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* Ai Configuration Service Implementation
*/
@Slf4j
@Service
public class AiConfigServiceImpl implements AiConfigService {
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
private final RestTemplate restTemplate;
private final GeneralConfigDao generalConfigDao;
public AiConfigServiceImpl(GeneralConfigDao generalConfigDao, RestTemplate restTemplate) {
this.restTemplate = restTemplate;
this.generalConfigDao = generalConfigDao;
}
@Override
public ValidationResult validateApiKey(String apiKey) {
if (!StringUtils.hasText(apiKey)) {
return ValidationResult.failure("API key cannot be empty");
}
if (!apiKey.startsWith("sk-")) {
return ValidationResult.failure("Invalid API key format. OpenAI API keys should start with 'sk-'");
}
try {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
headers.set("Content-Type", "application/json");
HttpEntity<String> entity = new HttpEntity<>(headers);
log.debug("Validating OpenAI API key by calling models endpoint");
ResponseEntity<String> response = restTemplate.exchange(
OPENAI_MODELS_ENDPOINT,
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode() == HttpStatus.OK) {
log.info("OpenAI API key validation successful");
return ValidationResult.success("API key is valid");
} else {
log.warn("OpenAI API key validation failed with status: {}", response.getStatusCode());
return ValidationResult.failure("API key validation failed: " + response.getStatusCode());
}
} catch (Exception e) {
log.error("Error validating OpenAI API key", e);
String errorMessage = e.getMessage();
// Parse common error messages
if (errorMessage.contains("401")) {
return ValidationResult.failure("Invalid API key - authentication failed");
} else if (errorMessage.contains("403")) {
return ValidationResult.failure("API key does not have permission to access models");
} else if (errorMessage.contains("429")) {
return ValidationResult.failure("Rate limit exceeded - please try again later");
} else if (errorMessage.contains("timeout") || errorMessage.contains("connect")) {
return ValidationResult.failure("Network error - unable to connect to OpenAI API");
} else {
return ValidationResult.failure("API key validation failed: " + errorMessage);
}
}
}
}
@@ -1,272 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
private final Map<String, Map<String, Object>> conversations = new ConcurrentHashMap<>();
private final Map<String, List<Map<String, Object>>> conversationMessages = new ConcurrentHashMap<>();
@Autowired
private ChatClientProviderService chatClientProviderService;
@Override
public ConversationDto createConversation() {
String conversationId = createNewConversation();
return getConversation(conversationId);
}
@Override
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId) {
// Validate conversation exists
if (!conversationExists(conversationId)) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("Error: Conversation not found: " + conversationId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
// Check if OpenAI is properly configured
if (!chatClientProviderService.isConfigured()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("Provider is not configured. Please configure your AI Provider.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
String userMessageId = addMessageToConversation(conversationId, message, "user");
// Get conversation history for context
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> conversationHistory = new ArrayList<>();
if (messagesList != null && messagesList.size() > 1) {
// Get all messages except the last one (which is the current user message we just added)
for (int i = 0; i < messagesList.size() - 1; i++) {
Map<String, Object> msgMap = messagesList.get(i);
conversationHistory.add(mapToMessageDto(msgMap));
}
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(conversationHistory)
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseDto responseDto = ChatResponseDto.builder()
.conversationId(conversationId)
.response(chunk)
.userMessageId(userMessageId)
.build();
return ServerSentEvent.builder(responseDto)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
String assistantMessageId = addMessageToConversation(conversationId, fullResponse.toString(), "assistant");
ChatResponseDto finalResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("")
.userMessageId(userMessageId)
.assistantMessageId(assistantMessageId)
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
.event("complete")
.build());
}))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseDto errorResponse = ChatResponseDto.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(userMessageId)
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
});
}
@Override
public ConversationDto getConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return null;
}
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation == null) {
return null;
}
List<Map<String, Object>> messagesList = conversationMessages.get(conversationId);
List<MessageDto> messages = messagesList != null
? messagesList.stream().map(this::mapToMessageDto).collect(Collectors.toList()) :
new ArrayList<>();
return ConversationDto.builder()
.conversationId((String) conversation.get("conversationId"))
.createdAt((LocalDateTime) conversation.get("createdAt"))
.updatedAt((LocalDateTime) conversation.get("updatedAt"))
.messages(messages)
.build();
}
@Override
public List<ConversationDto> getAllConversations() {
List<ConversationDto> result = new ArrayList<>();
for (Map.Entry<String, Map<String, Object>> entry : conversations.entrySet()) {
Map<String, Object> conv = entry.getValue();
List<Map<String, Object>> messages = conversationMessages.get(entry.getKey());
ConversationDto dto = ConversationDto.builder()
.conversationId((String) conv.get("conversationId"))
.createdAt((LocalDateTime) conv.get("createdAt"))
.updatedAt((LocalDateTime) conv.get("updatedAt"))
.messages(new ArrayList<>()) // Don't include messages in list view for performance
.build();
result.add(dto);
}
result.sort((a, b) -> b.getUpdatedAt().compareTo(a.getUpdatedAt()));
return result;
}
@Override
public boolean deleteConversation(String conversationId) {
if (conversationId == null || conversationId.isEmpty()) {
return false;
}
boolean existed = conversations.containsKey(conversationId);
if (existed) {
conversations.remove(conversationId);
conversationMessages.remove(conversationId);
log.info("Deleted conversation: {}", conversationId);
}
return existed;
}
@Override
public boolean conversationExists(String conversationId) {
return conversationId != null && !conversationId.isEmpty() && conversations.containsKey(conversationId);
}
private String createNewConversation() {
String conversationId = "conv-" + UUID.randomUUID().toString().substring(0, 8);
LocalDateTime now = LocalDateTime.now();
Map<String, Object> conversation = new HashMap<>();
conversation.put("conversationId", conversationId);
conversation.put("createdAt", now);
conversation.put("updatedAt", now);
conversations.put(conversationId, conversation);
conversationMessages.put(conversationId, new ArrayList<>());
log.info("Created new conversation: {}", conversationId);
return conversationId;
}
private MessageDto mapToMessageDto(Map<String, Object> messageMap) {
return MessageDto.builder()
.messageId((String) messageMap.get("messageId"))
.conversationId((String) messageMap.get("conversationId"))
.content((String) messageMap.get("content"))
.role((String) messageMap.get("role"))
.timestamp((LocalDateTime) messageMap.get("timestamp"))
.build();
}
private String addMessageToConversation(String conversationId, String content, String role) {
List<Map<String, Object>> messages = conversationMessages.computeIfAbsent(conversationId, k -> new ArrayList<>());
String messageId = "msg-" + UUID.randomUUID().toString().substring(0, 8);
Map<String, Object> message = new HashMap<>();
message.put("messageId", messageId);
message.put("conversationId", conversationId);
message.put("content", content);
message.put("role", role);
message.put("timestamp", LocalDateTime.now());
messages.add(message);
// Update conversation timestamp
Map<String, Object> conversation = conversations.get(conversationId);
if (conversation != null) {
conversation.put("updatedAt", LocalDateTime.now());
// Auto-generate title from first user message
if ("user".equals(role) && messages.stream().filter(m -> "user".equals(m.get("role"))).count() == 1) {
String title = content.length() > 30 ? content.substring(0, 27) + "..." : content;
conversation.put("title", title);
}
}
return messageId;
}
}
@@ -23,11 +23,11 @@
<artifactId>hertzbeat</artifactId>
<version>2.0-SNAPSHOT</version>
</parent>
<artifactId>hertzbeat-ai-agent</artifactId>
<artifactId>hertzbeat-ai</artifactId>
<version>${hertzbeat.version}</version>
<properties>
<spring-ai.version>1.0.3</spring-ai.version>
<java.version>17</java.version>
<java.version>21</java.version>
</properties>
<dependencies>
@@ -65,12 +65,15 @@
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-base</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-alerter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hertzbeat</groupId>
<artifactId>hertzbeat-manager</artifactId>
</dependency>
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>spring-boot3-starter-sureness</artifactId>
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
package org.apache.hertzbeat.ai.config;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -16,11 +16,11 @@
*/
package org.apache.hertzbeat.ai.agent.config;
package org.apache.hertzbeat.ai.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.common.support.event.AiProviderConfigChangeEvent;
import org.apache.hertzbeat.ai.agent.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
@@ -72,12 +72,7 @@ public class LlmConfig {
}
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
if (!modelProviderConfig.isEnable() || !modelProviderConfig.isStatus()) {
log.warn("LLM Provider is not enabled or status is not valid, ChatClient bean will not be created");
return null;
}
if (modelProviderConfig.getApiKey() == null) {
if (modelProviderConfig == null || modelProviderConfig.getApiKey() == null) {
log.warn("LLM Provider configuration is incomplete, ChatClient bean will not be created");
return null;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.config;
package org.apache.hertzbeat.ai.config;
import com.usthe.sureness.subject.SubjectSum;
import org.springframework.core.NamedInheritableThreadLocal;
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.config;
package org.apache.hertzbeat.ai.config;
import org.springframework.stereotype.Component;
@@ -179,4 +179,4 @@ public class PromptProvider {
Keep responses focused on monitoring topics and HertzBeat's comprehensive capabilities.
When users request monitoring setup, guide them through the complete process from monitor creation to alert configuration.
""";
}
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.controller;
package org.apache.hertzbeat.ai.controller;
import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.util.SurenessContextHolder;
@@ -23,11 +23,11 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.agent.service.ConversationService;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.dto.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -63,23 +63,6 @@ public class ChatController {
this.conversationService = conversationService;
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ConversationDto>> createConversation() {
try {
ConversationDto conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error creating conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to create conversation"));
}
}
/**
* Send a message and get a streaming response with conversation tracking
*
@@ -88,14 +71,13 @@ public class ChatController {
*/
@PostMapping(value = "/stream", produces = TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Send a chat message with streaming response", description = "Send a message to AI and get a streaming response with conversation tracking")
public Flux<ServerSentEvent<ChatResponseDto>> streamChat(@Valid @RequestBody ChatRequestContext context) {
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(@Valid @RequestBody ChatRequestContext context) {
try {
// Validate message is not empty
SubjectSum subject = SurenessContextHolder.getBindSubject();
log.info(subject.toString());
McpContextHolder.setSubject(subject);
if (context.getMessage() == null || context.getMessage().trim().isEmpty()) {
ChatResponseDto errorResponse = ChatResponseDto.builder()
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(context.getConversationId())
.response("Error: Message cannot be empty")
.build();
@@ -109,7 +91,7 @@ public class ChatController {
} catch (Exception e) {
log.error("Error in stream chat endpoint: ", e);
ChatResponseDto errorResponse = ChatResponseDto.builder()
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(context.getConversationId())
.response("An error occurred: " + e.getMessage())
.build();
@@ -119,6 +101,18 @@ public class ChatController {
}
}
/**
* Create a new conversation
*
* @return Created conversation details
*/
@PostMapping(path = "/conversations")
@Operation(summary = "Create a new conversation", description = "Create a new conversation")
public ResponseEntity<Message<ChatConversation>> createConversation() {
ChatConversation conversation = conversationService.createConversation();
return ResponseEntity.ok(Message.success(conversation));
}
/**
* Get all conversations
*
@@ -126,14 +120,9 @@ public class ChatController {
*/
@GetMapping(path = "/conversations")
@Operation(summary = "List all conversations", description = "Get a list of all conversations")
public ResponseEntity<Message<List<ConversationDto>>> listConversations() {
try {
List<ConversationDto> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
} catch (Exception e) {
log.error("Error listing conversations: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversations"));
}
public ResponseEntity<Message<List<ChatConversation>>> listConversations() {
List<ChatConversation> conversations = conversationService.getAllConversations();
return ResponseEntity.ok(Message.success(conversations));
}
/**
@@ -144,26 +133,10 @@ public class ChatController {
*/
@GetMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Get conversation history", description = "Get detailed information and message history for a specific conversation")
public ResponseEntity<Message<ConversationDto>> getConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
ConversationDto conversation = conversationService.getConversation(conversationId);
if (conversation == null) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success(conversation));
} catch (Exception e) {
log.error("Error getting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to retrieve conversation"));
}
public ResponseEntity<Message<ChatConversation>> getConversation(
@Parameter(description = "Conversation ID", example = "12345678") @PathVariable(value = "conversationId") Long conversationId) {
ChatConversation conversation = conversationService.getConversation(conversationId);
return ResponseEntity.ok(Message.success(conversation));
}
/**
@@ -175,23 +148,8 @@ public class ChatController {
@DeleteMapping(path = "/conversations/{conversationId}")
@Operation(summary = "Delete conversation", description = "Delete a specific conversation and all its messages")
public ResponseEntity<Message<Void>> deleteConversation(
@Parameter(description = "Conversation ID", example = "conv-12345678") @PathVariable("conversationId") String conversationId) {
try {
// Validate conversation ID
if (conversationId == null || conversationId.trim().isEmpty()) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation ID is required"));
}
boolean deleted = conversationService.deleteConversation(conversationId);
if (!deleted) {
return ResponseEntity.ok(Message.fail((byte) -1, "Conversation not found: " + conversationId));
}
return ResponseEntity.ok(Message.success("Conversation deleted successfully"));
} catch (Exception e) {
log.error("Error deleting conversation: ", e);
return ResponseEntity.ok(Message.fail((byte) -1, "Failed to delete conversation"));
}
@Parameter(description = "Conversation ID", example = "2345678") @PathVariable("conversationId") Long conversationId) {
conversationService.deleteConversation(conversationId);
return ResponseEntity.ok(Message.success());
}
}
}
@@ -15,13 +15,15 @@
* limitations under the License.
*/
package org.apache.hertzbeat.manager.service;
package org.apache.hertzbeat.ai.dao;
import org.apache.hertzbeat.manager.service.ai.SparkDeskAiServiceImpl;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* test case for {@link SparkDeskAiServiceImpl}
* Repository interface for Conversation entities
*/
class SparkDeskAiServiceTest {
@Repository
public interface ChatConversationDao extends JpaRepository<ChatConversation, Long> {
}
@@ -15,13 +15,16 @@
* limitations under the License.
*/
package org.apache.hertzbeat.manager.service;
package org.apache.hertzbeat.ai.dao;
import org.apache.hertzbeat.manager.service.ai.AlibabaAiServiceImpl;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* test case for {@link AlibabaAiServiceImpl}
* Repository interface for Message entities
*/
class AlibabaAiServiceTest {
@Repository
public interface ChatMessageDao extends JpaRepository<ChatMessage, Long> {
}
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
package org.apache.hertzbeat.ai.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -24,6 +24,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
/**
* Chat request context for AI chat endpoint.
@@ -40,10 +41,10 @@ public class ChatRequestContext {
/**
* Optional conversation ID for context
*/
private String conversationId;
private Long conversationId;
/**
* Conversation history messages for context
*/
private List<MessageDto> conversationHistory;
}
private List<ChatMessage> conversationHistory;
}
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.pojo.dto;
package org.apache.hertzbeat.ai.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
@@ -31,17 +31,18 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "Chat response")
public class ChatResponseDto {
public class ChatResponseChunk {
@Schema(description = "Conversation ID", example = "conv-123")
private String conversationId;
@Schema(description = "Conversation ID", example = "123")
private Long conversationId;
@Schema(description = "AI response message", example = "Here are your monitors...")
private String response;
@Schema(description = "User message ID", example = "msg-user-123")
private String userMessageId;
@Schema(description = "User message ID", example = "123")
private Long userMessageId;
@Schema(description = "Assistant message ID", example = "123")
private Long assistantMessageId;
@Schema(description = "Assistant message ID", example = "msg-assistant-123")
private String assistantMessageId;
}
}
@@ -16,9 +16,9 @@
*/
package org.apache.hertzbeat.ai.agent.service;
package org.apache.hertzbeat.ai.service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import reactor.core.publisher.Flux;
/**
@@ -16,10 +16,10 @@
*/
package org.apache.hertzbeat.ai.agent.service;
package org.apache.hertzbeat.ai.service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
@@ -30,13 +30,6 @@ import java.util.List;
*/
public interface ConversationService {
/**
* Create a new conversation
*
* @return Created conversation data
*/
ConversationDto createConversation();
/**
* Send a message and receive a streaming response
*
@@ -44,8 +37,14 @@ public interface ConversationService {
* @param conversationId Optional conversation ID for continuing a chat
* @return Flux of ServerSentEvent for streaming the response
*/
Flux<ServerSentEvent<ChatResponseDto>> streamChat(String message, String conversationId);
Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId);
/**
* Create a new conversation
*
* @return Created conversation data
*/
ChatConversation createConversation();
/**
* Get conversation history for a specific conversation
@@ -53,28 +52,19 @@ public interface ConversationService {
* @param conversationId Conversation ID
* @return Conversation data including messages
*/
ConversationDto getConversation(String conversationId);
ChatConversation getConversation(Long conversationId);
/**
* Get all conversations for the current user
*
* @return List of conversations
*/
List<ConversationDto> getAllConversations();
List<ChatConversation> getAllConversations();
/**
* Delete a conversation
*
* @param conversationId Conversation ID to delete
* @return true if deleted, false if conversation not found
*/
boolean deleteConversation(String conversationId);
/**
* Check if a conversation exists
*
* @param conversationId Conversation ID to check
* @return true if conversation exists, false otherwise
*/
boolean conversationExists(String conversationId);
}
void deleteConversation(Long conversationId);
}
@@ -16,10 +16,10 @@
*/
package org.apache.hertzbeat.ai.agent.service;
package org.apache.hertzbeat.ai.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.servlet.function.RouterFunction;
@@ -16,18 +16,18 @@
*/
package org.apache.hertzbeat.ai.agent.service.impl;
package org.apache.hertzbeat.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.config.PromptProvider;
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
import org.apache.hertzbeat.ai.agent.pojo.dto.ModelProviderConfig;
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.config.PromptProvider;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.apache.hertzbeat.common.entity.dto.ModelProviderConfig;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
@@ -66,14 +66,6 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
this.generalConfigDao = generalConfigDao;
}
public String complete(String message) {
ChatClient chatClient = applicationContext.getBean("openAiChatClient", ChatClient.class);
return chatClient.prompt()
.user(message)
.call()
.content();
}
@Override
public Flux<String> streamChat(ChatRequestContext context) {
try {
@@ -84,7 +76,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
// Add conversation history if available
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
for (MessageDto historyMessage : context.getConversationHistory()) {
for (ChatMessage historyMessage : context.getConversationHistory()) {
if ("user".equals(historyMessage.getRole())) {
messages.add(new UserMessage(historyMessage.getContent()));
} else if ("assistant".equals(historyMessage.getRole())) {
@@ -117,7 +109,7 @@ public class ChatClientProviderServiceImpl implements ChatClientProviderService
if (!isConfigured) {
GeneralConfig providerConfig = generalConfigDao.findByType("provider");
ModelProviderConfig modelProviderConfig = JsonUtil.fromJson(providerConfig.getContent(), ModelProviderConfig.class);
isConfigured = modelProviderConfig != null && modelProviderConfig.isStatus();
isConfigured = modelProviderConfig != null && modelProviderConfig.getApiKey() != null;
}
return isConfigured;
}
@@ -0,0 +1,160 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.ai.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.dao.ChatConversationDao;
import org.apache.hertzbeat.ai.dao.ChatMessageDao;
import org.apache.hertzbeat.ai.pojo.dto.ChatRequestContext;
import org.apache.hertzbeat.ai.pojo.dto.ChatResponseChunk;
import org.apache.hertzbeat.ai.service.ChatClientProviderService;
import org.apache.hertzbeat.ai.service.ConversationService;
import org.apache.hertzbeat.common.entity.ai.ChatConversation;
import org.apache.hertzbeat.common.entity.ai.ChatMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.UUID;
/**
* Implementation of the ConversationService interface for managing chat conversations.
*/
@Slf4j
@Service
public class ConversationServiceImpl implements ConversationService {
@Autowired
private ChatConversationDao conversationDao;
@Autowired
private ChatMessageDao messageDao;
@Autowired
private ChatClientProviderService chatClientProviderService;
@Override
public Flux<ServerSentEvent<ChatResponseChunk>> streamChat(String message, Long conversationId) {
// Check if provider is properly configured
if (!chatClientProviderService.isConfigured()) {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("Provider is not configured. Please configure your AI Provider.")
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
}
log.info("Starting streaming conversation: {}", conversationId);
// Add user message to conversation
ChatMessage chatMessage = ChatMessage.builder().conversationId(conversationId)
.content(message).role("user").build();
chatMessage = messageDao.save(chatMessage);
ChatConversation conversation = conversationDao.getReferenceById(conversationId);
if (conversation.getTitle().startsWith("conversation")) {
// Auto-generate title from first user message
String title = message.length() > 30 ? message.substring(0, 27) + "..." : message;
conversation.setTitle(title);
conversationDao.save(conversation);
}
ChatRequestContext context = ChatRequestContext.builder()
.message(message)
.conversationId(conversationId)
.conversationHistory(CollectionUtils.isEmpty(conversation.getMessages()) ? null
: conversation.getMessages().subList(0, conversation.getMessages().size() - 1))
.build();
// Stream response from AI service
StringBuilder fullResponse = new StringBuilder();
ChatMessage finalChatMessage = chatMessage;
return chatClientProviderService.streamChat(context)
.map(chunk -> {
fullResponse.append(chunk);
ChatResponseChunk responseChunk = ChatResponseChunk.builder()
.conversationId(conversationId)
.userMessageId(finalChatMessage.getId())
.response(chunk)
.build();
return ServerSentEvent.builder(responseChunk)
.event("message")
.build();
})
.concatWith(Flux.defer(() -> {
// Add the complete AI response to conversation
ChatMessage assistantMessage = ChatMessage.builder().conversationId(conversationId)
.content(fullResponse.toString()).role("assistant").build();
assistantMessage = messageDao.save(assistantMessage);
ChatResponseChunk finalResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("")
.assistantMessageId(assistantMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(finalResponse)
.event("complete")
.build());
}))
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", conversationId))
.doOnError(error -> log.error("Error in streaming chat for conversation {}: {}", conversationId, error.getMessage(), error))
.onErrorResume(error -> {
ChatResponseChunk errorResponse = ChatResponseChunk.builder()
.conversationId(conversationId)
.response("An error occurred: " + error.getMessage())
.userMessageId(finalChatMessage.getId())
.build();
return Flux.just(ServerSentEvent.builder(errorResponse)
.event("error")
.build());
});
}
@Override
public ChatConversation createConversation() {
ChatConversation conversation = new ChatConversation();
conversation.setTitle("conversation-" + UUID.randomUUID().toString().substring(0, 4));
return conversationDao.save(conversation);
}
@Override
public ChatConversation getConversation(Long conversationId) {
if (conversationId == null) {
return null;
}
return conversationDao.getReferenceById(conversationId);
}
@Override
public List<ChatConversation> getAllConversations() {
return conversationDao.findAll(Sort.by(Sort.Direction.DESC, "id"));
}
@Override
public void deleteConversation(Long conversationId) {
conversationDao.deleteById(conversationId);
}
}
@@ -16,14 +16,14 @@
*/
package org.apache.hertzbeat.ai.agent.service.impl;
package org.apache.hertzbeat.ai.service.impl;
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.agent.service.McpServerService;
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.apache.hertzbeat.ai.config.CustomSseServerTransport;
import org.apache.hertzbeat.ai.service.McpServerService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools;
package org.apache.hertzbeat.ai.tools;
/**
* Tools for alert definition and threshold configuration operations
@@ -88,4 +88,4 @@ public interface AlertDefineTools {
* @return Result message indicating success or failure
*/
String bindMonitorsToAlertRule(Long ruleId, String monitorIds);
}
}
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.tools;
package org.apache.hertzbeat.ai.tools;
/**
* Tools for alert operations and alarm data queries
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.tools;
package org.apache.hertzbeat.ai.tools;
/**
* Tools for metrics data operations and queries
@@ -16,7 +16,7 @@
*/
package org.apache.hertzbeat.ai.agent.tools;
package org.apache.hertzbeat.ai.tools;
import java.util.List;
@@ -94,4 +94,4 @@ public interface MonitorTools {
* @return Formatted string with parameter definitions including field names, types, and requirements
*/
String getMonitorAdditionalParams(String app);
}
}
@@ -15,19 +15,20 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
package org.apache.hertzbeat.ai.tools.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertDefineServiceAdapter;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.alert.service.AlertDefineService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.tools.AlertDefineTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
@@ -46,7 +47,9 @@ import java.util.Map;
@Service
public class AlertDefineToolsImpl implements AlertDefineTools {
@Autowired
private AlertDefineServiceAdapter alertDefineServiceAdapter;
private AlertDefineService alertDefineService;
@Autowired
private AppService appService;
@Override
@@ -240,17 +243,19 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
.datasource(datasource)
.enable(enable)
.build();
log.debug("Current security subject for addAlertDefine: {}", subjectSum);
AlertDefine createdAlertDefine = alertDefineServiceAdapter.addAlertDefine(alertDefine);
alertDefineService.addAlertDefine(alertDefine);
// Note: Monitor binding is handled separately via bind_monitors_to_alert_rule tool
String bindingNote = String.format(" (Use bind_monitors_to_alert_rule tool to associate specific monitors)");
log.info("Successfully created alert rule '{}' with ID: {}", name, createdAlertDefine.getId());
log.info("Successfully created alert rule '{}' with ID: {}", name, alertDefine.getId());
StringBuilder response = new StringBuilder();
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
type, name, createdAlertDefine.getId()));
type, name, alertDefine.getId()));
response.append(String.format("Expression: %s\n", expr));
response.append(String.format("Priority: %d (%s)\n", priority, severityLabel));
response.append(String.format("Trigger after: %d consecutive violations\n", times));
@@ -296,8 +301,11 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
pageSize = 10;
}
Page<AlertDefine> result = alertDefineServiceAdapter.getAlertDefines(
search, monitorType, enabled, "gmtCreate", "desc", pageIndex, pageSize);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefines: {}", subjectSum);
Page<AlertDefine> result = alertDefineService.getAlertDefines(null,
search, "gmtCreate", "desc", pageIndex, pageSize);
StringBuilder response = new StringBuilder();
response.append("Found ").append(result.getContent().size())
@@ -345,7 +353,21 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
try {
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
alertDefineServiceAdapter.toggleAlertDefineStatus(ruleId, enabled);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for toggleAlertDefineStatus: {}", subjectSum);
// First get the existing AlertDefine
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
throw new RuntimeException("AlertDefine with ID " + ruleId + " not found");
}
// Update the enable status
alertDefine.setEnable(enabled);
// Use modifyAlertDefine to save the changes
alertDefineService.modifyAlertDefine(alertDefine);
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
return String.format("Successfully %s alert rule ID: %d",
@@ -369,7 +391,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
try {
log.info("Getting alert rule details for ID: {}", ruleId);
AlertDefine alertDefine = alertDefineServiceAdapter.getAlertDefine(ruleId);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine alertDefine = alertDefineService.getAlertDefine(ruleId);
if (alertDefine == null) {
return "Alert rule with ID " + ruleId + " not found";
}
@@ -441,7 +466,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
List<Hierarchy> hierarchies;
hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.trim().toLowerCase(), "en-US");
hierarchies = appService.getAppHierarchy(app.trim().toLowerCase(), "en-US");
ObjectMapper mapper = new ObjectMapper();
@@ -459,10 +484,7 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
result.put("message", "No hierarchy data available");
}
String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
log.info("Hierarchy JSON: {}", jsonResult);
return jsonResult;
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
} catch (Exception e) {
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
@@ -494,7 +516,9 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
// Get the existing alert rule
AlertDefine existingRule = alertDefineServiceAdapter.getAlertDefine(ruleId);
log.debug("Current security subject for getAlertDefine: {}", subjectSum);
AlertDefine existingRule = alertDefineService.getAlertDefine(ruleId);
if (existingRule == null) {
return String.format("Error: Alert rule with ID %d not found", ruleId);
}
@@ -571,7 +595,9 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
log.debug("Current security subject for modifyAlertDefine: {}", subjectSum);
alertDefineService.modifyAlertDefine(existingRule);
log.info("Successfully added monitors {} to existing bindings for alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully added %d new monitor(s) to alert rule ID %d.\nTotal bound monitors: %s\nUpdated expression: %s",
@@ -608,7 +634,8 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
// Update the alert rule
existingRule.setExpr(newExpr);
alertDefineServiceAdapter.modifyAlertDefine(existingRule);
alertDefineService.modifyAlertDefine(existingRule);
log.info("Successfully bound monitors {} to alert rule ID: {}", validMonitorIds, ruleId);
return String.format("Successfully bound %d monitor(s) to alert rule ID %d.\nMonitor IDs: %s\nUpdated expression: %s",
@@ -633,7 +660,10 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
log.debug("Validating hierarchy relationships: app={}, metrics={}, fieldConditions={}", app, metrics, fieldConditions);
// Get hierarchy for the specified app
List<Hierarchy> hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.toLowerCase(), "en-US");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getAppHierarchy: {}", subjectSum);
List<Hierarchy> hierarchies = appService.getAppHierarchy(app.toLowerCase(), "en-US");
if (hierarchies == null || hierarchies.isEmpty()) {
return String.format("Error: App '%s' not found in hierarchy. Please use list_monitor_types to get valid app names.", app);
@@ -662,4 +692,4 @@ public class AlertDefineToolsImpl implements AlertDefineTools {
}
}
}
}
@@ -16,15 +16,15 @@
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.AlertServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.AlertTools;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.ai.tools.AlertTools;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.apache.hertzbeat.alert.dto.AlertSummary;
import org.apache.hertzbeat.alert.service.AlertService;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,7 +41,7 @@ import org.springframework.stereotype.Service;
@Service
public class AlertToolsImpl implements AlertTools {
@Autowired
private AlertServiceAdapter alertServiceAdapter;
private AlertService alertService;
@Override
@Tool(name = "query_alerts", description = """
@@ -116,7 +116,7 @@ public class AlertToolsImpl implements AlertTools {
// Handle different alert types
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
Page<SingleAlert> singleResult = alertServiceAdapter.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
Page<SingleAlert> singleResult = alertService.getSingleAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("SINGLE ALERTS:\n");
response.append("Found ").append(singleResult.getContent().size()).append(" single alerts (Total: ").append(singleResult.getTotalElements()).append("):\n\n");
@@ -154,7 +154,7 @@ public class AlertToolsImpl implements AlertTools {
response.append("\n");
}
Page<GroupAlert> groupResult = alertServiceAdapter.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
Page<GroupAlert> groupResult = alertService.getGroupAlerts(status, search, sort, order, pageIndex, pageSize);
response.append("GROUP ALERTS:\n");
response.append("Found ").append(groupResult.getContent().size()).append(" group alerts (Total: ").append(groupResult.getTotalElements()).append("):\n\n");
@@ -205,7 +205,7 @@ public class AlertToolsImpl implements AlertTools {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
AlertSummary summary = alertServiceAdapter.getAlertsSummary();
AlertSummary summary = alertService.getAlertsSummary();
StringBuilder response = new StringBuilder();
response.append("ALERTS SUMMARY\n");
@@ -16,14 +16,13 @@
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MetricsServiceAdapter;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.tools.MetricsTools;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.warehouse.service.MetricsDataService;
import org.apache.hertzbeat.ai.tools.MetricsTools;
import org.apache.hertzbeat.common.entity.dto.Field;
import org.apache.hertzbeat.common.entity.dto.MetricsData;
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
@@ -44,9 +43,7 @@ import java.util.Map;
@Service
public class MetricsToolsImpl implements MetricsTools {
@Autowired
private MetricsServiceAdapter metricsServiceAdapter;
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
private MetricsDataService metricsDataService;
@Override
@Tool(name = "query_realtime_metrics", description = """
@@ -78,7 +75,7 @@ public class MetricsToolsImpl implements MetricsTools {
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
MetricsData metricsData = metricsServiceAdapter.getMetricsData(monitorId, metrics);
MetricsData metricsData = metricsDataService.getMetricsData(monitorId, metrics);
if (metricsData == null) {
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
@@ -173,7 +170,7 @@ public class MetricsToolsImpl implements MetricsTools {
interval = true;
}
MetricsHistoryData historyData = metricsServiceAdapter.getMetricHistoryData(
MetricsHistoryData historyData = metricsDataService.getMetricHistoryData(
monitorId, app, metrics, fieldParameter, label, history, interval);
if (historyData == null) {
@@ -235,8 +232,10 @@ public class MetricsToolsImpl implements MetricsTools {
public String getWarehouseStatus() {
try {
log.info("Checking warehouse storage status");
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getWarehouseStorageServerStatus: {}", subjectSum);
Boolean status = metricsServiceAdapter.getWarehouseStorageServerStatus();
Boolean status = metricsDataService.getWarehouseStorageServerStatus();
StringBuilder response = new StringBuilder();
response.append("METRICS WAREHOUSE STATUS\n");
@@ -260,4 +259,4 @@ public class MetricsToolsImpl implements MetricsTools {
}
}
}
}
@@ -15,17 +15,18 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.tools.impl;
package org.apache.hertzbeat.ai.tools.impl;
import com.usthe.sureness.subject.SubjectSum;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.adapters.MonitorServiceAdapter;
import org.apache.hertzbeat.ai.agent.config.McpContextHolder;
import org.apache.hertzbeat.ai.agent.utils.UtilityClass;
import org.apache.hertzbeat.ai.config.McpContextHolder;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.service.AppService;
import org.apache.hertzbeat.ai.utils.UtilityClass;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.apache.hertzbeat.ai.agent.tools.MonitorTools;
import org.apache.hertzbeat.ai.tools.MonitorTools;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.hertzbeat.common.entity.manager.Monitor;
@@ -43,7 +44,9 @@ import java.util.Map;
@Service
public class MonitorToolsImpl implements MonitorTools {
@Autowired
private MonitorServiceAdapter monitorServiceAdapter;
private MonitorService monitorService;
@Autowired
private AppService appService;
/**
@@ -123,8 +126,12 @@ public class MonitorToolsImpl implements MonitorTools {
includeStats = false;
}
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject: {}", subjectSum);
Page<Monitor> result = monitorService.getMonitors(
ids, app, search, status, sort, order, pageIndex, pageSize, labels);
log.debug("MonitorService.getMonitors result: {}", result);
StringBuilder response = new StringBuilder();
response.append("MONITOR QUERY RESULTS\n");
@@ -133,10 +140,10 @@ public class MonitorToolsImpl implements MonitorTools {
// Include statistics if requested
if (includeStats) {
// Get status distribution by calling with different status values
long onlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorServiceAdapter.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
long onlineCount = monitorService.getMonitors(null, app, search, (byte) 1, null, null, 0, 1000, labels).getTotalElements();
long offlineCount = monitorService.getMonitors(null, app, search, (byte) 2, null, null, 0, 1000, labels).getTotalElements();
long unreachableCount = monitorService.getMonitors(null, app, search, (byte) 3, null, null, 0, 1000, labels).getTotalElements();
long pausedCount = monitorService.getMonitors(null, app, search, (byte) 0, null, null, 0, 1000, labels).getTotalElements();
response.append("STATUS OVERVIEW:\n");
response.append("- Online: ").append(onlineCount).append("\n");
@@ -266,7 +273,10 @@ public class MonitorToolsImpl implements MonitorTools {
// Validate that all required parameters for this monitor type are provided
try {
List<ParamDefine> requiredParams = monitorServiceAdapter.getMonitorParamDefines(app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for getMonitorParamDefines: {}", subjectSum);
List<ParamDefine> requiredParams = appService.getAppParamDefines(app.toLowerCase().trim());
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
List<String> missingParams = new ArrayList<>();
@@ -290,8 +300,12 @@ public class MonitorToolsImpl implements MonitorTools {
log.warn("Could not validate required parameters for monitor type '{}': {}", app, e.getMessage());
}
// Call adapter - it handles all the complexity (validation, defaults, app-specific logic)
Long monitorId = monitorServiceAdapter.addMonitor(monitor, params, null);
// Call monitor service - it handles all the complexity (validation, defaults, app-specific logic)
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current security subject for addMonitor: {}", subjectSum);
monitorService.addMonitor(monitor, params, null, null);
Long monitorId = monitor.getId();
log.info("Successfully added monitor '{}' with ID: {}", name, monitorId);
return String.format("Successfully added %s monitor '%s' with ID: %d (Host: %s, Interval: %d seconds)",
@@ -361,14 +375,14 @@ public class MonitorToolsImpl implements MonitorTools {
log.info("Listing available monitor types for language: {}", language);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in list_monitor_types tool: {}", subjectSum);
// Set default language if not provided
if (language == null || language.trim().isEmpty()) {
language = "en-US";
}
// Get available monitor types from adapter
Map<String, String> monitorTypes = monitorServiceAdapter.getAvailableMonitorTypes(language);
// Get available monitor types from app service
Map<String, String> monitorTypes = appService.getI18nApps(language);
if (monitorTypes == null || monitorTypes.isEmpty()) {
return "No monitor types are currently available.";
@@ -416,14 +430,14 @@ public class MonitorToolsImpl implements MonitorTools {
log.info("Getting parameter definitions for monitor type: {}", app);
SubjectSum subjectSum = McpContextHolder.getSubject();
log.debug("Current subject in get_monitor_param_defines tool: {}", subjectSum);
// Validate required parameter
if (app == null || app.trim().isEmpty()) {
return "Error: Monitor type/application parameter is required";
}
// Get parameter definitions from adapter
List<ParamDefine> paramDefines = monitorServiceAdapter.getMonitorParamDefines(app);
// Get parameter definitions from app service
List<ParamDefine> paramDefines = appService.getAppParamDefines(app.toLowerCase().trim());
if (paramDefines == null || paramDefines.isEmpty()) {
return String.format("No parameter definitions found for monitor type '%s'. "
@@ -15,16 +15,14 @@
* limitations under the License.
*/
package org.apache.hertzbeat.ai.agent.utils;
package org.apache.hertzbeat.ai.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.ai.agent.pojo.dto.Hierarchy;
import java.lang.reflect.Method;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -33,6 +31,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hertzbeat.manager.pojo.dto.Hierarchy;
/**
* Utility class providing helper methods for field expression validation, parsing,
@@ -227,51 +226,6 @@ public class UtilityClass {
return "VALID";
}
/**
* Helper method to validate operator
*/
public boolean isValidOperator(String operator) {
return operator != null && (operator.equals(">") || operator.equals("<")
|| operator.equals(">=") || operator.equals("<=")
|| operator.equals("==") || operator.equals("!="));
}
/**
* Helper method to validate priority
*/
public boolean isValidPriority(String priority) {
return priority != null && (priority.equalsIgnoreCase("critical")
|| priority.equalsIgnoreCase("warning") || priority.equalsIgnoreCase("info"));
}
/**
* Helper method to build expression
*/
public String buildExpression(String metric, String operator, String threshold) {
return String.format("%s %s %s", metric, operator, threshold);
}
/**
* Helper method to parse existing expression into components
*/
public String[] parseExpression(String expression) {
if (expression == null || expression.trim().isEmpty()) {
return null;
}
// Simple parsing for basic expressions like "metric > value"
String[] operators = {">", "<", ">=", "<=", "==", "!="};
for (String op : operators) {
if (expression.contains(" " + op + " ")) {
String[] parts = expression.split(" " + op + " ");
if (parts.length == 2) {
return new String[]{parts[0].trim(), op, parts[1].trim()};
}
}
}
return null;
}
/**
* Helper method to parse key-value pairs from a string
* Format: "key1:value1, key2:value2, ..."
@@ -473,19 +427,6 @@ public class UtilityClass {
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* Parse time range string to milliseconds
*/
public long parseTimeRangeToMillis(String timeRange) {
return switch (timeRange.toLowerCase()) {
case "1h" -> 60 * 60 * 1000L;
case "6h" -> 6 * 60 * 60 * 1000L;
case "24h" -> 24 * 60 * 60 * 1000L;
case "7d" -> 7 * 24 * 60 * 60 * 1000L;
default -> 24 * 60 * 60 * 1000L; // default to 24h
};
}
/**
* Helper method to convert monitor status byte to readable text
* @param status The status byte from monitor
@@ -504,143 +445,6 @@ public class UtilityClass {
};
}
/**
* Helper method to get metrics name for a metric type
*/
public String getMetricsNameForType(String metricType) {
return switch (metricType.toLowerCase()) {
case "cpu" -> "cpu";
case "memory" -> "memory";
case "disk" -> "disk";
case "network" -> "network";
default -> "system";
};
}
/**
* Helper method to check if a field represents usage for a metric type
*/
public boolean isUsageField(String field, String metricType) {
if (field == null) return false;
String fieldLower = field.toLowerCase();
String typeLower = metricType.toLowerCase();
return fieldLower.contains("usage")
|| fieldLower.contains("percent")
|| fieldLower.contains("util")
|| (typeLower.equals("cpu") && (fieldLower.contains("cpu") || fieldLower.contains("idle")))
|| (typeLower.equals("memory") && fieldLower.contains("memory"))
|| (typeLower.equals("disk") && fieldLower.contains("disk"));
}
/**
* Convert manager module Hierarchy objects to ai-agent module Hierarchy objects
* This handles the cross-module DTO conversion to avoid ClassCastException
*/
public List<Hierarchy> convertToAgentHierarchies(List<?> managerHierarchies) {
List<Hierarchy> agentHierarchies = new ArrayList<>();
for (Object managerHierarchy : managerHierarchies) {
Hierarchy agentHierarchy = convertToAgentHierarchy(managerHierarchy);
agentHierarchies.add(agentHierarchy);
}
return agentHierarchies;
}
/**
* Convert a single manager Hierarchy object to ai-agent Hierarchy object using reflection
*/
public Hierarchy convertToAgentHierarchy(Object managerHierarchy) {
try {
Hierarchy agentHierarchy = new Hierarchy();
// Use reflection to copy properties from manager DTO to agent DTO
Class<?> managerClass = managerHierarchy.getClass();
// Copy basic properties
agentHierarchy.setCategory(getStringField(managerHierarchy, managerClass, "category"));
agentHierarchy.setValue(getStringField(managerHierarchy, managerClass, "value"));
agentHierarchy.setLabel(getStringField(managerHierarchy, managerClass, "label"));
agentHierarchy.setIsLeaf(getBooleanField(managerHierarchy, managerClass, "isLeaf"));
agentHierarchy.setHide(getBooleanField(managerHierarchy, managerClass, "hide"));
agentHierarchy.setType(getByteField(managerHierarchy, managerClass, "type"));
agentHierarchy.setUnit(getStringField(managerHierarchy, managerClass, "unit"));
// Handle children recursively
List<?> managerChildren = getListField(managerHierarchy, managerClass, "children");
if (managerChildren != null && !managerChildren.isEmpty()) {
List<Hierarchy> agentChildren = convertToAgentHierarchies(managerChildren);
agentHierarchy.setChildren(agentChildren);
}
return agentHierarchy;
} catch (Exception e) {
log.error("Failed to convert manager hierarchy to agent hierarchy: {}", e.getMessage(), e);
throw new RuntimeException("Failed to convert hierarchy", e);
}
}
public String getStringField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value != null ? value.toString() : null;
} catch (Exception e) {
log.debug("Could not get string field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public Boolean getBooleanField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e) {
try {
// Try alternative getter pattern for boolean fields
Method isGetter = clazz.getMethod("is" + capitalize(fieldName));
Object value = isGetter.invoke(obj);
return value instanceof Boolean ? (Boolean) value : null;
} catch (Exception e2) {
log.debug("Could not get boolean field '{}': {}", fieldName, e.getMessage());
return null;
}
}
}
public Byte getByteField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof Byte ? (Byte) value : null;
} catch (Exception e) {
log.debug("Could not get byte field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public List<?> getListField(Object obj, Class<?> clazz, String fieldName) {
try {
Method getter = clazz.getMethod("get" + capitalize(fieldName));
Object value = getter.invoke(obj);
return value instanceof List ? (List<?>) value : null;
} catch (Exception e) {
log.debug("Could not get list field '{}': {}", fieldName, e.getMessage());
return null;
}
}
public String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
/**
* Extract existing monitor IDs from the alert expression
@@ -17,25 +17,23 @@
package org.apache.hertzbeat.alert;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* alarm module thread pool
* alarm module thread pool with Virtual Threads
*/
@Component
@Slf4j
public class AlerterWorkerPool {
private ThreadPoolExecutor workerExecutor;
private ThreadPoolExecutor notifyExecutor;
private ThreadPoolExecutor logWorkerExecutor;
private ExecutorService workerExecutor;
private ExecutorService notifyExecutor;
private ExecutorService logWorkerExecutor;
public AlerterWorkerPool() {
initWorkExecutor();
@@ -44,62 +42,31 @@ public class AlerterWorkerPool {
}
private void initWorkExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter workerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("alerter-worker-%d")
.build();
workerExecutor = new ThreadPoolExecutor(10,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
ThreadFactory factory = Thread.ofVirtual()
.name("alerter-worker-", 0)
.factory();
workerExecutor = Executors.newThreadPerTaskExecutor(factory);
}
private void initNotifyExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter notifyExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("notify-worker-%d")
.build();
notifyExecutor = new ThreadPoolExecutor(6,
6,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
ThreadFactory factory = Thread.ofVirtual()
.name("notify-worker-", 0)
.factory();
notifyExecutor = Executors.newThreadPerTaskExecutor(factory);
}
private void initLogWorkerExecutor() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("Alerter logWorkerExecutor has uncaughtException.");
log.error(throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("log-worker-%d")
.build();
logWorkerExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
ThreadFactory factory = Thread.ofVirtual()
.name("log-worker-", 0)
.factory();
logWorkerExecutor = Executors.newThreadPerTaskExecutor(factory);
}
/**
* Run the alerter task
* @param runnable task
* @throws RejectedExecutionException when The thread pool is full of
*/
public void executeJob(Runnable runnable) throws RejectedExecutionException {
public void executeJob(Runnable runnable){
workerExecutor.execute(runnable);
}
@@ -107,9 +74,8 @@ public class AlerterWorkerPool {
* Executes the given runnable task using the notifyExecutor.
*
* @param runnable the task to be executed
* @throws RejectedExecutionException if the task cannot be accepted for execution
*/
public void executeNotify(Runnable runnable) throws RejectedExecutionException {
public void executeNotify(Runnable runnable){
notifyExecutor.execute(runnable);
}
@@ -117,9 +83,8 @@ public class AlerterWorkerPool {
* Executes the given runnable task using the logWorkerExecutor.
*
* @param runnable the task to be executed
* @throws RejectedExecutionException if the task cannot be accepted for execution
*/
public void executeLogJob(Runnable runnable) throws RejectedExecutionException {
public void executeLogJob(Runnable runnable){
logWorkerExecutor.execute(runnable);
}
}
}
@@ -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);
}
@@ -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,23 @@ 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);
dataQueue.sendLogEntryToStorage(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
}
}
@@ -24,7 +24,7 @@ import java.util.Properties;
import java.util.ResourceBundle;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -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);
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
@@ -151,4 +151,4 @@ public class SmsClientFactory {
break;
}
}
}
}
@@ -17,7 +17,9 @@
package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hertzbeat.alert.dto.AlibabaCloudSlsExternAlert;
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
@@ -33,7 +35,9 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -46,19 +50,32 @@ public class AlibabaCloudSlsExternAlertService implements ExternAlertService {
private final AlarmCommonReduce alarmCommonReduce;
private static final AlibabaCloudSlsConverter CONVERTER = new AlibabaCloudSlsConverter();
public AlibabaCloudSlsExternAlertService(AlarmCommonReduce alarmCommonReduce) {
this.alarmCommonReduce = alarmCommonReduce;
}
@Override
public void addExternAlert(String content) {
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
if (externAlert == null) {
List<AlibabaCloudSlsExternAlert> externAlerts = new ArrayList<>();
if (BooleanUtils.isTrue(JsonUtil.isArray(content))) {
TypeReference<List<AlibabaCloudSlsExternAlert>> typeReference = new TypeReference<>() {};
externAlerts = JsonUtil.fromJson(content, typeReference);
} else {
AlibabaCloudSlsExternAlert externAlert = JsonUtil.fromJson(content, AlibabaCloudSlsExternAlert.class);
if (null != externAlert) {
externAlerts.add(externAlert);
}
}
if (null == externAlerts || externAlerts.isEmpty()) {
log.warn("Failure to parse external alert content. content: {}", content);
return;
}
SingleAlert singleAlert = new AlibabaCloudSlsConverter().convert(externAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
for (AlibabaCloudSlsExternAlert externAlert : externAlerts) {
SingleAlert singleAlert = CONVERTER.convert(externAlert);
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
}
}
@Override
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.AlibabaSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AlibabaSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -236,4 +236,4 @@ public class AlibabaSmsClientImpl implements SmsClient {
public boolean checkConfig() {
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
}
}
}
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.constants.SmsConstants;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import jakarta.xml.bind.DatatypeConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.TencentSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TencentSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.alert.util.CryptoUtils;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
@@ -19,7 +19,7 @@ package org.apache.hertzbeat.alert.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -168,4 +168,4 @@ public class TwilioSmsClientImpl implements SmsClient {
public boolean checkConfig() {
return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank());
}
}
}
@@ -18,7 +18,7 @@
package org.apache.hertzbeat.alert.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties;
import org.apache.hertzbeat.alert.service.SmsClient;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -190,4 +190,4 @@ public class UniSmsClientImpl implements SmsClient {
return true;
}
}
}
@@ -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);
}
/**
@@ -27,7 +27,7 @@ import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Properties;
import org.apache.hertzbeat.alert.dto.MailServerConfig;
import org.apache.hertzbeat.common.entity.dto.MailServerConfig;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
@@ -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);
});
}
}
@@ -105,19 +105,19 @@ class AlertDefineServiceTest {
@Test
void addAlertDefine() {
assertDoesNotThrow(() -> alertDefineService.addAlertDefine(alertDefine));
assertDoesNotThrow(() -> this.alertDefineService.addAlertDefine(alertDefine));
when(alertDefineDao.saveAndFlush(alertDefine)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> alertDefineService.addAlertDefine(alertDefine));
assertThrows(RuntimeException.class, () -> this.alertDefineService.addAlertDefine(alertDefine));
}
@Test
void modifyAlertDefine() {
AlertDefine alertDefine = AlertDefine.builder().id(1L).build();
when(alertDefineDao.saveAndFlush(alertDefine)).thenReturn(alertDefine);
assertDoesNotThrow(() -> alertDefineService.modifyAlertDefine(alertDefine));
assertDoesNotThrow(() -> this.alertDefineService.modifyAlertDefine(alertDefine));
reset();
when(alertDefineDao.saveAndFlush(alertDefine)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> alertDefineService.modifyAlertDefine(alertDefine));
assertThrows(RuntimeException.class, () -> this.alertDefineService.modifyAlertDefine(alertDefine));
}
@Test
@@ -28,9 +28,12 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.BeanUtils;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -147,4 +150,36 @@ public class AlibabaCloudSlsExternAlertServiceTest {
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
}
@Test
void testAddMergeExternAlert() {
String source = externAlertService.supportSource();
assertEquals("alibabacloud-sls", source);
AlibabaCloudSlsExternAlert externAlert = new AlibabaCloudSlsExternAlert();
externAlert.setAlertName("Test SLS alert");
externAlert.setFireTime((int) Instant.now().getEpochSecond());
externAlert.setAlertTime((int) Instant.now().getEpochSecond());
externAlert.setRegion("cn-hangzhou");
externAlert.setProject("project");
externAlert.setStatus("firing");
externAlert.setSeverity(AlibabaCloudSlsExternAlert.Severity.HIGH.getStatus());
Map<String, String> labels = new HashMap<>();
labels.put("labels-k", "labels-v");
externAlert.setLabels(labels);
Map<String, String> annotations = new HashMap<>();
annotations.put("annotations-k", "annotations-v");
externAlert.setAnnotations(annotations);
AlibabaCloudSlsExternAlert externAlert1 = new AlibabaCloudSlsExternAlert();
BeanUtils.copyProperties(externAlert, externAlert1);
List<AlibabaCloudSlsExternAlert> externAlerts = new ArrayList<>();
externAlerts.add(externAlert);
externAlerts.add(externAlert1);
externAlertService.addExternAlert(JsonUtil.toJson(externAlerts));
verify(alarmCommonReduce, times(2)).reduceAndSendAlarm(any(SingleAlert.class));
}
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -19,8 +19,8 @@ package org.apache.hertzbeat.alert.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hertzbeat.alert.config.SmsConfig;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmsConfig;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
@@ -89,4 +89,4 @@ public class SmsClientFactoryTest {
assertNull(smsClientFactory.getSmsClient());
}
}
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service;
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties;
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -61,4 +61,4 @@ class TwilioSmsClientImplTest {
assertEquals(expected, twilioSmsClient.checkConfig());
}
}
}
@@ -18,7 +18,7 @@
package org.apache.hertzbeat.alert.service.impl;
import com.google.common.collect.Lists;
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
@@ -70,4 +70,4 @@ public class SmsLocalSmsClientImplTest {
() -> smsLocalSmsClient.sendMessage(noticeReceiver, null, groupAlert));
}
}
}
@@ -17,7 +17,7 @@
package org.apache.hertzbeat.alert.service.impl;
import org.apache.hertzbeat.alert.config.UniSmsProperties;
import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties;
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
import org.apache.hertzbeat.common.support.exception.SendMessageException;
@@ -82,4 +82,4 @@ public class UniSmsClientImplTest {
}
}
}
@@ -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());
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -15,79 +15,80 @@
* limitations under the License.
*/
package org.apache.hertzbeat.collector.collect.http;
package org.apache.hertzbeat.collector.collect. http;
import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH;
import static org.apache.hertzbeat. common.constants.SignConstants.RIGHT_DASH;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com. google.gson.JsonParser;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java. io.InterruptedIOException;
import java.io.StringReader;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util. List;
import java.util. Map;
import java.util. Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javax.net.ssl.SSLException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml. parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax. xml.xpath.XPathConstants;
import javax.xml.xpath. XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import lombok.extern.slf4j.Slf4j;
import lombok.extern.slf4j. Slf4j;
import org. apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.collect.http.promethus.AbstractPrometheusParse;
import org.apache.hertzbeat.collector.collect.http.promethus.PrometheusParseCreator;
import org.apache.hertzbeat.collector.collect.prometheus.parser.MetricFamily;
import org.apache. hertzbeat.collector.collect. prometheus.parser.MetricFamily;
import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser;
import org.apache.hertzbeat.collector.constants.CollectorConstants;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.collector.util.CollectUtil;
import org.apache. hertzbeat.collector.dispatch. DispatchConstants;
import org. apache.hertzbeat.collector.util.CollectUtil;
import org.apache.hertzbeat.collector.util.JsonPathParser;
import org.apache.hertzbeat.collector.util.TimeExpressionUtil;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.job.Metrics;
import org.apache.hertzbeat.common.entity.job.protocol.HttpProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache. hertzbeat.common.constants. NetworkConstants;
import org.apache. hertzbeat.common.constants. SignConstants;
import org.apache.hertzbeat.common.entity.job. Metrics;
import org.apache. hertzbeat.common.entity. job.protocol.HttpProtocol;
import org.apache.hertzbeat. common.entity.message.CollectRep;
import org.apache.hertzbeat. common.util.Base64Util;
import org. apache.hertzbeat.common. util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpMethod;
import org.apache. hc.core5.http.ClassicHttpResponse;
import org. apache.hc.core5. http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache. hc.core5.http. HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.apache. hc.client5.http.auth.AuthScope;
import org.apache. hc.client5.http. auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.ClientProtocolException;
import org. apache.hc.client5. http.config.RequestConfig;
import org. apache.hc.client5. http.classic.methods.HttpGet;
import org. apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org. apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache. hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc. core5.http.protocol.HttpContext;
import org. apache.hc.core5. http.io.entity.EntityUtils;
import org.apache. hc.core5.util.Timeout;
import org. springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org. springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriUtils;
import org.xml.sax.InputSource;
@@ -96,9 +97,9 @@ import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java. util.regex.Pattern;
import java.util.HashSet;
import java.util.ArrayList;
import java.util. ArrayList;
import java.util.Collections;
/**
@@ -106,6 +107,28 @@ import java.util.Collections;
*/
@Slf4j
public class HttpCollectImpl extends AbstractCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private final Set<Integer> defaultSuccessStatusCodes = Set.of(
HttpStatus.SC_OK,
HttpStatus.SC_CREATED,
@@ -122,28 +145,37 @@ public class HttpCollectImpl extends AbstractCollect {
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
public void collect(CollectRep. MetricsData.Builder builder, Metrics metrics) {
long startTime = System.currentTimeMillis();
HttpProtocol httpProtocol = metrics.getHttp();
String url = httpProtocol.getUrl();
if (!StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) {
httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH);
if (! StringUtils.hasText(url) || !url.startsWith(RIGHT_DASH)) {
httpProtocol.setUrl(StringUtils.hasText(url) ? RIGHT_DASH + url.trim() : RIGHT_DASH);
}
if (CollectionUtils.isEmpty(httpProtocol.getSuccessCodes())) {
httpProtocol.setSuccessCodes(List.of(HttpStatus.SC_OK + ""));
httpProtocol. setSuccessCodes(List. of(HttpStatus.SC_OK + ""));
}
HttpContext httpContext = createHttpContext(metrics.getHttp());
HttpUriRequest request = createHttpRequest(metrics.getHttp());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpRequestWithConfig requestWithConfig = createHttpRequest(metrics.getHttp());
if (requestWithConfig == null) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Failed to create HTTP request");
return;
}
ClassicHttpRequest request = requestWithConfig. getRequest();
HttpContext httpContext = createHttpContext(metrics. getHttp(), requestWithConfig. getRequestConfig());
// Use HttpClientResponseHandler to handle the response and avoid deprecated execute method
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
boolean isSuccessInvoke = checkSuccessInvoke(metrics, statusCode);
log.debug("http response status: {}", statusCode);
if (!isSuccessInvoke) {
builder.setCode(CollectRep.Code.FAIL);
builder.setCode(CollectRep.Code. FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return;
return null;
}
long responseTime = System.currentTimeMillis() - startTime;
@@ -153,25 +185,20 @@ public class HttpCollectImpl extends AbstractCollect {
try {
if (DispatchConstants.PARSE_PROMETHEUS.equals(parseType)) {
if (entity != null) {
parseResponseByPrometheusExporter(entity.getContent(), metrics.getAliasFields(), builder);
parseResponseByPrometheusExporter(entity. getContent(), metrics.getAliasFields(), builder);
}
} else if (DispatchConstants.PARSE_HEADER.equals(parseType)) {
parseResponseByHeader(builder, metrics.getAliasFields(), response);
// Consume entity to release connection
EntityUtils.consumeQuietly(entity);
} else {
/*
this could create large objects, potentially impacting JVM memory space significantly.
Option 1: Parse using InputStream, but this requires significant code changes;
Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs.
*/
String resp = entity == null ? "" : EntityUtils.toString(entity, StandardCharsets.UTF_8);
String resp = entity == null ? "" : EntityUtils. toString(entity, StandardCharsets. UTF_8);
if (!StringUtils.hasText(resp)) {
log.info("http response entity is empty, status: {}.", statusCode);
}
switch (parseType) {
case DispatchConstants.PARSE_JSON_PATH ->
parseResponseByJsonPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
parseResponseByJsonPath(resp, metrics. getAliasFields(), metrics. getHttp(), builder, responseTime);
case DispatchConstants.PARSE_PROM_QL ->
parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder);
case DispatchConstants.PARSE_XML_PATH ->
@@ -179,7 +206,7 @@ public class HttpCollectImpl extends AbstractCollect {
case DispatchConstants.PARSE_WEBSITE ->
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime, statusCode);
case DispatchConstants.PARSE_SITE_MAP ->
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
parseResponseBySiteMap(resp, metrics. getAliasFields(), builder);
case DispatchConstants.PARSE_CONFIG ->
parseResponseByConfig(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
default ->
@@ -191,15 +218,20 @@ public class HttpCollectImpl extends AbstractCollect {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("parse response data error:" + e.getMessage());
}
return null;
};
try {
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (ClientProtocolException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.error(errorMsg);
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
builder. setCode(CollectRep. Code.UN_CONNECTABLE);
builder.setMsg(errorMsg);
} catch (UnknownHostException e2) {
String errorMsg = CommonUtil.getMessageFromThrowable(e2);
log.info(errorMsg);
builder.setCode(CollectRep.Code.UN_REACHABLE);
builder. setCode(CollectRep. Code.UN_REACHABLE);
builder.setMsg("unknown host:" + errorMsg);
} catch (InterruptedIOException | ConnectException | SSLException e3) {
String errorMsg = CommonUtil.getMessageFromThrowable(e3);
@@ -208,7 +240,7 @@ public class HttpCollectImpl extends AbstractCollect {
builder.setMsg(errorMsg);
} catch (IOException e4) {
String errorMsg = CommonUtil.getMessageFromThrowable(e4);
log.info(errorMsg);
log. info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e) {
@@ -216,17 +248,13 @@ public class HttpCollectImpl extends AbstractCollect {
log.error(errorMsg, e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
}
private void parseResponseByHeader(CollectRep.MetricsData.Builder builder, List<String> aliases, CloseableHttpResponse response) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
private void parseResponseByHeader(CollectRep.MetricsData.Builder builder, List<String> aliases, ClassicHttpResponse response) {
CollectRep.ValueRow. Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliases) {
if (!StringUtils.hasText(alias)) {
if (! StringUtils.hasText(alias)) {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
continue;
}
@@ -238,7 +266,7 @@ public class HttpCollectImpl extends AbstractCollect {
valueRowBuilder.addColumn(firstHeader.getValue());
}
builder.addValueRow(valueRowBuilder.build());
builder. addValueRow(valueRowBuilder.build());
}
@Override
@@ -263,7 +291,7 @@ public class HttpCollectImpl extends AbstractCollect {
private void addColumnForSummary(Long responseTime, CollectRep.ValueRow.Builder valueRowBuilder, int keywordNum, String alias) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
} else if (CollectorConstants.KEYWORD. equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
@@ -282,13 +310,13 @@ public class HttpCollectImpl extends AbstractCollect {
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(new InputSource(new StringReader(resp)));
NodeList urlList = document.getElementsByTagName("url");
for (int i = 0; i < urlList.getLength(); i++) {
for (int i = 0; i < urlList. getLength(); i++) {
Node urlNode = urlList.item(i);
NodeList childNodes = urlNode.getChildNodes();
for (int k = 0; k < childNodes.getLength(); k++) {
Node currentNode = childNodes.item(k);
// distinguish between text nodes and element nodes
if (currentNode.getNodeType() == Node.ELEMENT_NODE && "loc".equals(currentNode.getNodeName())) {
if (currentNode.getNodeType() == Node. ELEMENT_NODE && "loc".equals(currentNode.getNodeName())) {
// retrieves the value of the loc node
siteUrls.add(currentNode.getFirstChild().getNodeValue());
break;
@@ -318,10 +346,13 @@ public class HttpCollectImpl extends AbstractCollect {
long startTime = System.currentTimeMillis();
try {
HttpGet httpGet = new HttpGet(siteUrl);
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet)) {
statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<Integer> statusHandler = response -> {
EntityUtils.consume(response.getEntity());
}
return response.getCode();
};
statusCode = CommonHttpClient.getHttpClient().execute(httpGet, statusHandler);
} catch (ClientProtocolException e1) {
if (e1.getCause() != null) {
errorMsg = e1.getCause().getMessage();
@@ -345,7 +376,7 @@ public class HttpCollectImpl extends AbstractCollect {
} else if (NetworkConstants.STATUS_CODE.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(statusCode == null
? CommonConstants.NULL_VALUE : String.valueOf(statusCode));
} else if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
} else if (NetworkConstants. RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(String.valueOf(responseTime));
} else if (NetworkConstants.ERROR_MSG.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(errorMsg);
@@ -362,13 +393,13 @@ public class HttpCollectImpl extends AbstractCollect {
HttpProtocol http = metrics.getHttp();
List<String> aliasFields = metrics.getAliasFields();
String xpathExpression = http.getParseScript();
if (!StringUtils.hasText(xpathExpression)) {
if (! StringUtils.hasText(xpathExpression)) {
log.warn("Http collect parse type is xmlPath, but the xpath expression is empty.");
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("XPath expression is empty");
return;
}
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
int keywordNum = CollectUtil. countMatchKeyword(resp, http.getKeyword());
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
@@ -378,13 +409,13 @@ public class HttpCollectImpl extends AbstractCollect {
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
DocumentBuilder db = dbf. newDocumentBuilder();
Document document = db.parse(new InputSource(new StringReader(resp)));
XPathFactory xpathFactory = XPathFactory.newInstance();
XPath xpath = xpathFactory.newXPath();
NodeList nodeList = (NodeList) xpath.evaluate(xpathExpression, document, XPathConstants.NODESET);
NodeList nodeList = (NodeList) xpath. evaluate(xpathExpression, document, XPathConstants.NODESET);
if (nodeList == null || nodeList.getLength() == 0) {
log.debug("XPath expression '{}' returned no nodes.", xpathExpression);
@@ -397,10 +428,10 @@ public class HttpCollectImpl extends AbstractCollect {
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
} else if (CollectorConstants. KEYWORD.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
valueRowBuilder.addColumn(CommonConstants. NULL_VALUE);
}
}
builder.addValueRow(valueRowBuilder.build());
@@ -413,14 +444,14 @@ public class HttpCollectImpl extends AbstractCollect {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
if (NetworkConstants. RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
} else if (CollectorConstants. KEYWORD.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
try {
String value = (String) xpath.evaluate(alias, node, XPathConstants.STRING);
valueRowBuilder.addColumn(StringUtils.hasText(value) ? value : CommonConstants.NULL_VALUE);
valueRowBuilder. addColumn(StringUtils.hasText(value) ? value : CommonConstants.NULL_VALUE);
} catch (XPathExpressionException e) {
log.warn("Failed to evaluate XPath '{}' for node [{}]: {}", alias, node.getNodeName(), e.getMessage());
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
@@ -432,7 +463,7 @@ public class HttpCollectImpl extends AbstractCollect {
} catch (Exception e) {
log.warn("Failed to parse XML response with XPath '{}': {}", xpathExpression, e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setCode(CollectRep.Code. FAIL);
builder.setMsg("Failed to parse XML response: " + e.getMessage());
}
}
@@ -441,25 +472,12 @@ public class HttpCollectImpl extends AbstractCollect {
/**
* Parses the response body in Properties/Config format.
* Two modes are supported:
* 1. single-object mode: if http.parseScript is null, aliasFields are treated as indicator names.
* - If there is a locator in the indicator definition, use the locator as the key of the Properties.
* - Otherwise, use aliasField (metric name) as the key for Properties.
* Generate a single row of data.
* 2. array mode: if http.parseScript is not empty (e.g. “users”), treat it as an array base path.
* Treat aliasFields as the attribute name of an array element, and generate a single row of data for each array index. locator is invalid in this mode.
*
* @param resp Response body string
* @param aliasFields List of metrics aliases (i.e., the list of fields in metrics.fields).
* @param http http protocol configuration
* @param builder The metrics data builder.
* @param responseTime response time
*/
private void parseResponseByConfig(String resp, List<String> aliasFields, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime) {
if (!StringUtils.hasText(resp)) {
if (! StringUtils.hasText(resp)) {
log.warn("Http collect parse type is config, but response body is empty.");
builder.setCode(CollectRep.Code.FAIL);
builder. setCode(CollectRep. Code.FAIL);
builder.setMsg("Response body is empty");
return;
}
@@ -469,14 +487,14 @@ public class HttpCollectImpl extends AbstractCollect {
properties.load(reader);
} catch (IOException e) {
log.warn("Failed to parse config response: {}", e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setCode(CollectRep.Code. FAIL);
builder.setMsg("Failed to parse config response: " + e.getMessage());
return;
}
String arrayBasePath = http.getParseScript();
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
int keywordNum = CollectUtil. countMatchKeyword(resp, http.getKeyword());
if (!StringUtils.hasText(arrayBasePath)) {
if (! StringUtils.hasText(arrayBasePath)) {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
@@ -485,7 +503,7 @@ public class HttpCollectImpl extends AbstractCollect {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
} else {
String value = properties.getProperty(alias);
valueRowBuilder.addColumn(value != null ? value : CommonConstants.NULL_VALUE);
valueRowBuilder. addColumn(value != null ? value : CommonConstants. NULL_VALUE);
}
}
CollectRep.ValueRow valueRow = valueRowBuilder.build();
@@ -518,13 +536,13 @@ public class HttpCollectImpl extends AbstractCollect {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
for (String alias : aliasFields) {
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(responseTime.toString());
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Integer.toString(keywordNum));
valueRowBuilder.addColumn(responseTime. toString());
} else if (CollectorConstants.KEYWORD. equalsIgnoreCase(alias)) {
valueRowBuilder. addColumn(Integer.toString(keywordNum));
} else {
String currentKey = arrayBasePath + "[" + i + "]." + alias;
String value = properties.getProperty(currentKey);
valueRowBuilder.addColumn(value != null ? value : CommonConstants.NULL_VALUE);
valueRowBuilder. addColumn(value != null ? value : CommonConstants.NULL_VALUE);
}
}
CollectRep.ValueRow valueRow = valueRowBuilder.build();
@@ -549,24 +567,24 @@ public class HttpCollectImpl extends AbstractCollect {
for (int i = 0; i < valueRow.getColumnsCount(); i++) {
String columnValue = valueRow.getColumns(i);
String alias = aliasFields.get(i);
if (!CommonConstants.NULL_VALUE.equals(columnValue) && (!NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) && !CollectorConstants.KEYWORD.equalsIgnoreCase(alias))) {
if (! CommonConstants.NULL_VALUE.equals(columnValue) && (! NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) && !CollectorConstants.KEYWORD. equalsIgnoreCase(alias))) {
hasMeaningfulData = true;
break;
}
if ((NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias) || CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) && !CommonConstants.NULL_VALUE.equals(columnValue)) {
if ((NetworkConstants.RESPONSE_TIME. equalsIgnoreCase(alias) || CollectorConstants.KEYWORD. equalsIgnoreCase(alias)) && ! CommonConstants.NULL_VALUE.equals(columnValue)) {
hasMeaningfulData = true;
}
}
return hasMeaningfulData;
}
@SuppressWarnings("unchecked")
private void parseResponseByJsonPath(String resp, List<String> aliasFields, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime) {
List<Object> results = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript());
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
int keywordNum = CollectUtil. countMatchKeyword(resp, http.getKeyword());
for (int i = 0; i < results.size(); i++) {
Object objectValue = results.get(i);
// if a property is missing or empty due to target version issues, filter it. Refer to the app-elasticsearch.yml configuration under name: nodes
if (objectValue == null) {
continue;
}
@@ -578,8 +596,8 @@ public class HttpCollectImpl extends AbstractCollect {
if (value != null) {
valueRowBuilder.addColumn(String.valueOf(value));
} else {
if (alias.startsWith("$.")) {
List<Object> subResults = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript() + alias.substring(1));
if (alias.startsWith("$. ")) {
List<Object> subResults = JsonPathParser.parseContentWithJsonPath(resp, http.getParseScript() + alias. substring(1));
if (subResults != null && subResults.size() > i) {
Object resultValue = subResults.get(i);
valueRowBuilder.addColumn(resultValue == null ? CommonConstants.NULL_VALUE : String.valueOf(resultValue));
@@ -633,7 +651,7 @@ public class HttpCollectImpl extends AbstractCollect {
return;
}
MetricFamily metricFamily = metricFamilyMap.get(metrics);
if (null == metricFamily || CollectionUtils.isEmpty(metricFamily.getMetricList())) {
if (null == metricFamily || CollectionUtils.isEmpty(metricFamily. getMetricList())) {
return;
}
for (MetricFamily.Metric metric : metricFamily.getMetricList()) {
@@ -645,7 +663,7 @@ public class HttpCollectImpl extends AbstractCollect {
String columnValue = labelMap.get(aliasField);
if (columnValue != null) {
valueRowBuilder.addColumn(columnValue);
} else if (CommonConstants.PROM_VALUE.equals(aliasField) || CommonConstants.PROM_METRIC_VALUE.equals(aliasField)) {
} else if (CommonConstants. PROM_VALUE.equals(aliasField) || CommonConstants.PROM_METRIC_VALUE.equals(aliasField)) {
valueRowBuilder.addColumn(String.valueOf(metric.getValue()));
} else {
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
@@ -657,9 +675,9 @@ public class HttpCollectImpl extends AbstractCollect {
private void parseResponseByDefault(String resp, List<String> aliasFields, HttpProtocol http,
CollectRep.MetricsData.Builder builder, Long responseTime) {
CollectRep.MetricsData. Builder builder, Long responseTime) {
JsonElement element = JsonParser.parseString(resp);
int keywordNum = CollectUtil.countMatchKeyword(resp, http.getKeyword());
int keywordNum = CollectUtil. countMatchKeyword(resp, http.getKeyword());
if (element.isJsonArray()) {
JsonArray array = element.getAsJsonArray();
for (JsonElement jsonElement : array) {
@@ -691,46 +709,53 @@ public class HttpCollectImpl extends AbstractCollect {
* create httpContext
*
* @param httpProtocol http protocol
* @param requestConfig request config (can be null)
* @return context
*/
public HttpContext createHttpContext(HttpProtocol httpProtocol) {
HttpProtocol.Authorization auth = httpProtocol.getAuthorization();
public HttpContext createHttpContext(HttpProtocol httpProtocol, RequestConfig requestConfig) {
HttpClientContext clientContext = HttpClientContext.create();
// Set request config if provided
if (requestConfig != null) {
clientContext.setRequestConfig(requestConfig);
}
HttpProtocol. Authorization auth = httpProtocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (StringUtils.hasText(auth.getDigestAuthUsername())
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
BasicCredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(),
auth.getDigestAuthPassword());
auth.getDigestAuthPassword(). toCharArray());
AuthScope authScope = new AuthScope(httpProtocol.getHost(), Integer.parseInt(httpProtocol.getPort()));
provider.setCredentials(authScope, credentials);
clientContext.setCredentialsProvider(provider);
return clientContext;
}
}
return null;
return clientContext;
}
/**
* create http request
*
* @param httpProtocol http params
* @return http uri request
* @return http request with config
*/
public HttpUriRequest createHttpRequest(HttpProtocol httpProtocol) {
RequestBuilder requestBuilder;
String httpMethod = httpProtocol.getMethod().toUpperCase();
private HttpRequestWithConfig createHttpRequest(HttpProtocol httpProtocol) {
ClassicRequestBuilder requestBuilder;
String httpMethod = httpProtocol.getMethod(). toUpperCase();
if (HttpMethod.GET.matches(httpMethod)) {
requestBuilder = RequestBuilder.get();
requestBuilder = ClassicRequestBuilder.get();
} else if (HttpMethod.POST.matches(httpMethod)) {
requestBuilder = RequestBuilder.post();
requestBuilder = ClassicRequestBuilder.post();
} else if (HttpMethod.PUT.matches(httpMethod)) {
requestBuilder = RequestBuilder.put();
requestBuilder = ClassicRequestBuilder.put();
} else if (HttpMethod.DELETE.matches(httpMethod)) {
requestBuilder = RequestBuilder.delete();
} else if (HttpMethod.PATCH.matches(httpMethod)) {
requestBuilder = RequestBuilder.patch();
requestBuilder = ClassicRequestBuilder.delete();
} else if (HttpMethod. PATCH.matches(httpMethod)) {
requestBuilder = ClassicRequestBuilder.patch();
} else {
// not support the method
log.error("not support the http method: {}.", httpProtocol.getMethod());
@@ -741,17 +766,17 @@ public class HttpCollectImpl extends AbstractCollect {
boolean enableUrlEncoding = Boolean.parseBoolean(httpProtocol.getEnableUrlEncoding());
StringBuilder queryParams = new StringBuilder();
if (params != null && !params.isEmpty()) {
if (params != null && ! params.isEmpty()) {
for (Map.Entry<String, String> param : params.entrySet()) {
String key = param.getKey();
String value = param.getValue();
if (!StringUtils.hasText(key)) {
if (! StringUtils.hasText(key)) {
continue;
}
if (!queryParams.isEmpty()) {
queryParams.append("&");
if (! queryParams.isEmpty()) {
queryParams. append("&");
}
if (enableUrlEncoding) {
@@ -762,9 +787,9 @@ public class HttpCollectImpl extends AbstractCollect {
if (StringUtils.hasText(value)) {
String calculatedValue = TimeExpressionUtil.calculate(value);
if (enableUrlEncoding) {
calculatedValue = UriUtils.encodeQueryParam(calculatedValue, "UTF-8");
calculatedValue = UriUtils. encodeQueryParam(calculatedValue, "UTF-8");
}
queryParams.append("=").append(calculatedValue);
queryParams. append("=").append(calculatedValue);
}
}
}
@@ -775,7 +800,7 @@ public class HttpCollectImpl extends AbstractCollect {
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
// headers The custom request header is overwritten here
Map<String, String> headers = httpProtocol.getHeaders();
if (headers != null && !headers.isEmpty()) {
if (headers != null && ! headers.isEmpty()) {
for (Map.Entry<String, String> header : headers.entrySet()) {
if (StringUtils.hasText(header.getValue())) {
requestBuilder.addHeader(header.getKey(), header.getValue());
@@ -783,7 +808,7 @@ public class HttpCollectImpl extends AbstractCollect {
}
}
// add accept
if (DispatchConstants.PARSE_DEFAULT.equals(httpProtocol.getParseType())
if (DispatchConstants.PARSE_DEFAULT. equals(httpProtocol.getParseType())
|| DispatchConstants.PARSE_JSON_PATH.equals(httpProtocol.getParseType())) {
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
} else if (DispatchConstants.PARSE_XML_PATH.equals(httpProtocol.getParseType())) {
@@ -799,10 +824,10 @@ public class HttpCollectImpl extends AbstractCollect {
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value);
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
&& StringUtils.hasText(authorization. getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + SignConstants.DOUBLE_MARK + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth);
requestBuilder.addHeader(HttpHeaders. AUTHORIZATION, DispatchConstants.BASIC + SignConstants.BLANK + encodedAuth);
}
}
}
@@ -817,14 +842,14 @@ public class HttpCollectImpl extends AbstractCollect {
if (enableUrlEncoding) {
// if the url contains parameters directly
if (httpProtocol.getUrl().contains("?")) {
String path = httpProtocol.getUrl().substring(0, httpProtocol.getUrl().indexOf("?"));
String path = httpProtocol.getUrl(). substring(0, httpProtocol.getUrl().indexOf("?"));
String query = httpProtocol.getUrl().substring(httpProtocol.getUrl().indexOf("?") + 1);
uri = UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8");
} else {
uri = UriUtils.encodePath(httpProtocol.getUrl(), "UTF-8");
}
} else {
uri = httpProtocol.getUrl();
uri = httpProtocol. getUrl();
}
// append query params
@@ -834,10 +859,10 @@ public class HttpCollectImpl extends AbstractCollect {
String finalUri;
if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) {
finalUri = httpProtocol.getHost() + ":" + httpProtocol.getPort() + uri;
finalUri = httpProtocol.getHost() + ":" + httpProtocol. getPort() + uri;
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(httpProtocol.getHost());
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
String ipAddressType = IpDomainUtil. checkIpAddressType(httpProtocol.getHost());
String baseUri = NetworkConstants.IPV6. equals(ipAddressType)
? String.format("[%s]:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri)
: String.format("%s:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri);
boolean ssl = Boolean.parseBoolean(httpProtocol.getSsl());
@@ -855,21 +880,23 @@ public class HttpCollectImpl extends AbstractCollect {
throw e;
}
// custom timeout
// custom timeout - create RequestConfig separately
RequestConfig requestConfig = null;
int timeout = CollectUtil.getTimeout(httpProtocol.getTimeout(), 0);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
// build the request and return with config
return new HttpRequestWithConfig(requestBuilder. build(), requestConfig);
}
private boolean checkSuccessInvoke(Metrics metrics, int statusCode) {
List<String> successCodes = metrics.getHttp().getSuccessCodes();
List<String> successCodes = metrics.getHttp(). getSuccessCodes();
Set<Integer> successCodeSet = successCodes != null ? successCodes.stream().map(code -> {
try {
return Integer.valueOf(code);
@@ -117,11 +117,11 @@ public class SslCertificateCollectImpl extends AbstractCollect {
} else if (NAME_EXPIRED.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(Boolean.toString(expired));
} else if (NAME_START_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(x509Certificate.getNotBefore().toLocaleString());
valueRowBuilder.addColumn(x509Certificate.getNotBefore().toString());
} else if (NAME_START_TIMESTAMP.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(String.valueOf(x509Certificate.getNotBefore().getTime()));
} else if (NAME_END_TIME.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(x509Certificate.getNotAfter().toLocaleString());
valueRowBuilder.addColumn(x509Certificate.getNotAfter().toString());
} else if (NAME_END_TIMESTAMP.equalsIgnoreCase(alias)) {
valueRowBuilder.addColumn(String.valueOf(x509Certificate.getNotAfter().getTime()));
} else {
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -44,16 +44,17 @@ import org.apache.hertzbeat.common.entity.job.protocol.NginxProtocol;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.MediaType;
/**
@@ -62,6 +63,27 @@ import org.springframework.http.MediaType;
@Slf4j
public class NginxCollectImpl extends AbstractCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private static final String NGINX_STATUS_NAME = "nginx_status";
private static final String REQ_STATUS_NAME = "req_status";
private static final String AVAILABLE = "available";
@@ -93,34 +115,36 @@ public class NginxCollectImpl extends AbstractCollect {
nginxProtocol.setUrl(url == null ? RIGHT_DASH : RIGHT_DASH + url.trim());
}
HttpContext httpContext = createHttpContext(metrics.getNginx());
HttpUriRequest request = createHttpRequest(metrics.getNginx());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)){
// send an HTTP request and get the response data
int statusCode = response.getStatusLine().getStatusCode();
HttpRequestWithConfig requestWithConfig = createHttpRequest(metrics.getNginx());
HttpContext httpContext = createHttpContext(metrics.getNginx(), requestWithConfig.getRequestConfig());
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_OK) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + statusCode);
return;
return null;
}
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
Long responseTime = System.currentTimeMillis() - startTime;
// call different parsing methods based on the metrics name
if (StringUtils.equalsAny(metrics.getName(), NGINX_STATUS_NAME, AVAILABLE)) {
if (NGINX_STATUS_NAME.equals(metrics.getName()) || AVAILABLE.equals(metrics.getName())) {
parseNginxStatusResponse(builder, resp, metrics, responseTime);
} else if (REQ_STATUS_NAME.equals(metrics.getName())) {
parseReqStatusResponse(builder, resp, metrics, responseTime);
}
return null;
};
try {
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.info(errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
}
@@ -130,18 +154,21 @@ public class NginxCollectImpl extends AbstractCollect {
return DispatchConstants.PROTOCOL_NGINX;
}
private HttpContext createHttpContext(NginxProtocol nginxProtocol) {
private HttpContext createHttpContext(NginxProtocol nginxProtocol, RequestConfig requestConfig) {
HttpHost host = new HttpHost(nginxProtocol.getHost(), Integer.parseInt(nginxProtocol.getPort()));
HttpClientContext httpClientContext = new HttpClientContext();
httpClientContext.setTargetHost(host);
HttpClientContext httpClientContext = HttpClientContext.create();
if (requestConfig != null) {
httpClientContext.setRequestConfig(requestConfig);
}
return httpClientContext;
}
private HttpUriRequest createHttpRequest(NginxProtocol nginxProtocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(NginxProtocol nginxProtocol) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
String portWithUri = nginxProtocol.getPort() + CollectUtil.replaceUriSpecialChar(nginxProtocol.getUrl());
String host = nginxProtocol.getHost();
if (IpDomainUtil.isHasSchema(host)) {
requestBuilder.setUri(host + ":" + portWithUri);
} else {
@@ -162,16 +189,16 @@ public class NginxCollectImpl extends AbstractCollect {
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE);
RequestConfig requestConfig = null;
int timeout = Integer.parseInt(nginxProtocol.getTimeout());
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
/**
@@ -335,4 +362,4 @@ public class NginxCollectImpl extends AbstractCollect {
return GET + result;
}
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -47,24 +47,24 @@ import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.collector.collect.prometheus.parser.OnlineParser;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.auth.AuthCache;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.client5.http.impl.auth.DigestScheme;
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
@@ -74,11 +74,32 @@ import org.springframework.util.StringUtils;
*/
@Slf4j
public class PrometheusAutoCollectImpl implements PrometheusCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED,
HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet());
@Override
public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder,
Metrics metrics) {
@@ -89,11 +110,13 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
builder.setMsg(e.getMessage());
return null;
}
HttpContext httpContext = createHttpContext(metrics.getPrometheus());
HttpUriRequest request = createHttpRequest(metrics.getPrometheus());
try (CloseableHttpResponse response =
CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpRequestWithConfig requestWithConfig = createHttpRequest(metrics.getPrometheus());
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpContext httpContext = createHttpContext(metrics.getPrometheus(), requestWithConfig.getRequestConfig());
HttpClientResponseHandler<List<CollectRep.MetricsData>> responseHandler = response -> {
int statusCode = response.getCode();
boolean isSuccessInvoke = defaultSuccessStatusCodes.contains(statusCode);
log.debug("http response status: {}", statusCode);
if (!isSuccessInvoke) {
@@ -102,12 +125,18 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
return null;
}
try {
// Parse directly from the entity stream inside the handler loop while connection is open
return parseResponseByPrometheusExporter(response.getEntity().getContent(), builder);
} catch (Exception e) {
log.info("parse error: {}.", e.getMessage(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("parse response data error:" + e.getMessage());
return null;
}
};
try {
return CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (ClientProtocolException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.error(errorMsg);
@@ -133,31 +162,27 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
log.error(errorMsg, e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
return Collections.singletonList(builder.build());
}
@Override
public String supportProtocol() {
return DispatchConstants.PROTOCOL_PROMETHEUS;
}
private void validateParams(Metrics metrics) throws Exception {
if (metrics == null || metrics.getPrometheus() == null) {
throw new Exception("Prometheus collect must has prometheus params");
}
PrometheusProtocol protocol = metrics.getPrometheus();
if (protocol.getPath() == null
|| !StringUtils.hasText(protocol.getPath())
|| !protocol.getPath().startsWith(RIGHT_DASH)) {
|| !StringUtils.hasText(protocol.getPath())
|| !protocol.getPath().startsWith(RIGHT_DASH)) {
protocol.setPath(protocol.getPath() == null ? RIGHT_DASH : RIGHT_DASH + protocol.getPath().trim());
}
}
private List<CollectRep.MetricsData> parseResponseByPrometheusExporter(InputStream inputStream, CollectRep.MetricsData.Builder builder) throws IOException {
long endTime = System.currentTimeMillis();
builder.setTime(endTime);
@@ -201,40 +226,49 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
}
return metricsDataList;
}
/**
* create httpContext
*
* @param protocol prometheus protocol
* @param requestConfig request config
* @return context
*/
public HttpContext createHttpContext(PrometheusProtocol protocol) {
public HttpContext createHttpContext(PrometheusProtocol protocol, RequestConfig requestConfig) {
HttpClientContext clientContext = HttpClientContext.create();
if (requestConfig != null) {
clientContext.setRequestConfig(requestConfig);
}
PrometheusProtocol.Authorization auth = protocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (StringUtils.hasText(auth.getDigestAuthUsername())
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
BasicCredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials =
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword());
provider.setCredentials(AuthScope.ANY, credentials);
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword().toCharArray());
// Fix: Use specific AuthScope instead of AuthScope.ANY
AuthScope authScope = new AuthScope(protocol.getHost(), Integer.parseInt(protocol.getPort()));
provider.setCredentials(authScope, credentials);
AuthCache authCache = new BasicAuthCache();
authCache.put(new HttpHost(protocol.getHost(), Integer.parseInt(protocol.getPort())), new DigestScheme());
clientContext.setCredentialsProvider(provider);
clientContext.setAuthCache(authCache);
return clientContext;
}
}
return null;
return clientContext;
}
/**
* create http request
* @param protocol http params
* @return http uri request
* @return http uri request wrapper
*/
public HttpUriRequest createHttpRequest(PrometheusProtocol protocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(PrometheusProtocol protocol) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// params
Map<String, String> params = protocol.getParams();
if (params != null && !params.isEmpty()) {
@@ -260,7 +294,7 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
}
// add accept
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE);
if (protocol.getAuthorization() != null) {
PrometheusProtocol.Authorization authorization = protocol.getAuthorization();
if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) {
@@ -268,7 +302,7 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value);
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth);
@@ -280,17 +314,17 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
if (StringUtils.hasLength(protocol.getPayload()) && (HttpMethod.POST.matches(protocol.getMethod()) || HttpMethod.PUT.matches(protocol.getMethod()))) {
requestBuilder.setEntity(new StringEntity(protocol.getPayload(), StandardCharsets.UTF_8));
}
// uri
String uri = CollectUtil.replaceUriSpecialChar(protocol.getPath());
if (IpDomainUtil.isHasSchema(protocol.getHost())) {
requestBuilder.setUri(protocol.getHost() + SignConstants.DOUBLE_MARK + protocol.getPort() + uri);
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(protocol.getHost());
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uri)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uri);
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uri)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uri);
boolean ssl = Boolean.parseBoolean(protocol.getSsl());
if (ssl) {
requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri);
@@ -298,18 +332,19 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
}
}
RequestConfig requestConfig = null;
// custom timeout
int timeout = CollectUtil.getTimeout(protocol.getTimeout(), 0);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
/**
@@ -319,11 +354,11 @@ public class PrometheusAutoCollectImpl implements PrometheusCollect {
public static PrometheusAutoCollectImpl getInstance() {
return PrometheusAutoCollectImpl.SingleInstance.INSTANCE;
}
/**
* static instance
*/
private static class SingleInstance {
private static final PrometheusAutoCollectImpl INSTANCE = new PrometheusAutoCollectImpl();
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -30,25 +30,25 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.auth.AuthCache;
import org.apache.hc.client5.http.ClientProtocolException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.client5.http.impl.auth.DigestScheme;
import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
@@ -71,6 +71,27 @@ import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH;
@Slf4j
public class PrometheusProxyCollectImpl implements PrometheusCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED,
HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY,
HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet());
@@ -80,7 +101,7 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
@Override
public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
PrometheusProtocol prometheusProtocol = metrics.getPrometheus();
HttpUriRequest request;
try {
validateParams(metrics);
} catch (Exception e) {
@@ -89,17 +110,19 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
return Collections.singletonList(builder.build());
}
HttpContext httpContext = createHttpContext(prometheusProtocol);
request = createHttpRequest(prometheusProtocol);
HttpRequestWithConfig requestWithConfig = createHttpRequest(prometheusProtocol);
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpContext httpContext = createHttpContext(prometheusProtocol, requestWithConfig.getRequestConfig());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
// Use HttpClientResponseHandler to avoid deprecated execute method and manual resource closing
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
log.debug("Prometheus proxy collect, response status: {}", statusCode);
if (!defaultSuccessStatusCodes.contains(statusCode)) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return Collections.singletonList(builder.build());
return null;
}
String rawTextContent = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
@@ -118,6 +141,11 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
builder.addValueRow(valueRowBuilder.build());
builder.setCode(CollectRep.Code.SUCCESS);
return null;
};
try {
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (ClientProtocolException e1) {
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
log.error("Prometheus proxy collect error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e1);
@@ -143,10 +171,6 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
log.error("Prometheus proxy collect unknown error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} finally {
if (request != null) {
request.abort();
}
}
return Collections.singletonList(builder.build());
}
@@ -162,7 +186,7 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
}
PrometheusProtocol protocol = metrics.getPrometheus();
if (!StringUtils.hasText(protocol.getHost())
|| !StringUtils.hasText(protocol.getPort())) {
|| !StringUtils.hasText(protocol.getPort())) {
throw new Exception("Prometheus collect must has host and port params");
}
if (protocol.getPath() == null
@@ -171,42 +195,51 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
protocol.setPath(protocol.getPath() == null ? RIGHT_DASH : RIGHT_DASH + protocol.getPath().trim());
}
}
/**
* create httpContext
* This method is adapted from PrometheusAutoCollectImpl
* @param protocol prometheus protocol
* @param requestConfig request config
* @return context
*/
public HttpContext createHttpContext(PrometheusProtocol protocol) {
public HttpContext createHttpContext(PrometheusProtocol protocol, RequestConfig requestConfig) {
HttpClientContext clientContext = HttpClientContext.create();
if (requestConfig != null) {
clientContext.setRequestConfig(requestConfig);
}
PrometheusProtocol.Authorization auth = protocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (StringUtils.hasText(auth.getDigestAuthUsername())
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
BasicCredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials =
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword());
provider.setCredentials(AuthScope.ANY, credentials);
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword().toCharArray());
// Fix: Use specific AuthScope instead of AuthScope.ANY
AuthScope authScope = new AuthScope(protocol.getHost(), Integer.parseInt(protocol.getPort()));
provider.setCredentials(authScope, credentials);
AuthCache authCache = new BasicAuthCache();
HttpHost targetHost = new HttpHost(protocol.getHost(), Integer.parseInt(protocol.getPort()));
authCache.put(targetHost, new DigestScheme());
clientContext.setCredentialsProvider(provider);
clientContext.setAuthCache(authCache);
return clientContext;
}
}
return null;
return clientContext;
}
/**
* create http request
* This method is adapted from PrometheusAutoCollectImpl
* @param protocol http params
* @return http uri request
* @return http uri request wrapper
*/
public HttpUriRequest createHttpRequest(PrometheusProtocol protocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(PrometheusProtocol protocol) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// params
Map<String, String> params = protocol.getParams();
if (params != null && !params.isEmpty()) {
@@ -231,7 +264,7 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
if (headers == null || headers.keySet().stream().noneMatch(HttpHeaders.ACCEPT::equalsIgnoreCase)) {
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE + ";version=0.0.4,*/*;q=0.1");
}
if (protocol.getAuthorization() != null) {
PrometheusProtocol.Authorization authorization = protocol.getAuthorization();
if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) {
@@ -241,7 +274,7 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
}
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
if (StringUtils.hasText(authorization.getBasicAuthUsername())
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword();
String encodedAuth = Base64Util.encode(authStr);
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth);
@@ -255,15 +288,15 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
requestBuilder.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
}
}
String uriPath = CollectUtil.replaceUriSpecialChar(protocol.getPath());
if (IpDomainUtil.isHasSchema(protocol.getHost())) {
requestBuilder.setUri(protocol.getHost() + SignConstants.DOUBLE_MARK + protocol.getPort() + uriPath);
} else {
String ipAddressType = IpDomainUtil.checkIpAddressType(protocol.getHost());
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uriPath)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uriPath);
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uriPath)
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uriPath);
boolean ssl = Boolean.parseBoolean(protocol.getSsl());
if (ssl) {
requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri);
@@ -271,24 +304,22 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
}
}
RequestConfig requestConfig;
// custom timeout
int timeout = CollectUtil.getTimeout(protocol.getTimeout());
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.setConnectionRequestTimeout(timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
} else {
RequestConfig requestConfig = RequestConfig.custom()
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
requestConfig = RequestConfig.custom()
.setRedirectsEnabled(true)
.build();
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
/**
@@ -305,4 +336,4 @@ public class PrometheusProxyCollectImpl implements PrometheusCollect {
private static class SingleInstance {
private static final PrometheusProxyCollectImpl INSTANCE = new PrometheusProxyCollectImpl();
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -24,6 +24,15 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.util.Timeout;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
@@ -38,15 +47,6 @@ import org.apache.hertzbeat.common.entity.push.PushMetricsDto;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.http.MediaType;
/**
@@ -55,6 +55,27 @@ import org.springframework.http.MediaType;
@Slf4j
public class PushCollectImpl extends AbstractCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
private static final Map<Long, Long> timeMap = new ConcurrentHashMap<>();
// ms
@@ -83,20 +104,25 @@ public class PushCollectImpl extends AbstractCollect {
Long time = timeMap.getOrDefault(monitorId, curTime - FIRST_COLLECT_INTERVAL);
timeMap.put(monitorId, curTime);
HttpContext httpContext = createHttpContext(pushProtocol);
HttpUriRequest request = createHttpRequest(pushProtocol, monitorId, time);
HttpRequestWithConfig requestWithConfig = createHttpRequest(pushProtocol, monitorId, time);
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpContext httpContext = createHttpContext(pushProtocol, requestWithConfig.getRequestConfig());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<String> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != SUCCESS_CODE) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
return;
return null;
}
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
parseResponse(builder, resp, metrics);
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
};
try {
String resp = CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
if (resp != null) {
parseResponse(builder, resp, metrics);
}
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.error(errorMsg, e);
@@ -111,16 +137,17 @@ public class PushCollectImpl extends AbstractCollect {
return DispatchConstants.PROTOCOL_PUSH;
}
private HttpContext createHttpContext(PushProtocol pushProtocol) {
HttpHost host = new HttpHost(pushProtocol.getHost(), Integer.parseInt(pushProtocol.getPort()));
HttpClientContext httpClientContext = new HttpClientContext();
httpClientContext.setTargetHost(host);
private HttpContext createHttpContext(PushProtocol pushProtocol, RequestConfig requestConfig) {
HttpClientContext httpClientContext = HttpClientContext.create();
if (requestConfig != null) {
httpClientContext.setRequestConfig(requestConfig);
}
return httpClientContext;
}
private HttpUriRequest createHttpRequest(PushProtocol pushProtocol, Long monitorId, Long startTime) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(PushProtocol pushProtocol, Long monitorId, Long startTime) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// uri
String uri = CollectUtil.replaceUriSpecialChar(pushProtocol.getUri());
@@ -142,19 +169,17 @@ public class PushCollectImpl extends AbstractCollect {
requestBuilder.addParameter("time", String.valueOf(startTime));
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
//requestBuilder.setUri(pushProtocol.getUri());
RequestConfig requestConfig = null;
if (DEFAULT_TIMEOUT > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(DEFAULT_TIMEOUT)
.setSocketTimeout(DEFAULT_TIMEOUT)
// Using deprecated setConnectTimeout for request-level override
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(DEFAULT_TIMEOUT))
.setResponseTimeout(Timeout.ofMilliseconds(DEFAULT_TIMEOUT))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
private void parseResponse(CollectRep.MetricsData.Builder builder, String resp, Metrics metric) {
@@ -180,4 +205,4 @@ public class PushCollectImpl extends AbstractCollect {
}
builder.setTime(System.currentTimeMillis());
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -18,20 +18,20 @@
package org.apache.hertzbeat.collector.collect.redfish;
import java.nio.charset.StandardCharsets;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.util.Timeout;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.entity.job.protocol.RedfishProtocol;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.StringEntity;
import org.springframework.http.MediaType;
/**
@@ -58,11 +58,21 @@ public class RedfishClient {
redfishProtocol.getUsername(), redfishProtocol.getPassword(), Integer.parseInt(redfishProtocol.getTimeout()));
}
@SuppressWarnings("deprecation")
public ConnectSession connect() throws Exception {
HttpHost host = new HttpHost(this.host, this.port);
HttpClientContext httpClientContext = new HttpClientContext();
httpClientContext.setTargetHost(host);
RequestBuilder requestBuilder = RequestBuilder.post();
HttpClientContext httpClientContext = HttpClientContext.create();
// Configure RequestConfig if timeout is set
if (this.timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(this.timeout))
.setResponseTimeout(Timeout.ofMilliseconds(this.timeout))
.setRedirectsEnabled(true)
.build();
httpClientContext.setRequestConfig(requestConfig);
}
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.post();
String uri = REDFISH_SESSION_SERVICE;
if (IpDomainUtil.isHasSchema(this.host)) {
@@ -79,37 +89,31 @@ public class RedfishClient {
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
requestBuilder.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
requestBuilder.addHeader(HttpHeaders.CONTENT_ENCODING, StandardCharsets.UTF_8 + "");
// Content-Encoding header is usually for compression, setting charset is done in Content-Type usually,
// but keeping original logic of setting it explicitly if that was the intent, though standard is usually empty or gzip.
// original: requestBuilder.addHeader(HttpHeaders.CONTENT_ENCODING, StandardCharsets.UTF_8 + "");
final String json = "{\"UserName\": \"" + this.username + "\", \"Password\": \"" + this.password + "\"}";
StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8);
requestBuilder.setEntity(entity);
if (this.timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(this.timeout)
.setSocketTimeout(this.timeout)
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
ClassicHttpRequest request = requestBuilder.build();
HttpUriRequest request = requestBuilder.build();
Session session;
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpClientContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<Session> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_CREATED) {
throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
throw new org.apache.hc.client5.http.ClientProtocolException(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
}
String location = response.getFirstHeader(NetworkConstants.LOCATION).getValue();
String auth = response.getFirstHeader(NetworkConstants.X_AUTH_TOKEN).getValue();
session = new Session(auth, location, this.host, this.port);
return new Session(auth, location, this.host, this.port);
};
try {
Session session = CommonHttpClient.getHttpClient().execute(request, httpClientContext, responseHandler);
return new RedfishConnectSession(session);
} catch (Exception e) {
throw new Exception("Redfish session create error: " + e.getMessage());
} finally {
request.abort();
throw new Exception("Redfish session create error: " + e.getMessage(), e);
}
return new RedfishConnectSession(session);
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -18,15 +18,15 @@
package org.apache.hertzbeat.collector.collect.redfish;
import java.nio.charset.StandardCharsets;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.constants.SignConstants;
import org.apache.hertzbeat.common.util.IpDomainUtil;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;
/**
* Redfish connect session
@@ -54,15 +54,20 @@ public class RedfishConnectSession implements ConnectSession {
HttpDelete httpDelete = new HttpDelete(url);
httpDelete.setHeader(NetworkConstants.X_AUTH_TOKEN, session.token());
httpDelete.setHeader(NetworkConstants.LOCATION, session.location());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpDelete)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_OK) {
throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
throw new org.apache.hc.client5.http.ClientProtocolException(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
}
EntityUtils.consume(response.getEntity());
return null;
};
try {
CommonHttpClient.getHttpClient().execute(httpDelete, responseHandler);
} catch (Exception e) {
throw new Exception("Redfish session close error:" + e.getMessage());
} finally {
httpDelete.abort();
throw new Exception("Redfish session close error:" + e.getMessage(), e);
}
}
@@ -84,16 +89,19 @@ public class RedfishConnectSession implements ConnectSession {
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader(NetworkConstants.X_AUTH_TOKEN, session.token());
httpGet.setHeader(NetworkConstants.LOCATION, session.location());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(httpGet)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpClientResponseHandler<String> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != HttpStatus.SC_OK) {
throw new Exception(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
throw new org.apache.hc.client5.http.ClientProtocolException(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
}
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
};
try {
return CommonHttpClient.getHttpClient().execute(httpGet, responseHandler);
} catch (Exception e) {
throw new Exception("Redfish session get resource error:" + e.getMessage());
} finally {
httpGet.abort();
throw new Exception("Redfish session get resource error:" + e.getMessage(), e);
}
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -22,6 +22,10 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hertzbeat.collector.collect.AbstractCollect;
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
@@ -32,10 +36,6 @@ import org.apache.hertzbeat.common.entity.sd.ConnectionConfig;
import org.apache.hertzbeat.common.entity.sd.EurekaDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.XmlUtil;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.util.EntityUtils;
import org.springframework.util.CollectionUtils;
/**
@@ -57,38 +57,55 @@ public class EurekaSdCollectImpl extends AbstractCollect {
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
List<ConnectionConfig> configList = Lists.newArrayList();
HttpUriRequest request = RequestBuilder.get().setUri(metrics.getEureka_sd().getUrl() + APP_LIST_PATH).build();
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
ClassicHttpRequest request = ClassicRequestBuilder.get()
.setUri(metrics.getEureka_sd().getUrl() + APP_LIST_PATH)
.build();
// Use ResponseHandler to avoid manual resource management and deprecated execute methods
HttpClientResponseHandler<String> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != 200) {
log.warn("Failed to fetch eureka sd...");
builder.setMsg("StatusCode " + statusCode);
builder.setCode(CollectRep.Code.FAIL);
return;
return null;
}
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
EurekaDiscoveryResponseEntity responseEntity =
XmlUtil.fromXml(responseBody, EurekaDiscoveryResponseEntity.class);
if (responseEntity == null || CollectionUtils.isEmpty(responseEntity.getApplications())) {
return;
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
};
try {
String responseBody = CommonHttpClient.getHttpClient().execute(request, responseHandler);
if (responseBody != null) {
EurekaDiscoveryResponseEntity responseEntity =
XmlUtil.fromXml(responseBody, EurekaDiscoveryResponseEntity.class);
if (responseEntity == null || CollectionUtils.isEmpty(responseEntity.getApplications())) {
return;
}
responseEntity.getApplications()
.stream()
.filter(application -> !CollectionUtils.isEmpty(application.getInstances()))
.forEach(application -> convertTarget(configList, application));
configList.forEach(config -> {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(config.getHost());
valueRowBuilder.addColumn(config.getPort());
builder.addValueRow(valueRowBuilder.build());
});
}
responseEntity.getApplications()
.stream()
.filter(application -> !CollectionUtils.isEmpty(application.getInstances()))
.forEach(application -> convertTarget(configList, application));
configList.forEach(config -> {
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
valueRowBuilder.addColumn(config.getHost());
valueRowBuilder.addColumn(config.getPort());
builder.addValueRow(valueRowBuilder.build());
});
} catch (IOException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch eureka sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch eureka sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
@@ -108,4 +125,4 @@ public class EurekaSdCollectImpl extends AbstractCollect {
public String supportProtocol() {
return DispatchConstants.PROTOCOL_EUREKA_SD;
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -41,18 +41,18 @@ import org.apache.hertzbeat.common.entity.sd.ServiceDiscoveryResponseEntity;
import org.apache.hertzbeat.common.util.Base64Util;
import org.apache.hertzbeat.common.util.CommonUtil;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.http.HttpHeaders;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriUtils;
@@ -63,6 +63,28 @@ import org.springframework.web.util.UriUtils;
*/
@Slf4j
public class HttpSdCollectImpl extends AbstractCollect {
/**
* Inner class to wrap HTTP request with its configuration
*/
private static class HttpRequestWithConfig {
private final ClassicHttpRequest request;
private final RequestConfig requestConfig;
public HttpRequestWithConfig(ClassicHttpRequest request, RequestConfig requestConfig) {
this.request = request;
this.requestConfig = requestConfig;
}
public ClassicHttpRequest getRequest() {
return request;
}
public RequestConfig getRequestConfig() {
return requestConfig;
}
}
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
}
@@ -71,22 +93,30 @@ public class HttpSdCollectImpl extends AbstractCollect {
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
List<ConnectionConfig> configList = Lists.newArrayList();
HttpUriRequest request = createHttpRequest(metrics.getHttp_sd());
HttpContext httpContext = createHttpContext(metrics.getHttp_sd());
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
int statusCode = response.getStatusLine().getStatusCode();
HttpRequestWithConfig requestWithConfig = createHttpRequest(metrics.getHttp_sd());
if (requestWithConfig == null) {
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg("Failed to create HTTP request");
return;
}
ClassicHttpRequest request = requestWithConfig.getRequest();
HttpContext httpContext = createHttpContext(metrics.getHttp_sd(), requestWithConfig.getRequestConfig());
HttpClientResponseHandler<Void> responseHandler = response -> {
int statusCode = response.getCode();
if (statusCode != 200) {
log.warn("Failed to fetch sd...");
builder.setMsg("StatusCode " + statusCode);
builder.setCode(CollectRep.Code.FAIL);
return;
return null;
}
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
TypeReference<List<ServiceDiscoveryResponseEntity>> typeReference = new TypeReference<>() {};
final List<ServiceDiscoveryResponseEntity> responseEntityList = JsonUtil.fromJson(responseBody, typeReference);
if (CollectionUtils.isEmpty(responseEntityList)) {
return;
return null;
}
responseEntityList.stream()
@@ -100,11 +130,21 @@ public class HttpSdCollectImpl extends AbstractCollect {
valueRowBuilder.addColumn(config.getPort());
builder.addValueRow(valueRowBuilder.build());
});
return null;
};
try {
CommonHttpClient.getHttpClient().execute(request, httpContext, responseHandler);
} catch (IOException e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
} catch (Exception e) {
String errorMsg = CommonUtil.getMessageFromThrowable(e);
log.warn("Failed to fetch sd... {}", errorMsg);
builder.setCode(CollectRep.Code.FAIL);
builder.setMsg(errorMsg);
}
}
@@ -137,17 +177,23 @@ public class HttpSdCollectImpl extends AbstractCollect {
* create httpContext
*
* @param httpSdProtocol http sd protocol
* @param requestConfig request config
* @return context
*/
public HttpContext createHttpContext(HttpProtocol httpSdProtocol) {
public HttpContext createHttpContext(HttpProtocol httpSdProtocol, RequestConfig requestConfig) {
HttpClientContext clientContext = HttpClientContext.create();
if (requestConfig != null) {
clientContext.setRequestConfig(requestConfig);
}
HttpProtocol.Authorization auth = httpSdProtocol.getAuthorization();
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
HttpClientContext clientContext = new HttpClientContext();
if (org.springframework.util.StringUtils.hasText(auth.getDigestAuthUsername())
&& org.springframework.util.StringUtils.hasText(auth.getDigestAuthPassword())) {
CredentialsProvider provider = new BasicCredentialsProvider();
BasicCredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(auth.getDigestAuthUsername(),
auth.getDigestAuthPassword());
auth.getDigestAuthPassword().toCharArray());
URL url;
try {
url = new URL(httpSdProtocol.getUrl());
@@ -159,20 +205,20 @@ public class HttpSdCollectImpl extends AbstractCollect {
provider.setCredentials(authScope, credentials);
clientContext.setCredentialsProvider(provider);
return clientContext;
}
}
return null;
return clientContext;
}
/**
* create http request
*
* @param httpSdProtocol http request set
* @return http uri request
* @return http uri request wrapper
*/
public HttpUriRequest createHttpRequest(HttpProtocol httpSdProtocol) {
RequestBuilder requestBuilder = RequestBuilder.get();
@SuppressWarnings("deprecation")
private HttpRequestWithConfig createHttpRequest(HttpProtocol httpSdProtocol) {
ClassicRequestBuilder requestBuilder = ClassicRequestBuilder.get();
// The default request header can be overridden if customized
// keep-alive
@@ -227,16 +273,17 @@ public class HttpSdCollectImpl extends AbstractCollect {
throw e;
}
RequestConfig requestConfig = null;
// custom timeout
int timeout = CollectUtil.getTimeout(httpSdProtocol.getTimeout(), 0);
if (timeout > 0) {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
requestConfig = RequestConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(timeout))
.setResponseTimeout(Timeout.ofMilliseconds(timeout))
.setRedirectsEnabled(true)
.build();
requestBuilder.setConfig(requestConfig);
}
return requestBuilder.build();
return new HttpRequestWithConfig(requestBuilder.build(), requestConfig);
}
}
}
@@ -29,7 +29,7 @@ import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.CommonThreadPool;
import org.apache.hertzbeat.remoting.RemotingClient;
import org.junit.jupiter.api.BeforeEach;
@@ -113,7 +113,7 @@ class CollectServerTest {
RemotingClient remotingClient = mock(RemotingClient.class);
ReflectionTestUtils.setField(collectServer, "remotingClient", remotingClient);
ClusterMsg.Message message = mock(ClusterMsg.Message.class);
ClusterMessage message = mock(ClusterMessage.class);
collectServer.sendMsg(message);
@@ -135,7 +135,7 @@ class CollectServerTest {
collectNettyEventListener.onChannelActive(channel);
verify(timerDispatch, times(1)).goOnline();
verify(remotingClient, times(1)).sendMsg(any(ClusterMsg.Message.class));
verify(remotingClient, times(1)).sendMsg(any(ClusterMessage.class));
ScheduledExecutorService scheduledExecutor =
(ScheduledExecutorService) ReflectionTestUtils.getField(collectServer, "scheduledExecutor");
@@ -34,7 +34,7 @@ import org.apache.hertzbeat.collector.dispatch.WorkerPool;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -116,7 +116,7 @@ class CollectJobServiceTest {
collectJobService.collectSyncOneTimeJobData(job);
verify(collectServer, times(1)).sendMsg(any(ClusterMsg.Message.class));
verify(collectServer, times(1)).sendMsg(any(ClusterMessage.class));
}
@Test
@@ -137,7 +137,7 @@ class CollectJobServiceTest {
.build();
collectJobService.sendAsyncCollectData(metricsData);
verify(collectServer, times(1)).sendMsg(any(ClusterMsg.Message.class));
verify(collectServer, times(1)).sendMsg(any(ClusterMessage.class));
}
@Test
@@ -100,6 +100,35 @@
</build>
<profiles>
<profile>
<id>dev-driver</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencies>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.oracle.database.nls</groupId>
<artifactId>orai18n</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.ibm.db2</groupId>
<artifactId>jcc</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</profile>
<profile>
<id>inner</id>
<build>
@@ -28,8 +28,6 @@ spring:
jackson:
default-property-inclusion: ALWAYS
# need to disable spring boot mongodb auto config, or default mongodb connection tried and failed...
autoconfigure:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
management:
endpoints:
web:
@@ -55,7 +53,7 @@ spring:
collector:
info:
version: ${COLLECTOR_VERSION:1.7.3}
version: ${COLLECTOR_VERSION:1.8.0}
ip: ${COLLECTOR_IP:}
dispatch:
entrance:
@@ -1,50 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<configuration scan="true">
<springProperty scope="context" name="application_name" source="spring.application.name" defaultValue="collector"/>
<!-- Output logs to ConsoleAppender -->
<appender name="ConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<appender name="CONSOLE_RAW" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--<pattern>%d %p (%file:%line\)- %m%n</pattern>-->
<!-- Format output: %d for date, %thread for thread name, %-5level for level with 5 character width, %msg for log message, %n for new line -->
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="ConsoleAppender" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<includeCallerData>true</includeCallerData>
<appender-ref ref="CONSOLE_RAW"/>
</appender>
<appender name="SystemOutFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- Rolling policy for loggers, by date and by size -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- Archive log file path. %d{yyyy-MM-dd} specifies date format, %i specifies index -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/${application_name}-%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>
<!-- Log retention duration -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>7</maxHistory>
<!-- Maximum size of log retention -->
<totalSizeCap>5GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
<!-- Besides logging by day, log files cannot exceed 200M, if exceeded, log files will start from index 0 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- Append logs -->
<append>true</append>
<!-- Log file format -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger Line:%-3L - %msg%n</pattern>
<charset>utf-8</charset>
@@ -52,32 +32,23 @@
</appender>
<appender name="ErrOutFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/${application_name}-%d{yyyy-MM-dd}-error.%i.log.zip</fileNamePattern>
<!-- Log retention duration -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>7</maxHistory>
<!-- Maximum size of log retention -->
<totalSizeCap>5GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>50MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<!-- Append logs -->
<append>true</append>
<!-- Log file format -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger Line:%-3L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- This log file records error and above levels -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!-- Settings for this logger: for example, all output logs under the org.springframework package must be at level info or above to be output! -->
<!-- This can avoid outputting many common debug information of the spring framework! -->
<logger name="org.springframework" level="info" />
<logger name="org.json" level="error"/>
<logger name="io.netty" level="info"/>
@@ -91,9 +62,9 @@
<logger name="org.mongodb" level="warn"/>
<logger name="io.greptime" level="warn"/>
<!-- Production environment configuration -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ConsoleAppender"/>
<appender-ref ref="SystemOutFileAppender"/>
<appender-ref ref="ErrOutFileAppender"/>
</root>
@@ -101,12 +72,12 @@
<springProfile name="cluster">
<root level="INFO">
<appender-ref ref="ConsoleAppender"/>
<appender-ref ref="SystemOutFileAppender"/>
<appender-ref ref="ErrOutFileAppender"/>
</root>
</springProfile>
<!-- Development environment configuration -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="ConsoleAppender"/>
@@ -115,7 +86,6 @@
</root>
</springProfile>
<!-- Development environment configuration -->
<springProfile name="mysql">
<root level="INFO">
<appender-ref ref="ConsoleAppender"/>
@@ -124,4 +94,4 @@
</root>
</springProfile>
</configuration>
</configuration>
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -17,33 +17,30 @@
package org.apache.hertzbeat.collector.collect.common.http;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
/**
* common http client
* common http client for HttpComponents Client 5.x
*/
@Slf4j
public class CommonHttpClient {
@@ -84,11 +81,13 @@ public class CommonHttpClient {
/**
* ssl supported version
* Note: SSLv3 is often disabled in modern JDKs
*/
private static final String[] SUPPORTED_SSL = {"TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3"};
static {
try {
// 1. SSL Context Configuration
SSLContext sslContext = SSLContexts.createDefault();
X509TrustManager x509TrustManager = new X509TrustManager() {
@Override
@@ -102,7 +101,7 @@ public class CommonHttpClient {
for (X509Certificate certificate : x509Certificates) {
Date deadline = certificate.getNotAfter();
if (deadline != null && now.after(deadline)) {
throw new CertificateExpiredException();
throw new CertificateExpiredException("Server certificate expired at " + deadline);
}
}
}
@@ -112,57 +111,63 @@ public class CommonHttpClient {
public X509Certificate[] getAcceptedIssuers() { return null; }
};
sslContext.init(null, new TrustManager[]{x509TrustManager}, null);
SSLConnectionSocketFactory sslFactory = new SSLConnectionSocketFactory(sslContext, SUPPORTED_SSL, null, new NoopHostnameVerifier());
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", sslFactory)
// 2. TlsStrategy Configuration (Replaces deprecated SSLConnectionSocketFactory/Registry)
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
sslContext,
SUPPORTED_SSL,
null, // Supported cipher suites (null = default)
SSLBufferMode.STATIC,
NoopHostnameVerifier.INSTANCE
);
// 3. Connection Manager Configuration (Using Builder)
ConnectionConfig connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(CONNECT_TIMEOUT))
.setSocketTimeout(Timeout.ofMilliseconds(SOCKET_TIMEOUT))
.setValidateAfterInactivity(TimeValue.ofMilliseconds(INACTIVITY_VALIDATED_TIME))
.build();
connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tlsStrategy)
.setMaxConnTotal(MAX_TOTAL_CONNECTIONS)
.setMaxConnPerRoute(MAX_PER_ROUTE_CONNECTIONS)
.setDefaultConnectionConfig(connectionConfig)
.build();
// 4. Request Config (Connection Request Timeout & Redirects)
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(REQUIRE_CONNECT_TIMEOUT)
.setConnectTimeout(CONNECT_TIMEOUT)
.setSocketTimeout(SOCKET_TIMEOUT)
// auto redirect when 301 302 response status
.setConnectionRequestTimeout(Timeout.ofMilliseconds(REQUIRE_CONNECT_TIMEOUT))
.setRedirectsEnabled(true)
.build();
// connection pool
connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
connectionManager.setDefaultMaxPerRoute(MAX_PER_ROUTE_CONNECTIONS);
connectionManager.setValidateAfterInactivity(INACTIVITY_VALIDATED_TIME);
// 5. Build HttpClient
httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
// clean up unavailable expired connections
.evictExpiredConnections()
// clean up available but idle connections
.evictIdleConnections(100, TimeUnit.SECONDS)
.evictIdleConnections(TimeValue.ofSeconds(100))
.build();
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("http-connection-pool-cleaner-%d")
.setDaemon(true)
.build();
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1, threadFactory);
scheduledExecutor.scheduleWithFixedDelay(() -> {
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(40, TimeUnit.SECONDS);
}, 40L, 40L, TimeUnit.SECONDS);
// shutdown hook
// Shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(CommonHttpClient::close));
} catch (Exception ignored) {}
} catch (Exception e) {
log.error("Initialize CommonHttpClient error", e);
}
}
public static CloseableHttpClient getHttpClient() {
return httpClient;
}
public static void close() {
try {
httpClient.close();
if (httpClient != null) {
httpClient.close();
}
} catch (Exception e) {
log.error("close http client error", e);
}
}
}
}
@@ -17,65 +17,48 @@
package org.apache.hertzbeat.collector.dispatch;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Collection task worker thread pool
* Collection task worker thread pool with Virtual Threads
*/
@Component
@Slf4j
public class WorkerPool implements DisposableBean {
private ThreadPoolExecutor workerExecutor;
private ExecutorService workerExecutor;
public WorkerPool() {
initWorkExecutor();
}
private void initWorkExecutor() {
// thread factory
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler((thread, throwable) -> {
log.error("[Important] WorkerPool workerExecutor has uncaughtException.", throwable);
log.error("Thread Name {} : {}", thread.getName(), throwable.getMessage(), throwable);
})
.setDaemon(true)
.setNameFormat("collect-worker-%d")
.build();
int coreSize = Math.max(2, Runtime.getRuntime().availableProcessors());
int maxSize = Runtime.getRuntime().availableProcessors() * 16;
workerExecutor = new ThreadPoolExecutor(coreSize,
maxSize,
10,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
ThreadFactory virtualThreadFactory = Thread.ofVirtual()
.name("collect-vt-", 0)
.factory();
workerExecutor = Executors.newThreadPerTaskExecutor(virtualThreadFactory);
log.info("WorkerPool initialized with JDK 25 Virtual Threads successfully.");
}
/**
* Run the collection task thread
*
* @param runnable Task
* @throws RejectedExecutionException when thread pool full
* Run the collection task
* @param runnable Task
*/
public void executeJob(Runnable runnable) throws RejectedExecutionException {
public void executeJob(Runnable runnable) {
workerExecutor.execute(runnable);
}
@Override
public void destroy() throws Exception {
public void destroy() {
if (workerExecutor != null) {
workerExecutor.shutdownNow();
workerExecutor.close();
}
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -18,7 +18,6 @@
package org.apache.hertzbeat.collector.dispatch.entrance;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.protobuf.ByteString;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.CollectorInfoProperties;
@@ -33,7 +32,7 @@ import org.apache.hertzbeat.collector.dispatch.entrance.processor.GoOnlineProces
import org.apache.hertzbeat.collector.dispatch.entrance.processor.HeartbeatProcessor;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.entity.dto.CollectorInfo;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.CommonThreadPool;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.RemotingClient;
@@ -65,7 +64,7 @@ public class CollectServer implements CommandLineRunner {
private final TimerDispatch timerDispatch;
private final CollectorInfoProperties infoProperties;
private RemotingClient remotingClient;
private ScheduledExecutorService scheduledExecutor;
@@ -97,13 +96,13 @@ public class CollectServer implements CommandLineRunner {
nettyClientConfig.setServerPort(nettyProperties.getManagerPort());
this.remotingClient = new NettyRemotingClient(nettyClientConfig, new CollectNettyEventListener(), threadPool);
this.remotingClient.registerProcessor(ClusterMsg.MessageType.HEARTBEAT, new HeartbeatProcessor());
this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_CYCLIC_TASK, new CollectCyclicDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.DELETE_CYCLIC_TASK, new DeleteCyclicTaskProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.ISSUE_ONE_TIME_TASK, new CollectOneTimeDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_OFFLINE, new GoOfflineProcessor());
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_ONLINE, new GoOnlineProcessor());
this.remotingClient.registerProcessor(ClusterMsg.MessageType.GO_CLOSE, new GoCloseProcessor(this));
this.remotingClient.registerProcessor(ClusterMessage.MessageType.HEARTBEAT, new HeartbeatProcessor());
this.remotingClient.registerProcessor(ClusterMessage.MessageType.ISSUE_CYCLIC_TASK, new CollectCyclicDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMessage.MessageType.DELETE_CYCLIC_TASK, new DeleteCyclicTaskProcessor(this));
this.remotingClient.registerProcessor(ClusterMessage.MessageType.ISSUE_ONE_TIME_TASK, new CollectOneTimeDataProcessor(this));
this.remotingClient.registerProcessor(ClusterMessage.MessageType.GO_OFFLINE, new GoOfflineProcessor());
this.remotingClient.registerProcessor(ClusterMessage.MessageType.GO_ONLINE, new GoOnlineProcessor());
this.remotingClient.registerProcessor(ClusterMessage.MessageType.GO_CLOSE, new GoCloseProcessor(this));
}
public void shutdown() {
@@ -116,7 +115,7 @@ public class CollectServer implements CommandLineRunner {
return collectJobService;
}
public void sendMsg(final ClusterMsg.Message message) {
public void sendMsg(final ClusterMessage message) {
this.remotingClient.sendMsg(message);
}
@@ -143,10 +142,10 @@ public class CollectServer implements CommandLineRunner {
.build();
timerDispatch.goOnline();
// send online message
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setIdentity(identity)
.setType(ClusterMsg.MessageType.GO_ONLINE)
.setMsg(ByteString.copyFromUtf8(JsonUtil.toJson(collectorInfo)))
ClusterMessage message = ClusterMessage.builder()
.identity(identity)
.type(ClusterMessage.MessageType.GO_ONLINE)
.msg(JsonUtil.toJsonBytes(collectorInfo))
.build();
CollectServer.this.sendMsg(message);
@@ -163,13 +162,13 @@ public class CollectServer implements CommandLineRunner {
// schedule send heartbeat message
scheduledExecutor.scheduleAtFixedRate(() -> {
try {
ClusterMsg.Message heartbeat = ClusterMsg.Message.newBuilder()
.setIdentity(identity)
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.HEARTBEAT)
ClusterMessage heartbeat = ClusterMessage.builder()
.identity(identity)
.direction(ClusterMessage.Direction.REQUEST)
.type(ClusterMessage.MessageType.HEARTBEAT)
.build();
CollectServer.this.sendMsg(heartbeat);
log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis());
log.info("collector send cluster server heartbeat, time: {}.", System.currentTimeMillis());
} catch (Exception e) {
log.error("schedule send heartbeat to server error.{}", e.getMessage());
}
@@ -182,4 +181,4 @@ public class CollectServer implements CommandLineRunner {
log.info("handle idle event triggered. collector is going offline.");
}
}
}
}
@@ -6,7 +6,7 @@
* (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
* 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,
@@ -17,7 +17,6 @@
package org.apache.hertzbeat.collector.dispatch.entrance.internal;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.DispatchProperties;
import org.apache.hertzbeat.collector.dispatch.WorkerPool;
@@ -25,7 +24,7 @@ import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.entity.message.CollectRep;
import org.apache.hertzbeat.common.util.ArrowUtil;
import org.apache.hertzbeat.common.util.IpDomainUtil;
@@ -117,10 +116,10 @@ public class CollectJobService {
workerPool.executeJob(() -> {
List<CollectRep.MetricsData> metricsDataList = this.collectSyncJobData(oneTimeJob);
byte[] msg = ArrowUtil.serializeMetricsData(metricsDataList);
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setMsg(ByteString.copyFrom(msg))
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.RESPONSE_ONE_TIME_TASK_DATA)
ClusterMessage message = ClusterMessage.builder()
.msg(msg)
.direction(ClusterMessage.Direction.REQUEST)
.type(ClusterMessage.MessageType.RESPONSE_ONE_TIME_TASK_DATA)
.build();
this.collectServer.sendMsg(message);
});
@@ -153,22 +152,22 @@ public class CollectJobService {
*/
public void sendAsyncCollectData(CollectRep.MetricsData metricsData) {
byte[] msg = ArrowUtil.serializeMetricsData(List.of(metricsData));
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setIdentity(collectorIdentity)
.setMsg(ByteString.copyFrom(msg))
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_DATA)
ClusterMessage message = ClusterMessage.builder()
.identity(collectorIdentity)
.msg(msg)
.direction(ClusterMessage.Direction.REQUEST)
.type(ClusterMessage.MessageType.RESPONSE_CYCLIC_TASK_DATA)
.build();
this.collectServer.sendMsg(message);
}
public void sendAsyncServiceDiscoveryData(CollectRep.MetricsData metricsData) {
byte[] msg = ArrowUtil.serializeMetricsData(List.of(metricsData));
ClusterMsg.Message message = ClusterMsg.Message.newBuilder()
.setIdentity(collectorIdentity)
.setMsg(ByteString.copyFrom(msg))
.setDirection(ClusterMsg.Direction.REQUEST)
.setType(ClusterMsg.MessageType.RESPONSE_CYCLIC_TASK_SD_DATA)
ClusterMessage message = ClusterMessage.builder()
.identity(collectorIdentity)
.msg(msg)
.direction(ClusterMessage.Direction.REQUEST)
.type(ClusterMessage.MessageType.RESPONSE_CYCLIC_TASK_SD_DATA)
.build();
this.collectServer.sendMsg(message);
}
@@ -184,4 +183,4 @@ public class CollectJobService {
public void setCollectServer(CollectServer collectServer) {
this.collectServer = collectServer;
}
}
}
@@ -21,7 +21,7 @@ import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -37,8 +37,8 @@ public class CollectCyclicDataProcessor implements NettyRemotingProcessor {
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
Job job = JsonUtil.fromJson(message.getMsg().toStringUtf8(), Job.class);
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
Job job = JsonUtil.fromJson(message.getMsg(), Job.class);
if (job == null) {
log.error("collector receive cyclic task job is null");
return null;
@@ -20,7 +20,7 @@ package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import io.netty.channel.ChannelHandlerContext;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.common.entity.job.Job;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -35,8 +35,8 @@ public class CollectOneTimeDataProcessor implements NettyRemotingProcessor {
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
Job oneTimeJob = JsonUtil.fromJson(message.getMsg().toStringUtf8(), Job.class);
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
Job oneTimeJob = JsonUtil.fromJson(message.getMsg(), Job.class);
collectServer.getCollectJobService().collectSyncOneTimeJobData(oneTimeJob);
return null;
}
@@ -21,7 +21,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -39,9 +39,9 @@ public class DeleteCyclicTaskProcessor implements NettyRemotingProcessor {
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
TypeReference<List<Long>> typeReference = new TypeReference<>() {};
List<Long> jobIds = JsonUtil.fromJson(message.getMsg().toStringUtf8(), typeReference);
List<Long> jobIds = JsonUtil.fromJson(message.getMsgString(), typeReference);
if (jobIds == null || jobIds.isEmpty()) {
log.error("collector receive delete cyclic task job ids is null");
return null;
@@ -22,7 +22,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.entrance.CollectServer;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
import org.springframework.boot.SpringApplication;
@@ -41,12 +41,12 @@ public class GoCloseProcessor implements NettyRemotingProcessor {
}
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
if (message.getMsg().toStringUtf8().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go close. {}", message.getMsg());
if (message.getMsgString().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go close. {}", message.getMsgString());
}
this.timerDispatch.goOffline();
this.collectServer.shutdown();
@@ -17,12 +17,12 @@
package org.apache.hertzbeat.collector.dispatch.entrance.processor;
import com.google.protobuf.ByteString;
import io.netty.channel.ChannelHandlerContext;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.timer.TimerDispatch;
import org.apache.hertzbeat.common.constants.CommonConstants;
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
import org.apache.hertzbeat.common.entity.message.ClusterMessage;
import org.apache.hertzbeat.common.support.SpringContextHolder;
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
@@ -36,20 +36,21 @@ public class GoOfflineProcessor implements NettyRemotingProcessor {
private TimerDispatch timerDispatch;
@Override
public ClusterMsg.Message handle(ChannelHandlerContext ctx, ClusterMsg.Message message) {
public ClusterMessage handle(ChannelHandlerContext ctx, ClusterMessage message) {
if (this.timerDispatch == null) {
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
}
timerDispatch.goOffline();
log.info("receive offline message and handle success");
if (message.getMsg().toStringUtf8().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go offline. {}", message.getMsg());
if (message.getMsgString().contains(CommonConstants.COLLECTOR_AUTH_FAILED)) {
log.error("[Auth Failed]receive client auth failed message and go offline. {}", message.getMsgString());
return null;
}
return ClusterMsg.Message.newBuilder()
.setIdentity(message.getIdentity())
.setDirection(ClusterMsg.Direction.RESPONSE)
.setMsg(ByteString.copyFromUtf8(String.valueOf(CommonConstants.SUCCESS_CODE)))
return ClusterMessage.builder()
.identity(message.getIdentity())
.direction(ClusterMessage.Direction.RESPONSE)
.type(ClusterMessage.MessageType.GO_OFFLINE)
.msg(String.valueOf(CommonConstants.SUCCESS_CODE).getBytes(StandardCharsets.UTF_8))
.build();
}
}

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