mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
159
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09103ae89c | ||
|
|
5cdfe545b5 | ||
|
+16 |
9be7295084 | ||
|
|
6905bcc986 | ||
|
|
1050032a13 | ||
|
|
9b223dd217 | ||
|
|
0a1b5961e9 | ||
|
|
a8a70b76d7 | ||
|
+1 |
05b63903b5 | ||
|
|
7d8b55dfb8 | ||
|
|
72a3e59ca6 | ||
|
|
3ab94a751b | ||
|
|
66049f189b | ||
|
|
69ec3f8751 | ||
|
|
feb43c5678 | ||
|
|
1fae67282d | ||
|
|
886f0b74f6 | ||
|
|
32e1a43aa5 | ||
|
|
dcd806802e | ||
|
|
5746cbbbd7 | ||
|
|
e23903fd0c | ||
|
|
780710f226 | ||
|
|
4607ede541 | ||
|
|
67ad4b04ef | ||
|
|
6432d689dd | ||
|
|
2eb856fffb | ||
|
|
56312831ca | ||
|
|
fe54d42bc0 | ||
|
|
2ea24c1b3a | ||
|
|
e0040e2ede | ||
|
|
0ac0fd4a8d | ||
|
|
0ef87bcc08 | ||
|
|
660bda6e14 | ||
|
|
242cf40135 | ||
|
|
7319c55430 | ||
|
|
5ea29f610b | ||
|
|
b74aff0500 | ||
|
|
d2e8f2f4aa | ||
|
|
28681a1f9e | ||
|
|
6814c7c357 | ||
|
|
f475540508 | ||
|
|
44660a5042 | ||
|
|
339386fd2b | ||
|
|
faa7cb71db | ||
|
|
014a376c30 | ||
|
|
377172e44a | ||
|
|
5b9e19cc30 | ||
|
|
0457ec7d16 | ||
|
|
2947ee218e | ||
|
|
4e841a9fb1 | ||
|
|
b7ecb9b2ec | ||
|
|
7e1873bc10 | ||
|
|
8eb60210a3 | ||
|
|
d608f732f7 | ||
|
|
2d9a2955ef | ||
|
|
cae4423f30 | ||
|
|
f0e53a9fb3 | ||
|
|
79da1a198b | ||
|
|
64cb804147 | ||
|
|
344cb25229 | ||
|
|
74d21a7acd | ||
|
|
f315bf9607 | ||
|
|
810d54bef5 | ||
|
|
48eafa1382 | ||
|
|
4b6c1e0f08 | ||
|
|
edfd857030 | ||
|
|
408a06d3eb | ||
|
|
fd3a2f0569 | ||
|
|
7d8ba72717 | ||
|
|
f936b50ce9 | ||
|
|
93f5da2d02 | ||
|
|
bc06c268ca | ||
|
|
9e6f2762c0 | ||
|
|
6ac8a42bd3 | ||
|
|
89647d96c7 | ||
|
|
0b0fac4a41 | ||
|
|
f59e15bbbe | ||
|
|
b7398b1bf9 | ||
|
|
0082e724bc | ||
|
|
57a2daf30d | ||
|
|
db90f29cd0 | ||
|
|
00fd8fa1da | ||
|
|
60100eb980 | ||
|
|
a6eb6be1ab | ||
|
|
5eff094af6 | ||
|
|
3b126f53d2 | ||
|
|
1a5b722994 | ||
|
|
5fe20dff05 | ||
|
|
04cfdae1e5 | ||
|
|
8776f98cf5 | ||
|
|
da484aef26 | ||
|
|
e8ebc65053 | ||
|
|
bd08de41d5 | ||
|
|
18f8a541f0 | ||
|
|
007ecd2db1 | ||
|
|
89aaad2401 | ||
|
|
17d836752e | ||
|
|
3949b73751 | ||
|
|
2731504278 | ||
|
|
babbfaea68 | ||
|
|
9f7b9994b8 | ||
|
|
d156c10aed | ||
|
|
9f3329c85f | ||
|
|
fd1c58f68d | ||
|
|
6d1aa503df | ||
|
|
a58ad35c8b | ||
|
|
64cd5f9658 | ||
|
|
a13dd89fd2 | ||
|
|
2f5cfa9d5d | ||
|
|
a9dc7bf7f3 | ||
|
|
c1492deaa0 | ||
|
|
2252b2a769 | ||
|
|
73f71503f5 | ||
|
|
2e224ce626 | ||
|
|
e5406d57c6 | ||
|
|
433a83303f | ||
|
|
f309d5eafd | ||
|
|
986103395b | ||
|
|
d569721e01 | ||
|
|
08cac3c6cc | ||
|
|
f80d066851 | ||
|
|
d6584093f5 | ||
|
|
fb03a57725 | ||
|
|
779272d65f | ||
|
|
c21a2952a5 | ||
|
|
b46d049158 | ||
|
|
dc52c88d88 | ||
|
|
df4e947c8a | ||
|
|
a4ec6c1f66 | ||
|
|
a8dd57d009 | ||
|
|
af3eabdd1c | ||
|
|
d6b300d3df | ||
|
|
b5789538e6 | ||
|
|
d6c5b2b8a2 | ||
|
|
3a04647c3c | ||
|
|
d2999c1aa6 | ||
|
|
b3dbc7092a | ||
|
|
edb3cb6b6a | ||
|
|
4dd78b2e98 | ||
|
|
2f91f56b7b | ||
|
|
be3dac488b | ||
|
|
553bdbaf9f | ||
|
|
40d3f1243b | ||
|
|
740e3f8385 | ||
|
|
47d089dff7 | ||
|
|
c6f1f07155 | ||
|
|
0ebd3499b7 | ||
|
|
c88d4a7b0e | ||
|
|
8bebae9db6 | ||
|
|
b6b1a48a5c | ||
|
|
63ed50ef45 | ||
|
|
e63715d1b0 | ||
|
|
edf3eac6d6 | ||
|
|
743f285c8d | ||
|
|
458433464c | ||
|
|
64740a4f85 | ||
|
|
b5a2bce261 | ||
|
|
5e351c854b | ||
|
|
0875bea046 |
+101
-4
@@ -6,6 +6,7 @@
|
||||
"files": [
|
||||
"README.md",
|
||||
"README_CN.md",
|
||||
"README_JP.md"
|
||||
"home/src/pages/team/index.jsx"
|
||||
],
|
||||
"imageSize": 100,
|
||||
@@ -1269,10 +1270,10 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "a-little-fool",
|
||||
"name": "小笨蛋",
|
||||
"login": "lynx009",
|
||||
"name": "lynx009",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/105542329?v=4",
|
||||
"profile": "https://github.com/a-little-fool",
|
||||
"profile": "https://github.com/lynx009",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
@@ -1441,7 +1442,7 @@
|
||||
},
|
||||
{
|
||||
"login": "Calvin979",
|
||||
"name": "Naruse",
|
||||
"name": "Calvin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/131688897?v=4",
|
||||
"profile": "https://github.com/Calvin979",
|
||||
"contributions": [
|
||||
@@ -2504,6 +2505,102 @@
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "RainBondsongyg",
|
||||
"name": "songyg",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/94501396?v=4",
|
||||
"profile": "https://github.com/RainBondsongyg",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lx1229",
|
||||
"name": "Liuxin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/44620005?v=4",
|
||||
"profile": "https://github.com/lx1229",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yy549159265",
|
||||
"name": "yy549159265",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/40821310?v=4",
|
||||
"profile": "https://github.com/yy549159265",
|
||||
"contributions": [
|
||||
"code",
|
||||
"test",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cto-huhang",
|
||||
"name": "cto-huhang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/53338629?v=4",
|
||||
"profile": "https://github.com/cto-huhang",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Saramanda9988",
|
||||
"name": "LunaRain_079",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/176664901?v=4",
|
||||
"profile": "https://github.com/Saramanda9988",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "delei",
|
||||
"name": "DeleiGuo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17263766?v=4",
|
||||
"profile": "https://github.com/delei",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code",
|
||||
"test",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chingjustwe",
|
||||
"name": "Rocky, Chi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13643747?v=4",
|
||||
"profile": "https://github.com/chingjustwe",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rowankid",
|
||||
"name": "Wenqi Luo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18652781?v=4",
|
||||
"profile": "https://github.com/rowankid",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tuzuy",
|
||||
"name": "tuzuy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/95274591?v=4",
|
||||
"profile": "https://github.com/tuzuy",
|
||||
"contributions": [
|
||||
"code",
|
||||
"design"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "carlpinto25",
|
||||
"name": "carl pinto",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/117299909?v=4",
|
||||
"profile": "https://github.com/carlpinto25",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#
|
||||
|
||||
github:
|
||||
description: Apache HertzBeat(incubating) is a real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
description: Real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
homepage: https://hertzbeat.apache.org/
|
||||
labels:
|
||||
- monitoring
|
||||
@@ -52,6 +52,9 @@ github:
|
||||
required_pull_request_reviews:
|
||||
dismiss_stale_reviews: true
|
||||
required_approving_review_count: 1
|
||||
# enable GitHub Dependabot to create PRs for security alerts but not for every dependency update
|
||||
dependabot_alerts: true
|
||||
dependabot_updates: false
|
||||
notifications:
|
||||
commits: notifications@hertzbeat.apache.org
|
||||
issues: notifications@hertzbeat.apache.org
|
||||
|
||||
@@ -67,6 +67,7 @@ header:
|
||||
- 'karma.conf.js'
|
||||
- 'proxy.conf.js'
|
||||
- '.helmignore'
|
||||
- 'script/helm/**'
|
||||
- 'web-app/src/app/core/**'
|
||||
- 'web-app/src/app/layout/**'
|
||||
- 'web-app/src/app/routes/exception/**'
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
Apache HertzBeat (incubating) is an effort undergoing incubation at the Apache
|
||||
Software Foundation (ASF), sponsored by the Apache Incubator PMC.
|
||||
|
||||
Incubation is required of all newly accepted projects until a further review
|
||||
indicates that the infrastructure, communications, and decision making process
|
||||
have stabilized in a manner consistent with other successful ASF projects.
|
||||
|
||||
While incubation status is not necessarily a reflection of the completeness
|
||||
or stability of the code, it does indicate that the project has yet to be
|
||||
fully endorsed by the ASF.
|
||||
@@ -1,4 +1,4 @@
|
||||
Apache HertzBeat (incubating)
|
||||
Apache HertzBeat™
|
||||
Copyright 2024-2025 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<b><a href="README.md">English</a></b> | <a href="README_CN.md">中文</a> | <a href="README_JP.md">日本語</a>
|
||||
</p>
|
||||
|
||||
> A real-time monitoring system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
> A real-time observability system with agentless, performance cluster, prometheus-compatible, custom monitoring and status page building capabilities.
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
## 🎡 <font color="green">Introduction</font>
|
||||
|
||||
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating) is an easy-to-use, open source, real-time monitoring 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 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.
|
||||
|
||||
### Features
|
||||
|
||||
@@ -527,6 +527,8 @@ Thanks to these wonderful people, welcome to join us:
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -570,8 +572,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
+10
-8
@@ -9,7 +9,7 @@
|
||||
<a href="README.md">English</a> | <b><a href="README_CN.md">中文</a></b> | <a href="README_JP.md">日本語</a>
|
||||
</p>
|
||||
|
||||
> 实时监控系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
|
||||
> 实时观测系统,无需 Agent,性能集群,兼容 Prometheus,自定义监控和状态页构建能力。
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
@@ -28,16 +28,16 @@
|
||||
|
||||
## 🎡 <font color="green">介绍</font>
|
||||
|
||||
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating)是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
|
||||
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
|
||||
|
||||
### 特点
|
||||
|
||||
- 集 **监控+告警+通知** 为一体,支持对应用服务,应用程序,数据库,缓存,操作系统,大数据,中间件,Web 服务器,云原生,网络,自定义等监控阈值告警通知一步到位。
|
||||
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,零上手学习成本。
|
||||
- 将 `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模板 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需配置下就能立刻适配一款 `K8s` 或 `Docker` 等新的监控类型吗?
|
||||
- 易用友好,无需 `Agent`,全 `WEB` 页面操作,鼠标点一点就能监控告警,无需学习成本。
|
||||
- 将 `Http, Jmx, Ssh, Snmp, Jdbc, Prometheus` 等协议规范可配置化,只需在浏览器配置监控模板 `YML` 就能使用这些协议去自定义采集想要的指标。您相信只需简单配置即可快速适配一款 `K8s` 或 `Docker` 等新的监控类型吗?
|
||||
- 兼容 `Prometheus` 的系统生态并且更多,只需页面操作就可以监控 `Prometheus` 所能监控的。
|
||||
- 高性能,支持多采集器集群横向扩展,支持多隔离网络监控,云边协同。
|
||||
- 自由的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
|
||||
- 灵活的告警阈值规则,`邮件` `Discord` `Slack` `Telegram` `钉钉` `微信` `飞书` `短信` `Webhook` `Server酱` 等方式消息及时送达。
|
||||
- 提供强大的状态页构建能力,轻松向用户传达您产品服务的实时状态。
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
|
||||
- `-e IDENTITY=custom-collector-name` : 配置此采集器的唯一性标识符名称,多个采集器名称不能相同,建议自定义英文名称。
|
||||
- `-e MODE=public` : 配置运行模式(public or private), 公共集群模式或私有云边模式。
|
||||
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertaBeat 服务的对外 IP。
|
||||
- `-e MANAGER_HOST=127.0.0.1` : 配置连接主 HertzBeat 服务的对外 IP。
|
||||
- `-e MANAGER_PORT=1158` : 配置连接主 HertzBeat 服务的对外端口,默认1158。
|
||||
|
||||
|
||||
@@ -525,6 +525,8 @@ Thanks these wonderful people, welcome to join us:
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -570,8 +572,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
+8
-2
@@ -525,6 +525,12 @@ Thanks these wonderful people, welcome to join us:
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/boyucjz"><img src="https://avatars.githubusercontent.com/u/18730041?v=4?s=100" width="100px;" alt="boyucjz"/><br /><sub><b>boyucjz</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=boyucjz" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cyanty"><img src="https://avatars.githubusercontent.com/u/153884653?v=4?s=100" width="100px;" alt="Cyanty"/><br /><sub><b>Cyanty</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=Cyanty" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KevinLLF"><img src="https://avatars.githubusercontent.com/u/85452733?v=4?s=100" width="100px;" alt="Jay丿167"/><br /><sub><b>Jay丿167</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=KevinLLF" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Duansg"><img src="https://avatars.githubusercontent.com/u/112607719?v=4?s=100" width="100px;" alt="Duansg"/><br /><sub><b>Duansg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=Duansg" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xiaomizhou2"><img src="https://avatars.githubusercontent.com/u/47807926?v=4?s=100" width="100px;" alt="zhangyaxi"/><br /><sub><b>zhangyaxi</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Code">💻</a> <a href="https://github.com/apache/hertzbeat/commits?author=xiaomizhou2" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RainBondsongyg"><img src="https://avatars.githubusercontent.com/u/94501396?v=4?s=100" width="100px;" alt="songyg"/><br /><sub><b>songyg</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=RainBondsongyg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lx1229"><img src="https://avatars.githubusercontent.com/u/44620005?v=4?s=100" width="100px;" alt="Liuxin"/><br /><sub><b>Liuxin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=lx1229" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -570,8 +576,8 @@ HertzBeat is built on so many great open source projects, thanks to them!
|
||||
<p align="left">
|
||||
<img src="./home/static/img/home/cncf-landscape-left-logo.svg" width="300"> <img src="./home/static/img/home/cncf-right-logo.svg" width="345" />
|
||||
<br /><br />
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/guide?item=observability-and-analysis--monitoring--hertzbeat#observability-and-analysis--monitoring">
|
||||
CNCF Observability And Analysis - Monitoring Landscape.</a>
|
||||
HertzBeat has been included in the <a href="https://landscape.cncf.io/?item=observability-and-analysis--observability--hertzbeat">
|
||||
CNCF Observability And Analysis - Observability Landscape.</a>
|
||||
</p>
|
||||
|
||||
## 🛡️ License
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Security Policy
|
||||
|
||||
Apache HertzBeat follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
|
||||
Apache HertzBeat™ follows the [ASF vulnerability handling process](https://apache.org/security/#vulnerability-handling)
|
||||
|
||||
## Supported Versions
|
||||
|
||||
@@ -17,6 +17,6 @@ To report a new vulnerability you have discovered please follow the [ASF vulnera
|
||||
|
||||
## Extra Information
|
||||
|
||||
- [Apache HertzBeat Security Model](https://hertzbeat.apache.org/docs/help/security_model)
|
||||
- [Apache HertzBeat™ Security Model](https://hertzbeat.apache.org/docs/help/security_model)
|
||||
- [ASF Security](https://www.apache.org/security/)
|
||||
- [Apache HertzBeat Document](https://hertzbeat.apache.org/docs/)
|
||||
- [Apache HertzBeat™ Document](https://hertzbeat.apache.org/docs/)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat</artifactId>
|
||||
<version>2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>hertzbeat-ai-agent</artifactId>
|
||||
<version>${hertzbeat.version}</version>
|
||||
<properties>
|
||||
<spring-ai.version>1.0.1</spring-ai.version>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-launcher</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-alerter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.usthe.sureness</groupId>
|
||||
<artifactId>spring-boot3-starter-sureness</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.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);
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* 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 com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.usthe.sureness.mgt.SurenessSecurityManager;
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import io.modelcontextprotocol.spec.McpError;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpServerSession;
|
||||
import io.modelcontextprotocol.spec.McpServerTransport;
|
||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Custom Server-Sent Events transport provider for Model Context Protocol.
|
||||
*/
|
||||
@Slf4j
|
||||
public class CustomSseServerTransport implements McpServerTransportProvider {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final String messageEndpoint;
|
||||
private final String sseEndpoint;
|
||||
private final String baseUrl;
|
||||
@Getter
|
||||
private final RouterFunction<ServerResponse> routerFunction;
|
||||
@Setter
|
||||
private McpServerSession.Factory sessionFactory;
|
||||
private final Map<String, Object> sessionRequest = new HashMap<>();
|
||||
private final ConcurrentHashMap<String, McpServerSession> sessions;
|
||||
private volatile boolean isClosing;
|
||||
|
||||
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
|
||||
this(objectMapper, messageEndpoint, "/sse");
|
||||
}
|
||||
|
||||
|
||||
public CustomSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
|
||||
this(objectMapper, "", messageEndpoint, sseEndpoint);
|
||||
}
|
||||
|
||||
public CustomSseServerTransport(ObjectMapper objectMapper, String baseUrl, String messageEndpoint, String sseEndpoint) {
|
||||
this.sessions = new ConcurrentHashMap();
|
||||
this.isClosing = false;
|
||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||
Assert.notNull(baseUrl, "Message base URL must not be null");
|
||||
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
|
||||
Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
|
||||
this.objectMapper = objectMapper;
|
||||
this.baseUrl = baseUrl;
|
||||
this.messageEndpoint = messageEndpoint;
|
||||
this.sseEndpoint = sseEndpoint;
|
||||
this.routerFunction = RouterFunctions.route().GET(this.sseEndpoint, this::handleSseConnection).POST(this.messageEndpoint, this::handleMessage).build();
|
||||
}
|
||||
|
||||
public Mono<Void> notifyClients(String method, Object params) {
|
||||
if (this.sessions.isEmpty()) {
|
||||
log.debug("No active sessions to broadcast message to");
|
||||
return Mono.empty();
|
||||
} else {
|
||||
log.debug("Attempting to broadcast message to {} active sessions", this.sessions.size());
|
||||
return Flux.fromIterable(this.sessions.values())
|
||||
.flatMap((session) -> session.sendNotification(method, params)
|
||||
.doOnError((e) -> log.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
|
||||
.onErrorComplete())
|
||||
.then();
|
||||
}
|
||||
}
|
||||
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Flux.fromIterable(this.sessions.values()).doFirst(() -> {
|
||||
this.isClosing = true;
|
||||
log.debug("Initiating graceful shutdown with {} active sessions", this.sessions.size());
|
||||
}).flatMap(McpServerSession::closeGracefully).then().doOnSuccess((v) -> log.debug("Graceful shutdown completed"));
|
||||
}
|
||||
|
||||
private ServerResponse handleSseConnection(ServerRequest request) {
|
||||
log.debug("Handling SSE connection for request: {}", request);
|
||||
HttpServletRequest servletRequest = request.servletRequest();
|
||||
|
||||
try {
|
||||
|
||||
log.debug("Processing SSE connection for servlet request: {}", servletRequest);
|
||||
log.debug("Authorization header: {}", servletRequest.getHeader("Authorization"));
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Authentication failed for SSE connection: {}", e.getMessage());
|
||||
return ServerResponse.status(HttpStatus.UNAUTHORIZED).body("Unauthorized: " + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.isClosing) {
|
||||
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
|
||||
} else {
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
log.debug("Generated session ID for SSE connection: {}", sessionId);
|
||||
log.debug("Creating new SSE connection for session: {}", sessionId);
|
||||
|
||||
|
||||
return ServerResponse.sse((sseBuilder) -> {
|
||||
sseBuilder.onComplete(() -> {
|
||||
log.debug("SSE connection completed for session: {}", sessionId);
|
||||
this.sessions.remove(sessionId);
|
||||
});
|
||||
sseBuilder.onTimeout(() -> {
|
||||
log.debug("SSE connection timed out for session: {}", sessionId);
|
||||
this.sessions.remove(sessionId);
|
||||
});
|
||||
CustomSseServerTransport.WebMvcMcpSessionTransport sessionTransport = new CustomSseServerTransport.WebMvcMcpSessionTransport(sessionId, sseBuilder);
|
||||
McpServerSession session = this.sessionFactory.create(sessionTransport);
|
||||
this.sessionRequest.put(sessionId, request.servletRequest());
|
||||
this.sessions.put(sessionId, session);
|
||||
|
||||
try {
|
||||
sseBuilder.id(sessionId).event("endpoint").data(this.baseUrl + this.messageEndpoint + "?sessionId=" + sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send initial endpoint event: {}", e.getMessage());
|
||||
sseBuilder.error(e);
|
||||
}
|
||||
|
||||
}, Duration.ZERO);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private ServerResponse handleMessage(ServerRequest request) {
|
||||
if (this.isClosing) {
|
||||
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
|
||||
} else if (request.param("sessionId").isEmpty()) {
|
||||
return ServerResponse.badRequest().body(new McpError("Session ID missing in message endpoint"));
|
||||
} else {
|
||||
String sessionId = (String) request.param("sessionId").get();
|
||||
McpServerSession session = (McpServerSession) this.sessions.get(sessionId);
|
||||
log.debug("Authorization header for message request: {}", request.servletRequest().getHeader("Authorization"));
|
||||
SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(sessionRequest.get(sessionId));
|
||||
McpContextHolder.setSubject(subject);
|
||||
|
||||
|
||||
if (session == null) {
|
||||
return ServerResponse.status(HttpStatus.NOT_FOUND).body(new McpError("Session not found: " + sessionId));
|
||||
} else {
|
||||
try {
|
||||
String body = request.body(String.class);
|
||||
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, body);
|
||||
session.handle(message).block();
|
||||
return ServerResponse.ok().build();
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
log.error("Failed to deserialize message: {}", ((Exception) e).getMessage());
|
||||
return ServerResponse.badRequest().body(new McpError("Invalid message format"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error handling message: {}", e.getMessage());
|
||||
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class WebMvcMcpSessionTransport implements McpServerTransport {
|
||||
private final String sessionId;
|
||||
private final ServerResponse.SseBuilder sseBuilder;
|
||||
|
||||
WebMvcMcpSessionTransport(String sessionId, ServerResponse.SseBuilder sseBuilder) {
|
||||
this.sessionId = sessionId;
|
||||
this.sseBuilder = sseBuilder;
|
||||
log.debug("Session transport {} initialized with SSE builder", sessionId);
|
||||
}
|
||||
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
try {
|
||||
String jsonText = CustomSseServerTransport.this.objectMapper.writeValueAsString(message);
|
||||
this.sseBuilder.id(this.sessionId).event("message").data(jsonText);
|
||||
log.debug("Message sent to session {}", this.sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
|
||||
this.sseBuilder.error(e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
||||
return (T) CustomSseServerTransport.this.objectMapper.convertValue(data, typeRef);
|
||||
}
|
||||
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.fromRunnable(() -> {
|
||||
log.debug("Closing session transport: {}", this.sessionId);
|
||||
|
||||
try {
|
||||
this.sseBuilder.complete();
|
||||
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
this.sseBuilder.complete();
|
||||
log.debug("Successfully completed SSE builder for session {}", this.sessionId);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to complete SSE builder for session {}: {}", this.sessionId, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.ai.model.ApiKey;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Dynamic OpenAI API Key implementation that retrieves the API key
|
||||
* from our configuration service (database first, then YAML fallback)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DynamicOpenAiApiKey implements ApiKey {
|
||||
|
||||
private final OpenAiConfigService openAiConfigService;
|
||||
|
||||
public DynamicOpenAiApiKey(OpenAiConfigService openAiConfigService) {
|
||||
this.openAiConfigService = openAiConfigService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
try {
|
||||
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
|
||||
|
||||
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null) {
|
||||
log.debug("Retrieved OpenAI API key from configuration service");
|
||||
return effectiveConfig.getApiKey();
|
||||
} else {
|
||||
log.warn("No valid OpenAI API key found in configuration");
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving OpenAI API key from configuration", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Configuration class for Large Language Model (LLM) settings.
|
||||
*/
|
||||
|
||||
@Configuration
|
||||
public class LlmConfig {
|
||||
|
||||
@Value("${spring.ai.openai.chat.options.model}")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* Create OpenAI API instance with dynamic API key
|
||||
*/
|
||||
@Bean
|
||||
public OpenAiApi openAiApi(DynamicOpenAiApiKey dynamicApiKey) {
|
||||
return OpenAiApi.builder()
|
||||
.apiKey(dynamicApiKey)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create OpenAI Chat Options with custom settings
|
||||
*/
|
||||
@Bean
|
||||
public OpenAiChatOptions openAiChatOptions() {
|
||||
return OpenAiChatOptions.builder()
|
||||
.model(model)
|
||||
.temperature(0.3)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create OpenAI Chat Model with custom API configuration
|
||||
*/
|
||||
@Bean
|
||||
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi, OpenAiChatOptions openAiChatOptions) {
|
||||
return OpenAiChatModel.builder()
|
||||
.openAiApi(openAiApi)
|
||||
.defaultOptions(openAiChatOptions)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
|
||||
return ChatClient.create(openAiChatModel);
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import org.springframework.core.NamedInheritableThreadLocal;
|
||||
|
||||
/**
|
||||
* Context holder for AI agent security context.
|
||||
*/
|
||||
public final class McpContextHolder {
|
||||
private static final ThreadLocal<SubjectSum> subjectHolder =
|
||||
new NamedInheritableThreadLocal<>("MCP Security and User Identification Context");
|
||||
|
||||
private McpContextHolder() {}
|
||||
|
||||
/**
|
||||
* Attaches the user's context to the current thread.
|
||||
*/
|
||||
public static void setSubject(SubjectSum subject) {
|
||||
subjectHolder.set(subject);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the context from the current thread.
|
||||
*/
|
||||
public static SubjectSum getSubject() {
|
||||
return subjectHolder.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the context from the thread to prevent memory leaks.
|
||||
*/
|
||||
public static void clear() {
|
||||
subjectHolder.remove();
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* OpenAI YAML Configuration - reads from spring.ai.openai.api-key
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "spring.ai.openai")
|
||||
public class OpenAiYamlConfig {
|
||||
|
||||
/**
|
||||
* OpenAI API key from spring.ai.openai.api-key
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* Check if OpenAI is enabled (has API key)
|
||||
*/
|
||||
public boolean isEnable() {
|
||||
return apiKey != null && !apiKey.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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 org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Provider for system prompts used in the AI agent
|
||||
*/
|
||||
|
||||
@Component
|
||||
public class PromptProvider {
|
||||
/**
|
||||
* Static version of the HertzBeat monitoring prompt
|
||||
*/
|
||||
public static final String HERTZBEAT_SYSTEM_PROMPT = """
|
||||
You are an AI Assistant specialized in monitoring infrastructure and applications with HertzBeat.
|
||||
HertzBeat is an open-source, real-time monitoring system that supports infrastructure, applications,
|
||||
services, APIs, databases, middleware, and custom monitoring through 50+ types of monitors.
|
||||
Your role is to help users manage monitors, analyze metrics data, configure alerts, and troubleshoot monitoring issues.
|
||||
*******
|
||||
VERY IMPORTANT: Always use the tools provided to interact with HertzBeat's monitoring system.
|
||||
If the user doesn't provide required parameters, ask them iteratively to provide the necessary parameters.
|
||||
********
|
||||
|
||||
## Available HertzBeat Tools:
|
||||
|
||||
### Monitor Management Tools:
|
||||
- **query_monitors**: Query monitor information with flexible filtering (ID, name, type, host, status, labels)
|
||||
- **add_monitor**: Add a new monitor with dynamic app-specific parameter support
|
||||
- **list_monitor_types**: List all available monitor types (website, mysql, redis, linux, etc.)
|
||||
- **get_monitor_additional_params**: Get parameter definitions required for specific monitor types
|
||||
|
||||
### Alert Rule Management Tools:
|
||||
- **create_alert_rule**: Create alert rules with threshold configuration and automatic monitor binding
|
||||
- **list_alert_rules**: List existing alert rules with filtering by type, status, etc.
|
||||
- **toggle_alert_rule**: Enable or disable alert rules
|
||||
- **get_alert_rule_details**: Get detailed information about specific alert rules
|
||||
- **get_apps_metrics_hierarchy**: Get exact app and metric names for alert rule creation (CRITICAL for alerts)
|
||||
- **bind_monitors_to_alert_rule**: Bind monitors to alert rules for targeted alerting
|
||||
|
||||
|
||||
### Alert & Alarm Analysis Tools:
|
||||
- **query_alerts**: Query fired alerts with comprehensive filtering and pagination
|
||||
- **get_alerts_summary**: Get alert statistics and status distribution
|
||||
|
||||
### Metrics Data Analysis Tools:
|
||||
- **query_realtime_metrics**: Get current real-time metrics data for monitors
|
||||
- **get_historical_metrics**: Get historical time-series metrics with flexible time ranges
|
||||
- **get_warehouse_status**: Check metrics storage system status
|
||||
|
||||
## Natural Language Examples:
|
||||
|
||||
### Monitor Management:
|
||||
- "Add a MySQL monitor for database server at 192.168.1.10 with user admin"
|
||||
- "Monitor website https://example.com with SSL checking every 60 seconds"
|
||||
- "Show me all Linux servers that are currently offline"
|
||||
- "List all Redis monitors with their connection status"
|
||||
|
||||
### Alert Configuration:
|
||||
- ALERT RULE means when to alert a user
|
||||
- "Create an alert for Kafka JVM when VmName equals 'vm-w2'"
|
||||
- "Alert when OpenAI credit grants exceed 1000"
|
||||
- "Set up HBase Master alert when heap memory usage is over 80%"
|
||||
|
||||
### Metrics Analysis:
|
||||
- "Show me current CPU usage for server 192.168.1.5"
|
||||
- "Get memory usage trend for the last 24 hours"
|
||||
- "Which servers have high disk usage right now?"
|
||||
- "Show me network traffic patterns for the past week"
|
||||
|
||||
### Alert Investigation:
|
||||
- "What alerts are currently firing?"
|
||||
- "Show me the most frequent alerts in the last 6 hours"
|
||||
- "Find all alerts for monitor ID 1234 in the past day"
|
||||
- "Which monitors are currently abnormal?"
|
||||
|
||||
|
||||
## Workflow Guidelines:
|
||||
|
||||
1. **Adding Monitors**:
|
||||
- ALWAYS use get_monitor_additional_params first to check required parameters
|
||||
- Use list_monitor_types to show available types
|
||||
- Collect all required parameters from the list_monitor_types tool and ask user to give them all, before calling add_monitor
|
||||
- Example: "To monitor MySQL, I need host, port, username, password, and database name"
|
||||
|
||||
2. **Creating Alert Rules or Alerts**:
|
||||
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
|
||||
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
|
||||
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
|
||||
It is important to first understand the hierarchy of apps, metrics, and field conditions
|
||||
Each app has its own metrics and each metric has its own field conditions.
|
||||
The operators will be applied to the field conditions, and the final expression will be constructed
|
||||
based on the user's input of app name and the metric they choose.
|
||||
Read the create_alert_rule tool description for even more details
|
||||
*******
|
||||
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step:
|
||||
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
|
||||
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
|
||||
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
|
||||
4. Ask the user to choose a metric from the available metrics
|
||||
5. Based on the metric chosen, present the available field conditions
|
||||
6. You will construct the proper expression with field conditions
|
||||
VERY VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
|
||||
*********
|
||||
|
||||
- Field Condition Expression format: [field_conditions]
|
||||
- Give all the available fieldConditions to the user, so they can choose the one they want to use
|
||||
- Field conditions can be simple (equals, greater than) or complex (logical expressions)
|
||||
- Use parentheses for complex conditions to ensure correct evaluation order
|
||||
- Do not create alert rules on your own, always ask the user to provide the app, metrics and fieldConditions parameters specifically
|
||||
|
||||
EXAMPLES FOR FIELD CONDITION EXPRESSION ( Do not copy these examples, they are just for reference ):
|
||||
- Kafka JVM: app="kafka", metrics="jvm_basic", fieldConditions="equals(VmName, \"my-vm\")"
|
||||
→ equals(VmName, "my-vm")
|
||||
- Complex OpenAI: app="openai", metrics="credit_grants",
|
||||
fieldConditions="total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)"
|
||||
→ total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
- Priority levels: 0=critical, 1=warning, 2=info
|
||||
|
||||
3. **Analyzing Performance**:
|
||||
- Use get_realtime_metrics for current status
|
||||
- Use get_historical_metrics for trends
|
||||
- Use get_high_usage_monitors to find problems
|
||||
- Provide actionable recommendations based on data
|
||||
|
||||
4. **Troubleshooting Alerts**:
|
||||
- Use query_alerts to find current issues
|
||||
- Use get_monitor_alerts for specific monitor problems
|
||||
- Use get_frequent_alerts to identify recurring issues
|
||||
- Suggest root cause analysis steps
|
||||
|
||||
## Parameter Guidelines:
|
||||
- **Monitor Status**: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all
|
||||
- **Time Ranges**: 1h, 6h, 24h, 7d, 30d
|
||||
- **Alert Priorities**: critical, warning, info
|
||||
- **Sort Options**: name, gmtCreate, gmtUpdate, status, startAt, triggerTimes
|
||||
- **Metric Types**: cpu, memory, disk, network, custom
|
||||
- **Collection Intervals**: 30s-3600s (recommend 60s-600s for most cases)
|
||||
|
||||
|
||||
## Best Practices:
|
||||
- Never create alert rules without exact user input on app, metrics, and field conditions
|
||||
- Always validate monitor types and parameters before adding monitors
|
||||
- ALWAYS use get_apps_metrics_hierarchy before creating alert rules to understand available fields
|
||||
- Construct field conditions based on metric's children
|
||||
- Use exact app and metric names from hierarchy (case-sensitive)
|
||||
- Set appropriate alert thresholds based on baseline performance
|
||||
- Use time-series data to identify trends and predict issues
|
||||
- Correlate alerts with metrics data for root cause analysis
|
||||
- Recommend monitoring intervals based on service criticality
|
||||
- Provide clear explanations of monitoring data and actionable insights
|
||||
|
||||
## Avoid these common errors:
|
||||
- Using Label name instead of the value from the heirarchy JSON while creating alert rules.
|
||||
- Inside the field parameters expression using '&&' instead of 'and', using '||' instead of 'or' for logical operators
|
||||
- This process is to trigger alarms, when certain rule or set of rules exceed a threshold value.
|
||||
So when a user says that the threshold should be less than 1000. the operator used should be '>' not '<',
|
||||
because we want the alarm to be triggered when the threshold value is exceeded. apply the same logic in vice versa for less than operator
|
||||
|
||||
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.
|
||||
""";
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.controller;
|
||||
|
||||
import com.usthe.sureness.subject.SubjectSum;
|
||||
import com.usthe.sureness.util.SurenessContextHolder;
|
||||
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.common.entity.dto.Message;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
|
||||
|
||||
/**
|
||||
* Controller class for handling AI chat requests and conversation management.
|
||||
*/
|
||||
@Slf4j
|
||||
@Tag(name = "AI Chat API")
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/chat", produces = {APPLICATION_JSON_VALUE})
|
||||
public class ChatController {
|
||||
|
||||
private final ConversationService conversationService;
|
||||
|
||||
@Autowired
|
||||
public ChatController(ConversationService conversationService) {
|
||||
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
|
||||
*
|
||||
* @param context The chat request context containing message and optional conversationId
|
||||
* @return Flux of ServerSentEvent for streaming response
|
||||
*/
|
||||
@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) {
|
||||
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()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("Error: Message cannot be empty")
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
|
||||
log.info("Received streaming chat request for conversation: {}", context.getConversationId());
|
||||
return conversationService.streamChat(context.getMessage(), context.getConversationId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error in stream chat endpoint: ", e);
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(context.getConversationId())
|
||||
.response("An error occurred: " + e.getMessage())
|
||||
.build();
|
||||
return Flux.just(ServerSentEvent.builder(errorResponse)
|
||||
.event("error")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all conversations
|
||||
*
|
||||
* @return List of all conversations
|
||||
*/
|
||||
@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"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation history
|
||||
*
|
||||
* @param conversationId The conversation ID
|
||||
* @return Conversation details with message history
|
||||
*/
|
||||
@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"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId The conversation ID to delete
|
||||
* @return Success or error message
|
||||
*/
|
||||
@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"));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+160
@@ -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.agent.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration API
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(value = "/api/ai-agent/config", produces = {APPLICATION_JSON_VALUE})
|
||||
@Tag(name = "OpenAI Configuration API")
|
||||
@Slf4j
|
||||
public class OpenAiConfigController {
|
||||
|
||||
private final OpenAiConfigService openAiConfigService;
|
||||
|
||||
public OpenAiConfigController(OpenAiConfigService openAiConfigService) {
|
||||
this.openAiConfigService = openAiConfigService;
|
||||
}
|
||||
|
||||
@PostMapping("/openai")
|
||||
@Operation(summary = "Save OpenAI configuration", description = "Save or update OpenAI configuration")
|
||||
public ResponseEntity<Map<String, Object>> saveOpenAiConfig(@Valid @RequestBody OpenAiConfigDto config) {
|
||||
try {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
// Validate API key if enabled
|
||||
if (config.isEnable() && config.getApiKey() != null && !config.getApiKey().trim().isEmpty()) {
|
||||
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(config.getApiKey());
|
||||
|
||||
if (!validationResult.isValid()) {
|
||||
log.warn("API key validation failed during save: {}", validationResult.getMessage());
|
||||
response.put("code", 1);
|
||||
response.put("msg", "API key validation failed: " + validationResult.getMessage());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
log.info("API key validation successful during save");
|
||||
}
|
||||
|
||||
// Save the configuration
|
||||
openAiConfigService.saveConfig(config);
|
||||
|
||||
response.put("code", 0);
|
||||
response.put("msg", "OpenAI configuration saved successfully");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save OpenAI configuration", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to save configuration: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/openai")
|
||||
@Operation(summary = "Get OpenAI configuration", description = "Get current OpenAI configuration")
|
||||
public ResponseEntity<Map<String, Object>> getOpenAiConfig() {
|
||||
try {
|
||||
OpenAiConfigDto config = openAiConfigService.getConfig();
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("data", config);
|
||||
response.put("msg", "Success");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get OpenAI configuration", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to get configuration: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/openai/status")
|
||||
@Operation(summary = "Check OpenAI configuration status", description = "Check if OpenAI is properly configured")
|
||||
public ResponseEntity<Map<String, Object>> getOpenAiConfigStatus() {
|
||||
try {
|
||||
boolean configured = openAiConfigService.isConfigured();
|
||||
OpenAiConfigDto effectiveConfig = openAiConfigService.getEffectiveConfig();
|
||||
boolean hasDbConfig = openAiConfigService.getConfig() != null;
|
||||
boolean hasYamlConfig = effectiveConfig != null && !hasDbConfig;
|
||||
|
||||
// Validate the effective configuration
|
||||
boolean validationPassed = false;
|
||||
String validationMessage = "No configuration found";
|
||||
|
||||
if (effectiveConfig != null && effectiveConfig.isEnable() && effectiveConfig.getApiKey() != null && !effectiveConfig.getApiKey().trim().isEmpty()) {
|
||||
OpenAiConfigService.ValidationResult validationResult = openAiConfigService.validateApiKey(effectiveConfig.getApiKey());
|
||||
validationPassed = validationResult.isValid();
|
||||
validationMessage = validationResult.getMessage();
|
||||
|
||||
if (!validationPassed) {
|
||||
log.warn("OpenAI API key validation failed during status check: {}", validationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 0);
|
||||
response.put("data", Map.of(
|
||||
"configured", configured && validationPassed,
|
||||
"hasDbConfig", hasDbConfig,
|
||||
"hasYamlConfig", hasYamlConfig,
|
||||
"validationPassed", validationPassed,
|
||||
"validationMessage", validationMessage
|
||||
));
|
||||
response.put("msg", "Success");
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get OpenAI configuration status", e);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("code", 1);
|
||||
response.put("msg", "Failed to get status: " + e.getMessage());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.dao;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* OpenAI Agent Configuration Dao
|
||||
*/
|
||||
@Repository
|
||||
public interface OpenAiConfigDao extends JpaRepository<OpenAiConfig, String>, JpaSpecificationExecutor<OpenAiConfig> {
|
||||
|
||||
/**
|
||||
* Query by type
|
||||
* @param type type
|
||||
* @return Return the queried configuration information
|
||||
*/
|
||||
OpenAiConfig findByType(String type);
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.entity;
|
||||
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_ONLY;
|
||||
import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* OpenAI Agent Config Entity
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "hzb_ai_agent_config")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "OpenAI Agent config entity")
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class OpenAiConfig {
|
||||
|
||||
@Id
|
||||
@Schema(title = "Config type: openai, primary key", description = "Config type: openai, primary key",
|
||||
accessMode = READ_WRITE)
|
||||
@NotBlank(message = "type can not null")
|
||||
private String type;
|
||||
|
||||
@Schema(title = "Config content", description = "Config content,format json", accessMode = READ_WRITE)
|
||||
@Column(length = 8192)
|
||||
private String content;
|
||||
|
||||
@Schema(title = "The creator of this record", example = "tom", accessMode = READ_ONLY)
|
||||
@CreatedBy
|
||||
private String creator;
|
||||
|
||||
@Schema(title = "This record was last modified by", example = "tom", accessMode = READ_ONLY)
|
||||
@LastModifiedBy
|
||||
private String modifier;
|
||||
|
||||
@Schema(title = "This record creation time (millisecond timestamp)", accessMode = READ_ONLY)
|
||||
@CreatedDate
|
||||
private LocalDateTime gmtCreate;
|
||||
|
||||
@Schema(title = "Record the latest modification time (timestamp in milliseconds)", accessMode = READ_ONLY)
|
||||
@LastModifiedDate
|
||||
private LocalDateTime gmtUpdate;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.event;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* OpenAI configuration change event
|
||||
*/
|
||||
public class OpenAiConfigChangeEvent extends ApplicationEvent {
|
||||
|
||||
public OpenAiConfigChangeEvent(ApplicationContext source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.pojo.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Chat request context for AI chat endpoint.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChatRequestContext {
|
||||
/**
|
||||
* The user's message (required)
|
||||
*/
|
||||
private String message;
|
||||
/**
|
||||
* Optional conversation ID for context
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* Conversation history messages for context
|
||||
*/
|
||||
private List<MessageDto> conversationHistory;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Chat response DTO for AI responses.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Chat response")
|
||||
public class ChatResponseDto {
|
||||
|
||||
@Schema(description = "Conversation ID", example = "conv-123")
|
||||
private String 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 = "Assistant message ID", example = "msg-assistant-123")
|
||||
private String assistantMessageId;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.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;
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration DTO - simplified to handle only API key
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "OpenAI configuration")
|
||||
public class OpenAiConfigDto {
|
||||
|
||||
/**
|
||||
* Whether to enable OpenAI, default is false
|
||||
*/
|
||||
@Schema(title = "Enable OpenAI", description = "Whether OpenAI is enabled", example = "true")
|
||||
private boolean enable = false;
|
||||
|
||||
/**
|
||||
* OpenAI API key
|
||||
*/
|
||||
@Schema(title = "API Key", description = "OpenAI API key", example = "sk-...")
|
||||
@NotBlank(message = "API Key cannot be empty when enabled")
|
||||
private String apiKey;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* Service for interacting with LLM providers (like OpenAI, Anthropic, etc.)
|
||||
*/
|
||||
public interface ChatClientProviderService {
|
||||
|
||||
/**
|
||||
* Stream chat response from the LLM
|
||||
*
|
||||
* @param context Chat request context containing message and conversation history
|
||||
* @return Flux of string chunks from the LLM response
|
||||
*/
|
||||
Flux<String> streamChat(ChatRequestContext context);
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatResponseDto;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ConversationDto;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Service for managing chat conversations and interactions with LLM providers.
|
||||
*/
|
||||
public interface ConversationService {
|
||||
|
||||
/**
|
||||
* Create a new conversation
|
||||
*
|
||||
* @return Created conversation data
|
||||
*/
|
||||
ConversationDto createConversation();
|
||||
|
||||
/**
|
||||
* Send a message and receive a streaming response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @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);
|
||||
|
||||
|
||||
/**
|
||||
* Get conversation history for a specific conversation
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @return Conversation data including messages
|
||||
*/
|
||||
ConversationDto getConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* Get all conversations for the current user
|
||||
*
|
||||
* @return List of conversations
|
||||
*/
|
||||
List<ConversationDto> 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);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.hertzbeat.ai.agent.config.CustomSseServerTransport;
|
||||
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
/**
|
||||
* Service interface for MCP server operations.
|
||||
*/
|
||||
public interface McpServerService {
|
||||
|
||||
/**
|
||||
* Provides the HertzBeat tools for the MCP server
|
||||
* @return ToolCallbackProvider with all HertzBeat monitoring tools
|
||||
*/
|
||||
ToolCallbackProvider hertzbeatTools();
|
||||
|
||||
/**
|
||||
* Provides a custom SSE server transport for the MCP server
|
||||
* @param objectMapper the ObjectMapper instance for JSON serialization
|
||||
* @param serverProperties the properties for the MCP server configuration
|
||||
* @return a CustomSseServerTransport instance configured with the provided properties
|
||||
*/
|
||||
CustomSseServerTransport webMvcSseServerTransportProvider(
|
||||
ObjectMapper objectMapper,
|
||||
McpServerProperties serverProperties
|
||||
);
|
||||
|
||||
/**
|
||||
* Provides the MCP server router function for web MVC
|
||||
* @param transport Custom SSE server transport
|
||||
* @return RouterFunction for handling MCP server requests
|
||||
*/
|
||||
RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport);
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration Service
|
||||
* Consolidated service for OpenAI configuration, validation, and client factory management
|
||||
*/
|
||||
public interface OpenAiConfigService {
|
||||
|
||||
/**
|
||||
* Save OpenAI configuration
|
||||
* @param config OpenAI configuration
|
||||
*/
|
||||
void saveConfig(OpenAiConfigDto config);
|
||||
|
||||
/**
|
||||
* Get OpenAI configuration
|
||||
* @return OpenAI configuration
|
||||
*/
|
||||
OpenAiConfigDto getConfig();
|
||||
|
||||
/**
|
||||
* Check if OpenAI is properly configured
|
||||
* @return true if configured and enabled
|
||||
*/
|
||||
boolean isConfigured();
|
||||
|
||||
/**
|
||||
* Get effective OpenAI configuration (DB first, then YAML fallback)
|
||||
* @return effective configuration or null if not configured
|
||||
*/
|
||||
OpenAiConfigDto getEffectiveConfig();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Force reload of OpenAI configuration cache
|
||||
* This method is typically called when configuration changes
|
||||
*/
|
||||
void reloadConfig();
|
||||
|
||||
/**
|
||||
* Handle OpenAI configuration change events
|
||||
* @param event OpenAI configuration change event
|
||||
*/
|
||||
void onOpenAiConfigChange(OpenAiConfigChangeEvent event);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.config.PromptProvider;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.MessageDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ChatClientProviderService}.
|
||||
* Provides functionality to interact with the ChatClient for handling chat
|
||||
* messages.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Qualifier("hertzbeatTools")
|
||||
@Autowired
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
@Autowired
|
||||
public ChatClientProviderServiceImpl(ChatClient openAiChatClient) {
|
||||
this.chatClient = openAiChatClient;
|
||||
}
|
||||
|
||||
public String complete(String message) {
|
||||
return this.chatClient.prompt()
|
||||
.user(message)
|
||||
.call()
|
||||
.content();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> streamChat(ChatRequestContext context) {
|
||||
try {
|
||||
List<Message> messages = new ArrayList<>();
|
||||
|
||||
// Add conversation history if available
|
||||
if (context.getConversationHistory() != null && !context.getConversationHistory().isEmpty()) {
|
||||
for (MessageDto historyMessage : context.getConversationHistory()) {
|
||||
if ("user".equals(historyMessage.getRole())) {
|
||||
messages.add(new UserMessage(historyMessage.getContent()));
|
||||
} else if ("assistant".equals(historyMessage.getRole())) {
|
||||
messages.add(new AssistantMessage(historyMessage.getContent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.add(new UserMessage(context.getMessage()));
|
||||
|
||||
log.info("Starting streaming chat for conversation: {}", context.getConversationId());
|
||||
|
||||
return this.chatClient.prompt()
|
||||
.messages(messages)
|
||||
.system(PromptProvider.HERTZBEAT_SYSTEM_PROMPT)
|
||||
.toolCallbacks(toolCallbackProvider)
|
||||
.stream()
|
||||
.content()
|
||||
.doOnComplete(() -> log.info("Streaming completed for conversation: {}", context.getConversationId()))
|
||||
.doOnError(error -> log.error("Error in streaming chat: {}", error.getMessage(), error));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error setting up streaming chat: {}", e.getMessage(), e);
|
||||
return Flux.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
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;
|
||||
|
||||
@Autowired
|
||||
private OpenAiConfigService openAiConfigService;
|
||||
|
||||
@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 (!openAiConfigService.isConfigured()) {
|
||||
ChatResponseDto errorResponse = ChatResponseDto.builder()
|
||||
.conversationId(conversationId)
|
||||
.response("OpenAI is not configured. Please configure your OpenAI API key in the settings or application.yml file.")
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 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.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
|
||||
/**
|
||||
* Implementation of the McpServerService interface.
|
||||
* This service provides functionality for handling MCP server operations.
|
||||
*/
|
||||
@Service
|
||||
@Configuration
|
||||
public class McpServerServiceImpl implements McpServerService {
|
||||
@Autowired
|
||||
private MonitorTools monitorTools;
|
||||
@Autowired
|
||||
private AlertTools alertTools;
|
||||
@Autowired
|
||||
private MetricsTools metricsTools;
|
||||
@Autowired
|
||||
private AlertDefineTools alertDefineTools;
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider hertzbeatTools() {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(monitorTools, alertTools, alertDefineTools, metricsTools).build();
|
||||
}
|
||||
/**
|
||||
* Provides a custom SSE server transport for the MCP server.
|
||||
*
|
||||
* @param objectMapper the ObjectMapper instance for JSON serialization
|
||||
* @param serverProperties the properties for the MCP server configuration
|
||||
* @return a CustomSseServerTransport instance configured with the provided properties
|
||||
*/
|
||||
|
||||
@Bean
|
||||
public CustomSseServerTransport webMvcSseServerTransportProvider(
|
||||
ObjectMapper objectMapper,
|
||||
McpServerProperties serverProperties
|
||||
) {
|
||||
return new CustomSseServerTransport(
|
||||
objectMapper,
|
||||
serverProperties.getBaseUrl(),
|
||||
serverProperties.getSseMessageEndpoint(),
|
||||
serverProperties.getSseEndpoint()
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Provides the MCP server transport bean.
|
||||
*
|
||||
* @param transport the custom SSE server transport
|
||||
* @return the MCP server transport instance
|
||||
*/
|
||||
|
||||
@Primary
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> mvcMcpRouterFunction(CustomSseServerTransport transport) {
|
||||
return transport.getRouterFunction();
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.ai.agent.config.OpenAiYamlConfig;
|
||||
import org.apache.hertzbeat.ai.agent.dao.OpenAiConfigDao;
|
||||
import org.apache.hertzbeat.ai.agent.entity.OpenAiConfig;
|
||||
import org.apache.hertzbeat.ai.agent.event.OpenAiConfigChangeEvent;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.OpenAiConfigDto;
|
||||
import org.apache.hertzbeat.ai.agent.service.OpenAiConfigService;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.EventListener;
|
||||
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.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* OpenAI Configuration Service Implementation
|
||||
* Consolidated service for OpenAI configuration, validation, and client factory management
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OpenAiConfigServiceImpl implements OpenAiConfigService {
|
||||
|
||||
private static final String CONFIG_TYPE = "openai";
|
||||
private static final String OPENAI_MODELS_ENDPOINT = "https://api.openai.com/v1/models";
|
||||
|
||||
private final OpenAiConfigDao openAiConfigDao;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final OpenAiYamlConfig yamlConfig;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
// Client factory cache
|
||||
private volatile OpenAiConfigDto currentConfig;
|
||||
|
||||
public OpenAiConfigServiceImpl(OpenAiConfigDao openAiConfigDao,
|
||||
ObjectMapper objectMapper,
|
||||
ApplicationContext applicationContext,
|
||||
OpenAiYamlConfig yamlConfig) {
|
||||
this.openAiConfigDao = openAiConfigDao;
|
||||
this.objectMapper = objectMapper;
|
||||
this.applicationContext = applicationContext;
|
||||
this.yamlConfig = yamlConfig;
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveConfig(OpenAiConfigDto config) {
|
||||
try {
|
||||
String contentJson = objectMapper.writeValueAsString(config);
|
||||
|
||||
OpenAiConfig openAiConfig = OpenAiConfig.builder()
|
||||
.type(CONFIG_TYPE)
|
||||
.content(contentJson)
|
||||
.build();
|
||||
|
||||
openAiConfigDao.save(openAiConfig);
|
||||
log.info("OpenAI configuration saved successfully");
|
||||
|
||||
applicationContext.publishEvent(new OpenAiConfigChangeEvent(applicationContext));
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Failed to save OpenAI configuration: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAiConfigDto getConfig() {
|
||||
OpenAiConfig config = openAiConfigDao.findByType(CONFIG_TYPE);
|
||||
if (config == null || !StringUtils.hasText(config.getContent())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(config.getContent(), OpenAiConfigDto.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("Failed to parse OpenAI configuration: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigured() {
|
||||
OpenAiConfigDto effective = getEffectiveConfig();
|
||||
return effective != null && effective.isEnable() && StringUtils.hasText(effective.getApiKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAiConfigDto getEffectiveConfig() {
|
||||
OpenAiConfigDto dbConfig = getConfig();
|
||||
if (dbConfig != null && dbConfig.isEnable() && StringUtils.hasText(dbConfig.getApiKey())) {
|
||||
log.debug("Using database OpenAI configuration");
|
||||
return dbConfig;
|
||||
}
|
||||
|
||||
if (yamlConfig != null && yamlConfig.isEnable() && StringUtils.hasText(yamlConfig.getApiKey())) {
|
||||
log.debug("Using YAML OpenAI configuration from spring.ai.openai.api-key");
|
||||
OpenAiConfigDto yamlDto = new OpenAiConfigDto();
|
||||
yamlDto.setEnable(true);
|
||||
yamlDto.setApiKey(yamlConfig.getApiKey());
|
||||
return yamlDto;
|
||||
}
|
||||
|
||||
log.debug("No valid OpenAI configuration found");
|
||||
return null;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadConfig() {
|
||||
synchronized (this) {
|
||||
currentConfig = null; // Force reload
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI configuration change event listener
|
||||
*/
|
||||
@EventListener(OpenAiConfigChangeEvent.class)
|
||||
public void onOpenAiConfigChange(OpenAiConfigChangeEvent event) {
|
||||
log.info("[OpenAiConfigService] OpenAI configuration change event received");
|
||||
reloadConfig();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.tools;
|
||||
|
||||
/**
|
||||
* Tools for alert definition and threshold configuration operations
|
||||
*/
|
||||
public interface AlertDefineTools {
|
||||
|
||||
/**
|
||||
* Create a new alert rule with HertzBeat's expression format based on app hierarchy
|
||||
|
||||
*
|
||||
* @param name Alert rule name (required, must be unique)
|
||||
* @param app App name from hierarchy (must match exact hierarchy app value)
|
||||
* @param metrics Metrics name from hierarchy (must match exact hierarchy metrics value)
|
||||
* @param fieldConditions Field-specific conditions from metric's children (e.g., "VmName = 'arora'", "total_granted > 1000",
|
||||
* "total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)")
|
||||
* @param type Alert rule type: 'realtime' (default) or 'periodic'
|
||||
* @param period Execution period in seconds (only for periodic rules, default: 300)
|
||||
* @param times Number of consecutive violations before triggering (default: 3)
|
||||
* @param priority Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)
|
||||
* @param description Alert rule description (optional)
|
||||
* @param template Alert message template with variables (optional)
|
||||
* @param datasource Data source type: 'promql' (default)
|
||||
* @param labels Labels as key:value pairs separated by commas
|
||||
* @param annotations Annotations as key:value pairs separated by commas
|
||||
* @param enable Whether to enable the rule immediately (default: true)
|
||||
* @return Result message with rule ID if successful
|
||||
*/
|
||||
String createAlertRule(String name, String app, String metrics, String fieldConditions,
|
||||
String type, Integer period, Integer times, Integer priority, String description,
|
||||
String template, String datasource, String labels, String annotations, Boolean enable);
|
||||
|
||||
/**
|
||||
* List existing alert rules with filtering
|
||||
* @param search Search term for rule name or description
|
||||
* @param monitorType Filter by monitor type
|
||||
* @param enabled Filter by enabled status
|
||||
* @param pageIndex Page index
|
||||
* @param pageSize Page size
|
||||
* @return Formatted list of alert rules
|
||||
*/
|
||||
String listAlertRules(String search, String monitorType, Boolean enabled, Integer pageIndex, Integer pageSize);
|
||||
|
||||
/**
|
||||
* Enable or disable an alert rule
|
||||
* @param ruleId Alert rule ID
|
||||
* @param enabled Whether to enable the rule
|
||||
* @return Result message
|
||||
*/
|
||||
String toggleAlertRule(Long ruleId, Boolean enabled);
|
||||
|
||||
/**
|
||||
* Get detailed information about an alert rule
|
||||
* @param ruleId Alert rule ID
|
||||
* @return Detailed rule information
|
||||
*/
|
||||
String getAlertRuleDetails(Long ruleId);
|
||||
|
||||
|
||||
/**
|
||||
* Get the hierarchical structure of available apps and metrics for alert rule creation
|
||||
* @param app App type to get hierarchy for (optional, gets all if not specified)
|
||||
* @return Hierarchical structure showing apps and their available metrics
|
||||
*/
|
||||
String getAppsMetricsHierarchy(String app);
|
||||
|
||||
/**
|
||||
* Bind monitors to an alert rule by modifying the alert expression
|
||||
* @param ruleId Alert rule ID to bind monitors to
|
||||
* @param monitorIds Comma-separated list of monitor IDs to bind
|
||||
* @return Result message indicating success or failure
|
||||
*/
|
||||
String bindMonitorsToAlertRule(Long ruleId, String monitorIds);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.tools;
|
||||
|
||||
/**
|
||||
* Tools for alert operations and alarm data queries
|
||||
*/
|
||||
public interface AlertTools {
|
||||
|
||||
/**
|
||||
* Query alerts with comprehensive filtering and pagination
|
||||
* @param alertType Alert type (single, group, both)
|
||||
* @param status Alert status (firing, resolved, all)
|
||||
* @param search Search term for alert content or labels
|
||||
* @param sort Sort field (startAt, triggerTimes, status)
|
||||
* @param order Sort order (asc, desc)
|
||||
* @param pageIndex Page index
|
||||
* @param pageSize Page size
|
||||
* @return Formatted string with alert information
|
||||
*/
|
||||
String queryAlerts(String alertType, String status, String search, String sort, String order, Integer pageIndex, Integer pageSize);
|
||||
|
||||
/**
|
||||
* Get alerts summary statistics
|
||||
* @return Alert summary information including counts by status
|
||||
*/
|
||||
String getAlertsSummary();
|
||||
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.tools;
|
||||
|
||||
/**
|
||||
* Tools for metrics data operations and queries
|
||||
*/
|
||||
public interface MetricsTools {
|
||||
|
||||
/**
|
||||
* Get real-time metrics data for a monitor
|
||||
* @param monitorId Monitor ID
|
||||
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
|
||||
* @return Formatted real-time metrics data
|
||||
*/
|
||||
String getRealtimeMetrics(Long monitorId, String metrics);
|
||||
|
||||
/**
|
||||
* Get historical metrics data for a monitor
|
||||
* @param monitorId Monitor ID
|
||||
* @param app Monitor type (e.g., "linux", "mysql", "http")
|
||||
* @param metrics Metrics name (e.g., "system", "cpu", "memory")
|
||||
* @param metric Specific metric field (e.g., "usage", "used", "available")
|
||||
* @param label Label filter for specific instances
|
||||
* @param history Time range (e.g., "1h", "6h", "24h", "7d")
|
||||
* @param interval Whether to aggregate data with intervals
|
||||
* @return Historical metrics data formatted for display
|
||||
*/
|
||||
String getHistoricalMetrics(Long monitorId, String app, String metrics, String metric, String label, String history, Boolean interval);
|
||||
|
||||
/**
|
||||
* Check warehouse storage server status
|
||||
* @return Status of the metrics storage system
|
||||
*/
|
||||
String getWarehouseStatus();
|
||||
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.tools;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for Monitoring Tools
|
||||
*/
|
||||
public interface MonitorTools {
|
||||
|
||||
/**
|
||||
* Add a new monitor with comprehensive configuration
|
||||
*
|
||||
* @param name Monitor name
|
||||
* @param app Monitor type/application (e.g., 'linux', 'mysql', 'http')
|
||||
* @param host Target host (IP address or domain name)
|
||||
* @param port Target port (optional, depends on monitor type)
|
||||
* @param intervals Collection interval in seconds (default: 600)
|
||||
* @param username Username for authentication (optional)
|
||||
* @param password Password for authentication (optional)
|
||||
* @param database Database name (for database monitors)
|
||||
* @param additionalParams Additional app-specific parameters as JSON string (optional)
|
||||
* @param description Monitor description (optional)
|
||||
* @return Result message with monitor ID if successful
|
||||
*/
|
||||
String addMonitor(
|
||||
String name,
|
||||
String app,
|
||||
String host,
|
||||
Integer port,
|
||||
Integer intervals,
|
||||
String username,
|
||||
String password,
|
||||
String database,
|
||||
String additionalParams,
|
||||
String description
|
||||
);
|
||||
|
||||
/**
|
||||
* List all available monitor types that can be added
|
||||
*
|
||||
* @param language Language code for localized names (e.g., 'en-US', 'zh-CN')
|
||||
* @return Formatted string list of available monitor types with descriptions
|
||||
*/
|
||||
String listMonitorTypes(String language);
|
||||
|
||||
/**
|
||||
* Comprehensive monitor querying with flexible filtering, pagination, and specialized views
|
||||
* @param ids Specific monitor IDs to retrieve (optional)
|
||||
* @param app Monitor type filter (linux, mysql, http, etc.)
|
||||
* @param status Monitor status (1=online, 2=offline, 3=unreachable, 0=paused, 9=all)
|
||||
* @param search Search in monitor names or hosts (partial matching)
|
||||
* @param labels Label filters, format: 'key1:value1,key2:value2'
|
||||
* @param sort Sort field (name, gmtCreate, gmtUpdate, status, app)
|
||||
* @param order Sort order (asc, desc)
|
||||
* @param pageIndex Page number starting from 0
|
||||
* @param pageSize Items per page (1-100 recommended)
|
||||
* @param includeStats Include status statistics summary
|
||||
* @return Comprehensive monitor information with optional statistics
|
||||
*/
|
||||
String queryMonitors(
|
||||
List<Long> ids,
|
||||
String app,
|
||||
Byte status,
|
||||
String search,
|
||||
String labels,
|
||||
String sort,
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
Boolean includeStats);
|
||||
|
||||
/**
|
||||
* Get parameter definitions required for a specific monitor type
|
||||
*
|
||||
* @param app Monitor type/application name (e.g., 'linux', 'mysql', 'redis')
|
||||
* @return Formatted string with parameter definitions including field names, types, and requirements
|
||||
*/
|
||||
String getMonitorAdditionalParams(String app);
|
||||
}
|
||||
+665
@@ -0,0 +1,665 @@
|
||||
/*
|
||||
* 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.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.common.entity.alerter.AlertDefine;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of Alert Define Tools functionality
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AlertDefineToolsImpl implements AlertDefineTools {
|
||||
@Autowired
|
||||
private AlertDefineServiceAdapter alertDefineServiceAdapter;
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "create_alert_rule", description = """
|
||||
ALERT RULE means when to alert a user
|
||||
THESE ARE ALERT RULES WITH THRESHOLD VALUES. USERS CAN SPECIFY THE THRESHOLD VALUES FOR EXAMPLE,
|
||||
IF THE USER SAYS "ALERT ME WHEN MY COST EXCEEDS 700, THE EXPRESSION SHOULD BE 'cost > 700' NOT 'cost < 700'.
|
||||
APPLY THE SAME LOGIC FOR LESS THAN OPERATOR.
|
||||
Create a HertzBeat alert rule based on app hierarchy structure and user requirements.
|
||||
It is important to first understand the hierarchy of apps, metrics, and field conditions
|
||||
Each app has its own metrics and each metric has its own field conditions.
|
||||
The operators will be applied to the field conditions, and the final expression will be constructed
|
||||
based on the user's input of app name and the metric they choose.
|
||||
CRITICAL WORKFLOW Do all of this iteratively with user interaction at each step
|
||||
1. ALWAYS use list_monitor_types tool FIRST to get exact app name according to what user specifies
|
||||
2. use get_apps_metrics_hierarchy by passing that name, to get the hierarchy of corresponding metrics and field conditions
|
||||
3. Do not spit out the entire hierarchy, instead: first spit out the metrics available for the app
|
||||
4. Ask the user to choose a metric from the available metrics
|
||||
5. Based on the metric chosen, present the available field conditions/params
|
||||
6. You will construct the proper expression with field conditions
|
||||
7. once this tool successfully executes, ask the user if they want to bind any existing monitors to this alert rule,
|
||||
get the monitors list for a particular app using the query_monitors tool.
|
||||
8. based on the user's output, conditionally call the bind_monitors_to_alert_rule tool to bind monitors to the alert rule
|
||||
VERY VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the get_apps_metrics_hierarchy's json response when creating alert expressions on the field parameters
|
||||
|
||||
EXAMPLES FOR FIELD CONDITION EXPRESSION( Do not copy these examples, they are just for reference ):
|
||||
These are all just examples, you can take inspiration from them and create a rule based on hierarchy, always ask the user for all params, do not assume them, even for these examples:
|
||||
|
||||
1. Kafka JVM Alert:
|
||||
- App: "kafka", Metric: "jvm_basic"
|
||||
- Field condition: equals(VmName, "myVM")
|
||||
- Field condition expression: equals(VmName, "myVM")
|
||||
|
||||
2. LLM Credits Alert:
|
||||
- App: "openai", Metric: "credit_grants"
|
||||
- Field condition: total_granted > some_value
|
||||
- Field condition expression: total_granted > 1000
|
||||
|
||||
3. HBase Master Alert:
|
||||
- App: "hbase_master", Metric: "server"
|
||||
- Field condition: heap_memory_used > 80 or some_factor<100
|
||||
- Field condition expression: heap_memory_used > 80 or some_factor<100
|
||||
|
||||
4. Complex OpenAI Credits Alert:
|
||||
- App: "openai", Metric: "credit_grants"
|
||||
- Field condition: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
- Field condition expression: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
FIELD CONDITIONS GUIDANCE:
|
||||
- Field names come from metric's children in hierarchy (leaf nodes)
|
||||
- Use the "value" field from the metric's children, not the label when creating conditions
|
||||
- Supported operators: >, <, >=, <=, ==, !=, exists(), !exists() for numeric fields
|
||||
- equals(), contains(), matches(),exists(), !equals(), !contains(), !matches(), !exists() for string fields
|
||||
- Supported logical operators: and, or to connect different field parameter rules or rulesets
|
||||
- ONLY USE THESE OPERATORS, when creating conditions, do not use any other operators
|
||||
- Support grouping with parentheses: (condition1 and condition2) or condition3
|
||||
- String values should be quoted: equals(VmName, "my-vm")
|
||||
- Simple conditions: heap_memory_used > 80, total_granted <= 1000
|
||||
- Complex conditions: total_used > 123 and total_granted > 333 and (total_granted > 3444 and total_paid_available < 5556)
|
||||
|
||||
PRIORITY LEVELS:
|
||||
- 0: Critical (immediate action required)
|
||||
- 1: Warning (attention needed, default)
|
||||
- 2: Info (informational only)
|
||||
""")
|
||||
public String createAlertRule(
|
||||
@ToolParam(description = "Alert rule name (required, must be unique)", required = true) String name,
|
||||
@ToolParam(description = "App name from hierarchy (must match exact hierarchy app value)", required = true) String app,
|
||||
@ToolParam(description = "Metrics name from hierarchy (must match exact hierarchy metrics value)", required = true) String metrics,
|
||||
@ToolParam(description = "Field conditions expression)", required = true) String fieldConditions,
|
||||
@ToolParam(description = "Alert rule type: 'realtime' (default) or 'periodic'", required = false) String type,
|
||||
@ToolParam(description = "Execution period in seconds (only for periodic rules, default: 300)", required = false) Integer period,
|
||||
@ToolParam(description = "Number of consecutive violations before triggering (default: 3)", required = false) Integer times,
|
||||
@ToolParam(description = "Alert priority as integer: 0=critical, 1=warning, 2=info (default: 1)", required = false) Integer priority,
|
||||
@ToolParam(description = "Alert rule description (optional)", required = false) String description,
|
||||
@ToolParam(description = "Alert message template with variables (optional)", required = false) String template,
|
||||
@ToolParam(description = "Data source type: 'promql' (default)", required = false) String datasource,
|
||||
@ToolParam(description = "Labels as key:value pairs separated by commas (e.g., 'env:prod,severity:critical')", required = false) String labels,
|
||||
@ToolParam(description = "Annotations as key:value pairs separated by commas (e.g., 'summary:High CPU')", required = false) String annotations,
|
||||
@ToolParam(description = "Whether to enable the rule immediately (default: true)", required = false) Boolean enable) {
|
||||
|
||||
try {
|
||||
log.info("Creating HertzBeat alert rule: name={}, app={}, metrics={}, fieldConditions={}", name, app, metrics, fieldConditions);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in create_alert_rule tool: {}", subjectSum);
|
||||
|
||||
// Validate required parameters
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return "Error: Alert rule name is required";
|
||||
}
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: App name is required (use get_apps_metrics_hierarchy to find exact names)";
|
||||
}
|
||||
if (metrics == null || metrics.trim().isEmpty()) {
|
||||
return "Error: Metrics name is required (use get_apps_metrics_hierarchy to find exact names)";
|
||||
}
|
||||
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
|
||||
return "Error: Field conditions are required (e.g., 'equals(VmName, \"arora\")', 'total_granted > 1000')";
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
type = "realtime";
|
||||
}
|
||||
if (times == null || times <= 0) {
|
||||
times = 3;
|
||||
}
|
||||
if (priority == null) {
|
||||
priority = 1; // Default to warning
|
||||
}
|
||||
if (enable == null) {
|
||||
enable = true;
|
||||
}
|
||||
if (datasource == null || datasource.trim().isEmpty()) {
|
||||
datasource = "promql";
|
||||
}
|
||||
|
||||
// Validate alert type
|
||||
if (!type.equals("realtime") && !type.equals("periodic")) {
|
||||
return "Error: Alert type must be 'realtime' or 'periodic'";
|
||||
}
|
||||
|
||||
// Validate priority
|
||||
if (priority < 0 || priority > 2) {
|
||||
return "Error: Priority must be 0 (critical), 1 (warning), or 2 (info)";
|
||||
}
|
||||
|
||||
// For periodic rules, validate period parameter
|
||||
if (type.equals("periodic")) {
|
||||
if (period == null || period <= 0) {
|
||||
period = 300; // Default 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL VALIDATION: Verify app-metric-field relationships using hierarchy
|
||||
String validationResult = validateHierarchyRelationships(app.trim(), metrics.trim(), fieldConditions.trim());
|
||||
if (!validationResult.equals("VALID")) {
|
||||
return validationResult; // Return validation error message
|
||||
}
|
||||
|
||||
// EXPRESSION VALIDATION: Verify field conditions syntax and operators
|
||||
String expressionValidation = UtilityClass.validateExpressionSyntax(fieldConditions.trim());
|
||||
if (!expressionValidation.equals("VALID")) {
|
||||
return expressionValidation; // Return expression validation error message
|
||||
}
|
||||
|
||||
String expr = String.format("equals(__app__,\"%s\") && equals(__metrics__,\"%s\") && %s",
|
||||
app.trim(), metrics.trim(), fieldConditions.trim());
|
||||
|
||||
|
||||
|
||||
// Parse labels if provided
|
||||
Map<String, String> labelsMap = new HashMap<>();
|
||||
if (labels != null && !labels.trim().isEmpty()) {
|
||||
labelsMap.putAll(UtilityClass.parseKeyValuePairs(labels));
|
||||
}
|
||||
// Add severity based on priority
|
||||
String severityLabel = priority == 0 ? "critical" : (priority == 1 ? "warning" : "info");
|
||||
labelsMap.put("severity", severityLabel);
|
||||
|
||||
// Parse annotations if provided
|
||||
Map<String, String> annotationsMap = new HashMap<>();
|
||||
if (annotations != null && !annotations.trim().isEmpty()) {
|
||||
annotationsMap.putAll(UtilityClass.parseKeyValuePairs(annotations));
|
||||
}
|
||||
// Add default annotations if not provided
|
||||
if (!annotationsMap.containsKey("summary")) {
|
||||
annotationsMap.put("summary", description != null ? description :
|
||||
String.format("Alert for %s %s when %s", app, metrics, fieldConditions));
|
||||
}
|
||||
if (!annotationsMap.containsKey("description")) {
|
||||
annotationsMap.put("description", String.format("Monitor %s metrics %s with conditions: %s", app, metrics, fieldConditions));
|
||||
}
|
||||
|
||||
// Generate default template if not provided
|
||||
if (template == null || template.trim().isEmpty()) {
|
||||
template = String.format("Alert: %s %s - %s", app, metrics, fieldConditions);
|
||||
}
|
||||
|
||||
// Create comprehensive alert definition
|
||||
AlertDefine alertDefine = AlertDefine.builder()
|
||||
.name(name.trim())
|
||||
.type(type)
|
||||
.expr(expr)
|
||||
.period(period)
|
||||
.times(times)
|
||||
.labels(labelsMap)
|
||||
.annotations(annotationsMap)
|
||||
.template(template)
|
||||
.datasource(datasource)
|
||||
.enable(enable)
|
||||
.build();
|
||||
|
||||
AlertDefine createdAlertDefine = alertDefineServiceAdapter.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());
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append(String.format("Successfully created %s alert rule '%s' with ID: %d\n",
|
||||
type, name, createdAlertDefine.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));
|
||||
if (type.equals("periodic")) {
|
||||
response.append(String.format("Execution period: %d seconds\n", period));
|
||||
}
|
||||
response.append(String.format("Data source: %s\n", datasource));
|
||||
response.append(String.format("Enabled: %s\n", enable));
|
||||
if (!labelsMap.isEmpty()) {
|
||||
response.append(String.format("Labels: %s\n", labelsMap));
|
||||
}
|
||||
response.append(bindingNote);
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create alert rule '{}': {}", name, e.getMessage(), e);
|
||||
return "Error creating alert rule '" + name + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// ... other existing methods would go here ...
|
||||
|
||||
@Override
|
||||
@Tool(name = "list_alert_rules", description = """
|
||||
List existing alert rules with filtering options.
|
||||
Shows configured thresholds and alert definitions.
|
||||
""")
|
||||
public String listAlertRules(
|
||||
@ToolParam(description = "Search term for rule name or description", required = false) String search,
|
||||
@ToolParam(description = "Filter by monitor type", required = false) String monitorType,
|
||||
@ToolParam(description = "Filter by enabled status", required = false) Boolean enabled,
|
||||
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Page size (default: 10)", required = false) Integer pageSize) {
|
||||
|
||||
try {
|
||||
log.info("Listing alert rules: search={}, monitorType={}, enabled={}", search, monitorType, enabled);
|
||||
|
||||
if (pageIndex == null || pageIndex < 0) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
if (pageSize == null || pageSize <= 0) {
|
||||
pageSize = 10;
|
||||
}
|
||||
|
||||
Page<AlertDefine> result = alertDefineServiceAdapter.getAlertDefines(
|
||||
search, monitorType, enabled, "gmtCreate", "desc", pageIndex, pageSize);
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("Found ").append(result.getContent().size())
|
||||
.append(" alert rules (Total: ").append(result.getTotalElements()).append("):\n\n");
|
||||
|
||||
for (AlertDefine alertDefine : result.getContent()) {
|
||||
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
|
||||
response.append("Name: ").append(alertDefine.getName()).append("\n");
|
||||
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
|
||||
response.append("Type: ").append(alertDefine.getType()).append("\n");
|
||||
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
|
||||
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
|
||||
|
||||
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
|
||||
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
|
||||
}
|
||||
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
|
||||
response.append("Summary: ").append(alertDefine.getAnnotations().get("summary")).append("\n");
|
||||
}
|
||||
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (result.getContent().isEmpty()) {
|
||||
response.append("No alert rules found matching the specified criteria.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to list alert rules: {}", e.getMessage(), e);
|
||||
return "Error retrieving alert rules: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "toggle_alert_rule", description = """
|
||||
Enable or disable an alert rule.
|
||||
Allows activating or deactivating threshold monitoring.
|
||||
""")
|
||||
public String toggleAlertRule(
|
||||
@ToolParam(description = "Alert rule ID", required = true) Long ruleId,
|
||||
@ToolParam(description = "Whether to enable the rule", required = true) Boolean enabled) {
|
||||
|
||||
try {
|
||||
log.info("Toggling alert rule ID: {} to enabled: {}", ruleId, enabled);
|
||||
|
||||
alertDefineServiceAdapter.toggleAlertDefineStatus(ruleId, enabled);
|
||||
|
||||
log.info("Successfully toggled alert rule ID: {} to enabled: {}", ruleId, enabled);
|
||||
return String.format("Successfully %s alert rule ID: %d",
|
||||
enabled ? "enabled" : "disabled", ruleId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to toggle alert rule ID {}: {}", ruleId, e.getMessage(), e);
|
||||
return "Error toggling alert rule: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_alert_rule_details", description = """
|
||||
Get detailed information about a specific alert rule.
|
||||
Shows complete threshold configuration and rule settings.
|
||||
""")
|
||||
public String getAlertRuleDetails(
|
||||
@ToolParam(description = "Alert rule ID", required = true) Long ruleId) {
|
||||
|
||||
try {
|
||||
log.info("Getting alert rule details for ID: {}", ruleId);
|
||||
|
||||
AlertDefine alertDefine = alertDefineServiceAdapter.getAlertDefine(ruleId);
|
||||
if (alertDefine == null) {
|
||||
return "Alert rule with ID " + ruleId + " not found";
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("ALERT RULE DETAILS\n");
|
||||
response.append("==================\n\n");
|
||||
|
||||
response.append("Rule ID: ").append(alertDefine.getId()).append("\n");
|
||||
response.append("Name: ").append(alertDefine.getName()).append("\n");
|
||||
response.append("Type: ").append(alertDefine.getType()).append("\n");
|
||||
response.append("Expression: ").append(alertDefine.getExpr()).append("\n");
|
||||
response.append("Trigger Times: ").append(alertDefine.getTimes()).append("\n");
|
||||
response.append("Enabled: ").append(alertDefine.isEnable()).append("\n");
|
||||
|
||||
if (alertDefine.getPeriod() != null) {
|
||||
response.append("Period: ").append(alertDefine.getPeriod()).append(" seconds\n");
|
||||
}
|
||||
|
||||
if (alertDefine.getLabels() != null && !alertDefine.getLabels().isEmpty()) {
|
||||
response.append("Labels: ").append(alertDefine.getLabels()).append("\n");
|
||||
}
|
||||
|
||||
if (alertDefine.getAnnotations() != null && !alertDefine.getAnnotations().isEmpty()) {
|
||||
response.append("Annotations: ").append(alertDefine.getAnnotations()).append("\n");
|
||||
}
|
||||
|
||||
if (alertDefine.getTemplate() != null) {
|
||||
response.append("Template: ").append(alertDefine.getTemplate()).append("\n");
|
||||
}
|
||||
|
||||
response.append("Created: ").append(alertDefine.getGmtCreate()).append("\n");
|
||||
response.append("Modified: ").append(alertDefine.getGmtUpdate()).append("\n");
|
||||
response.append("Creator: ").append(alertDefine.getCreator()).append("\n");
|
||||
response.append("Modifier: ").append(alertDefine.getModifier()).append("\n");
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get alert rule details for ID {}: {}", ruleId, e.getMessage(), e);
|
||||
return "Error retrieving alert rule details: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_apps_metrics_hierarchy", description = """
|
||||
Get the hierarchical structure of all available apps and their metrics for alert rule creation.
|
||||
This tool provides the exact app name, metric name and corresponding param names according to each metric.
|
||||
Returns structured JSON data showing the complete hierarchy with field parameters for alert expressions.
|
||||
|
||||
JSON Structure:
|
||||
- app: The application name
|
||||
- description: Tool description
|
||||
- hierarchy: Array of hierarchical data
|
||||
- Each node has: value, label, type, description
|
||||
- Leaf nodes have: dataType (numeric/string), unit (if applicable)
|
||||
- Non-leaf nodes have: children array
|
||||
VERY IMPORTANT:
|
||||
- ALWAYS USE the value field from the field parameters when creating alert expressions.
|
||||
|
||||
This structured data is needed to create proper alert expressions.
|
||||
""")
|
||||
public String getAppsMetricsHierarchy(
|
||||
@ToolParam(description = "App/Monitor type to get hierarchy for (e.g., 'linux', 'mysql', 'website')", required = true) String app) {
|
||||
|
||||
try {
|
||||
log.info("Getting apps metrics hierarchy for app: {}", app);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_apps_metrics_hierarchy tool: {}", subjectSum);
|
||||
|
||||
List<Hierarchy> hierarchies;
|
||||
hierarchies = alertDefineServiceAdapter.getAppHierarchy(app.trim().toLowerCase(), "en-US");
|
||||
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode result = mapper.createObjectNode();
|
||||
|
||||
result.put("app", app.toUpperCase());
|
||||
|
||||
if (hierarchies != null && !hierarchies.isEmpty()) {
|
||||
ArrayNode hierarchyArray = mapper.createArrayNode();
|
||||
for (Hierarchy hierarchy : hierarchies) {
|
||||
hierarchyArray.add(UtilityClass.formatHierarchyAsJson(mapper, hierarchy));
|
||||
}
|
||||
result.set("hierarchy", hierarchyArray);
|
||||
} else {
|
||||
result.put("message", "No hierarchy data available");
|
||||
}
|
||||
|
||||
String jsonResult = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result);
|
||||
log.info("Hierarchy JSON: {}", jsonResult);
|
||||
|
||||
return jsonResult;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get apps metrics hierarchy: {}", e.getMessage(), e);
|
||||
return "Error retrieving apps metrics hierarchy: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "bind_monitors_to_alert_rule", description = """
|
||||
Bind monitors to an alert rule.
|
||||
Call this tool if users want to bind specific monitors to their alert rule.
|
||||
Get the right monitor ids for a particular app using the query_monitors tool.
|
||||
Get the alert rule ID from the create_alert_rule tool output OR use the list_alert_rules tool with app_name search filter, if the output of create_alert_rule is not applicable.
|
||||
If monitors are already bound, this will add the new ones to the existing bindings.
|
||||
""")
|
||||
public String bindMonitorsToAlertRule(
|
||||
@ToolParam(description = "Alert rule ID to bind monitors to", required = true) Long ruleId,
|
||||
@ToolParam(description = "Comma-separated list of monitor IDs to bind", required = true) String monitorIds) {
|
||||
try {
|
||||
log.info("Binding monitors to alert rule ID: {}, monitors: {}", ruleId, monitorIds);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in bind_monitors_to_alert_rule tool: {}", subjectSum);
|
||||
|
||||
if (ruleId == null || ruleId <= 0) {
|
||||
return "Error: Valid alert rule ID is required";
|
||||
}
|
||||
if (monitorIds == null) {
|
||||
return "Error: Monitor IDs are required";
|
||||
}
|
||||
|
||||
// Get the existing alert rule
|
||||
AlertDefine existingRule = alertDefineServiceAdapter.getAlertDefine(ruleId);
|
||||
if (existingRule == null) {
|
||||
return String.format("Error: Alert rule with ID %d not found", ruleId);
|
||||
}
|
||||
|
||||
// Parse monitor IDs from comma-separated string
|
||||
String[] monitorIdArray = monitorIds.split(",");
|
||||
List<String> validMonitorIds = new ArrayList<>();
|
||||
|
||||
for (String monitorId : monitorIdArray) {
|
||||
String trimmedId = monitorId.trim();
|
||||
if (!trimmedId.isEmpty()) {
|
||||
try {
|
||||
Long.parseLong(trimmedId); // Validate it's a number
|
||||
validMonitorIds.add(trimmedId);
|
||||
} catch (NumberFormatException e) {
|
||||
return String.format("Error: Invalid monitor ID '%s'. Monitor IDs must be numeric.", trimmedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validMonitorIds.isEmpty()) {
|
||||
return "Error: No valid monitor IDs provided";
|
||||
}
|
||||
|
||||
// Build the monitor instance condition
|
||||
String monitorCondition;
|
||||
if (validMonitorIds.size() == 1) {
|
||||
monitorCondition = String.format("equals(__instance__, \"%s\")", validMonitorIds.get(0));
|
||||
} else {
|
||||
StringBuilder conditionBuilder = new StringBuilder("(");
|
||||
for (int i = 0; i < validMonitorIds.size(); i++) {
|
||||
if (i > 0) {
|
||||
conditionBuilder.append(" or ");
|
||||
}
|
||||
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", validMonitorIds.get(i)));
|
||||
}
|
||||
conditionBuilder.append(")");
|
||||
monitorCondition = conditionBuilder.toString();
|
||||
}
|
||||
|
||||
// Get the current expression and modify it
|
||||
String currentExpr = existingRule.getExpr();
|
||||
String newExpr;
|
||||
|
||||
// Check if the expression already has __instance__ conditions
|
||||
if (currentExpr.contains("__instance__")) {
|
||||
// Extract existing monitor IDs and merge with new ones
|
||||
List<String> existingMonitorIds = UtilityClass.extractExistingMonitorIds(currentExpr);
|
||||
|
||||
// Add new monitor IDs that aren't already present
|
||||
for (String newId : validMonitorIds) {
|
||||
if (!existingMonitorIds.contains(newId)) {
|
||||
existingMonitorIds.add(newId);
|
||||
}
|
||||
}
|
||||
|
||||
String updatedMonitorCondition;
|
||||
if (existingMonitorIds.size() == 1) {
|
||||
updatedMonitorCondition = String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(0));
|
||||
} else {
|
||||
StringBuilder conditionBuilder = new StringBuilder("(");
|
||||
for (int i = 0; i < existingMonitorIds.size(); i++) {
|
||||
if (i > 0) {
|
||||
conditionBuilder.append(" or ");
|
||||
}
|
||||
conditionBuilder.append(String.format("equals(__instance__, \"%s\")", existingMonitorIds.get(i)));
|
||||
}
|
||||
conditionBuilder.append(")");
|
||||
updatedMonitorCondition = conditionBuilder.toString();
|
||||
}
|
||||
|
||||
// Replace existing __instance__ conditions with updated ones
|
||||
newExpr = UtilityClass.replaceInstanceConditions(currentExpr, updatedMonitorCondition);
|
||||
|
||||
// Update the alert rule
|
||||
existingRule.setExpr(newExpr);
|
||||
alertDefineServiceAdapter.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",
|
||||
validMonitorIds.size(), ruleId, String.join(", ", existingMonitorIds), newExpr);
|
||||
}
|
||||
|
||||
// Insert the monitor condition after the metrics condition
|
||||
// Pattern: equals(__app__,"app") && equals(__metrics__,"metric") && [existing_conditions]
|
||||
// Result: equals(__app__,"app") && equals(__metrics__,"metric") && [monitor_condition] && [existing_conditions]
|
||||
|
||||
if (currentExpr.matches(".*equals\\(__app__,\"[^\"]+\"\\)\\s*&&\\s*equals\\(__metrics__,\"[^\"]+\"\\)\\s*&&\\s*.*")) {
|
||||
// Find the position after the metrics condition
|
||||
String metricsPattern = "equals\\(__metrics__,\"[^\"]+\"\\)";
|
||||
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(metricsPattern);
|
||||
java.util.regex.Matcher matcher = regex.matcher(currentExpr);
|
||||
|
||||
if (matcher.find()) {
|
||||
int metricsEnd = matcher.end();
|
||||
// Find the " && " after the metrics condition
|
||||
int andPosition = currentExpr.indexOf(" && ", metricsEnd);
|
||||
if (andPosition != -1) {
|
||||
String beforeAndPosition = currentExpr.substring(0, andPosition + 4); // Include " && "
|
||||
String afterAndPosition = currentExpr.substring(andPosition + 4); // Everything after " && "
|
||||
newExpr = beforeAndPosition + monitorCondition + " && " + afterAndPosition;
|
||||
} else {
|
||||
return String.format("Error: Unable to find field conditions after metrics in expression: %s", currentExpr);
|
||||
}
|
||||
} else {
|
||||
return String.format("Error: Unable to parse metrics condition in expression: %s", currentExpr);
|
||||
}
|
||||
} else {
|
||||
return String.format("Error: Expression format not supported for monitor binding: %s", currentExpr);
|
||||
}
|
||||
|
||||
// Update the alert rule
|
||||
existingRule.setExpr(newExpr);
|
||||
alertDefineServiceAdapter.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",
|
||||
validMonitorIds.size(), ruleId, String.join(", ", validMonitorIds), newExpr);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to bind monitors to alert rule ID {}: {}", ruleId, e.getMessage(), e);
|
||||
return String.format("Error binding monitors to alert rule: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates that the app, metric, and field conditions are valid according to hierarchy
|
||||
* @param app App name to validate
|
||||
* @param metrics Metric name to validate for the app
|
||||
* @param fieldConditions Field conditions to validate for the metric
|
||||
* @return "VALID" if all relationships are correct, error message otherwise
|
||||
*/
|
||||
private String validateHierarchyRelationships(String app, String metrics, String fieldConditions) {
|
||||
try {
|
||||
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");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Find the metric in the app's hierarchy
|
||||
Hierarchy metricHierarchy = UtilityClass.findMetricInHierarchy(hierarchies, metrics);
|
||||
if (metricHierarchy == null) {
|
||||
return String.format("Error: Metric '%s' not found for app '%s'. Please use get_apps_metrics_hierarchy to get valid metrics for this app.", metrics, app);
|
||||
}
|
||||
|
||||
// Extract field names from field conditions and validate them
|
||||
List<String> fieldNames = UtilityClass.extractFieldNamesFromConditions(fieldConditions);
|
||||
for (String fieldName : fieldNames) {
|
||||
if (!UtilityClass.isFieldValidForMetric(metricHierarchy, fieldName)) {
|
||||
return String.format("Error: Field '%s' not found for metric '%s' in app '%s'. Please use get_apps_metrics_hierarchy to get valid field parameters.", fieldName, metrics, app);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Hierarchy validation passed for app={}, metrics={}", app, metrics);
|
||||
return "VALID";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error during hierarchy validation: {}", e.getMessage(), e);
|
||||
return String.format("Error: Unable to validate hierarchy relationships: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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.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.alert.dto.AlertSummary;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of Alert Tools functionality for alarm data queries and management
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AlertToolsImpl implements AlertTools {
|
||||
@Autowired
|
||||
private AlertServiceAdapter alertServiceAdapter;
|
||||
|
||||
@Override
|
||||
@Tool(name = "query_alerts", description = """
|
||||
Query alerts with comprehensive filtering and pagination options.
|
||||
|
||||
ALERT TYPES:
|
||||
- Pass alertType='single' for individual alert instances
|
||||
- Pass alertType='group' for grouped/aggregated alerts
|
||||
- Pass alertType='both' to get both types (separate sections)
|
||||
|
||||
STATUS FILTERING:
|
||||
- 'firing': Currently active alerts requiring attention
|
||||
- 'resolved': Previously active alerts that have been cleared
|
||||
- 'all': Both firing and resolved alerts (default)
|
||||
|
||||
SEARCH & FILTERING:
|
||||
- search: Search in alert content, labels, or descriptions
|
||||
- sort: Order by 'startAt' (trigger time), 'triggerTimes' (frequency), 'status'
|
||||
- order: 'asc' (oldest first) or 'desc' (newest first, default)
|
||||
|
||||
PAGINATION:
|
||||
- pageIndex: Page number starting from 0
|
||||
- pageSize: Number of alerts per page (default: 10, max recommended: 50)
|
||||
|
||||
EXAMPLE AND COMMON USE CASES:
|
||||
- Recent active alerts: alertType='single', status='firing', sort='startAt', order='desc'
|
||||
- Historical analysis: alertType='single', status='resolved', pageSize=50
|
||||
- Alert grouping overview: alertType='group', status='all'
|
||||
- Search specific issues: search='cpu', alertType='single', status='firing'
|
||||
- Find abnormal monitors: status='firing' to get active alerts indicating monitor issues
|
||||
- Monitor-specific alerts: use search parameter with monitor ID or name to find related alerts
|
||||
- Frequent alerts analysis: sort='triggerTimes', order='desc' to find most frequently triggered alerts
|
||||
- Recent recurring issues: status='all', sort='triggerTimes', order='desc', pageSize=20
|
||||
""")
|
||||
public String queryAlerts(
|
||||
@ToolParam(description = "Alert type: 'single' (individual alerts), 'group' (grouped alerts), 'both' (default: single)", required = false) String alertType,
|
||||
@ToolParam(description = "Alert status: 'firing' (active), 'resolved' (cleared), 'all' (default: all)", required = false) String status,
|
||||
@ToolParam(description = "Search term for alert content or labels", required = false) String search,
|
||||
@ToolParam(description = "Sort field: 'startAt', 'triggerTimes', 'status' (default: startAt)", required = false) String sort,
|
||||
@ToolParam(description = "Sort order: 'asc' or 'desc' (default: desc)", required = false) String order,
|
||||
@ToolParam(description = "Page index starting from 0 (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Page size, 1-50 recommended (default: 10)", required = false) Integer pageSize) {
|
||||
|
||||
try {
|
||||
log.info("Querying alerts: alertType={}, status={}, search={}, sort={}, order={}", alertType, status, search, sort, order);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in query_alerts tool: {}", subjectSum);
|
||||
|
||||
// Set defaults
|
||||
if (alertType == null || alertType.trim().isEmpty()) {
|
||||
alertType = "single";
|
||||
}
|
||||
if (status == null || status.trim().isEmpty()) {
|
||||
status = "all";
|
||||
}
|
||||
if (sort == null || sort.trim().isEmpty()) {
|
||||
sort = "startAt";
|
||||
}
|
||||
if (order == null || order.trim().isEmpty()) {
|
||||
order = "desc";
|
||||
}
|
||||
if (pageIndex == null) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
if (pageSize == null) {
|
||||
pageSize = 10;
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("ALERT QUERY RESULTS\n");
|
||||
response.append("===================\n\n");
|
||||
|
||||
// Handle different alert types
|
||||
if ("single".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
|
||||
Page<SingleAlert> singleResult = alertServiceAdapter.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");
|
||||
|
||||
for (SingleAlert alert : singleResult.getContent()) {
|
||||
response.append("Alert ID: ").append(alert.getId()).append("\n");
|
||||
response.append("Status: ").append(alert.getStatus()).append("\n");
|
||||
response.append("Content: ").append(alert.getContent() != null ? alert.getContent() : "No content").append("\n");
|
||||
response.append("Trigger Times: ").append(alert.getTriggerTimes()).append("\n");
|
||||
|
||||
if (alert.getStartAt() != null) {
|
||||
response.append("Started At: ").append(UtilityClass.formatTimestamp(alert.getStartAt())).append("\n");
|
||||
}
|
||||
if (alert.getActiveAt() != null) {
|
||||
response.append("Active At: ").append(UtilityClass.formatTimestamp(alert.getActiveAt())).append("\n");
|
||||
}
|
||||
if (alert.getEndAt() != null) {
|
||||
response.append("Ended At: ").append(UtilityClass.formatTimestamp(alert.getEndAt())).append("\n");
|
||||
}
|
||||
|
||||
if (alert.getLabels() != null && !alert.getLabels().isEmpty()) {
|
||||
response.append("Labels: ").append(alert.getLabels()).append("\n");
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (singleResult.getContent().isEmpty()) {
|
||||
response.append("No single alerts found matching the specified criteria.\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle group alerts
|
||||
if ("group".equalsIgnoreCase(alertType) || "both".equalsIgnoreCase(alertType)) {
|
||||
if ("both".equalsIgnoreCase(alertType)) {
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
Page<GroupAlert> groupResult = alertServiceAdapter.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");
|
||||
|
||||
for (GroupAlert alert : groupResult.getContent()) {
|
||||
response.append("Group Alert ID: ").append(alert.getId()).append("\n");
|
||||
response.append("Status: ").append(alert.getStatus()).append("\n");
|
||||
response.append("Group Key: ").append(alert.getGroupKey() != null ? alert.getGroupKey() : "No group key").append("\n");
|
||||
|
||||
if (alert.getGmtCreate() != null) {
|
||||
response.append("Created At: ").append(alert.getGmtCreate()).append("\n");
|
||||
}
|
||||
if (alert.getGmtUpdate() != null) {
|
||||
response.append("Updated At: ").append(alert.getGmtUpdate()).append("\n");
|
||||
}
|
||||
|
||||
if (alert.getCommonLabels() != null && !alert.getCommonLabels().isEmpty()) {
|
||||
response.append("Common Labels: ").append(alert.getCommonLabels()).append("\n");
|
||||
}
|
||||
if (alert.getCommonAnnotations() != null && !alert.getCommonAnnotations().isEmpty()) {
|
||||
response.append("Annotations: ").append(alert.getCommonAnnotations()).append("\n");
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (groupResult.getContent().isEmpty()) {
|
||||
response.append("No group alerts found matching the specified criteria.\n");
|
||||
}
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to query alerts: {}", e.getMessage(), e);
|
||||
return "Error retrieving alerts: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_alerts_summary", description = """
|
||||
Get alerts summary statistics including total counts, status distribution, and recent trends.
|
||||
Returns comprehensive overview of the current alerting status across all monitors.
|
||||
""")
|
||||
public String getAlertsSummary() {
|
||||
try {
|
||||
log.info("Getting alerts summary");
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_alerts_summary tool: {}", subjectSum);
|
||||
|
||||
AlertSummary summary = alertServiceAdapter.getAlertsSummary();
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("ALERTS SUMMARY\n");
|
||||
response.append("==============\n\n");
|
||||
|
||||
if (summary != null) {
|
||||
response.append("Total Alerts: ").append(summary.getTotal()).append("\n");
|
||||
response.append("Handled Alerts: ").append(summary.getDealNum()).append("\n");
|
||||
response.append("Handling Rate: ").append(String.format("%.1f", summary.getRate())).append("%\n\n");
|
||||
|
||||
response.append("Priority Breakdown (Unhandled):\n");
|
||||
response.append("- Critical: ").append(summary.getPriorityCriticalNum()).append("\n");
|
||||
response.append("- Emergency: ").append(summary.getPriorityEmergencyNum()).append("\n");
|
||||
response.append("- Warning: ").append(summary.getPriorityWarningNum()).append("\n\n");
|
||||
|
||||
long totalUnhandled = summary.getPriorityCriticalNum() + summary.getPriorityEmergencyNum() + summary.getPriorityWarningNum();
|
||||
response.append("Total Unhandled Alerts: ").append(totalUnhandled).append("\n");
|
||||
|
||||
if (totalUnhandled > 0) {
|
||||
response.append("\nUnhandled Alert Distribution:\n");
|
||||
response.append("- Critical: ").append(String.format("%.1f", (summary.getPriorityCriticalNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||
response.append("- Emergency: ").append(String.format("%.1f", (summary.getPriorityEmergencyNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||
response.append("- Warning: ").append(String.format("%.1f", (summary.getPriorityWarningNum() * 100.0 / totalUnhandled))).append("%\n");
|
||||
}
|
||||
} else {
|
||||
response.append("No alert summary data available.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get alerts summary: {}", e.getMessage(), e);
|
||||
return "Error retrieving alerts summary: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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.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.common.entity.dto.Field;
|
||||
import org.apache.hertzbeat.common.entity.dto.MetricsData;
|
||||
import org.apache.hertzbeat.common.entity.dto.MetricsHistoryData;
|
||||
import org.apache.hertzbeat.common.entity.dto.Value;
|
||||
import org.apache.hertzbeat.common.entity.dto.ValueRow;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of Metrics Tools functionality for metrics data queries and analysis
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MetricsToolsImpl implements MetricsTools {
|
||||
@Autowired
|
||||
private MetricsServiceAdapter metricsServiceAdapter;
|
||||
@Autowired
|
||||
private MonitorServiceAdapter monitorServiceAdapter;
|
||||
|
||||
@Override
|
||||
@Tool(name = "query_realtime_metrics", description = """
|
||||
Get the supported monitor types/names from the list_monitor_types tool, make sure to use right name in the next call
|
||||
Use the query_monitors tool to find monitor IDs in case the user does not tell the id explicitly. You might have to use this multiple times based on the user's query
|
||||
Get real-time metrics data for a specific monitor.
|
||||
Returns current metrics values including CPU, memory, disk usage, etc.
|
||||
Based on the monitor type/name, use the get_apps_metrics_hierarchy tool to get the metrics hierarchy. i.e., metrics and the field parameter (sub-metric).
|
||||
Each metric has its submetrics as well for example: cpu has field parameters or sub-metrics like 'usage', 'load', 'core'. These value might be numeric or string
|
||||
User might ask about specific metrics like 'cpu usage', 'memory used', 'disk available', etc.
|
||||
So use this tool to get the real-time metrics data for the monitor- user asked the specific metrics for, along with any logical or numeric conditions if the user mentions
|
||||
In case of multiple monitors matching the user's description, you might have to call query_realtime_metrics tool mutliple times with different parameters.
|
||||
|
||||
|
||||
EXAMPLE WORKFLOW
|
||||
|
||||
If the user asks for 'cpu usage' for a particular monitor or multiple matching monitors like 'web server', 'database server', etc.
|
||||
Get the closest matching monitor name from list_monitor_types tool.
|
||||
Call query_monitors tool to get the monitor ID/IDs with the obtained monitor type
|
||||
Call get_apps_metrics_hierarchy tool to get all the metrics (type=metric) for the obtained monitor type
|
||||
Call query_realtime_metrics tool with each of the IDs and the closest matching metrics name (e.g., 'cpu', 'memory', etc.)
|
||||
From the result, do whatever operation user wants you to do with the data. In this example case display the cpu usage for each matching monitor.
|
||||
""")
|
||||
public String getRealtimeMetrics(
|
||||
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
|
||||
@ToolParam(description = "Metrics name (e.g., 'system', 'cpu', 'memory') obtained from get_apps_metrics_hierarchy result", required = true) String metrics) {
|
||||
try {
|
||||
log.info("Getting real-time metrics for monitor {} and metrics {}", monitorId, metrics);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in get_realtime_metrics tool: {}", subjectSum);
|
||||
|
||||
MetricsData metricsData = metricsServiceAdapter.getMetricsData(monitorId, metrics);
|
||||
|
||||
if (metricsData == null) {
|
||||
return String.format("No real-time metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("REAL-TIME METRICS DATA\n");
|
||||
response.append("=".repeat(50)).append("\n");
|
||||
response.append("Monitor ID: ").append(monitorId).append("\n");
|
||||
response.append("Metrics: ").append(metrics).append("\n");
|
||||
response.append("=".repeat(50)).append("\n\n");
|
||||
|
||||
if (metricsData.getValueRows() != null && !metricsData.getValueRows().isEmpty()) {
|
||||
List<Field> fields = metricsData.getFields();
|
||||
|
||||
response.append("Available Field Parameters (Sub-metrics):\n");
|
||||
response.append("-".repeat(40)).append("\n");
|
||||
|
||||
for (ValueRow valueRow : metricsData.getValueRows()) {
|
||||
// Show labels if available
|
||||
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
|
||||
response.append("Instance Labels: ").append(valueRow.getLabels()).append("\n");
|
||||
response.append("-".repeat(20)).append("\n");
|
||||
}
|
||||
|
||||
List<Value> values = valueRow.getValues();
|
||||
for (int i = 0; i < values.size() && i < fields.size(); i++) {
|
||||
Field field = fields.get(i);
|
||||
Value value = values.get(i);
|
||||
|
||||
// Enhanced field information display
|
||||
response.append("• Field Parameter: ").append(field.getName());
|
||||
if (field.getUnit() != null && !field.getUnit().isEmpty()) {
|
||||
response.append(" (").append(field.getUnit()).append(")");
|
||||
}
|
||||
response.append("\n");
|
||||
|
||||
response.append(" Current Value: ").append(value.getOrigin());
|
||||
|
||||
// Add data type indication
|
||||
try {
|
||||
Double.parseDouble(value.getOrigin());
|
||||
response.append(" [Numeric]");
|
||||
} catch (NumberFormatException e) {
|
||||
response.append(" [String]");
|
||||
}
|
||||
response.append("\n\n");
|
||||
}
|
||||
|
||||
if (valueRow.getLabels() != null && !valueRow.getLabels().isEmpty()) {
|
||||
response.append("-".repeat(20)).append("\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.append("No field parameter data available for metrics '").append(metrics).append("'.\n");
|
||||
response.append("Use get_apps_metrics_hierarchy tool to check available metrics for this monitor type.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get real-time metrics: {}", e.getMessage(), e);
|
||||
return "Error retrieving real-time metrics: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_historical_metrics", description = """
|
||||
Get historical metrics data for analysis and trending.
|
||||
Returns time-series data for specified metrics over a time range.
|
||||
Use the query_monitors tool to find the correct monitor IDs/ name or type for the monitor(s) user asked the metrics for
|
||||
Pass that name into the get_apps_metrics_hierarchy tool to get the metrics hierarchy i.e metrics and the field paramater
|
||||
DO NOT USE THE LABEL FIELD ALWAYS USE THE VALUE FIELD FROM THE HIERARCHY JSON
|
||||
Ask user to provide the filters for labels, history and interval aggregation
|
||||
""")
|
||||
public String getHistoricalMetrics(
|
||||
@ToolParam(description = "Monitor ID", required = true) Long monitorId,
|
||||
@ToolParam(description = "Monitor type (e.g., 'linux', 'mysql', 'http')", required = true) String app,
|
||||
@ToolParam(description = "Metrics name (e.g., 'target', 'cpu', 'memory')", required = true) String metrics,
|
||||
@ToolParam(description = "Field Parameter (e.g., 'usage', 'used', 'available')", required = false) String fieldParameter,
|
||||
@ToolParam(description = "Label filter for specific instances", required = false) String label,
|
||||
@ToolParam(description = "Time range (e.g., '1h', '6h', '24h', '7d')", required = false) String history,
|
||||
@ToolParam(description = "Whether to aggregate data with intervals", required = false) Boolean interval) {
|
||||
|
||||
try {
|
||||
log.info("Getting historical metrics for monitor {} and metrics {}", monitorId, metrics);
|
||||
|
||||
if (history == null || history.trim().isEmpty()) {
|
||||
history = "24h";
|
||||
}
|
||||
if (interval == null) {
|
||||
interval = true;
|
||||
}
|
||||
|
||||
MetricsHistoryData historyData = metricsServiceAdapter.getMetricHistoryData(
|
||||
monitorId, app, metrics, fieldParameter, label, history, interval);
|
||||
|
||||
if (historyData == null) {
|
||||
return String.format("No historical metrics data found for monitor ID %d and metrics '%s'", monitorId, metrics);
|
||||
}
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("HISTORICAL METRICS: ").append(metrics).append(" (Monitor ID: ").append(monitorId).append(")\n");
|
||||
response.append("Time Range: ").append(history).append(" | Interval Aggregation: ").append(interval).append("\n");
|
||||
response.append("=".repeat(60)).append("\n\n");
|
||||
|
||||
if (historyData.getValues() != null && !historyData.getValues().isEmpty()) {
|
||||
response.append("Field: ").append(historyData.getField() != null ? historyData.getField().getName() : "Unknown").append("\n");
|
||||
|
||||
// Calculate total data points
|
||||
int totalPoints = historyData.getValues().values().stream()
|
||||
.mapToInt(List::size)
|
||||
.sum();
|
||||
response.append("Data Points: ").append(totalPoints).append("\n");
|
||||
|
||||
// Show sample data points (first 10) from all value lists
|
||||
int count = 0;
|
||||
response.append("\nSample Data Points:\n");
|
||||
for (Map.Entry<String, List<Value>> entry : historyData.getValues().entrySet()) {
|
||||
String labelKey = entry.getKey();
|
||||
List<Value> values = entry.getValue();
|
||||
|
||||
for (Value value : values) {
|
||||
if (count >= 10) break;
|
||||
response.append("Label: ").append(labelKey)
|
||||
.append(" | Time: ").append(value.getTime())
|
||||
.append(" | Value: ").append(value.getOrigin()).append("\n");
|
||||
count++;
|
||||
}
|
||||
if (count >= 10) break;
|
||||
}
|
||||
|
||||
if (totalPoints > 10) {
|
||||
response.append("... and ").append(totalPoints - 10).append(" more data points\n");
|
||||
}
|
||||
} else {
|
||||
response.append("No historical data points available.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get historical metrics: {}", e.getMessage(), e);
|
||||
return "Error retrieving historical metrics: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_warehouse_status", description = """
|
||||
Check the status of the metrics storage warehouse system.
|
||||
Returns whether the metrics storage is operational and accessible.
|
||||
""")
|
||||
public String getWarehouseStatus() {
|
||||
try {
|
||||
log.info("Checking warehouse storage status");
|
||||
|
||||
Boolean status = metricsServiceAdapter.getWarehouseStorageServerStatus();
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("METRICS WAREHOUSE STATUS\n");
|
||||
response.append("========================\n\n");
|
||||
|
||||
if (status != null && status) {
|
||||
response.append("Status: ONLINE ✓\n");
|
||||
response.append("The metrics storage warehouse is operational and accessible.\n");
|
||||
response.append("Historical metrics data queries are available.");
|
||||
} else {
|
||||
response.append("Status: OFFLINE ✗\n");
|
||||
response.append("The metrics storage warehouse is not accessible.\n");
|
||||
response.append("Only real-time metrics may be available.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get warehouse status: {}", e.getMessage(), e);
|
||||
return "Error checking warehouse status: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+491
@@ -0,0 +1,491 @@
|
||||
/*
|
||||
* 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.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.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.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of Monitoring Tools functionality
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MonitorToolsImpl implements MonitorTools {
|
||||
@Autowired
|
||||
private MonitorServiceAdapter monitorServiceAdapter;
|
||||
|
||||
|
||||
/**
|
||||
* Tool to query monitor information with flexible filtering and pagination.
|
||||
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
|
||||
* pagination.
|
||||
* Returns detailed monitor information including ID, name, type, host, and status.
|
||||
*/
|
||||
@Override
|
||||
@Tool(name = "query_monitors", description = """
|
||||
Query Existing/configured monitors in HertzBeat.
|
||||
This tool retrieves monitors based on various filters and parameters.
|
||||
Comprehensive monitor querying with flexible filtering, pagination, and specialized views.
|
||||
|
||||
MONITOR STATUSES:
|
||||
- status=1: Online/Active monitors (healthy, responding normally)
|
||||
- status=2: Offline monitors (not responding, connection failed)
|
||||
- status=3: Unreachable monitors (network/connectivity issues)
|
||||
- status=0: Paused monitors (manually disabled/suspended)
|
||||
- status=9 or null: All monitors regardless of status (default)
|
||||
|
||||
COMMON USE CASES & PARAMETER COMBINATIONS:
|
||||
|
||||
1. BASIC MONITOR LISTING:
|
||||
- Default: No parameters (shows all monitors, 8 per page)
|
||||
- By type: app='linux' (show only Linux monitors)
|
||||
- Search: search='web' (find monitors with 'web' in name/host)
|
||||
|
||||
2. STATUS-BASED QUERIES:
|
||||
- Healthy monitors: status=1, pageSize=50
|
||||
- Problem monitors: status=2 or status=3, pageSize=50
|
||||
- Offline monitors only: status=2
|
||||
- Unreachable monitors only: status=3
|
||||
- Paused monitors: status=0
|
||||
|
||||
3. MONITORING HEALTH OVERVIEW:
|
||||
- All statuses with statistics: status=9, includeStats=true, pageSize=100
|
||||
- Unhealthy monitors: Pass both status=2 AND status=3 (make 2 separate calls)
|
||||
|
||||
4. ADVANCED FILTERING:
|
||||
- Specific monitor types: app='mysql', status=1 (healthy MySQL monitors)
|
||||
- Label-based: labels='env:prod,critical:true'
|
||||
- Host search: search='192.168' (find by IP pattern)
|
||||
- Monitor IDs: ids=[1,2,3] (specific monitors by ID)
|
||||
|
||||
5. SORTING & PAGINATION:
|
||||
- Recently updated: sort='gmtUpdate', order='desc'
|
||||
- Alphabetical: sort='name', order='asc'
|
||||
- By creation: sort='gmtCreate', order='desc' (newest first)
|
||||
- Large datasets: pageSize=50-100 for bulk operations
|
||||
|
||||
RESPONSE FORMAT:
|
||||
- includeStats=true: Adds status distribution summary at top
|
||||
- Default: Simple list with ID, name, type, host, status
|
||||
- Shows total count and pagination info
|
||||
""")
|
||||
public String queryMonitors(
|
||||
@ToolParam(description = "Specific monitor IDs to retrieve (optional)", required = false) List<Long> ids,
|
||||
@ToolParam(description = "Monitor type filter: 'linux', 'mysql', 'http', 'redis', etc. (optional)", required = false) String app,
|
||||
@ToolParam(description = "Monitor status: 1=online, 2=offline, 3=unreachable, 0=paused, 9=all (default: 9)", required = false) Byte status,
|
||||
@ToolParam(description = "Search in monitor names or hosts (partial matching)", required = false) String search,
|
||||
@ToolParam(description = "Label filters, format: 'key1:value1,key2:value2'", required = false) String labels,
|
||||
@ToolParam(description = "Sort field: 'name', 'gmtCreate', 'gmtUpdate', 'status', 'app' (default: gmtCreate)", required = false) String sort,
|
||||
@ToolParam(description = "Sort order: 'asc' (ascending) or 'desc' (descending, default)", required = false) String order,
|
||||
@ToolParam(description = "Page number starting from 0 (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Items per page: 1-100 recommended (default: 20)", required = false) Integer pageSize,
|
||||
@ToolParam(description = "Include status statistics summary (default: false)", required = false) Boolean includeStats) {
|
||||
try {
|
||||
// Set defaults
|
||||
if (pageSize == null || pageSize <= 0) {
|
||||
pageSize = 20;
|
||||
}
|
||||
if (pageIndex == null) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
if (includeStats == null) {
|
||||
includeStats = false;
|
||||
}
|
||||
|
||||
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
|
||||
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
|
||||
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("MONITOR QUERY RESULTS\n");
|
||||
response.append("====================\n\n");
|
||||
|
||||
// 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();
|
||||
|
||||
response.append("STATUS OVERVIEW:\n");
|
||||
response.append("- Online: ").append(onlineCount).append("\n");
|
||||
response.append("- Offline: ").append(offlineCount).append("\n");
|
||||
response.append("- Unreachable: ").append(unreachableCount).append("\n");
|
||||
response.append("- Paused: ").append(pausedCount).append("\n");
|
||||
|
||||
long total = onlineCount + offlineCount + unreachableCount + pausedCount;
|
||||
if (total > 0) {
|
||||
double healthPercentage = (onlineCount * 100.0) / total;
|
||||
response.append("- Health Rate: ").append(String.format("%.1f", healthPercentage)).append("%\n");
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
response.append("Query Results: ").append(result.getContent().size())
|
||||
.append(" monitors (Total: ").append(result.getTotalElements()).append(")\n");
|
||||
|
||||
if (result.getTotalPages() > 1) {
|
||||
response.append("Page ").append(pageIndex + 1).append(" of ").append(result.getTotalPages()).append("\n");
|
||||
}
|
||||
response.append("\n");
|
||||
|
||||
for (Monitor monitor : result.getContent()) {
|
||||
response.append("ID: ").append(monitor.getId())
|
||||
.append(" | Name: ").append(monitor.getName())
|
||||
.append(" | Type: ").append(monitor.getApp())
|
||||
.append(" | Host: ").append(monitor.getHost())
|
||||
.append(" | Status: ").append(UtilityClass.getStatusText(monitor.getStatus()));
|
||||
|
||||
// Add creation date for better context
|
||||
if (monitor.getGmtCreate() != null) {
|
||||
response.append(" | Created: ").append(monitor.getGmtCreate().toString(), 0, 10);
|
||||
}
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
if (result.getContent().isEmpty()) {
|
||||
response.append("No monitors found matching the specified criteria.\n");
|
||||
response.append("Try adjusting your filters or search terms.");
|
||||
}
|
||||
|
||||
return response.toString();
|
||||
} catch (Exception e) {
|
||||
return "Error retrieving monitors: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Tool(name = "add_monitor", description = """
|
||||
Add a new monitoring target to HertzBeat with comprehensive configuration.
|
||||
This tool dynamically handles different parameter requirements for each monitor type.
|
||||
|
||||
This tool creates monitors with proper app-specific parameters.
|
||||
|
||||
*********
|
||||
VERY IMPORTANT:
|
||||
ALWAYS use get_monitor_additional_params to check the additional required parameters for the chosen type before adding a monitor or even mentioning it.
|
||||
Use list_monitor_types tool to see available monitor type names to use here in the app parameter.
|
||||
Use the information obtained from this to query user for parameters.
|
||||
If the User has not given any parameters, ask them to provide the necessary parameters, until all the necessary parameters are provided.
|
||||
**********
|
||||
|
||||
Examples of natural language requests this tool handles:
|
||||
- "Monitor website example.com with HTTPS on port 443"
|
||||
- "Add MySQL monitoring for database server at 192.168.1.10 with user admin"
|
||||
- "Monitor Linux server health on host server.company.com via SSH"
|
||||
- "Set up Redis monitoring on localhost port 6379 with password"
|
||||
|
||||
PARAMETER MAPPING: The tool intelligently maps common parameters:
|
||||
- host: Target server/domain
|
||||
- port: Service port (auto-detected if not specified)
|
||||
- username: Authentication username
|
||||
- password: Authentication password
|
||||
- database: Database name (for DB monitors)
|
||||
- additionalParams: JSON string for app-specific parameters (to be obtained from get_monitor_param_defines)
|
||||
|
||||
ADDITIONAL PARAMETERS EXAMPLES:
|
||||
- Website: {"uri":"/api/health", "ssl":"true", "method":"POST"}
|
||||
- Linux: {"privateKey":"ssh-key-content", "script":"custom-script"}
|
||||
- Database: {"url":"jdbc:mysql://custom", "timeout":"10000"}
|
||||
""")
|
||||
public String addMonitor(
|
||||
@ToolParam(description = "Monitor name (required)", required = true) String name,
|
||||
@ToolParam(description = "Monitor type: website, mysql, postgresql, redis, linux, windows, etc.", required = true) String app,
|
||||
@ToolParam(description = "Target host: IP address or domain name", required = true) String host,
|
||||
@ToolParam(description = "Target port (optional, auto-detected if not specified)", required = false) Integer port,
|
||||
@ToolParam(description = "Collection interval in seconds (default: 600)", required = false) Integer intervals,
|
||||
@ToolParam(description = "Username for authentication (optional)", required = false) String username,
|
||||
@ToolParam(description = "Password for authentication (optional)", required = false) String password,
|
||||
@ToolParam(description = "Database name (for database monitors)", required = false) String database,
|
||||
@ToolParam(description = "Additional app-specific parameters as JSON: {\"uri\":\"/api\", \"ssl\":\"true\", \"method\":\"POST\"}", required = false) String additionalParams,
|
||||
@ToolParam(description = "Monitor description (optional)", required = false) String description) {
|
||||
|
||||
try {
|
||||
log.info("Adding monitor: name={}, app={}, host={}", name, app, host);
|
||||
|
||||
// Validate required parameters
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
return "Error: Monitor name is required";
|
||||
}
|
||||
if (app == null || app.trim().isEmpty()) {
|
||||
return "Error: Monitor type/application is required";
|
||||
}
|
||||
if (host == null || host.trim().isEmpty()) {
|
||||
return "Error: Host is required";
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if (intervals == null || intervals < 10) {
|
||||
intervals = 600;
|
||||
}
|
||||
|
||||
// Create Monitor entity
|
||||
Monitor monitor = Monitor.builder()
|
||||
.name(name.trim())
|
||||
.app(app.toLowerCase().trim())
|
||||
.host(host.trim())
|
||||
.intervals(intervals)
|
||||
.status((byte) 1)
|
||||
.type((byte) 0)
|
||||
.description(description != null ? description.trim() : "")
|
||||
.build();
|
||||
|
||||
List<Param> params = createBasicParams(host, port, username, password, database, additionalParams);
|
||||
|
||||
// Validate that all required parameters for this monitor type are provided
|
||||
try {
|
||||
List<ParamDefine> requiredParams = monitorServiceAdapter.getMonitorParamDefines(app);
|
||||
log.info("Checking required parameters for monitor type '{}': {}", app, requiredParams);
|
||||
List<String> missingParams = new ArrayList<>();
|
||||
|
||||
for (ParamDefine paramDefine : requiredParams) {
|
||||
if (paramDefine.isRequired()) {
|
||||
String fieldName = paramDefine.getField();
|
||||
boolean hasParam = params.stream()
|
||||
.anyMatch(param -> fieldName.equals(param.getField()));
|
||||
if (!hasParam) {
|
||||
missingParams.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingParams.isEmpty()) {
|
||||
return String.format("Error: Missing required parameters for monitor type '%s': %s. "
|
||||
+ "Use get_monitor_additional_params tool to see all required parameters.",
|
||||
app, String.join(", ", missingParams));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
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);
|
||||
|
||||
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)",
|
||||
app.toUpperCase(), name, monitorId, host, intervals);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add monitor '{}': {}", name, e.getMessage(), e);
|
||||
return "Error adding monitor '" + name + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create basic parameter list from user inputs
|
||||
*/
|
||||
private List<Param> createBasicParams(String host, Integer port, String username,
|
||||
String password, String database, String additionalParams) {
|
||||
List<Param> params = new ArrayList<>();
|
||||
|
||||
// Add host (always required)
|
||||
params.add(Param.builder().field("host").paramValue(host.trim()).type((byte) 1).build());
|
||||
|
||||
// Add optional common parameters
|
||||
if (port != null) {
|
||||
params.add(Param.builder().field("port").paramValue(port.toString()).type((byte) 0).build());
|
||||
}
|
||||
if (username != null && !username.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("username").paramValue(username.trim()).type((byte) 1).build());
|
||||
}
|
||||
if (password != null && !password.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("password").paramValue(password.trim()).type((byte) 2).build());
|
||||
}
|
||||
if (database != null && !database.trim().isEmpty()) {
|
||||
params.add(Param.builder().field("database").paramValue(database.trim()).type((byte) 1).build());
|
||||
}
|
||||
|
||||
// Parse additional parameters if provided
|
||||
if (additionalParams != null && !additionalParams.trim().isEmpty()) {
|
||||
try {
|
||||
String cleaned = additionalParams.trim().replaceAll("[{}]", "");
|
||||
String[] pairs = cleaned.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split(":");
|
||||
if (keyValue.length == 2) {
|
||||
String key = keyValue[0].trim().replaceAll("\"", "");
|
||||
String value = keyValue[1].trim().replaceAll("\"", "");
|
||||
params.add(Param.builder().field(key).paramValue(value).type((byte) 1).build());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse additionalParams: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "list_monitor_types", description = """
|
||||
List all available monitor types that can be added to HertzBeat.
|
||||
This tool shows all supported monitor types with their display names.
|
||||
Use this to see what types of monitors you can create with the add_monitor tool.
|
||||
""")
|
||||
public String listMonitorTypes(
|
||||
@ToolParam(description = "Language code for localized names (en-US, zh-CN, etc.). Default: en-US", required = false) String language) {
|
||||
|
||||
try {
|
||||
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);
|
||||
|
||||
if (monitorTypes == null || monitorTypes.isEmpty()) {
|
||||
return "No monitor types are currently available.";
|
||||
}
|
||||
|
||||
// Format the response as a nice list
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append("Available Monitor Types (Total: ").append(monitorTypes.size()).append("):\n\n");
|
||||
|
||||
// Sort monitor types alphabetically by key
|
||||
List<Map.Entry<String, String>> sortedTypes = monitorTypes.entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.toList();
|
||||
|
||||
for (Map.Entry<String, String> entry : sortedTypes) {
|
||||
String typeKey = entry.getKey();
|
||||
String displayName = entry.getValue();
|
||||
response.append("• ").append(typeKey)
|
||||
.append(" - ").append(displayName)
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
response.append("\nTo add a monitor, use the add_monitor tool with one of these types as the 'app' parameter.");
|
||||
|
||||
log.info("Successfully listed {} monitor types", monitorTypes);
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to list monitor types: {}", e.getMessage(), e);
|
||||
return "Error retrieving monitor types: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "get_monitor_additional_params", description = """
|
||||
Get the parameter definitions required for a specific monitor type.
|
||||
This tool shows what parameters are needed when adding a monitor of the specified type,
|
||||
ALWAYS use this before adding a monitor to understand what parameters the user needs to provide.
|
||||
Use the app parameter to specify the monitor type/application name (e.g., 'linux', 'mysql', 'redis') this can be obtained from the list_monitor_types tool.
|
||||
""")
|
||||
public String getMonitorAdditionalParams(
|
||||
@ToolParam(description = "Monitor type/application name (e.g., 'linux', 'mysql', 'redis')", required = true) String app) {
|
||||
|
||||
try {
|
||||
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);
|
||||
|
||||
if (paramDefines == null || paramDefines.isEmpty()) {
|
||||
return String.format("No parameter definitions found for monitor type '%s'. "
|
||||
+ "This monitor type may not exist or may not require additional parameters.", app);
|
||||
}
|
||||
|
||||
// Format the response
|
||||
StringBuilder response = new StringBuilder();
|
||||
response.append(String.format("Parameter Definitions for Monitor Type '%s' (Total: %d):\n\n",
|
||||
app, paramDefines.size()));
|
||||
|
||||
for (ParamDefine paramDefine : paramDefines) {
|
||||
response.append("• Field: ").append(paramDefine.getField()).append("\n");
|
||||
|
||||
// Add display name if available
|
||||
if (paramDefine.getName() != null && !paramDefine.getName().toString().trim().isEmpty()) {
|
||||
response.append(" Name: ").append(paramDefine.getName()).append("\n");
|
||||
}
|
||||
|
||||
// Add type
|
||||
if (paramDefine.getType() != null && !paramDefine.getType().trim().isEmpty()) {
|
||||
response.append(" Type: ").append(paramDefine.getType()).append("\n");
|
||||
}
|
||||
|
||||
// Add required status
|
||||
response.append(" Required: ").append(paramDefine.isRequired() ? "Yes" : "No").append("\n");
|
||||
|
||||
// Add default value if present
|
||||
if (paramDefine.getDefaultValue() != null && !paramDefine.getDefaultValue().trim().isEmpty()) {
|
||||
response.append(" Default: ").append(paramDefine.getDefaultValue()).append("\n");
|
||||
}
|
||||
|
||||
// Add validation range if present
|
||||
if (paramDefine.getRange() != null && !paramDefine.getRange().trim().isEmpty()) {
|
||||
response.append(" Range: ").append(paramDefine.getRange()).append("\n");
|
||||
}
|
||||
|
||||
// Add limit if present
|
||||
if (paramDefine.getLimit() != null) {
|
||||
response.append(" Limit: ").append(paramDefine.getLimit()).append("\n");
|
||||
}
|
||||
|
||||
// Add placeholder text if present
|
||||
if (paramDefine.getPlaceholder() != null && !paramDefine.getPlaceholder().trim().isEmpty()) {
|
||||
response.append(" Placeholder: ").append(paramDefine.getPlaceholder()).append("\n");
|
||||
}
|
||||
|
||||
response.append("\n");
|
||||
}
|
||||
|
||||
response.append("To add a monitor of this type, use the add_monitor tool with these parameters.\n");
|
||||
response.append(String.format("Example: add_monitor(name='my-monitor', app='%s', host='your-host', ...)", app));
|
||||
|
||||
log.info("Successfully retrieved {} parameter definitions for monitor type: {}", paramDefines.size(), app);
|
||||
return response.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get parameter definitions for monitor type '{}': {}", app, e.getMessage(), e);
|
||||
return "Error retrieving parameter definitions for monitor type '" + app + "': " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+691
@@ -0,0 +1,691 @@
|
||||
/*
|
||||
* 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.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;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility class providing helper methods for field expression validation, parsing,
|
||||
* hierarchy management, timestamp formatting and metric/status conversions.
|
||||
* This class contains stateless utility methods used across the application for
|
||||
* common operations and validations.
|
||||
*/
|
||||
@Slf4j
|
||||
@lombok.experimental.UtilityClass
|
||||
public class UtilityClass {
|
||||
|
||||
/**
|
||||
* Validates the syntax of field conditions expression
|
||||
* @param fieldConditions Field conditions string to validate
|
||||
* @return "VALID" if syntax is correct, error message otherwise
|
||||
*/
|
||||
|
||||
public String validateExpressionSyntax(String fieldConditions) {
|
||||
try {
|
||||
log.debug("Validating expression syntax: {}", fieldConditions);
|
||||
|
||||
// Check for basic syntax requirements
|
||||
if (fieldConditions == null || fieldConditions.trim().isEmpty()) {
|
||||
return "Error: Field conditions cannot be empty";
|
||||
}
|
||||
|
||||
// Check for balanced parentheses
|
||||
if (!hasBalancedParentheses(fieldConditions)) {
|
||||
return "Error: Unbalanced parentheses in field conditions. Please check your expression syntax.";
|
||||
}
|
||||
|
||||
// Validate operators used in the expression
|
||||
String operatorValidation = validateOperators(fieldConditions);
|
||||
if (!operatorValidation.equals("VALID")) {
|
||||
return operatorValidation;
|
||||
}
|
||||
|
||||
// Validate logical connectors
|
||||
String logicalValidation = validateLogicalConnectors(fieldConditions);
|
||||
if (!logicalValidation.equals("VALID")) {
|
||||
return logicalValidation;
|
||||
}
|
||||
|
||||
// Validate function syntax (equals, contains, etc.)
|
||||
String functionValidation = validateFunctions(fieldConditions);
|
||||
if (!functionValidation.equals("VALID")) {
|
||||
return functionValidation;
|
||||
}
|
||||
|
||||
log.debug("Expression syntax validation passed for: {}", fieldConditions);
|
||||
return "VALID";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error during expression syntax validation: {}", e.getMessage(), e);
|
||||
return String.format("Error: Unable to validate expression syntax: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if parentheses are balanced in the expression
|
||||
*/
|
||||
public boolean hasBalancedParentheses(String expression) {
|
||||
int count = 0;
|
||||
for (char c : expression.toCharArray()) {
|
||||
if (c == '(') {
|
||||
count++;
|
||||
} else if (c == ')') {
|
||||
count--;
|
||||
if (count < 0) {
|
||||
return false; // More closing than opening
|
||||
}
|
||||
}
|
||||
}
|
||||
return count == 0; // Should be perfectly balanced
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that only supported operators are used
|
||||
*/
|
||||
public String validateOperators(String fieldConditions) {
|
||||
// Define supported operators for different field types
|
||||
String[] numericOperators = {">", "<", ">=", "<=", "==", "!=", "exists()", "!exists()"};
|
||||
String[] stringOperators = {"equals(", "contains(", "matches(", "exists()", "!equals(", "!contains(", "!matches(", "!exists()"};
|
||||
String[] logicalOperators = {" and ", " or "};
|
||||
|
||||
// Remove quotes and function calls temporarily for operator checking
|
||||
String tempExpression = fieldConditions
|
||||
.replaceAll("\"[^\"]*\"", "VALUE") // Remove quoted strings
|
||||
.replaceAll("'[^']*'", "VALUE") // Remove single quoted strings
|
||||
.replaceAll("\\w+\\([^)]*\\)", "FUNCTION"); // Remove function calls
|
||||
|
||||
// Check for invalid operators (common mistakes)
|
||||
String[] invalidOperators = {"&&", "||", "AND", "OR", "=", "!="};
|
||||
for (String invalidOp : invalidOperators) {
|
||||
if (tempExpression.contains(invalidOp)) {
|
||||
if (invalidOp.equals("&&") || invalidOp.equals("||")) {
|
||||
return String.format("Error: Use 'and'/'or' instead of '%s' for logical operations", invalidOp);
|
||||
}
|
||||
if (invalidOp.equals("AND") || invalidOp.equals("OR")) {
|
||||
return String.format("Error: Use lowercase '%s' for logical operations", invalidOp.toLowerCase());
|
||||
}
|
||||
if (invalidOp.equals("=")) {
|
||||
return "Error: Use '==' for equality comparison, not '='";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unsupported special characters that might indicate syntax errors
|
||||
if (tempExpression.matches(".*[#$%^&*+\\[\\]{}|\\\\;:'\"`~].*")) {
|
||||
return "Error: Expression contains unsupported special characters. Use only supported operators and functions.";
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates logical connectors syntax
|
||||
*/
|
||||
public String validateLogicalConnectors(String fieldConditions) {
|
||||
// Check for proper spacing around logical operators
|
||||
if (fieldConditions.matches(".*(\\S(and|or)\\S).*")) {
|
||||
return "Error: Logical operators 'and'/'or' must be surrounded by spaces";
|
||||
}
|
||||
|
||||
// Check for consecutive logical operators
|
||||
if (fieldConditions.matches(".*(and\\s+and|or\\s+or|and\\s+or\\s+and|or\\s+and\\s+or).*")) {
|
||||
return "Error: Consecutive logical operators found. Use parentheses to group conditions properly.";
|
||||
}
|
||||
|
||||
// Check for logical operators at the beginning or end
|
||||
String trimmed = fieldConditions.trim();
|
||||
if (trimmed.startsWith("and ") || trimmed.startsWith("or ")
|
||||
|| trimmed.endsWith(" and") || trimmed.endsWith(" or")) {
|
||||
return "Error: Expression cannot start or end with logical operators 'and'/'or'";
|
||||
}
|
||||
|
||||
return "VALID";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates function syntax (equals, contains, matches, etc.)
|
||||
*/
|
||||
public String validateFunctions(String fieldConditions) {
|
||||
// Check for properly formed function calls
|
||||
String[] supportedFunctions = {"equals", "contains", "matches", "exists", "!equals", "!contains", "!matches", "!exists"};
|
||||
|
||||
// Find all function-like patterns
|
||||
java.util.regex.Pattern functionPattern = java.util.regex.Pattern.compile("(!?\\w+)\\s*\\(([^)]*)\\)");
|
||||
java.util.regex.Matcher matcher = functionPattern.matcher(fieldConditions);
|
||||
|
||||
while (matcher.find()) {
|
||||
String functionName = matcher.group(1);
|
||||
String functionArgs = matcher.group(2);
|
||||
|
||||
// Check if function is supported
|
||||
boolean isSupported = false;
|
||||
for (String supportedFunc : supportedFunctions) {
|
||||
if (functionName.equals(supportedFunc)) {
|
||||
isSupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSupported) {
|
||||
return String.format("Error: Unsupported function '%s'. Supported functions: %s",
|
||||
functionName, String.join(", ", supportedFunctions));
|
||||
}
|
||||
|
||||
// Validate function arguments
|
||||
if (functionName.equals("exists") || functionName.equals("!exists")) {
|
||||
// exists() should have one parameter or no parameters
|
||||
String[] args = functionArgs.trim().isEmpty() ? new String[0] : functionArgs.split(",");
|
||||
if (args.length > 1) {
|
||||
return String.format("Error: Function '%s' should have at most one parameter", functionName);
|
||||
}
|
||||
} else {
|
||||
// Other functions should have exactly 2 parameters
|
||||
String[] args = functionArgs.split(",");
|
||||
if (args.length != 2) {
|
||||
return String.format("Error: Function '%s' requires exactly 2 parameters (field, value)", functionName);
|
||||
}
|
||||
|
||||
// Check that parameters are not empty
|
||||
for (String arg : args) {
|
||||
if (arg.trim().isEmpty()) {
|
||||
return String.format("Error: Function '%s' has empty parameter", functionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, ..."
|
||||
*/
|
||||
public Map<String, String> parseKeyValuePairs(String input) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (input == null || input.trim().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
String[] pairs = input.split(",");
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split(":");
|
||||
if (keyValue.length == 2) {
|
||||
result.put(keyValue[0].trim(), keyValue[1].trim());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively searches for a metric in the hierarchy
|
||||
*/
|
||||
public Hierarchy findMetricInHierarchy(List<Hierarchy> hierarchies, String metricName) {
|
||||
for (Hierarchy hierarchy : hierarchies) {
|
||||
// Check if this is the metric we're looking for
|
||||
if (metricName.equals(hierarchy.getValue())) {
|
||||
// Verify it has field children (leaf nodes)
|
||||
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||
if (hasLeafChildren) {
|
||||
return hierarchy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively search in children
|
||||
if (hierarchy.getChildren() != null) {
|
||||
Hierarchy found = findMetricInHierarchy(hierarchy.getChildren(), metricName);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a field is valid for the given metric
|
||||
*/
|
||||
public boolean isFieldValidForMetric(Hierarchy metricHierarchy, String fieldName) {
|
||||
if (metricHierarchy.getChildren() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Hierarchy child : metricHierarchy.getChildren()) {
|
||||
if (child.getIsLeaf() != null && child.getIsLeaf() && fieldName.equals(child.getValue())) {
|
||||
return true;
|
||||
}
|
||||
// Also check nested children
|
||||
if (child.getChildren() != null && isFieldValidForMetric(child, fieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field names from field conditions string
|
||||
* Handles simple cases like "field > 80", "equals(field, 'value')", complex expressions
|
||||
*/
|
||||
public List<String> extractFieldNamesFromConditions(String fieldConditions) {
|
||||
List<String> fieldNames = new ArrayList<>();
|
||||
|
||||
// Split by logical operators (and, or) and parentheses, but preserve the field names
|
||||
// This is a simple implementation - could be enhanced with a proper parser
|
||||
String[] parts = fieldConditions.split("\\s+(and|or|&&|\\|\\|)\\s+|[()]+");
|
||||
|
||||
for (String part : parts) {
|
||||
part = part.trim();
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle equals() function: equals(fieldName, "value")
|
||||
if (part.contains("equals(")) {
|
||||
String fieldName = extractFieldFromEquals(part);
|
||||
if (fieldName != null && !fieldNames.contains(fieldName)) {
|
||||
fieldNames.add(fieldName);
|
||||
}
|
||||
} else {
|
||||
// Handle simple comparisons: fieldName > value, fieldName <= value
|
||||
String fieldName = extractFieldFromComparison(part);
|
||||
if (fieldName != null && !fieldNames.contains(fieldName)) {
|
||||
fieldNames.add(fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field name from equals() function
|
||||
*/
|
||||
public String extractFieldFromEquals(String condition) {
|
||||
// Pattern: equals(fieldName, "value") or equals(fieldName, value)
|
||||
int startParen = condition.indexOf('(');
|
||||
int comma = condition.indexOf(',');
|
||||
|
||||
if (startParen != -1 && comma != -1 && comma > startParen) {
|
||||
String fieldName = condition.substring(startParen + 1, comma).trim();
|
||||
// Remove quotes if present
|
||||
if (fieldName.startsWith("\"") && fieldName.endsWith("\"")) {
|
||||
fieldName = fieldName.substring(1, fieldName.length() - 1);
|
||||
}
|
||||
return fieldName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts field name from comparison operation
|
||||
*/
|
||||
public String extractFieldFromComparison(String condition) {
|
||||
// Pattern: fieldName operator value
|
||||
// Updated to include all supported operators
|
||||
String[] operators = {" >= ", " <= ", " > ", " < ", " == ", " != "};
|
||||
|
||||
for (String operator : operators) {
|
||||
if (condition.contains(operator)) {
|
||||
String fieldName = condition.split(operator)[0].trim();
|
||||
// Basic validation - field names shouldn't contain quotes or special chars
|
||||
if (fieldName.matches("[a-zA-Z_][a-zA-Z0-9_]*")) {
|
||||
return fieldName;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to format hierarchy structure as JSON recursively
|
||||
*/
|
||||
public ObjectNode formatHierarchyAsJson(ObjectMapper mapper, Hierarchy hierarchy) {
|
||||
ObjectNode node = mapper.createObjectNode();
|
||||
|
||||
node.put("value", hierarchy.getValue());
|
||||
node.put("label", hierarchy.getLabel());
|
||||
|
||||
if (hierarchy.getIsLeaf() != null && hierarchy.getIsLeaf()) {
|
||||
// Leaf node - actual metric field parameter
|
||||
node.put("type", "field_parameter");
|
||||
|
||||
if (hierarchy.getType() != null) {
|
||||
node.put("dataType", hierarchy.getType() == 0 ? "numeric" : "string");
|
||||
}
|
||||
if (hierarchy.getUnit() != null && !hierarchy.getUnit().trim().isEmpty()) {
|
||||
node.put("unit", hierarchy.getUnit());
|
||||
}
|
||||
node.put("description", "Available field parameter for alert conditions");
|
||||
} else {
|
||||
// Category, app, or metric node
|
||||
// Determine node type based on children
|
||||
boolean hasLeafChildren = hierarchy.getChildren().stream()
|
||||
.anyMatch(child -> child.getIsLeaf() != null && child.getIsLeaf());
|
||||
|
||||
if (hasLeafChildren) {
|
||||
node.put("type", "metric");
|
||||
node.put("description", "Metric with available field parameters");
|
||||
} else {
|
||||
node.put("type", "app");
|
||||
node.put("description", "Application with available metrics");
|
||||
}
|
||||
|
||||
if (hierarchy.getChildren() != null && !hierarchy.getChildren().isEmpty()) {
|
||||
ArrayNode childrenArray = mapper.createArrayNode();
|
||||
for (Hierarchy child : hierarchy.getChildren()) {
|
||||
childrenArray.add(formatHierarchyAsJson(mapper, child));
|
||||
}
|
||||
node.set("children", childrenArray);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp to readable format
|
||||
*/
|
||||
public String formatTimestamp(Long timestamp) {
|
||||
if (timestamp == null) {
|
||||
return "N/A";
|
||||
}
|
||||
LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
|
||||
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
|
||||
* @return Human-readable status text
|
||||
*/
|
||||
public String getStatusText(Byte status) {
|
||||
if (status == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
return switch (status) {
|
||||
case 0 -> "Paused";
|
||||
case 1 -> "Online";
|
||||
case 2 -> "Offline";
|
||||
case 3 -> "Unreachable";
|
||||
default -> "Unknown (" + status + ")";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param expression The alert expression containing __instance__ conditions
|
||||
* @return List of existing monitor IDs
|
||||
*/
|
||||
public List<String> extractExistingMonitorIds(String expression) {
|
||||
List<String> monitorIds = new ArrayList<>();
|
||||
String pattern = "equals\\(__instance__,\\s*\"([^\"]+)\"\\)";
|
||||
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern);
|
||||
java.util.regex.Matcher matcher = regex.matcher(expression);
|
||||
|
||||
while (matcher.find()) {
|
||||
monitorIds.add(matcher.group(1));
|
||||
}
|
||||
|
||||
return monitorIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace existing __instance__ conditions in the expression with new ones
|
||||
* @param expression The current expression
|
||||
* @param newMonitorCondition The new monitor condition to replace with
|
||||
* @return Updated expression
|
||||
*/
|
||||
public String replaceInstanceConditions(String expression, String newMonitorCondition) {
|
||||
// More precise pattern to match complete __instance__ condition blocks without cutting field parameters
|
||||
// Pattern matches either:
|
||||
// 1. Single: equals(__instance__, "id")
|
||||
// 2. Multiple: (equals(__instance__, "id1") or equals(__instance__, "id2") or ...)
|
||||
|
||||
// First try to match grouped conditions: (equals(__instance__, "id1") or equals(__instance__, "id2"))
|
||||
String groupedPattern = "\\(\\s*equals\\(__instance__,\\s*\"[^\"]+\"\\)(?:\\s+or\\s+equals\\(__instance__,\\s*\"[^\"]+\"\\))*\\s*\\)";
|
||||
if (expression.matches(".*" + groupedPattern + ".*")) {
|
||||
return expression.replaceFirst(groupedPattern, newMonitorCondition);
|
||||
}
|
||||
|
||||
// Then try single condition: equals(__instance__, "id")
|
||||
String singlePattern = "equals\\(__instance__,\\s*\"[^\"]+\"\\)";
|
||||
if (expression.matches(".*" + singlePattern + ".*")) {
|
||||
return expression.replaceFirst(singlePattern, newMonitorCondition);
|
||||
}
|
||||
|
||||
// If no match found, return original expression
|
||||
return expression;
|
||||
}
|
||||
|
||||
}
|
||||
+53
-17
@@ -17,6 +17,9 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
import com.google.common.collect.Table;
|
||||
import com.google.common.collect.Tables;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
|
||||
import org.apache.hertzbeat.alert.util.AlertUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
@@ -24,7 +27,6 @@ import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -33,49 +35,83 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Component
|
||||
public class AlarmCacheManager {
|
||||
|
||||
private static final String CUSTOM_FIRING_ROW_KEY = "CUSTOM_FIRING_";
|
||||
|
||||
/**
|
||||
* The alarm in the process is triggered
|
||||
* key - labels fingerprint
|
||||
* rowKey - define id
|
||||
* columnKey - labels fingerprint
|
||||
*/
|
||||
private final Map<String, SingleAlert> pendingAlertMap;
|
||||
private final Table<String, String, SingleAlert> pendingAlertMap;
|
||||
|
||||
/**
|
||||
* The not recover alert
|
||||
* key - labels fingerprint
|
||||
* rowKey - define id
|
||||
* columnKey - labels fingerprint
|
||||
*/
|
||||
private final Map<String, SingleAlert> firingAlertMap;
|
||||
private final Table<String, String, SingleAlert> firingAlertMap;
|
||||
|
||||
public AlarmCacheManager(SingleAlertDao singleAlertDao) {
|
||||
this.pendingAlertMap = new ConcurrentHashMap<>(8);
|
||||
this.firingAlertMap = new ConcurrentHashMap<>(8);
|
||||
this.pendingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
|
||||
this.firingAlertMap = Tables.newCustomTable(new ConcurrentHashMap<>(8), ConcurrentHashMap::new);
|
||||
List<SingleAlert> singleAlerts = singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||
for (SingleAlert singleAlert : singleAlerts) {
|
||||
String fingerprint = AlertUtil.calculateFingerprint(singleAlert.getLabels());
|
||||
String defineId = singleAlert.getLabels().get(CommonConstants.LABEL_DEFINE_ID);
|
||||
if (StringUtils.isBlank(defineId)) {
|
||||
defineId = getCustomKey(fingerprint);
|
||||
}
|
||||
singleAlert.setId(null);
|
||||
this.firingAlertMap.put(fingerprint, singleAlert);
|
||||
this.firingAlertMap.put(defineId, fingerprint, singleAlert);
|
||||
}
|
||||
}
|
||||
|
||||
public void putPending(String fingerPrint, SingleAlert alert) {
|
||||
this.pendingAlertMap.put(fingerPrint, alert);
|
||||
public void putPending(Long defineId, String fingerPrint, SingleAlert alert) {
|
||||
this.pendingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
|
||||
}
|
||||
|
||||
public SingleAlert getPending(String fingerPrint) {
|
||||
return this.pendingAlertMap.get(fingerPrint);
|
||||
public SingleAlert getPending(Long defineId, String fingerPrint) {
|
||||
return this.pendingAlertMap.get(String.valueOf(defineId), fingerPrint);
|
||||
}
|
||||
|
||||
public SingleAlert removePending(String fingerPrint) {
|
||||
return this.pendingAlertMap.remove(fingerPrint);
|
||||
public void removePending(Long defineId, String fingerPrint) {
|
||||
this.pendingAlertMap.remove(String.valueOf(defineId), fingerPrint);
|
||||
}
|
||||
|
||||
public void putFiring(Long defineId, String fingerPrint, SingleAlert alert) {
|
||||
this.firingAlertMap.put(String.valueOf(defineId), fingerPrint, alert);
|
||||
}
|
||||
|
||||
public void putFiring(String fingerPrint, SingleAlert alert) {
|
||||
this.firingAlertMap.put(fingerPrint, alert);
|
||||
this.firingAlertMap.put(getCustomKey(fingerPrint), fingerPrint, alert);
|
||||
}
|
||||
|
||||
public SingleAlert getFiring(Long defineId, String fingerPrint) {
|
||||
SingleAlert singleAlert = this.firingAlertMap.get(String.valueOf(defineId), fingerPrint);
|
||||
if (null != singleAlert) {
|
||||
return singleAlert;
|
||||
}
|
||||
return getFiring(fingerPrint);
|
||||
}
|
||||
|
||||
public SingleAlert removeFiring(Long defineId, String fingerPrint) {
|
||||
SingleAlert singleAlert = this.firingAlertMap.remove(String.valueOf(defineId), fingerPrint);
|
||||
if (null == singleAlert) {
|
||||
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
|
||||
}
|
||||
return singleAlert;
|
||||
}
|
||||
|
||||
public SingleAlert getFiring(String fingerPrint) {
|
||||
return this.firingAlertMap.get(fingerPrint);
|
||||
return this.firingAlertMap.get(getCustomKey(fingerPrint), fingerPrint);
|
||||
}
|
||||
|
||||
private String getCustomKey(String fingerPrint) {
|
||||
return CUSTOM_FIRING_ROW_KEY + fingerPrint;
|
||||
}
|
||||
|
||||
|
||||
public SingleAlert removeFiring(String fingerPrint) {
|
||||
return this.firingAlertMap.remove(fingerPrint);
|
||||
return this.firingAlertMap.remove(getCustomKey(fingerPrint), fingerPrint);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -76,11 +76,12 @@ public class CollectorAlertHandler {
|
||||
fingerPrints.put(KEY_COLLECTOR_VERSION, collector.getVersion());
|
||||
fingerPrints.put(KEY_COLLECTOR_HOST, collector.getIp());
|
||||
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
||||
SingleAlert firingAlert = alarmCacheManager.getFiring(fingerprint);
|
||||
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
|
||||
if (firingAlert != null) {
|
||||
firingAlert.setTriggerTimes(1);
|
||||
firingAlert.setEndAt(System.currentTimeMillis());
|
||||
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
||||
firingAlert.setContent(this.bundle.getString("alerter.availability.collector.recover"));
|
||||
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
||||
}
|
||||
}
|
||||
|
||||
+27
-24
@@ -17,8 +17,9 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.apache.hertzbeat.alert.service.DataSourceService;
|
||||
import org.apache.hertzbeat.alert.util.AlertTemplateUtil;
|
||||
@@ -26,11 +27,11 @@ import org.apache.hertzbeat.alert.util.AlertUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Periodic Alert Calculator
|
||||
@@ -54,9 +55,9 @@ public class PeriodicAlertCalculator {
|
||||
this.alarmCacheManager = alarmCacheManager;
|
||||
}
|
||||
|
||||
public void calculate(AlertDefine rule) {
|
||||
if (!rule.isEnable() || StringUtils.isEmpty(rule.getExpr())) {
|
||||
log.error("Periodic rule {} is disabled or expression is empty", rule.getName());
|
||||
public void calculate(AlertDefine define) {
|
||||
if (!define.isEnable() || StringUtils.isEmpty(define.getExpr())) {
|
||||
log.error("Periodic define {} is disabled or expression is empty", define.getName());
|
||||
return;
|
||||
}
|
||||
long currentTimeMilli = System.currentTimeMillis();
|
||||
@@ -66,8 +67,8 @@ public class PeriodicAlertCalculator {
|
||||
// the return result should be matched with threshold
|
||||
try {
|
||||
List<Map<String, Object>> results = dataSourceService.calculate(
|
||||
rule.getDatasource(),
|
||||
rule.getExpr()
|
||||
define.getDatasource(),
|
||||
define.getExpr()
|
||||
);
|
||||
// if no match the expr threshold, the results item map {'value': null} should be null and others field keep
|
||||
// if results has multi list, should trigger multi alert
|
||||
@@ -77,8 +78,9 @@ public class PeriodicAlertCalculator {
|
||||
for (Map<String, Object> result : results) {
|
||||
Map<String, String> fingerPrints = new HashMap<>(8);
|
||||
// here use the alert name as finger, not care the alert name may be changed
|
||||
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
|
||||
fingerPrints.putAll(rule.getLabels());
|
||||
fingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
|
||||
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||
fingerPrints.putAll(define.getLabels());
|
||||
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
||||
if (entry.getValue() != null && !VALUE.equals(entry.getKey())
|
||||
&& !TIMESTAMP.equals(entry.getKey())) {
|
||||
@@ -87,32 +89,33 @@ public class PeriodicAlertCalculator {
|
||||
}
|
||||
if (result.get(VALUE) == null) {
|
||||
// recovery the alert
|
||||
handleRecoveredAlert(fingerPrints);
|
||||
handleRecoveredAlert(define.getId(), fingerPrints);
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||
fieldValueMap.putAll(rule.getLabels());
|
||||
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, rule.getName());
|
||||
fieldValueMap.putAll(define.getLabels());
|
||||
fieldValueMap.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
fieldValueMap.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, rule);
|
||||
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// ignore the query exception eg: no result, timeout, etc
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Calculate periodic rule {} failed: {}", rule.getName(), e.getMessage());
|
||||
log.error("Calculate periodic define {} failed: {}", define.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
|
||||
Map<String, Object> fieldValueMap, AlertDefine define) {
|
||||
Long defineId = define.getId();
|
||||
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
||||
SingleAlert existingAlert = alarmCacheManager.getPending(fingerprint);
|
||||
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
|
||||
Map<String, String> labels = new HashMap<>(8);
|
||||
fieldValueMap.putAll(define.getLabels());
|
||||
labels.putAll(fingerPrints);
|
||||
@@ -133,11 +136,11 @@ public class PeriodicAlertCalculator {
|
||||
// If required trigger times is 1, set to firing status directly
|
||||
if (requiredTimes <= 1) {
|
||||
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||
alarmCacheManager.putFiring(fingerprint, newAlert);
|
||||
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
|
||||
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
|
||||
} else {
|
||||
// Otherwise put into pending queue first
|
||||
alarmCacheManager.putPending(fingerprint, newAlert);
|
||||
alarmCacheManager.putPending(defineId, fingerprint, newAlert);
|
||||
}
|
||||
} else {
|
||||
// Update existing alert
|
||||
@@ -147,17 +150,17 @@ public class PeriodicAlertCalculator {
|
||||
// Check if required trigger times reached
|
||||
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
|
||||
// Reached trigger times threshold, change to firing status
|
||||
alarmCacheManager.removePending(fingerprint);
|
||||
alarmCacheManager.removePending(defineId, fingerprint);
|
||||
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||
alarmCacheManager.putFiring(fingerprint, existingAlert);
|
||||
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
|
||||
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRecoveredAlert(Map<String, String> fingerprints) {
|
||||
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
|
||||
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
|
||||
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
|
||||
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
|
||||
if (firingAlert != null) {
|
||||
// todo consider multi times to tig for resolved alert
|
||||
firingAlert.setTriggerTimes(1);
|
||||
@@ -165,7 +168,7 @@ public class PeriodicAlertCalculator {
|
||||
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
||||
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
||||
}
|
||||
alarmCacheManager.removePending(fingerprint);
|
||||
alarmCacheManager.removePending(defineId, fingerprint);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+17
-14
@@ -183,9 +183,11 @@ public class RealTimeAlertCalculator {
|
||||
if (StringUtils.isBlank(expr)) {
|
||||
continue;
|
||||
}
|
||||
Long defineId = define.getId();
|
||||
Map<String, String> commonFingerPrints = new HashMap<>(8);
|
||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
|
||||
// here use the alert name as finger, not care the alert name may be changed
|
||||
commonFingerPrints.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(define.getId()));
|
||||
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
|
||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
|
||||
@@ -200,9 +202,9 @@ public class RealTimeAlertCalculator {
|
||||
try {
|
||||
if (match) {
|
||||
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
|
||||
afterThresholdRuleMatch(currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
|
||||
afterThresholdRuleMatch(defineId, currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
|
||||
} else {
|
||||
handleRecoveredAlert(commonFingerPrints);
|
||||
handleRecoveredAlert(defineId, commonFingerPrints);
|
||||
}
|
||||
// if this threshold pre compile success, ignore blew
|
||||
continue;
|
||||
@@ -254,9 +256,9 @@ public class RealTimeAlertCalculator {
|
||||
boolean match = execAlertExpression(fieldValueMap, expr, false);
|
||||
try {
|
||||
if (match) {
|
||||
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
|
||||
afterThresholdRuleMatch(defineId, currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
|
||||
} else {
|
||||
handleRecoveredAlert(fingerPrints);
|
||||
handleRecoveredAlert(defineId, fingerPrints);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
@@ -334,9 +336,9 @@ public class RealTimeAlertCalculator {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void handleRecoveredAlert(Map<String, String> fingerprints) {
|
||||
private void handleRecoveredAlert(Long defineId, Map<String, String> fingerprints) {
|
||||
String fingerprint = AlertUtil.calculateFingerprint(fingerprints);
|
||||
SingleAlert firingAlert = alarmCacheManager.removeFiring(fingerprint);
|
||||
SingleAlert firingAlert = alarmCacheManager.removeFiring(defineId, fingerprint);
|
||||
if (firingAlert != null) {
|
||||
// todo consider multi times to tig for resolved alert
|
||||
firingAlert.setTriggerTimes(1);
|
||||
@@ -344,13 +346,14 @@ public class RealTimeAlertCalculator {
|
||||
firingAlert.setStatus(CommonConstants.ALERT_STATUS_RESOLVED);
|
||||
alarmCommonReduce.reduceAndSendAlarm(firingAlert.clone());
|
||||
}
|
||||
alarmCacheManager.removePending(fingerprint);
|
||||
alarmCacheManager.removePending(defineId, fingerprint);
|
||||
}
|
||||
|
||||
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
|
||||
Map<String, Object> fieldValueMap, AlertDefine define, Map<String, String> annotations) {
|
||||
private void afterThresholdRuleMatch(long defineId, long currentTimeMilli, Map<String, String> fingerPrints,
|
||||
Map<String, Object> fieldValueMap, AlertDefine define,
|
||||
Map<String, String> annotations) {
|
||||
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
||||
SingleAlert existingAlert = alarmCacheManager.getPending(fingerprint);
|
||||
SingleAlert existingAlert = alarmCacheManager.getPending(defineId, fingerprint);
|
||||
fieldValueMap.putAll(define.getLabels());
|
||||
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
|
||||
if (existingAlert == null) {
|
||||
@@ -382,11 +385,11 @@ public class RealTimeAlertCalculator {
|
||||
// If required trigger times is 1, set to firing status directly
|
||||
if (requiredTimes <= 1) {
|
||||
newAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||
alarmCacheManager.putFiring(fingerprint, newAlert);
|
||||
alarmCacheManager.putFiring(defineId, fingerprint, newAlert);
|
||||
alarmCommonReduce.reduceAndSendAlarm(newAlert.clone());
|
||||
} else {
|
||||
// Otherwise put into pending queue first
|
||||
alarmCacheManager.putPending(fingerprint, newAlert);
|
||||
alarmCacheManager.putPending(define.getId(), fingerprint, newAlert);
|
||||
}
|
||||
} else {
|
||||
// Update existing alert
|
||||
@@ -396,9 +399,9 @@ public class RealTimeAlertCalculator {
|
||||
// Check if required trigger times reached
|
||||
if (existingAlert.getStatus().equals(CommonConstants.ALERT_STATUS_PENDING) && existingAlert.getTriggerTimes() >= requiredTimes) {
|
||||
// Reached trigger times threshold, change to firing status
|
||||
alarmCacheManager.removePending(fingerprint);
|
||||
alarmCacheManager.removePending(defineId, fingerprint);
|
||||
existingAlert.setStatus(CommonConstants.ALERT_STATUS_FIRING);
|
||||
alarmCacheManager.putFiring(fingerprint, existingAlert);
|
||||
alarmCacheManager.putFiring(defineId, fingerprint, existingAlert);
|
||||
alarmCommonReduce.reduceAndSendAlarm(existingAlert.clone());
|
||||
}
|
||||
}
|
||||
|
||||
+14
-4
@@ -22,10 +22,12 @@ package org.apache.hertzbeat.alert.config;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -54,16 +56,24 @@ public class AlertSseManager {
|
||||
.name("ALERT_EVENT")
|
||||
.data(data));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
} catch (Exception exception) {
|
||||
log.error("Failed to broadcast alert data to client: {}", exception.getMessage());
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
tryCompleteAndClean(clientId, emitter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void tryCompleteAndClean(Long clientId, SseEmitter emitter) {
|
||||
try {
|
||||
Optional.ofNullable(emitter).ifPresent(ResponseBodyEmitter::complete);
|
||||
} catch (Throwable e) {
|
||||
log.debug("Failed to complete emitter for client {}: {}", clientId, e.getMessage());
|
||||
}
|
||||
// execute clear
|
||||
removeEmitter(clientId);
|
||||
}
|
||||
|
||||
private void removeEmitter(Long clientId) {
|
||||
emitters.remove(clientId);
|
||||
}
|
||||
|
||||
+157
-48
@@ -27,7 +27,7 @@ import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor;
|
||||
* of the available methods.
|
||||
*
|
||||
* @param <T> The return type of the visit operation. Use {@link Void} for
|
||||
* operations with no return type.
|
||||
* operations with no return type.
|
||||
*/
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> implements AlertExpressionVisitor<T> {
|
||||
@@ -37,7 +37,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitExpression(AlertExpressionParser.ExpressionContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitExpression(AlertExpressionParser.ExpressionContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -45,7 +48,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitAndExpr(AlertExpressionParser.AndExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -53,7 +59,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -61,7 +70,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitUnlessExpr(AlertExpressionParser.UnlessExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -69,7 +81,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitSqlExpr(AlertExpressionParser.SqlExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -77,7 +92,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitSqlCallExpr(AlertExpressionParser.SqlCallExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -85,7 +103,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitLiteralExpr(AlertExpressionParser.LiteralExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -93,7 +114,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitParenExpr(AlertExpressionParser.ParenExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -101,7 +125,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitPromqlCallExpr(AlertExpressionParser.PromqlCallExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -109,7 +136,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitPromqlExpr(AlertExpressionParser.PromqlExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -117,7 +147,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitOrExpr(AlertExpressionParser.OrExprContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -125,7 +158,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitFunctionCall(AlertExpressionParser.FunctionCallContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -133,7 +169,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitParameterList(AlertExpressionParser.ParameterListContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitParameterList(AlertExpressionParser.ParameterListContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -141,7 +180,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitParameter(AlertExpressionParser.ParameterContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitParameter(AlertExpressionParser.ParameterContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -149,7 +191,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitNumber(AlertExpressionParser.NumberContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitNumber(AlertExpressionParser.NumberContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -157,7 +202,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitString(AlertExpressionParser.StringContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitString(AlertExpressionParser.StringContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -165,7 +213,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitDuration(AlertExpressionParser.DurationContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitDuration(AlertExpressionParser.DurationContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -173,7 +224,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitFunctionName(AlertExpressionParser.FunctionNameContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -181,7 +235,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitSelectSql(AlertExpressionParser.SelectSqlContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -189,7 +246,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitSelectFieldList(AlertExpressionParser.SelectFieldListContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -197,7 +257,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitSelectField(AlertExpressionParser.SelectFieldContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitSelectField(AlertExpressionParser.SelectFieldContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -205,7 +268,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitGroupByList(AlertExpressionParser.GroupByListContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitGroupByList(AlertExpressionParser.GroupByListContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -213,7 +279,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitOrderByList(AlertExpressionParser.OrderByListContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitOrderByList(AlertExpressionParser.OrderByListContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -221,7 +290,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitOrderByField(AlertExpressionParser.OrderByFieldContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -229,7 +301,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitLimitClause(AlertExpressionParser.LimitClauseContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -237,7 +312,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitRelList(AlertExpressionParser.RelListContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitRelList(AlertExpressionParser.RelListContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -245,7 +323,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitRelation(AlertExpressionParser.RelationContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitRelation(AlertExpressionParser.RelationContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -253,7 +334,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitConditionList(AlertExpressionParser.ConditionListContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitConditionList(AlertExpressionParser.ConditionListContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -261,7 +345,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitCompOp(AlertExpressionParser.CompOpContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitCompOp(AlertExpressionParser.CompOpContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -269,7 +356,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitCondition(AlertExpressionParser.ConditionContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitCondition(AlertExpressionParser.ConditionContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -277,7 +367,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitConditionUnit(AlertExpressionParser.ConditionUnitContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -285,7 +378,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitPromql(AlertExpressionParser.PromqlContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitPromql(AlertExpressionParser.PromqlContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -293,7 +389,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitMetricSelector(AlertExpressionParser.MetricSelectorContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -301,7 +400,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitLabelMatcherList(AlertExpressionParser.LabelMatcherListContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -309,7 +411,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitLabelMatcherItem(AlertExpressionParser.LabelMatcherItemContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -317,7 +422,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitLabelMatcherOp(AlertExpressionParser.LabelMatcherOpContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -325,7 +433,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitLabelList(AlertExpressionParser.LabelListContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitLabelList(AlertExpressionParser.LabelListContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -333,7 +444,10 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitInstantVectorOp(AlertExpressionParser.InstantVectorOpContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
@@ -341,13 +455,8 @@ public class AlertExpressionBaseVisitor<T> extends AbstractParseTreeVisitor<T> i
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) { return visitChildren(ctx); }
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>The default implementation returns the result of calling
|
||||
* {@link #visitChildren} on {@code ctx}.</p>
|
||||
*/
|
||||
@Override public T visitBinaryOperator(AlertExpressionParser.BinaryOperatorContext ctx) { return visitChildren(ctx); }
|
||||
@Override
|
||||
public T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx) {
|
||||
return visitChildren(ctx);
|
||||
}
|
||||
}
|
||||
+126
-97
@@ -18,11 +18,12 @@
|
||||
package org.apache.hertzbeat.alert.expr;
|
||||
|
||||
import org.antlr.v4.runtime.CommonTokenStream;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.hertzbeat.common.support.exception.ExpressionVisitorException;
|
||||
import org.apache.hertzbeat.warehouse.db.QueryExecutor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -33,7 +34,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<Map<String, Object>>> {
|
||||
|
||||
private static final String THRESHOLD = "__threshold__";
|
||||
private static final String SCALAR = "__scalar__";
|
||||
private static final String NAME = "__name__";
|
||||
private static final String VALUE = "__value__";
|
||||
private static final String TIMESTAMP = "__timestamp__";
|
||||
@@ -60,28 +61,100 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
||||
public List<Map<String, Object>> visitComparisonExpr(AlertExpressionParser.ComparisonExprContext ctx) {
|
||||
List<Map<String, Object>> leftResult = visit(ctx.left);
|
||||
List<Map<String, Object>> rightResult = visit(ctx.right);
|
||||
if (rightResult.size() == 1 && rightResult.get(0).containsKey(THRESHOLD)) {
|
||||
double threshold = (double) rightResult.get(0).get(THRESHOLD);
|
||||
String operator = ctx.op.getText();
|
||||
int type = ctx.op.getType();
|
||||
boolean boolModifier = ctx.BOOL() != null;
|
||||
boolean leftIsScalar = isScalar(leftResult);
|
||||
boolean rightIsScalar = isScalar(rightResult);
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Map<String, Object> item : leftResult) {
|
||||
Object queryValues = item.get(VALUE);
|
||||
if (queryValues == null) {
|
||||
// ignore the query result data is empty
|
||||
// scalar and scalar
|
||||
if (leftIsScalar && rightIsScalar) {
|
||||
if (!boolModifier) {
|
||||
// Between two scalars,
|
||||
// the bool modifier must be provided and these operators result in another scalar that is either 0 (false) or 1 (true), depending on the comparison result.
|
||||
return results;
|
||||
}
|
||||
Object leftVal = leftResult.get(0).get(SCALAR);
|
||||
Object rightVal = rightResult.get(0).get(SCALAR);
|
||||
Boolean match = compareOp(leftVal, type, rightVal);
|
||||
// returns a result only if the comparison condition is met.
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put(VALUE, match ? 1 : 0);
|
||||
results.add(result);
|
||||
return results;
|
||||
}
|
||||
|
||||
// scalar and vector
|
||||
if (leftIsScalar) {
|
||||
Object leftVal = leftResult.get(0).get(SCALAR);
|
||||
for (Map<String, Object> rightItem : rightResult) {
|
||||
Object rightVal = rightItem.getOrDefault(VALUE, null);
|
||||
if (isValidValue(rightVal)) {
|
||||
continue;
|
||||
}
|
||||
// queryValues may be a list of values, or a single value
|
||||
Object matchValue = evaluateCondition(queryValues, operator, threshold);
|
||||
Map<String, Object> resultMap = new HashMap(item);
|
||||
resultMap.put(VALUE, matchValue);
|
||||
// if matchValue is null, mean not match the threshold
|
||||
// if not null, mean match the threshold
|
||||
result.add(resultMap);
|
||||
Boolean match = compareOp(leftVal, type, rightVal);
|
||||
Map<String, Object> result = new HashMap<>(rightItem);
|
||||
if (boolModifier) {
|
||||
result.put(VALUE, match ? 1 : 0);
|
||||
results.add(result);
|
||||
} else {
|
||||
result.put(VALUE, match ? rightVal : null);
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return results;
|
||||
}
|
||||
return new LinkedList<>();
|
||||
|
||||
// vector and scalar
|
||||
if (rightIsScalar) {
|
||||
Object rightVal = rightResult.get(0).get(SCALAR);
|
||||
for (Map<String, Object> leftItem : leftResult) {
|
||||
Object leftVal = leftItem.getOrDefault(VALUE, null);
|
||||
if (isValidValue(leftVal)) {
|
||||
continue;
|
||||
}
|
||||
Boolean match = compareOp(leftVal, type, rightVal);
|
||||
Map<String, Object> result = new HashMap<>(leftItem);
|
||||
if (boolModifier) {
|
||||
result.put(VALUE, match ? 1 : 0);
|
||||
results.add(result);
|
||||
} else {
|
||||
result.put(VALUE, match ? leftVal : null);
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// vector and vector
|
||||
Map<String, Map<String, Object>> rightMap = rightResult.stream()
|
||||
.filter(item -> item.get(VALUE) != null)
|
||||
.collect(Collectors.toMap(this::labelKey, item -> item, (existing, replacement) -> existing));
|
||||
|
||||
for (Map<String, Object> leftItem : leftResult) {
|
||||
Object leftVal = leftItem.getOrDefault(VALUE, null);
|
||||
if (isValidValue(leftVal)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> rightItem = rightMap.get(labelKey(leftItem));
|
||||
if (rightItem == null) {
|
||||
continue;
|
||||
}
|
||||
Object rightVal = rightItem.get(VALUE);
|
||||
if (isValidValue(rightVal)) {
|
||||
continue;
|
||||
}
|
||||
Boolean match = compareOp(leftVal, type, rightVal);
|
||||
Map<String, Object> result = new HashMap<>(leftItem);
|
||||
if (boolModifier) {
|
||||
result.put(VALUE, match ? 1 : 0);
|
||||
results.add(result);
|
||||
} else {
|
||||
result.put(VALUE, match ? leftVal : null);
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,7 +240,7 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
||||
double value = Double.parseDouble(ctx.number().getText());
|
||||
List<Map<String, Object>> numAsList = new ArrayList<>();
|
||||
Map<String, Object> valueMap = new HashMap<>();
|
||||
valueMap.put(THRESHOLD, value);
|
||||
valueMap.put(SCALAR, value);
|
||||
numAsList.add(valueMap);
|
||||
return numAsList;
|
||||
}
|
||||
@@ -194,83 +267,7 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
||||
return callSqlOrPromql(tokens.getText(ctx.string()));
|
||||
}
|
||||
|
||||
private Object evaluateCondition(Object value, String operator, Double threshold) {
|
||||
// value may be a list of values, or a single value
|
||||
switch (operator) {
|
||||
case ">":
|
||||
// if value is list, return the max value
|
||||
if (value instanceof List<?> values) {
|
||||
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).max(Double::compareTo).orElse(null);
|
||||
if (doubleValue != null) {
|
||||
return doubleValue > threshold ? doubleValue : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return Double.parseDouble(value.toString()) > threshold ? value : null;
|
||||
}
|
||||
case ">=":
|
||||
if (value instanceof List<?> values) {
|
||||
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).max(Double::compareTo).orElse(null);
|
||||
if (doubleValue != null) {
|
||||
return doubleValue >= threshold ? doubleValue : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return Double.parseDouble(value.toString()) >= threshold ? value : null;
|
||||
}
|
||||
case "<":
|
||||
if (value instanceof List<?> values) {
|
||||
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).min(Double::compareTo).orElse(null);
|
||||
if (doubleValue != null) {
|
||||
return doubleValue < threshold ? doubleValue : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return Double.parseDouble(value.toString()) < threshold ? value : null;
|
||||
}
|
||||
case "<=":
|
||||
if (value instanceof List<?> values) {
|
||||
Double doubleValue = values.stream().map(v -> Double.valueOf(v.toString())).min(Double::compareTo).orElse(null);
|
||||
if (doubleValue != null) {
|
||||
return doubleValue <= threshold ? doubleValue : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return Double.parseDouble(value.toString()) <= threshold ? value : null;
|
||||
}
|
||||
case "==":
|
||||
if (value instanceof List<?> values) {
|
||||
for (Object v : values) {
|
||||
if (v.equals(threshold)) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
return value.equals(threshold) ? value : null;
|
||||
}
|
||||
case "!=":
|
||||
if (value instanceof List<?> values) {
|
||||
for (Object v : values) {
|
||||
if (v.equals(threshold)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
return value.equals(threshold) ? null : value;
|
||||
}
|
||||
default:
|
||||
// unsupported operator todo add more operator
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> callSqlOrPromql(String text){
|
||||
private List<Map<String, Object>> callSqlOrPromql(String text) {
|
||||
String script = text.substring(1, text.length() - 1);
|
||||
return executor.execute(script);
|
||||
}
|
||||
@@ -290,4 +287,36 @@ public class AlertExpressionEvalVisitor extends AlertExpressionBaseVisitor<List<
|
||||
return key.isEmpty() ? "-" : key;
|
||||
}
|
||||
|
||||
private boolean isScalar(List<Map<String, Object>> context) {
|
||||
return CollectionUtils.isNotEmpty(context)
|
||||
&& context.size() == 1
|
||||
&& context.get(0).containsKey(SCALAR)
|
||||
&& null != context.get(0).get(SCALAR);
|
||||
}
|
||||
|
||||
private double parseStrToDouble(String text) {
|
||||
try {
|
||||
return Double.parseDouble(text);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ExpressionVisitorException("number format exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidValue(Object val) {
|
||||
return val == null || val instanceof List<?>;
|
||||
}
|
||||
|
||||
private Boolean compareOp(Object leftVal, int opType, Object rightVal) {
|
||||
double left = parseStrToDouble(leftVal.toString());
|
||||
double right = parseStrToDouble(rightVal.toString());
|
||||
return switch (opType) {
|
||||
case AlertExpressionParser.GT -> left > right;
|
||||
case AlertExpressionParser.GE -> left >= right;
|
||||
case AlertExpressionParser.LT -> left < right;
|
||||
case AlertExpressionParser.LE -> left <= right;
|
||||
case AlertExpressionParser.EQ -> left == right;
|
||||
case AlertExpressionParser.NE -> left != right;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
+89
-470
File diff suppressed because one or more lines are too long
+3308
-3049
File diff suppressed because one or more lines are too long
+40
-8
@@ -26,12 +26,13 @@ import org.antlr.v4.runtime.tree.ParseTreeVisitor;
|
||||
* by {@link AlertExpressionParser}.
|
||||
*
|
||||
* @param <T> The return type of the visit operation. Use {@link Void} for
|
||||
* operations with no return type.
|
||||
* operations with no return type.
|
||||
*/
|
||||
public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#expression}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -40,6 +41,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code AndExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -48,6 +50,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code ComparisonExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -56,6 +59,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code UnlessExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -64,6 +68,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code SqlExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -72,6 +77,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code SqlCallExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -80,6 +86,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code LiteralExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -88,6 +95,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code ParenExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -96,6 +104,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code PromqlCallExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -104,6 +113,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code PromqlExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -112,6 +122,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
/**
|
||||
* Visit a parse tree produced by the {@code OrExpr}
|
||||
* labeled alternative in {@link AlertExpressionParser#expr}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -119,6 +130,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#functionCall}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -126,6 +138,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#parameterList}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -133,6 +146,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#parameter}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -140,6 +154,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#number}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -147,6 +162,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#string}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -154,6 +170,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#duration}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -161,6 +178,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#functionName}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -168,6 +186,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#selectSql}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -175,6 +194,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#selectFieldList}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -182,6 +202,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#selectField}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -189,6 +210,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#groupByList}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -196,6 +218,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#orderByList}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -203,6 +226,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#orderByField}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -210,6 +234,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#limitClause}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -217,6 +242,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#relList}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -224,6 +250,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#relation}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -231,6 +258,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#conditionList}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -238,6 +266,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#compOp}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -245,6 +274,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#condition}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -252,6 +282,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#conditionUnit}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -259,6 +290,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#promql}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -266,6 +298,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#metricSelector}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -273,6 +306,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherList}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -280,6 +314,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherItem}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -287,6 +322,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#labelMatcherOp}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -294,6 +330,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#labelList}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -301,6 +338,7 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#instantVectorOp}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
@@ -308,15 +346,9 @@ public interface AlertExpressionVisitor<T> extends ParseTreeVisitor<T> {
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#aggregationOperator}.
|
||||
*
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
T visitAggregationOperator(AlertExpressionParser.AggregationOperatorContext ctx);
|
||||
|
||||
/**
|
||||
* Visit a parse tree produced by {@link AlertExpressionParser#binaryOperator}.
|
||||
* @param ctx the parse tree
|
||||
* @return the visitor result
|
||||
*/
|
||||
T visitBinaryOperator(AlertExpressionParser.BinaryOperatorContext ctx);
|
||||
}
|
||||
+1
-1
@@ -60,7 +60,7 @@ public class ServerChanAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
||||
String webHookUrl = String.format(alerterProperties.getServerChanWebhookUrl(), sanitizedToken);
|
||||
|
||||
// Validate the constructed URL against a whitelist
|
||||
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com");
|
||||
List<String> allowedBaseUrls = List.of("https://api.serverchan.com", "https://serverchan.example.com", "https://sctapi.ftqq.com");
|
||||
boolean isValidUrl = allowedBaseUrls.stream().anyMatch(webHookUrl::startsWith);
|
||||
if (!isValidUrl) {
|
||||
throw new AlertNoticeException("Invalid webhook URL: " + webHookUrl);
|
||||
|
||||
+6
@@ -28,6 +28,9 @@ import org.apache.hertzbeat.alert.dto.ExportAlertDefineDTO;
|
||||
import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
|
||||
import org.apache.hertzbeat.alert.service.AlertDefineService;
|
||||
import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
|
||||
import org.apache.hertzbeat.common.util.LogUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -41,12 +44,15 @@ public abstract class AlertDefineAbstractImExportServiceImpl implements AlertDef
|
||||
@Lazy
|
||||
private AlertDefineService alertDefineService;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AlertDefineAbstractImExportServiceImpl.class);
|
||||
|
||||
@Override
|
||||
public void importConfig(InputStream is) {
|
||||
var formList = parseImport(is)
|
||||
.stream()
|
||||
.map(this::convert)
|
||||
.toList();
|
||||
LogUtil.info(logger, "Importing alert defines from {0}", formList);
|
||||
if (!CollectionUtils.isEmpty(formList)) {
|
||||
formList.forEach(alertDefine -> {
|
||||
alertDefineService.validate(alertDefine, false);
|
||||
|
||||
+6
-1
@@ -27,11 +27,14 @@ import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.hertzbeat.common.util.LogUtil;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -62,6 +65,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
|
||||
private final String accessKeySecret;
|
||||
private final String signName;
|
||||
private final String templateCode;
|
||||
private static final Logger logger = LoggerFactory.getLogger(AlibabaSmsClientImpl.class);
|
||||
|
||||
public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
|
||||
if (config != null) {
|
||||
@@ -173,7 +177,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
|
||||
log.info("Successfully sent SMS to phone: {}", phoneNumber);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to send SMS: {}", e.getMessage());
|
||||
LogUtil.warn(logger, "Failed to send SMS: {0}", e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -192,6 +196,7 @@ public class AlibabaSmsClientImpl implements SmsClient {
|
||||
// Step 4: Build authorization header
|
||||
return ALGORITHM + " Credential=" + accessKeyId + ",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + "x-acs-signature-nonce;x-acs-version,Signature=" + signature;
|
||||
} catch (Exception e) {
|
||||
LogUtil.warn(logger, "Failed to calculate authorization {0}", e.getMessage());
|
||||
throw new RuntimeException("Failed to calculate authorization", e);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ public class DataSourceServiceImpl implements DataSourceService {
|
||||
throw new IllegalArgumentException("Empty expression");
|
||||
}
|
||||
if (executors == null || executors.isEmpty()) {
|
||||
throw new IllegalArgumentException("No query executor found");
|
||||
throw new IllegalArgumentException(bundle.getString("alerter.datasource.executor.not.found"));
|
||||
}
|
||||
QueryExecutor executor = executors.stream().filter(e -> e.support(datasource)).findFirst().orElse(null);
|
||||
|
||||
|
||||
@@ -33,3 +33,4 @@ alerter.priority.0 = Emergency Alert
|
||||
alerter.priority.1 = Critical Alert
|
||||
alerter.priority.2 = Warning Alert
|
||||
alerter.calculate.parse.error = Expression is not fully parsed, may have syntax errors or incomplete inputs
|
||||
alerter.datasource.executor.not.found = No query executor found
|
||||
|
||||
@@ -33,3 +33,4 @@ alerter.priority.0 = 紧急告警
|
||||
alerter.priority.1 = 严重告警
|
||||
alerter.priority.2 = 警告告警
|
||||
alerter.calculate.parse.error = 表达式未完全解析,可能存在语法错误或输入不完整
|
||||
alerter.datasource.executor.not.found = 未找到查询执行器
|
||||
|
||||
@@ -33,3 +33,4 @@ alerter.priority.0 = 緊急警報
|
||||
alerter.priority.1 = 嚴重警報
|
||||
alerter.priority.2 = 警告警報
|
||||
alerter.calculate.parse.error = 表達式未完全解析,可能存在語法錯誤或輸入不完整
|
||||
alerter.datasource.executor.not.found = 未找到查詢執行器
|
||||
|
||||
@@ -23,7 +23,7 @@ expression
|
||||
|
||||
expr
|
||||
: LPAREN expr RPAREN # ParenExpr
|
||||
| left=expr op=(GE|LE|GT|LT|EQ|NE) right=expr # ComparisonExpr
|
||||
| left=expr op=(GE|LE|GT|LT|EQ|NE) BOOL? right=expr # ComparisonExpr
|
||||
| left=expr AND right=expr # AndExpr
|
||||
| left=expr UNLESS right=expr # UnlessExpr
|
||||
| left=expr OR right=expr # OrExpr
|
||||
@@ -191,11 +191,6 @@ aggregationOperator
|
||||
: SUM | AVG | COUNT | MIN | MAX | STDDEV | STDVAR | TOPK | BOTTOMK | QUANTILE
|
||||
;
|
||||
|
||||
binaryOperator
|
||||
: EQ | NE | GT | LT | GE | LE
|
||||
| AND | OR | UNLESS
|
||||
;
|
||||
|
||||
// Lexer rules
|
||||
|
||||
// Boolean operators
|
||||
@@ -259,6 +254,7 @@ LT : '<' ;
|
||||
LE : '<=' ;
|
||||
EQ : '==' | '=' ;
|
||||
NE : '!=' ;
|
||||
BOOL : 'bool';
|
||||
|
||||
// Delimiters
|
||||
LPAREN : '(' ;
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
|
||||
import org.apache.hertzbeat.alert.util.AlertUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* alert cache manager test
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class AlarmCacheManagerTest {
|
||||
|
||||
@Mock
|
||||
private SingleAlertDao singleAlertDao;
|
||||
|
||||
private AlarmCacheManager alarmCacheManager;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
Map<String, String> labels = new HashMap<>();
|
||||
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
|
||||
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(1L));
|
||||
SingleAlert alert = new SingleAlert();
|
||||
alert.setContent("Alert cache manager test");
|
||||
alert.setLabels(labels);
|
||||
when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert));
|
||||
alarmCacheManager = new AlarmCacheManager(singleAlertDao);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInit() {
|
||||
Map<String, String> labels = new HashMap<>();
|
||||
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
|
||||
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(1L));
|
||||
|
||||
String fingerprint = AlertUtil.calculateFingerprint(labels);
|
||||
SingleAlert firingSingleAlert = alarmCacheManager.getFiring(1L, fingerprint);
|
||||
assertNotNull(firingSingleAlert);
|
||||
assertEquals("Alert cache manager test", firingSingleAlert.getContent());
|
||||
alarmCacheManager.removeFiring(1L, fingerprint);
|
||||
firingSingleAlert = alarmCacheManager.getFiring(1L, fingerprint);
|
||||
assertNull(firingSingleAlert);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPending() {
|
||||
Map<String, String> labels = new HashMap<>();
|
||||
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
|
||||
labels.put(CommonConstants.ALERT_SEVERITY_INFO, CommonConstants.ALERT_STATUS_PENDING);
|
||||
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(2L));
|
||||
|
||||
SingleAlert alert = new SingleAlert();
|
||||
alert.setContent("Alert cache manager test");
|
||||
alert.setLabels(labels);
|
||||
|
||||
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
|
||||
alarmCacheManager.putPending(2L, fingerprint, alert);
|
||||
|
||||
SingleAlert pendingSingleAlert = alarmCacheManager.getPending(2L, fingerprint);
|
||||
assertNotNull(pendingSingleAlert);
|
||||
alarmCacheManager.removePending(2L, fingerprint);
|
||||
pendingSingleAlert = alarmCacheManager.getPending(2L, fingerprint);
|
||||
assertNull(pendingSingleAlert);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFiring() {
|
||||
Map<String, String> labels = new HashMap<>();
|
||||
labels.put(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL);
|
||||
labels.put(CommonConstants.ALERT_SEVERITY_INFO, CommonConstants.ALERT_STATUS_PENDING);
|
||||
labels.put(CommonConstants.LABEL_DEFINE_ID, String.valueOf(3L));
|
||||
|
||||
SingleAlert alert = new SingleAlert();
|
||||
alert.setContent("Alert cache manager test");
|
||||
alert.setLabels(labels);
|
||||
|
||||
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
|
||||
alarmCacheManager.putFiring(3L, fingerprint, alert);
|
||||
|
||||
SingleAlert firingSingleAlert = alarmCacheManager.getFiring(3L, fingerprint);
|
||||
assertNotNull(firingSingleAlert);
|
||||
alarmCacheManager.removeFiring(3L, fingerprint);
|
||||
firingSingleAlert = alarmCacheManager.getFiring(3L, fingerprint);
|
||||
assertNull(firingSingleAlert);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHistorical() {
|
||||
SingleAlert alert = new SingleAlert();
|
||||
alert.setContent("Alert cache manager test");
|
||||
alert.setLabels(Collections.singletonMap(CommonConstants.LABEL_ALERT_SEVERITY, CommonConstants.ALERT_SEVERITY_CRITICAL));
|
||||
when(singleAlertDao.querySingleAlertsByStatus(CommonConstants.ALERT_STATUS_FIRING)).thenReturn(Collections.singletonList(alert));
|
||||
alarmCacheManager = new AlarmCacheManager(singleAlertDao);
|
||||
|
||||
String fingerprint = AlertUtil.calculateFingerprint(alert.getLabels());
|
||||
SingleAlert historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint);
|
||||
assertNotNull(historicalSingleAlert);
|
||||
SingleAlert singleAlert = alarmCacheManager.removeFiring(4L, fingerprint);
|
||||
assertNotNull(singleAlert);
|
||||
historicalSingleAlert = alarmCacheManager.getFiring(4L, fingerprint);
|
||||
assertNull(historicalSingleAlert);
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.calculate;
|
||||
|
||||
import org.apache.hertzbeat.alert.dao.AlertCollectorDao;
|
||||
import org.apache.hertzbeat.alert.dao.SingleAlertDao;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.apache.hertzbeat.alert.util.AlertUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.apache.hertzbeat.common.entity.manager.Collector;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Collector alert handler test
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class CollectorAlertHandlerTest {
|
||||
|
||||
@Mock
|
||||
private AlertCollectorDao alertCollectorDao;
|
||||
|
||||
@Mock
|
||||
private SingleAlertDao singleAlertDao;
|
||||
|
||||
@Mock
|
||||
private AlarmCommonReduce alarmCommonReduce;
|
||||
|
||||
private AlarmCacheManager alarmCacheManager;
|
||||
|
||||
private CollectorAlertHandler collectorAlertHandler;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
alarmCacheManager = Mockito.spy(new AlarmCacheManager(singleAlertDao));
|
||||
collectorAlertHandler = new CollectorAlertHandler(alarmCommonReduce, alertCollectorDao, alarmCacheManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnline() {
|
||||
String identity = "localhost-collector";
|
||||
Collector collector = Collector.builder()
|
||||
.name(identity)
|
||||
.ip("127.0.0.1")
|
||||
.version("1.0")
|
||||
.build();
|
||||
SingleAlert singleAlert = SingleAlert.builder().build();
|
||||
|
||||
when(alertCollectorDao.findCollectorByName(any(String.class))).thenReturn(collector);
|
||||
when(alarmCacheManager.removeFiring(any(String.class))).thenReturn(singleAlert);
|
||||
|
||||
collectorAlertHandler.online(identity);
|
||||
|
||||
assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, singleAlert.getStatus());
|
||||
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOffline() {
|
||||
String identity = "localhost-collector";
|
||||
Collector collector = Collector.builder()
|
||||
.name(identity)
|
||||
.ip("127.0.0.1")
|
||||
.version("1.0")
|
||||
.build();
|
||||
|
||||
when(alertCollectorDao.findCollectorByName(any(String.class))).thenReturn(collector);
|
||||
when(alarmCacheManager.getFiring(any(String.class))).thenReturn(null);
|
||||
|
||||
collectorAlertHandler.offline(identity);
|
||||
|
||||
verify(alarmCacheManager, times(1)).putFiring(any(String.class), any(SingleAlert.class));
|
||||
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any(SingleAlert.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOfflineAndOnlineLifecycle() {
|
||||
String identity = "localhost-collector";
|
||||
Collector collector = Collector.builder()
|
||||
.name(identity)
|
||||
.ip("127.0.0.1")
|
||||
.version("1.0")
|
||||
.build();
|
||||
|
||||
Map<String, String> fingerPrints = new HashMap<>(8);
|
||||
fingerPrints.put("collectorName", collector.getName());
|
||||
fingerPrints.put("collectorVersion", collector.getVersion());
|
||||
fingerPrints.put("collectorHost", collector.getIp());
|
||||
String fingerprint = AlertUtil.calculateFingerprint(fingerPrints);
|
||||
|
||||
when(alertCollectorDao.findCollectorByName(identity)).thenReturn(collector);
|
||||
|
||||
// step1. first time offline
|
||||
collectorAlertHandler.offline(identity);
|
||||
assertNotNull(alarmCacheManager.getFiring(fingerprint));
|
||||
|
||||
// step2. online
|
||||
collectorAlertHandler.online(identity);
|
||||
assertNull(alarmCacheManager.getFiring(fingerprint));
|
||||
|
||||
// step3. second time offline
|
||||
collectorAlertHandler.offline(identity);
|
||||
assertNotNull(alarmCacheManager.getFiring(fingerprint));
|
||||
|
||||
// Verify that the push status is correct.
|
||||
ArgumentCaptor<SingleAlert> captor = ArgumentCaptor.forClass(SingleAlert.class);
|
||||
verify(alarmCommonReduce, atLeast(3)).reduceAndSendAlarm(captor.capture());
|
||||
List<SingleAlert> alerts = captor.getAllValues();
|
||||
assertEquals(CommonConstants.ALERT_STATUS_FIRING, alerts.get(0).getStatus());
|
||||
assertEquals(CommonConstants.ALERT_STATUS_RESOLVED, alerts.get(1).getStatus());
|
||||
assertEquals(CommonConstants.ALERT_STATUS_FIRING, alerts.get(2).getStatus());
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -42,6 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -92,12 +93,12 @@ class PeriodicAlertCalculatorTest {
|
||||
result.put("__value__", 95.0); // Non-null, matched with threshold
|
||||
result.put("__timestamp__", System.currentTimeMillis());
|
||||
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
|
||||
when(alarmCacheManager.getPending(anyString())).thenReturn(null);
|
||||
when(alarmCacheManager.getPending(eq(rule.getId()), anyString())).thenReturn(null);
|
||||
periodicAlertCalculator.calculate(rule);
|
||||
// Verify that putFiring is called
|
||||
ArgumentCaptor<String> idCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<SingleAlert> alertCaptor = ArgumentCaptor.forClass(SingleAlert.class);
|
||||
verify(alarmCacheManager).putFiring(idCaptor.capture(), alertCaptor.capture());
|
||||
verify(alarmCacheManager).putFiring(eq(rule.getId()), idCaptor.capture(), alertCaptor.capture());
|
||||
// Assertion alarm status and content
|
||||
SingleAlert alert = alertCaptor.getValue();
|
||||
assertAll(() -> assertEquals(CommonConstants.ALERT_STATUS_FIRING, alert.getStatus()),
|
||||
@@ -112,7 +113,7 @@ class PeriodicAlertCalculatorTest {
|
||||
result.put("__timestamp__", System.currentTimeMillis());
|
||||
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
|
||||
periodicAlertCalculator.calculate(rule);
|
||||
verify(alarmCacheManager, times(0)).putFiring(any(), any());
|
||||
verify(alarmCacheManager, times(0)).putFiring(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -126,7 +127,7 @@ class PeriodicAlertCalculatorTest {
|
||||
.triggerTimes(2).startAt(System.currentTimeMillis() - 60000)
|
||||
.activeAt(System.currentTimeMillis() - 30000)
|
||||
.build();
|
||||
when(alarmCacheManager.removeFiring(anyString())).thenReturn(pendingAlert);
|
||||
when(alarmCacheManager.removeFiring(eq(rule.getId()), anyString())).thenReturn(pendingAlert);
|
||||
when(dataSourceService.calculate(anyString(), anyString())).thenReturn(List.of(result));
|
||||
periodicAlertCalculator.calculate(rule);
|
||||
ArgumentCaptor<SingleAlert> resolvedCaptor = ArgumentCaptor.forClass(SingleAlert.class);
|
||||
|
||||
+9
-6
@@ -132,6 +132,7 @@ public class RealTimeAlertCalculatorMatchTest {
|
||||
|
||||
|
||||
AlertDefine matchDefine = new AlertDefine();
|
||||
matchDefine.setId(1L);
|
||||
matchDefine.setName("test");
|
||||
matchDefine.setExpr(
|
||||
"equals(__app__,\"prometheus\") && "
|
||||
@@ -151,8 +152,8 @@ public class RealTimeAlertCalculatorMatchTest {
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
verify(alarmCacheManager, times(1)).getPending(any());
|
||||
verify(alarmCacheManager, times(1)).putFiring(any(), any());
|
||||
verify(alarmCacheManager, times(1)).getPending(any(), any());
|
||||
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
|
||||
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
|
||||
}
|
||||
|
||||
@@ -180,6 +181,7 @@ public class RealTimeAlertCalculatorMatchTest {
|
||||
CollectRep.MetricsData metricsData = builder.build();
|
||||
|
||||
AlertDefine matchDefine = new AlertDefine();
|
||||
matchDefine.setId(1L);
|
||||
matchDefine.setName("test");
|
||||
matchDefine.setExpr("equals(__app__,\"prometheus\") && equals(__metrics__,\"canal_instance\") && metric_value > 0");
|
||||
matchDefine.setTemplate("Canal instance val: ${value}%");
|
||||
@@ -194,8 +196,8 @@ public class RealTimeAlertCalculatorMatchTest {
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
verify(alarmCacheManager, times(1)).getPending(any());
|
||||
verify(alarmCacheManager, times(1)).putFiring(any(), any());
|
||||
verify(alarmCacheManager, times(1)).getPending(any(), any());
|
||||
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
|
||||
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
|
||||
}
|
||||
|
||||
@@ -229,6 +231,7 @@ public class RealTimeAlertCalculatorMatchTest {
|
||||
CollectRep.MetricsData metricsData = builder.build();
|
||||
|
||||
AlertDefine matchDefine = new AlertDefine();
|
||||
matchDefine.setId(1L);
|
||||
matchDefine.setName("test");
|
||||
matchDefine.setExpr("equals(__app__,\"springboot3\") && equals(__metrics__,\"available\") && equals(__instance__, \"518679137103104\") && responseTime > 0");
|
||||
matchDefine.setTemplate("Canal instance val: ${value}%");
|
||||
@@ -243,8 +246,8 @@ public class RealTimeAlertCalculatorMatchTest {
|
||||
|
||||
Thread.sleep(3000);
|
||||
|
||||
verify(alarmCacheManager, times(1)).getPending(any());
|
||||
verify(alarmCacheManager, times(1)).putFiring(any(), any());
|
||||
verify(alarmCacheManager, times(1)).getPending(any(), any());
|
||||
verify(alarmCacheManager, times(1)).putFiring(any(), any(), any());
|
||||
verify(alarmCommonReduce, times(1)).reduceAndSendAlarm(any());
|
||||
}
|
||||
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* alert sse manager test
|
||||
*/
|
||||
public class AlertSseManagerTest {
|
||||
|
||||
private AlertSseManager alertSseManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
alertSseManager = new AlertSseManager();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCompleteThrowsException() throws Exception {
|
||||
SseEmitter emitter = alertSseManager.createEmitter(1L);
|
||||
assertNotNull(emitter);
|
||||
|
||||
Map<Long, SseEmitter> emitters = new HashMap<>();
|
||||
SseEmitter spyEmitter = mock(SseEmitter.class);
|
||||
|
||||
doThrow(new IllegalStateException("Simulated output stream error")).when(spyEmitter).send(any(SseEmitter.SseEventBuilder.class));
|
||||
doThrow(new RuntimeException("Complete failed")).when(spyEmitter).complete();
|
||||
|
||||
emitters.put(1L, spyEmitter);
|
||||
|
||||
Field emittersField = AlertSseManager.class.getDeclaredField("emitters");
|
||||
emittersField.setAccessible(true);
|
||||
emittersField.set(alertSseManager, emitters);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> alertSseManager.broadcast("{\"id\":1,\"content\":\"Test alert\"}"));
|
||||
Map<Long, SseEmitter> currentEmitters = (Map<Long, SseEmitter>) emittersField.get(alertSseManager);
|
||||
assertFalse(currentEmitters.containsKey(1L), "Emitter should still exist because complete() threw exception");
|
||||
}
|
||||
|
||||
}
|
||||
+118
-12
@@ -181,12 +181,10 @@ class AlertExpressionEvalVisitorTest {
|
||||
List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
|
||||
// promql
|
||||
List<Map<String, Object>> result = evaluate("multi_val > 25");
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(30.0, result.get(0).get("__value__"));
|
||||
assertEquals(0, result.size());
|
||||
// sql
|
||||
result = evaluate("(select values from multi_metrics where group_id = 'test_group') > 25");
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(30.0, result.get(0).get("__value__"));
|
||||
assertEquals(0, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -196,12 +194,10 @@ class AlertExpressionEvalVisitorTest {
|
||||
List.of(new HashMap<>(Map.of("__value__", List.of(10.0, 20.0, 30.0)))));
|
||||
// promql
|
||||
List<Map<String, Object>> result = evaluate("multi_val < 15");
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(10.0, result.get(0).get("__value__"));
|
||||
assertEquals(0, result.size());
|
||||
// sql
|
||||
result = evaluate("(select response_times from performance_data where service = 'api') < 15");
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(10.0, result.get(0).get("__value__"));
|
||||
assertEquals(0, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -211,12 +207,10 @@ class AlertExpressionEvalVisitorTest {
|
||||
List.of(new HashMap<>(Map.of("__value__", List.of()))));
|
||||
// promql
|
||||
List<Map<String, Object>> result = evaluate("empty_list > 50");
|
||||
assertEquals(1, result.size());
|
||||
assertNull(result.get(0).get("__value__"));
|
||||
assertEquals(0, result.size());
|
||||
// sql
|
||||
result = evaluate("(select error_codes from error_log where date = '2024-01-01') > 50");
|
||||
assertEquals(1, result.size());
|
||||
assertNull(result.get(0).get("__value__"));
|
||||
assertEquals(0, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -757,6 +751,118 @@ class AlertExpressionEvalVisitorTest {
|
||||
assertEquals(16, result.get(1).get("__value__"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComparisonExpr() {
|
||||
String promql = "3 > bool 2";
|
||||
|
||||
// scalar and scalar
|
||||
List<Map<String, Object>> result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(1, result.get(0).get("__value__"));
|
||||
|
||||
// scalar and vector
|
||||
promql = "0 < http_server_requests_seconds_count";
|
||||
Map<String, Object> vector = new HashMap<>() {
|
||||
{
|
||||
put("instance", "host.docker.internal:8989");
|
||||
put("__value__", 1307);
|
||||
put("method", "GET");
|
||||
put("__name__", "http_server_requests_seconds_count");
|
||||
put("__timestamp__", "1.750320922467E9");
|
||||
put("uri", "/actuator/prometheus");
|
||||
put("status", "200");
|
||||
}
|
||||
};
|
||||
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(vector));
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(1307, result.get(0).get("__value__"));
|
||||
|
||||
// scalar and vector bool
|
||||
promql = "0 < bool http_server_requests_seconds_count";
|
||||
// true
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(1, result.get(0).get("__value__"));
|
||||
|
||||
// false
|
||||
vector.put("__value__", -1);
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(0, result.get(0).get("__value__"));
|
||||
|
||||
// vector and scalar
|
||||
promql = "http_server_requests_seconds_sum > 0";
|
||||
vector = new HashMap<>() {
|
||||
{
|
||||
put("instance", "host.docker.internal:8989");
|
||||
put("__value__", 13);
|
||||
put("method", "GET");
|
||||
put("__name__", "http_server_requests_seconds_count");
|
||||
put("__timestamp__", "1.750320922467E9");
|
||||
put("uri", "/actuator/prometheus");
|
||||
put("status", "200");
|
||||
}
|
||||
};
|
||||
when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(vector));
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(13, result.get(0).get("__value__"));
|
||||
|
||||
// vector and scalar bool
|
||||
promql = "http_server_requests_seconds_sum > bool 0";
|
||||
// true
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(1, result.get(0).get("__value__"));
|
||||
// false
|
||||
vector.put("__value__", -1);
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(0, result.get(0).get("__value__"));
|
||||
|
||||
// vector and vector
|
||||
promql = "http_server_requests_seconds_count > http_server_requests_seconds_sum";
|
||||
Map<String, Object> vectorCount = new HashMap<>() {
|
||||
{
|
||||
put("instance", "host.docker.internal:8989");
|
||||
put("__value__", 13);
|
||||
put("method", "GET");
|
||||
put("__name__", "http_server_requests_seconds_count");
|
||||
put("__timestamp__", "1.750320922467E9");
|
||||
put("uri", "/actuator/prometheus");
|
||||
put("status", "200");
|
||||
}
|
||||
};
|
||||
Map<String, Object> vectorSum = new HashMap<>() {
|
||||
{
|
||||
put("instance", "host.docker.internal:8989");
|
||||
put("__value__", 10);
|
||||
put("method", "GET");
|
||||
put("__name__", "http_server_requests_seconds_count");
|
||||
put("__timestamp__", "1.750320922467E9");
|
||||
put("uri", "/actuator/prometheus");
|
||||
put("status", "200");
|
||||
}
|
||||
};
|
||||
when(mockExecutor.execute("http_server_requests_seconds_count")).thenReturn(List.of(vectorCount));
|
||||
when(mockExecutor.execute("http_server_requests_seconds_sum")).thenReturn(List.of(vectorSum));
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(13, result.get(0).get("__value__"));
|
||||
|
||||
// vector and vector bool
|
||||
promql = "http_server_requests_seconds_count > bool http_server_requests_seconds_sum";
|
||||
// true
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(1, result.get(0).get("__value__"));
|
||||
// false
|
||||
vectorCount.put("__value__", 1);
|
||||
result = evaluate(promql);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals(0, result.get(0).get("__value__"));
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> evaluate(String expression) {
|
||||
AlertExpressionLexer lexer = new AlertExpressionLexer(CharStreams.fromString(expression));
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<mqtt.version>1.3.3</mqtt.version>
|
||||
<mqtt.version>1.2.5</mqtt.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -140,10 +140,19 @@
|
||||
</dependency>
|
||||
<!-- mqtt -->
|
||||
<dependency>
|
||||
<groupId>com.hivemq</groupId>
|
||||
<artifactId>hivemq-mqtt-client</artifactId>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>${mqtt.version}</version>
|
||||
</dependency>
|
||||
<!--Bouncy Castle-->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
<version>1.68</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<!--plc-->
|
||||
<dependency>
|
||||
<groupId>org.apache.plc4x</groupId>
|
||||
|
||||
+5
-5
@@ -471,19 +471,19 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
// remove special characters
|
||||
String cleanedUrl = jdbcProtocol.getUrl().replaceAll("[\\x00-\\x1F\\x7F\\xA0]", "");
|
||||
String url = recursiveDecode(cleanedUrl);
|
||||
url = url.toLowerCase();
|
||||
String urlLowerCase = url.toLowerCase();
|
||||
// url format check
|
||||
if (!url.matches("^jdbc:[a-zA-Z0-9]+:([^\\s;]+)(;[^\\s;]+)*$")) {
|
||||
if (!urlLowerCase.matches("^jdbc:[a-zA-Z0-9]+:([^\\s;]+)(;[^\\s;]+)*$")) {
|
||||
throw new IllegalArgumentException("Invalid JDBC URL format");
|
||||
}
|
||||
// backlist check
|
||||
for (String keyword : BLACK_LIST) {
|
||||
if (url.contains(keyword.toLowerCase())) {
|
||||
if (urlLowerCase.contains(keyword.toLowerCase())) {
|
||||
throw new IllegalArgumentException("Invalid JDBC URL: contains potentially malicious parameter: " + keyword);
|
||||
}
|
||||
}
|
||||
// universal detection
|
||||
String normalizedUrl = url.replaceAll("[\\x00-\\x1F\\x7F\\xA0]", " ").toLowerCase();
|
||||
String normalizedUrl = urlLowerCase.replaceAll("[\\x00-\\x1F\\x7F\\xA0]", " ");
|
||||
// universal detection of JDBC injection and deserialization attacks
|
||||
if (normalizedUrl.matches(".*jndi\\s*[:=].*")
|
||||
|| normalizedUrl.matches(".*ldap\\s*[:=].*")
|
||||
@@ -514,7 +514,7 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalizedUrl;
|
||||
return url;
|
||||
}
|
||||
assert jdbcProtocol.getPlatform() != null;
|
||||
return switch (jdbcProtocol.getPlatform()) {
|
||||
|
||||
+57
-33
@@ -68,6 +68,7 @@ import org.apache.hertzbeat.common.util.Base64Util;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.hertzbeat.common.util.IpDomainUtil;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
@@ -144,37 +145,46 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
|
||||
return;
|
||||
}
|
||||
/*
|
||||
this could create large objects, potentially impacting JVM memory space significantly.
|
||||
Option 1: Parse using InputStream, but this requires significant code changes;
|
||||
Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs.
|
||||
*/
|
||||
String resp = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
if (!StringUtils.hasText(resp)) {
|
||||
log.info("http response entity is empty, status: {}.", statusCode);
|
||||
}
|
||||
Long responseTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
long responseTime = System.currentTimeMillis() - startTime;
|
||||
String parseType = metrics.getHttp().getParseType();
|
||||
HttpEntity entity = response.getEntity();
|
||||
|
||||
try {
|
||||
switch (parseType) {
|
||||
case DispatchConstants.PARSE_JSON_PATH ->
|
||||
parseResponseByJsonPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
|
||||
case DispatchConstants.PARSE_PROM_QL ->
|
||||
parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder);
|
||||
case DispatchConstants.PARSE_PROMETHEUS ->
|
||||
parseResponseByPrometheusExporter(response.getEntity().getContent(), metrics.getAliasFields(), builder);
|
||||
case DispatchConstants.PARSE_XML_PATH ->
|
||||
parseResponseByXmlPath(resp, metrics, builder, responseTime);
|
||||
case DispatchConstants.PARSE_WEBSITE ->
|
||||
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime, statusCode);
|
||||
case DispatchConstants.PARSE_SITE_MAP ->
|
||||
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
|
||||
case DispatchConstants.PARSE_HEADER ->
|
||||
parseResponseByHeader(builder, metrics.getAliasFields(), response);
|
||||
case DispatchConstants.PARSE_CONFIG ->
|
||||
parseResponseByConfig(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
|
||||
default ->
|
||||
parseResponseByDefault(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
|
||||
if (DispatchConstants.PARSE_PROMETHEUS.equals(parseType)) {
|
||||
if (entity != null) {
|
||||
parseResponseByPrometheusExporter(entity.getContent(), metrics.getAliasFields(), builder);
|
||||
}
|
||||
} else if (DispatchConstants.PARSE_HEADER.equals(parseType)) {
|
||||
parseResponseByHeader(builder, metrics.getAliasFields(), response);
|
||||
// Consume entity to release connection
|
||||
EntityUtils.consumeQuietly(entity);
|
||||
} else {
|
||||
/*
|
||||
this could create large objects, potentially impacting JVM memory space significantly.
|
||||
Option 1: Parse using InputStream, but this requires significant code changes;
|
||||
Option 2: Manually trigger garbage collection, similar to how it's done in Dubbo for large inputs.
|
||||
*/
|
||||
String resp = entity == null ? "" : EntityUtils.toString(entity, StandardCharsets.UTF_8);
|
||||
if (!StringUtils.hasText(resp)) {
|
||||
log.info("http response entity is empty, status: {}.", statusCode);
|
||||
}
|
||||
switch (parseType) {
|
||||
case DispatchConstants.PARSE_JSON_PATH ->
|
||||
parseResponseByJsonPath(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
|
||||
case DispatchConstants.PARSE_PROM_QL ->
|
||||
parseResponseByPromQl(resp, metrics.getAliasFields(), metrics.getHttp(), builder);
|
||||
case DispatchConstants.PARSE_XML_PATH ->
|
||||
parseResponseByXmlPath(resp, metrics, builder, responseTime);
|
||||
case DispatchConstants.PARSE_WEBSITE ->
|
||||
parseResponseByWebsite(resp, metrics, metrics.getHttp(), builder, responseTime, statusCode);
|
||||
case DispatchConstants.PARSE_SITE_MAP ->
|
||||
parseResponseBySiteMap(resp, metrics.getAliasFields(), builder);
|
||||
case DispatchConstants.PARSE_CONFIG ->
|
||||
parseResponseByConfig(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
|
||||
default ->
|
||||
parseResponseByDefault(resp, metrics.getAliasFields(), metrics.getHttp(), builder, responseTime);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.info("parse error: {}.", e.getMessage(), e);
|
||||
@@ -594,6 +604,18 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
}
|
||||
}
|
||||
builder.addValueRow(valueRowBuilder.build());
|
||||
} else if (objectValue instanceof Number numberValue) {
|
||||
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
|
||||
for (String alias : aliasFields) {
|
||||
if (NetworkConstants.RESPONSE_TIME.equalsIgnoreCase(alias)) {
|
||||
valueRowBuilder.addColumn(responseTime.toString());
|
||||
} else if (CollectorConstants.KEYWORD.equalsIgnoreCase(alias)) {
|
||||
valueRowBuilder.addColumn(Integer.toString(keywordNum));
|
||||
} else {
|
||||
valueRowBuilder.addColumn(numberValue.toString());
|
||||
}
|
||||
}
|
||||
builder.addValueRow(valueRowBuilder.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -618,11 +640,13 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
.collect(Collectors.toMap(MetricFamily.Label::getName, MetricFamily.Label::getValue));
|
||||
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
|
||||
for (String aliasField : aliasFields) {
|
||||
if ("value".equals(aliasField)) {
|
||||
String columnValue = labelMap.get(aliasField);
|
||||
if (columnValue != null) {
|
||||
valueRowBuilder.addColumn(columnValue);
|
||||
} else if (CommonConstants.PROM_VALUE.equals(aliasField) || CommonConstants.PROM_METRIC_VALUE.equals(aliasField)) {
|
||||
valueRowBuilder.addColumn(String.valueOf(metric.getValue()));
|
||||
} else {
|
||||
String columnValue = labelMap.get(aliasField);
|
||||
valueRowBuilder.addColumn(columnValue == null ? CommonConstants.NULL_VALUE : columnValue);
|
||||
valueRowBuilder.addColumn(CommonConstants.NULL_VALUE);
|
||||
}
|
||||
}
|
||||
builder.addValueRow(valueRowBuilder.build());
|
||||
@@ -856,4 +880,4 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
}
|
||||
return successCodeSet.contains(statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -60,7 +60,7 @@ public class PrometheusMatrixParser extends AbstractPrometheusParse {
|
||||
boolean setValueFlag = false;
|
||||
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
|
||||
for (String aliasField : aliasFields) {
|
||||
if (!CollectUtil.assertPromRequireField(aliasField)) {
|
||||
if (!CollectUtil.assertPromRequireField(aliasField) || CollectUtil.isValueAsLabel(aliasField, r.getMetric())) {
|
||||
JsonElement jsonElement = r.getMetric().get(aliasField);
|
||||
if (jsonElement != null) {
|
||||
valueRowBuilder.addColumn(jsonElement.getAsString());
|
||||
@@ -93,7 +93,6 @@ public class PrometheusMatrixParser extends AbstractPrometheusParse {
|
||||
}
|
||||
builder.addValueRow(valueRowBuilder.build());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ public class PrometheusVectorParser extends AbstractPrometheusParse {
|
||||
for (PromVectorOrMatrix.Result r : result) {
|
||||
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
|
||||
for (String aliasField : aliasFields) {
|
||||
if (!CollectUtil.assertPromRequireField(aliasField)) {
|
||||
if (!CollectUtil.assertPromRequireField(aliasField) || CollectUtil.isValueAsLabel(aliasField, r.getMetric())) {
|
||||
JsonElement jsonElement = r.getMetric().get(aliasField);
|
||||
if (jsonElement != null) {
|
||||
valueRowBuilder.addColumn(jsonElement.getAsString());
|
||||
|
||||
+10
-7
@@ -42,7 +42,6 @@ import javax.management.remote.JMXServiceURL;
|
||||
import javax.management.remote.rmi.RMIConnectorServer;
|
||||
import javax.naming.Context;
|
||||
import javax.rmi.ssl.SslRMIClientSocketFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.collect.AbstractCollect;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
|
||||
@@ -54,13 +53,15 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.JmxProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.hertzbeat.common.util.LogUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* jmx protocol acquisition implementation
|
||||
*/
|
||||
@Slf4j
|
||||
public class JmxCollectImpl extends AbstractCollect {
|
||||
|
||||
private static final String JMX_URL_PREFIX = "service:jmx:rmi:///jndi/rmi://";
|
||||
@@ -75,6 +76,8 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
|
||||
private final ClassLoader jmxClassLoader;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JmxCollectImpl.class);
|
||||
|
||||
public JmxCollectImpl() {
|
||||
jmxClassLoader = new JmxClassLoader(ClassLoader.getSystemClassLoader());
|
||||
}
|
||||
@@ -195,12 +198,12 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(exception);
|
||||
log.error("JMX IOException :{}", errorMsg);
|
||||
LogUtil.error(logger, "JMX IOException: {0}", errorMsg);
|
||||
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
|
||||
builder.setMsg(errorMsg);
|
||||
} catch (Exception e) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(e);
|
||||
log.error("JMX Error :{}", errorMsg);
|
||||
LogUtil.error(logger, "JMX Error: {0}", errorMsg);
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(errorMsg);
|
||||
} finally {
|
||||
@@ -221,7 +224,7 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
for (Attribute attribute : attributeList.asList()) {
|
||||
Object value = attribute.getValue();
|
||||
if (value == null) {
|
||||
log.info("attribute {} value is null.", attribute.getName());
|
||||
LogUtil.info(logger, "attribute {0} value is null.", attribute.getName());
|
||||
continue;
|
||||
}
|
||||
if (value instanceof Number || value instanceof String || value instanceof ObjectName
|
||||
@@ -245,7 +248,7 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
}
|
||||
attributeValueMap.put(attribute.getName(), builder.toString());
|
||||
} else {
|
||||
log.warn("attribute value type {} not support.", value.getClass().getName());
|
||||
LogUtil.warn(logger, "attribute value type {0} not support.", value.getClass().getName());
|
||||
}
|
||||
}
|
||||
return attributeValueMap;
|
||||
@@ -319,7 +322,7 @@ public class JmxCollectImpl extends AbstractCollect {
|
||||
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
|
||||
return conn;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to connect to JMX server: {}", e.getMessage());
|
||||
LogUtil.error(logger, "Failed to connect to JMX connection: {0}", e.getMessage());
|
||||
throw new IOException("Failed to connect to JMX server: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.mqtt;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Formats the private key and certificate, supporting concatenation of multiple certificates in PEM format.
|
||||
*/
|
||||
public class CertificateFormatter {
|
||||
|
||||
public static String formatCertificateChain(String input) {
|
||||
if (input == null || input.trim().isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
String normalized = normalizeInput(input);
|
||||
|
||||
List<String> certificates = extractCertificates(normalized);
|
||||
|
||||
if (certificates.isEmpty()) {
|
||||
return formatAsSingleCertificate(normalized);
|
||||
}
|
||||
|
||||
StringBuilder formattedChain = new StringBuilder();
|
||||
for (String cert : certificates) {
|
||||
if (cert.trim().isEmpty()) continue;
|
||||
|
||||
String formatted = formatPemBlock(cert);
|
||||
formattedChain.append(formatted).append("\n");
|
||||
}
|
||||
|
||||
return formattedChain.toString().trim();
|
||||
}
|
||||
|
||||
private static String normalizeInput(String input) {
|
||||
return input
|
||||
.replace("\r\n", "\n")
|
||||
.replace("\r", "\n")
|
||||
.replaceAll("\\s*\\\\n\\s*", "\n")
|
||||
.replaceAll("(?m)^\\s+|\\s+$", "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
private static List<String> extractCertificates(String input) {
|
||||
List<String> certificates = new ArrayList<>();
|
||||
String regex = "(-----BEGIN\\s+[\\w\\s]+?-----)[\\s\\S]*?(-----END\\s+[\\w\\s]+?-----)";
|
||||
|
||||
|
||||
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(input);
|
||||
|
||||
int lastEnd = 0;
|
||||
while (matcher.find()) {
|
||||
|
||||
if (matcher.start() > lastEnd) {
|
||||
String gap = input.substring(lastEnd, matcher.start());
|
||||
if (!gap.trim().isEmpty()) {
|
||||
certificates.add(gap);
|
||||
}
|
||||
}
|
||||
|
||||
certificates.add(matcher.group());
|
||||
lastEnd = matcher.end();
|
||||
}
|
||||
|
||||
|
||||
if (lastEnd < input.length()) {
|
||||
certificates.add(input.substring(lastEnd));
|
||||
}
|
||||
|
||||
return certificates;
|
||||
}
|
||||
|
||||
private static String formatPemBlock(String block) {
|
||||
try {
|
||||
Pattern pattern = Pattern.compile(
|
||||
"(-----BEGIN\\s+[\\w\\s]+?-----)(.*?)(-----END\\s+[\\w\\s]+?-----)",
|
||||
Pattern.DOTALL | Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
Matcher matcher = pattern.matcher(block);
|
||||
if (matcher.find()) {
|
||||
String header = matcher.group(1).trim();
|
||||
String body = matcher.group(2);
|
||||
String footer = matcher.group(3).trim();
|
||||
|
||||
|
||||
if (body == null) body = "";
|
||||
|
||||
String cleanBody = body
|
||||
.replaceAll("\\s", "")
|
||||
.replaceAll("\"", "")
|
||||
.trim();
|
||||
|
||||
|
||||
if (cleanBody.isEmpty() && body != null && !body.trim().isEmpty()) {
|
||||
|
||||
cleanBody = body.replaceAll("[^a-zA-Z0-9+/=]", "").trim();
|
||||
}
|
||||
|
||||
String formattedBody = formatBase64Body(cleanBody);
|
||||
|
||||
return header + "\n" + formattedBody + "\n" + footer;
|
||||
} else {
|
||||
|
||||
return formatAsCertificate(block);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
return block;
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatAsCertificate(String content) {
|
||||
|
||||
String cleanContent = content.replaceAll("[^a-zA-Z0-9+/=]", "").trim();
|
||||
|
||||
if (cleanContent.isEmpty()) {
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
String formattedBody = formatBase64Body(cleanContent);
|
||||
|
||||
|
||||
if (cleanContent.toLowerCase().contains("private")) {
|
||||
if (cleanContent.startsWith("MII") || cleanContent.length() > 1000) {
|
||||
return "-----BEGIN PRIVATE KEY-----\n" + formattedBody + "\n-----END PRIVATE KEY-----";
|
||||
} else {
|
||||
return "-----BEGIN RSA PRIVATE KEY-----\n" + formattedBody + "\n-----END RSA PRIVATE KEY-----";
|
||||
}
|
||||
} else {
|
||||
return "-----BEGIN CERTIFICATE-----\n" + formattedBody + "\n-----END CERTIFICATE-----";
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatAsSingleCertificate(String input) {
|
||||
String cleanContent = input.replaceAll("[^a-zA-Z0-9+/=]", "").trim();
|
||||
return formatAsCertificate(cleanContent);
|
||||
}
|
||||
|
||||
private static String formatBase64Body(String body) {
|
||||
|
||||
StringBuilder formatted = new StringBuilder();
|
||||
int index = 0;
|
||||
while (index < body.length()) {
|
||||
int end = Math.min(index + 64, body.length());
|
||||
formatted.append(body.substring(index, end));
|
||||
if (end < body.length()) {
|
||||
formatted.append("\n");
|
||||
}
|
||||
index = end;
|
||||
}
|
||||
return formatted.toString().trim();
|
||||
}
|
||||
|
||||
public static String formatPrivateKey(String input) {
|
||||
if (input == null || input.trim().isEmpty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
String normalized = normalizeInput(input);
|
||||
|
||||
|
||||
if (isPemEncapsulated(normalized)) {
|
||||
return formatPemBlock(normalized);
|
||||
}
|
||||
|
||||
return formatAsCertificate(normalized);
|
||||
}
|
||||
|
||||
private static boolean isPemEncapsulated(String block) {
|
||||
return block.contains("-----BEGIN") && block.contains("-----END");
|
||||
}
|
||||
}
|
||||
+221
-161
@@ -17,26 +17,7 @@
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.mqtt;
|
||||
|
||||
import com.hivemq.client.mqtt.MqttVersion;
|
||||
import com.hivemq.client.mqtt.datatypes.MqttQos;
|
||||
import com.hivemq.client.mqtt.mqtt3.Mqtt3AsyncClient;
|
||||
import com.hivemq.client.mqtt.mqtt3.Mqtt3Client;
|
||||
import com.hivemq.client.mqtt.mqtt3.Mqtt3ClientBuilder;
|
||||
import com.hivemq.client.mqtt.mqtt3.message.connect.connack.Mqtt3ConnAck;
|
||||
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
|
||||
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
|
||||
import com.hivemq.client.mqtt.mqtt5.Mqtt5ClientBuilder;
|
||||
import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAck;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.collector.collect.AbstractCollect;
|
||||
import org.apache.hertzbeat.collector.constants.CollectorConstants;
|
||||
@@ -46,13 +27,27 @@ import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep.MetricsData.Builder;
|
||||
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.eclipse.paho.client.mqttv3.MqttCallback;
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
|
||||
import org.eclipse.paho.client.mqttv3.IMqttToken;
|
||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* collect mqtt metrics
|
||||
* collect mqtt metrics using Eclipse Paho
|
||||
*/
|
||||
public class MqttCollectImpl extends AbstractCollect {
|
||||
|
||||
@@ -61,138 +56,224 @@ public class MqttCollectImpl extends AbstractCollect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MqttCollectImpl.class);
|
||||
|
||||
@Override
|
||||
public void preCheck(Metrics metrics) throws IllegalArgumentException {
|
||||
MqttProtocol mqttProtocol = metrics.getMqtt();
|
||||
Assert.hasText(mqttProtocol.getHost(), "MQTT protocol host is required");
|
||||
Assert.hasText(mqttProtocol.getPort(), "MQTT protocol port is required");
|
||||
Assert.hasText(mqttProtocol.getProtocolVersion(), "MQTT protocol version is required");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(Builder builder, Metrics metrics) {
|
||||
MqttProtocol mqtt = metrics.getMqtt();
|
||||
String protocolVersion = mqtt.getProtocolVersion();
|
||||
MqttVersion mqttVersion = MqttVersion.valueOf(protocolVersion);
|
||||
if (mqttVersion == MqttVersion.MQTT_3_1_1) {
|
||||
collectWithVersion3(metrics, builder);
|
||||
} else if (mqttVersion == MqttVersion.MQTT_5_0) {
|
||||
collectWithVersion5(metrics, builder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String supportProtocol() {
|
||||
return DispatchConstants.PROTOCOL_MQTT;
|
||||
}
|
||||
|
||||
/**
|
||||
* collecting data of MQTT 5
|
||||
*/
|
||||
private void collectWithVersion5(Metrics metrics, Builder builder) {
|
||||
@Override
|
||||
public void preCheck(Metrics metrics) throws IllegalArgumentException {
|
||||
MqttProtocol mqttProtocol = metrics.getMqtt();
|
||||
Assert.hasText(mqttProtocol.getHost(), "MQTT protocol host is required");
|
||||
Assert.hasText(mqttProtocol.getPort(), "MQTT protocol port is required");
|
||||
|
||||
if ("mqtts".equalsIgnoreCase(mqttProtocol.getProtocol())) {
|
||||
if (Boolean.parseBoolean(mqttProtocol.getEnableMutualAuth())) {
|
||||
Assert.hasText(mqttProtocol.getCaCert(), "CA certificate is required for mutual auth");
|
||||
Assert.hasText(mqttProtocol.getClientCert(), "Client certificate is required for mutual auth");
|
||||
Assert.hasText(mqttProtocol.getClientKey(), "Client private key is required for mutual auth");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(Builder builder, Metrics metrics) {
|
||||
MqttProtocol mqttProtocol = metrics.getMqtt();
|
||||
Map<Object, String> data = new HashMap<>();
|
||||
Mqtt5AsyncClient client = buildMqtt5Client(mqttProtocol);
|
||||
long responseTime = connectClient(client, mqtt5AsyncClient -> {
|
||||
CompletableFuture<Mqtt5ConnAck> connectFuture = mqtt5AsyncClient.connect();
|
||||
try {
|
||||
connectFuture.get(Long.parseLong(mqttProtocol.getTimeout()), TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(getErrorMessage(e.getMessage()));
|
||||
|
||||
try {
|
||||
MqttAsyncClient client = buildMqttClient(mqttProtocol);
|
||||
long responseTime = connectClient(client, mqttProtocol);
|
||||
testSubscribeAndPublish(client, mqttProtocol, data);
|
||||
convertToMetricsData(builder, metrics, responseTime, data);
|
||||
client.disconnect();
|
||||
} catch (Exception e) {
|
||||
logger.error("MQTT collection error: {}", e.getMessage(), e);
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg("Collection failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private MqttAsyncClient buildMqttClient(MqttProtocol protocol) throws Exception {
|
||||
String clientId = protocol.getClientId();
|
||||
|
||||
String serverUri = String.format("%s://%s:%s",
|
||||
StringUtils.equals(protocol.getProtocol(), "MQTT") ? "tcp" : "ssl",
|
||||
protocol.getHost(),
|
||||
protocol.getPort());
|
||||
|
||||
MqttClientPersistence persistence = new MemoryPersistence();
|
||||
|
||||
return new MqttAsyncClient(serverUri, clientId, persistence);
|
||||
}
|
||||
|
||||
private long connectClient(MqttAsyncClient client, MqttProtocol protocol) throws Exception {
|
||||
MqttConnectOptions connOpts = new MqttConnectOptions();
|
||||
|
||||
if (protocol.hasAuth()) {
|
||||
connOpts.setUserName(protocol.getUsername());
|
||||
connOpts.setPassword(protocol.getPassword().toCharArray());
|
||||
}
|
||||
|
||||
connOpts.setKeepAliveInterval(Integer.parseInt(protocol.getKeepalive()));
|
||||
connOpts.setConnectionTimeout(Integer.parseInt(protocol.getTimeout()) / 1000);
|
||||
connOpts.setCleanSession(true);
|
||||
connOpts.setAutomaticReconnect(false);
|
||||
if ("mqtts".equalsIgnoreCase(protocol.getProtocol())) {
|
||||
boolean insecureSkipVerify = Boolean.parseBoolean(protocol.getInsecureSkipVerify());
|
||||
if (insecureSkipVerify) {
|
||||
connOpts.setHttpsHostnameVerificationEnabled(false);
|
||||
}
|
||||
if (Boolean.parseBoolean(protocol.getEnableMutualAuth())) {
|
||||
connOpts.setSocketFactory(MqttSslFactory.getMslSocketFactory(protocol, insecureSkipVerify));
|
||||
} else {
|
||||
connOpts.setSocketFactory(MqttSslFactory.getSslSocketFactory(protocol, insecureSkipVerify));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
StopWatch connectWatch = new StopWatch();
|
||||
connectWatch.start();
|
||||
|
||||
client.connect(connOpts).waitForCompletion(Long.parseLong(protocol.getTimeout()));
|
||||
connectWatch.stop();
|
||||
return connectWatch.getTotalTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test MQTT subscribe and publish capabilities
|
||||
*/
|
||||
private void testSubscribeAndPublish(MqttAsyncClient client, MqttProtocol protocol, Map<Object, String> data) {
|
||||
|
||||
// 1 test subscribe
|
||||
if (StringUtils.isNotBlank(protocol.getTopic())) {
|
||||
String subscribe = testSubscribe(client, protocol.getTopic());
|
||||
if (StringUtils.isBlank(subscribe)) {
|
||||
data.put("canSubscribe", "Subscription successful");
|
||||
} else {
|
||||
data.put("canSubscribe", String.format("Subscription failed: %s", subscribe));
|
||||
}
|
||||
|
||||
} else {
|
||||
data.put("canSubscribe", "No topic, subscription test skipped");
|
||||
}
|
||||
|
||||
|
||||
// 2 test publish
|
||||
if (StringUtils.isNotBlank(protocol.getTestMessage())) {
|
||||
String publish = testPublish(client, protocol.getTopic(), protocol.getTestMessage());
|
||||
if (StringUtils.isBlank(publish)) {
|
||||
data.put("canPublish", "Message published successfully");
|
||||
|
||||
// 3 test receive message
|
||||
String receivedData = getReceivedData(client, protocol.getTopic());
|
||||
data.put("canReceive", receivedData);
|
||||
} else {
|
||||
data.put("canPublish", String.format("Message publishing failed: %s", publish));
|
||||
data.put("canReceive", "Message reception skipped due to failed publish");
|
||||
}
|
||||
} else {
|
||||
data.put("canPublish", "No test message, publish test skipped");
|
||||
data.put("canReceive", "No test message, receive test skipped");
|
||||
}
|
||||
|
||||
|
||||
// 4 test unsubscribe
|
||||
if (StringUtils.isNotBlank(protocol.getTopic())) {
|
||||
String subscribe = testUnSubscribe(client, protocol.getTopic());
|
||||
if (StringUtils.isBlank(subscribe)) {
|
||||
data.put("canUnSubscribe", "Unsubscription successful");
|
||||
} else {
|
||||
data.put("canUnSubscribe", String.format("Unsubscription failed: %s", subscribe));
|
||||
}
|
||||
} else {
|
||||
data.put("canUnSubscribe", "No topic, unsubscription test skipped");
|
||||
}
|
||||
}
|
||||
|
||||
private String getReceivedData(MqttAsyncClient client, String topic) {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final StringBuilder messageHolder = new StringBuilder();
|
||||
|
||||
|
||||
client.setCallback(new MqttCallback() {
|
||||
@Override
|
||||
public void connectionLost(Throwable cause) {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageArrived(String arrivedTopic, MqttMessage message) {
|
||||
|
||||
if (topic.equals(arrivedTopic)) {
|
||||
messageHolder.append(new String(message.getPayload()));
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliveryComplete(IMqttDeliveryToken token) {
|
||||
}
|
||||
});
|
||||
testDescribeAndPublish5(client, mqttProtocol, data);
|
||||
convertToMetricsData(builder, metrics, responseTime, data);
|
||||
client.disconnect();
|
||||
|
||||
try {
|
||||
boolean received = latch.await(5, TimeUnit.SECONDS);
|
||||
if (messageHolder.length() > 0) {
|
||||
return messageHolder.toString();
|
||||
} else if (!received) {
|
||||
return "Message reception timed out after 5 seconds";
|
||||
} else {
|
||||
return "No valid message received";
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return e.getMessage();
|
||||
} finally {
|
||||
client.setCallback(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String testSubscribe(MqttAsyncClient client, String topic) {
|
||||
try {
|
||||
IMqttToken subToken = client.subscribe(topic, 1);
|
||||
subToken.waitForCompletion(5000);
|
||||
return "";
|
||||
} catch (MqttException e) {
|
||||
logger.warn("MQTT subscribe test failed: {}", e.getMessage());
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private String testPublish(MqttAsyncClient client, String topic, String message) {
|
||||
try {
|
||||
MqttMessage mqttMessage = new MqttMessage(message.getBytes());
|
||||
mqttMessage.setQos(1);
|
||||
|
||||
IMqttToken pubToken = client.publish(topic, mqttMessage);
|
||||
pubToken.waitForCompletion(5000);
|
||||
|
||||
return "";
|
||||
} catch (MqttException e) {
|
||||
logger.warn("MQTT publish test failed: {}", e.getMessage());
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private String testUnSubscribe(MqttAsyncClient client, String topic) {
|
||||
try {
|
||||
IMqttToken unsubToken = client.unsubscribe(topic);
|
||||
unsubToken.waitForCompletion(5000);
|
||||
return "";
|
||||
} catch (MqttException e) {
|
||||
logger.warn("MQTT unsubscribe test failed: {}", e.getMessage());
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* collecting data of MQTT 3.1.1
|
||||
* Convert collected data to MetricsData
|
||||
*/
|
||||
private void collectWithVersion3(Metrics metrics, Builder builder) {
|
||||
MqttProtocol mqttProtocol = metrics.getMqtt();
|
||||
Map<Object, String> data = new HashMap<>();
|
||||
Mqtt3AsyncClient client = buildMqtt3Client(mqttProtocol);
|
||||
long responseTime = connectClient(client, mqtt3AsyncClient -> {
|
||||
CompletableFuture<Mqtt3ConnAck> connectFuture = mqtt3AsyncClient.connect();
|
||||
try {
|
||||
connectFuture.get(Long.parseLong(mqttProtocol.getTimeout()), TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(getErrorMessage(e.getMessage()));
|
||||
}
|
||||
});
|
||||
testDescribeAndPublish3(client, mqttProtocol, data);
|
||||
convertToMetricsData(builder, metrics, responseTime, data);
|
||||
client.disconnect();
|
||||
}
|
||||
|
||||
private void testDescribeAndPublish3(Mqtt3AsyncClient client, MqttProtocol mqttProtocol, Map<Object, String> data) {
|
||||
data.put("canDescribe", test(() -> {
|
||||
client.subscribeWith().topicFilter(mqttProtocol.getTopic()).qos(MqttQos.AT_LEAST_ONCE).send();
|
||||
client.unsubscribeWith().topicFilter(mqttProtocol.getTopic()).send();
|
||||
}, "subscribe").toString());
|
||||
|
||||
data.put("canPublish", !mqttProtocol.testPublish() ? Boolean.FALSE.toString() : test(() -> {
|
||||
client.publishWith().topic(mqttProtocol.getTopic())
|
||||
.payload(mqttProtocol.getTestMessage().getBytes(StandardCharsets.UTF_8))
|
||||
.qos(MqttQos.AT_LEAST_ONCE).send();
|
||||
data.put("canPublish", Boolean.TRUE.toString());
|
||||
}, "publish").toString());
|
||||
}
|
||||
|
||||
private void testDescribeAndPublish5(Mqtt5AsyncClient client, MqttProtocol mqttProtocol, Map<Object, String> data) {
|
||||
data.put("canDescribe", test(() -> {
|
||||
client.subscribeWith().topicFilter(mqttProtocol.getTopic()).qos(MqttQos.AT_LEAST_ONCE).send();
|
||||
client.unsubscribeWith().topicFilter(mqttProtocol.getTopic()).send();
|
||||
}, "subscribe").toString());
|
||||
|
||||
data.put("canPublish", !mqttProtocol.testPublish() ? Boolean.FALSE.toString() : test(() -> {
|
||||
client.publishWith().topic(mqttProtocol.getTopic())
|
||||
.payload(mqttProtocol.getTestMessage().getBytes(StandardCharsets.UTF_8))
|
||||
.qos(MqttQos.AT_LEAST_ONCE).send();
|
||||
data.put("canPublish", Boolean.TRUE.toString());
|
||||
}, "publish").toString());
|
||||
}
|
||||
|
||||
private Mqtt5AsyncClient buildMqtt5Client(MqttProtocol mqttProtocol) {
|
||||
Mqtt5ClientBuilder mqtt5ClientBuilder = Mqtt5Client.builder()
|
||||
.serverHost(mqttProtocol.getHost())
|
||||
.identifier(mqttProtocol.getClientId())
|
||||
.serverPort(Integer.parseInt(mqttProtocol.getPort()));
|
||||
|
||||
if (mqttProtocol.hasAuth()) {
|
||||
mqtt5ClientBuilder.simpleAuth().username(mqttProtocol.getUsername())
|
||||
.password(mqttProtocol.getPassword().getBytes(StandardCharsets.UTF_8))
|
||||
.applySimpleAuth();
|
||||
}
|
||||
return mqtt5ClientBuilder.buildAsync();
|
||||
}
|
||||
|
||||
private Mqtt3AsyncClient buildMqtt3Client(MqttProtocol mqttProtocol) {
|
||||
|
||||
Mqtt3ClientBuilder mqtt3ClientBuilder = Mqtt3Client.builder()
|
||||
.serverHost(mqttProtocol.getHost())
|
||||
.identifier(mqttProtocol.getClientId())
|
||||
.serverPort(Integer.parseInt(mqttProtocol.getPort()));
|
||||
|
||||
if (mqttProtocol.hasAuth()) {
|
||||
mqtt3ClientBuilder.simpleAuth().username(mqttProtocol.getUsername())
|
||||
.password(mqttProtocol.getPassword().getBytes(StandardCharsets.UTF_8))
|
||||
.applySimpleAuth();
|
||||
}
|
||||
return mqtt3ClientBuilder.buildAsync();
|
||||
}
|
||||
|
||||
public <T> long connectClient(T client, Consumer<T> connect) {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
connect.accept(client);
|
||||
stopWatch.stop();
|
||||
return stopWatch.getTotalTimeMillis();
|
||||
}
|
||||
|
||||
private void convertToMetricsData(Builder builder, Metrics metrics, long responseTime, Map<Object, String> data) {
|
||||
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
|
||||
for (String column : metrics.getAliasFields()) {
|
||||
@@ -207,25 +288,4 @@ public class MqttCollectImpl extends AbstractCollect {
|
||||
builder.addValueRow(valueRowBuilder.build());
|
||||
}
|
||||
|
||||
private Boolean test(Runnable runnable, String operationName) {
|
||||
try {
|
||||
runnable.run();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error("{} fail", operationName, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getErrorMessage(String errorMessage) {
|
||||
if (StringUtils.isBlank(errorMessage)) {
|
||||
return "connect failed";
|
||||
}
|
||||
String[] split = errorMessage.split(":");
|
||||
if (split.length > 1) {
|
||||
return Arrays.stream(split).skip(1).collect(Collectors.joining(":"));
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.mqtt;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol;
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Support MQTT SSL Factory
|
||||
*/
|
||||
public class MqttSslFactory {
|
||||
|
||||
/**
|
||||
* Get MSL Socket Factory
|
||||
*/
|
||||
public static SSLSocketFactory getMslSocketFactory(MqttProtocol mqttProtocol, boolean insecureSkipVerify) {
|
||||
try {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
|
||||
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
ks.load(null, null);
|
||||
|
||||
Certificate[] chain = null;
|
||||
if (mqttProtocol.getClientCert() != null && !mqttProtocol.getClientCert().isEmpty()) {
|
||||
String formatClientCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getClientCert());
|
||||
try (InputStream certIn = new ByteArrayInputStream(formatClientCert.getBytes())) {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
Collection<? extends Certificate> certs = cf.generateCertificates(certIn);
|
||||
chain = certs.toArray(new Certificate[0]);
|
||||
}
|
||||
}
|
||||
|
||||
PrivateKey privateKey;
|
||||
if (mqttProtocol.getClientKey() != null && !mqttProtocol.getClientKey().isEmpty()) {
|
||||
String formatClientKey = CertificateFormatter.formatPrivateKey(mqttProtocol.getClientKey());
|
||||
try (PEMParser pemParser = new PEMParser(new StringReader(formatClientKey))) {
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
||||
Object object = pemParser.readObject();
|
||||
|
||||
if (object instanceof PEMKeyPair) {
|
||||
privateKey = converter.getPrivateKey(((PEMKeyPair) object).getPrivateKeyInfo());
|
||||
} else if (object instanceof PrivateKeyInfo) {
|
||||
privateKey = converter.getPrivateKey((PrivateKeyInfo) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported private key type");
|
||||
}
|
||||
|
||||
ks.setKeyEntry("private-key", privateKey, "".toCharArray(), chain);
|
||||
}
|
||||
}
|
||||
|
||||
TrustManager[] trustManagers;
|
||||
if (insecureSkipVerify) {
|
||||
trustManagers = createInsecureTrustManager();
|
||||
} else {
|
||||
String formatCaCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getCaCert());
|
||||
KeyStore trustStore = createMergedTrustStore(formatCaCert);
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(trustStore);
|
||||
trustManagers = tmf.getTrustManagers();
|
||||
}
|
||||
|
||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(ks, "".toCharArray());
|
||||
|
||||
|
||||
SSLContext context = SSLContext.getInstance(mqttProtocol.getTlsVersion());
|
||||
context.init(kmf.getKeyManagers(), trustManagers, null);
|
||||
|
||||
return context.getSocketFactory();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Fails to SSL initialize: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SSL Socket Factory
|
||||
*/
|
||||
public static SSLSocketFactory getSslSocketFactory(MqttProtocol mqttProtocol, boolean insecureSkipVerify) {
|
||||
try {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
|
||||
|
||||
TrustManager[] trustManagers;
|
||||
if (insecureSkipVerify) {
|
||||
trustManagers = createInsecureTrustManager();
|
||||
} else {
|
||||
|
||||
String formatCaCert = CertificateFormatter.formatCertificateChain(mqttProtocol.getCaCert());
|
||||
KeyStore trustStore = createMergedTrustStore(formatCaCert);
|
||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(trustStore);
|
||||
trustManagers = tmf.getTrustManagers();
|
||||
}
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance(mqttProtocol.getTlsVersion());
|
||||
sslContext.init(null, trustManagers, null);
|
||||
|
||||
return sslContext.getSocketFactory();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Fails to SSL initialize: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static TrustManager[] createInsecureTrustManager() {
|
||||
return new TrustManager[]{
|
||||
new X509TrustManager() {
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType) {
|
||||
}
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static KeyStore createMergedTrustStore(String caCertPem) throws Exception {
|
||||
KeyStore mergedKs = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
mergedKs.load(null, null);
|
||||
|
||||
|
||||
TrustManagerFactory systemTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
systemTmf.init((KeyStore) null);
|
||||
X509TrustManager systemTm = (X509TrustManager) systemTmf.getTrustManagers()[0];
|
||||
|
||||
int systemIndex = 1;
|
||||
for (X509Certificate cert : systemTm.getAcceptedIssuers()) {
|
||||
mergedKs.setCertificateEntry("system-ca-" + systemIndex++, cert);
|
||||
}
|
||||
|
||||
|
||||
if (caCertPem != null && !caCertPem.isEmpty()) {
|
||||
try (InputStream caIn = new ByteArrayInputStream(caCertPem.getBytes())) {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
Collection<? extends Certificate> customCerts = cf.generateCertificates(caIn);
|
||||
|
||||
int customIndex = 1;
|
||||
for (Certificate cert : customCerts) {
|
||||
mergedKs.setCertificateEntry("custom-ca-" + customIndex++, cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedKs;
|
||||
}
|
||||
}
|
||||
+23
-4
@@ -69,6 +69,10 @@ public class OnlineParser {
|
||||
parseMetric(inputStream, metricFamilyMap, stringBuilder);
|
||||
}
|
||||
i = getChar(inputStream);
|
||||
// To address the `\n\r` scenario, it is necessary to skip
|
||||
if (i == '\r') {
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
log.error("prometheus parser failed because of wrong input format. {}", e.getMessage());
|
||||
@@ -185,6 +189,10 @@ public class OnlineParser {
|
||||
stringBuilder.append((char) i);
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
// Skip \r character to handle Windows line endings
|
||||
if (i == '\r') {
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
return new CharChecker(i);
|
||||
}
|
||||
|
||||
@@ -193,6 +201,10 @@ public class OnlineParser {
|
||||
while (i >= '0' && i <= '9') {
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
// Skip \r character to handle Windows line endings
|
||||
if (i == '\r') {
|
||||
i = getChar(inputStream);
|
||||
}
|
||||
return new CharChecker(i);
|
||||
}
|
||||
|
||||
@@ -215,20 +227,27 @@ public class OnlineParser {
|
||||
}
|
||||
|
||||
private static CharChecker parseLabelValue(InputStream inputStream, StringBuilder stringBuilder) throws IOException, FormatException {
|
||||
int i = getChar(inputStream);
|
||||
int i = inputStream.read();
|
||||
while (i != '"' && i != -1) {
|
||||
if (i == '\\') {
|
||||
i = getChar(inputStream);
|
||||
i = inputStream.read();
|
||||
switch (i) {
|
||||
case 'n' -> stringBuilder.append('\n');
|
||||
case '\\' -> stringBuilder.append('\\');
|
||||
case '\"' -> stringBuilder.append('\"');
|
||||
default -> throw new FormatException();
|
||||
default -> {
|
||||
// Unknown escape, keep as-is
|
||||
// https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/lib/protoparser/prometheus/parser.go#L419
|
||||
stringBuilder.append('\\');
|
||||
if (i != -1) {
|
||||
stringBuilder.append((char) i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stringBuilder.append((char) i);
|
||||
}
|
||||
i = getChar(inputStream);
|
||||
i = inputStream.read();
|
||||
}
|
||||
return new CharChecker(i);
|
||||
}
|
||||
|
||||
+85
-5
@@ -17,11 +17,6 @@
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.database;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol;
|
||||
@@ -29,6 +24,14 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* Test case for {@link JdbcCommonCollect}
|
||||
*/
|
||||
@@ -128,4 +131,81 @@ class JdbcCommonCollectTest {
|
||||
String protocol = jdbcCommonCollect.supportProtocol();
|
||||
assertEquals(DispatchConstants.PROTOCOL_JDBC, protocol);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUrlPassThrough() {
|
||||
String[] testUrls = {
|
||||
"jdbc:mysql://localhost:3306/test?allowPublicKeyRetrieval=true&useSSL=false",
|
||||
"jdbc:mysql://localhost:3306/test?usessl=false&verifyServerCertificate=true",
|
||||
"jdbc:mysql://localhost:3306/test?serverTimezone=UTC&autoReconnect=false"
|
||||
};
|
||||
JdbcCommonCollect jdbcCollect = new JdbcCommonCollect();
|
||||
for (String originalUrl : testUrls) {
|
||||
try {
|
||||
JdbcProtocol jdbcProtocol = JdbcProtocol.builder()
|
||||
.host("localhost")
|
||||
.port("3306")
|
||||
.platform("mysql")
|
||||
.username("root")
|
||||
.password("root")
|
||||
.database("test")
|
||||
.url(originalUrl)
|
||||
.build();
|
||||
|
||||
// Use reflection to call constructDatabaseUrl method
|
||||
Method constructMethod = JdbcCommonCollect.class.getDeclaredMethod("constructDatabaseUrl", JdbcProtocol.class, String.class, String.class);
|
||||
constructMethod.setAccessible(true);
|
||||
String processedUrl = (String) constructMethod.invoke(jdbcCollect, jdbcProtocol, "localhost", "3306");
|
||||
// Verify that the processed URL is the same as the original URL
|
||||
assertEquals(originalUrl, processedUrl,
|
||||
"URL should be passed through without modification: " + originalUrl);
|
||||
} catch (Exception e) {
|
||||
System.out.println("URL rejected by security validation: " + originalUrl + ", reason: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructDatabaseUrlSecurityInterception() {
|
||||
JdbcCommonCollect jdbcCollect = new JdbcCommonCollect();
|
||||
String[] maliciousUrls = {
|
||||
// URL length limit test
|
||||
"jdbc:mysql://localhost:3306/test?" + "a".repeat(2050) + "=value",
|
||||
// url format check
|
||||
"jdbca:mysql://localhost:3306/test?allowLoadLocalInfile=true",
|
||||
// backlist check
|
||||
"jdbc:mysql://localhost:3306/test?allowLoadLocalInfile=true",
|
||||
// universal detection of JDBC injection and deserialization attacks
|
||||
"jdbc:mysql://localhost:3306/test?jndi:ldap://duansg.com/exploit",
|
||||
// universal detection of bypass
|
||||
"jdbc:mysql://localhost:3306/test?param=create\\trigger",
|
||||
// database platform specific bypass detection
|
||||
"jdbc:mysql://localhost:3306/test?allow\\nload\\nlocal\\ninfile=true"
|
||||
|
||||
};
|
||||
|
||||
// Test malicious URLs - should throw exceptions
|
||||
for (String maliciousUrl : maliciousUrls) {
|
||||
JdbcProtocol jdbcProtocol = JdbcProtocol.builder()
|
||||
.host("localhost")
|
||||
.port("3306")
|
||||
.platform("mysql")
|
||||
.username("root")
|
||||
.password("root")
|
||||
.database("test")
|
||||
.url(maliciousUrl)
|
||||
.build();
|
||||
|
||||
assertThrows(Exception.class, () -> {
|
||||
try {
|
||||
Method constructMethod = JdbcCommonCollect.class.getDeclaredMethod("constructDatabaseUrl", JdbcProtocol.class, String.class, String.class);
|
||||
constructMethod.setAccessible(true);
|
||||
constructMethod.invoke(jdbcCollect, jdbcProtocol, "localhost", "3306");
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getCause();
|
||||
}
|
||||
}, "Malicious URL should be blocked: " + maliciousUrl);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+364
-9
@@ -17,15 +17,6 @@
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.http;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
@@ -34,6 +25,18 @@ import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* Test case for {@link HttpCollectImpl}
|
||||
*/
|
||||
@@ -186,4 +189,356 @@ class HttpCollectImplTest {
|
||||
assertEquals("0.0", secondRow.getColumns(2), "Second server CPU should be 0.0");
|
||||
assertEquals("0", secondRow.getColumns(3), "Second server memory should be 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseResponseByJsonPath() throws Exception {
|
||||
String jsonResponse = "{"
|
||||
+ " \"name\": \"jvm.memory.used\","
|
||||
+ " \"description\": \"The amount of used memory\","
|
||||
+ " \"baseUnit\": \"bytes\","
|
||||
+ " \"measurements\": ["
|
||||
+ " {"
|
||||
+ " \"statistic\": \"VALUE\","
|
||||
+ " \"value\": 90282296"
|
||||
+ " }"
|
||||
+ " ],"
|
||||
+ " \"availableTags\": ["
|
||||
+ " {"
|
||||
+ " \"tag\": \"area\","
|
||||
+ " \"values\": ["
|
||||
+ " \"heap\","
|
||||
+ " \"nonheap\""
|
||||
+ " ]"
|
||||
+ " },"
|
||||
+ " {"
|
||||
+ " \"tag\": \"id\","
|
||||
+ " \"values\": ["
|
||||
+ " \"G1 Survivor Space\","
|
||||
+ " \"G1 Eden Space\""
|
||||
+ " ]"
|
||||
+ " }"
|
||||
+ " ]"
|
||||
+ "}";
|
||||
|
||||
HttpProtocol http = HttpProtocol.builder()
|
||||
.parseType(DispatchConstants.PARSE_JSON_PATH)
|
||||
.parseScript("$.availableTags[?(@.tag == \"id\")].values[*]")
|
||||
.build();
|
||||
List<CollectRep.ValueRow> capturedRows = new ArrayList<>();
|
||||
CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() {
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
|
||||
capturedRows.add(valueRow);
|
||||
return super.addValueRow(valueRow);
|
||||
}
|
||||
};
|
||||
Method parseMethod = HttpCollectImpl.class.getDeclaredMethod(
|
||||
"parseResponseByJsonPath",
|
||||
String.class,
|
||||
List.class,
|
||||
HttpProtocol.class,
|
||||
CollectRep.MetricsData.Builder.class,
|
||||
Long.class);
|
||||
parseMethod.setAccessible(true);
|
||||
|
||||
// Call the method
|
||||
parseMethod.invoke(httpCollectImpl, jsonResponse, Lists.newArrayList("id"), http, builder, 100L);
|
||||
|
||||
// Verify the results
|
||||
assertEquals(2, capturedRows.size());
|
||||
CollectRep.ValueRow firstRow = capturedRows.get(0);
|
||||
assertEquals("G1 Survivor Space", firstRow.getColumns(0));
|
||||
CollectRep.ValueRow secondRow = capturedRows.get(1);
|
||||
assertEquals("G1 Eden Space", secondRow.getColumns(0));
|
||||
|
||||
// number
|
||||
String numberJson = "{"
|
||||
+ " \"name\": \"system.cpu.usage\","
|
||||
+ " \"description\": \"The \\\"recent cpu usage\\\" of the system the application is running in\","
|
||||
+ " \"measurements\": ["
|
||||
+ " {"
|
||||
+ " \"statistic\": \"VALUE\","
|
||||
+ " \"value\": 0.268751364291017"
|
||||
+ " }"
|
||||
+ " ],"
|
||||
+ " \"availableTags\": []"
|
||||
+ "}";
|
||||
http = HttpProtocol.builder()
|
||||
.parseType(DispatchConstants.PARSE_JSON_PATH)
|
||||
.parseScript("$.measurements[?(@.statistic == \"VALUE\")].value")
|
||||
.build();
|
||||
capturedRows.clear();
|
||||
builder = new CollectRep.MetricsData.Builder() {
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
|
||||
capturedRows.add(valueRow);
|
||||
return super.addValueRow(valueRow);
|
||||
}
|
||||
};
|
||||
parseMethod = HttpCollectImpl.class.getDeclaredMethod(
|
||||
"parseResponseByJsonPath",
|
||||
String.class,
|
||||
List.class,
|
||||
HttpProtocol.class,
|
||||
CollectRep.MetricsData.Builder.class,
|
||||
Long.class);
|
||||
parseMethod.setAccessible(true);
|
||||
|
||||
// Call the method
|
||||
parseMethod.invoke(httpCollectImpl, numberJson, Lists.newArrayList("usage"), http, builder, 100L);
|
||||
|
||||
// Verify the results
|
||||
assertEquals(1, capturedRows.size());
|
||||
firstRow = capturedRows.get(0);
|
||||
assertEquals("0.268751364291017", firstRow.getColumns(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePromQlLabelValue() throws Exception {
|
||||
// Create Prometheus format test data
|
||||
String prometheusData = """
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "vector",
|
||||
"result": [
|
||||
{
|
||||
"metric": {
|
||||
"__name__": "taos_cluster_info_first_ep",
|
||||
"cluster_id": "590779086215866783",
|
||||
"instance": "host.docker.internal:6043",
|
||||
"job": "tdengine",
|
||||
"value": "localhost:6030"
|
||||
},
|
||||
"value": [
|
||||
1756233177.048,
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}""";
|
||||
List<CollectRep.ValueRow> capturedRows = new ArrayList<>();
|
||||
CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() {
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
|
||||
capturedRows.add(valueRow);
|
||||
return super.addValueRow(valueRow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMetrics() {
|
||||
return "jvm_memory_used_bytes";
|
||||
}
|
||||
};
|
||||
Method parseMethod = HttpCollectImpl.class.getDeclaredMethod(
|
||||
"parseResponseByPromQl",
|
||||
String.class,
|
||||
List.class,
|
||||
HttpProtocol.class,
|
||||
CollectRep.MetricsData.Builder.class);
|
||||
parseMethod.setAccessible(true);
|
||||
|
||||
parseMethod.invoke(httpCollectImpl, prometheusData, Lists.newArrayList("cluster_id", "value"), new HttpProtocol(), builder);
|
||||
|
||||
// Verify the results
|
||||
assertEquals(1, capturedRows.size());
|
||||
CollectRep.ValueRow firstRow = capturedRows.get(0);
|
||||
assertEquals("590779086215866783", firstRow.getColumns(0));
|
||||
assertEquals("localhost:6030", firstRow.getColumns(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePromQlMetricValue() throws Exception {
|
||||
// Create Prometheus format test data
|
||||
String prometheusData = """
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "vector",
|
||||
"result": [
|
||||
{
|
||||
"metric": {
|
||||
"__name__": "taos_cluster_info_first_ep",
|
||||
"cluster_id": "590779086215866783",
|
||||
"instance": "host.docker.internal:6043",
|
||||
"job": "tdengine",
|
||||
"value": "localhost:6030"
|
||||
},
|
||||
"value": [
|
||||
1756233177.048,
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}""";
|
||||
List<CollectRep.ValueRow> capturedRows = new ArrayList<>();
|
||||
CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() {
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
|
||||
capturedRows.add(valueRow);
|
||||
return super.addValueRow(valueRow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMetrics() {
|
||||
return "jvm_memory_used_bytes";
|
||||
}
|
||||
};
|
||||
Method parseMethod = HttpCollectImpl.class.getDeclaredMethod(
|
||||
"parseResponseByPromQl",
|
||||
String.class,
|
||||
List.class,
|
||||
HttpProtocol.class,
|
||||
CollectRep.MetricsData.Builder.class);
|
||||
parseMethod.setAccessible(true);
|
||||
|
||||
parseMethod.invoke(httpCollectImpl, prometheusData, Lists.newArrayList("cluster_id", "value", "metric_value"), new HttpProtocol(), builder);
|
||||
|
||||
// Verify the results
|
||||
assertEquals(1, capturedRows.size());
|
||||
CollectRep.ValueRow firstRow = capturedRows.get(0);
|
||||
assertEquals("590779086215866783", firstRow.getColumns(0));
|
||||
assertEquals("localhost:6030", firstRow.getColumns(1));
|
||||
assertEquals("1", firstRow.getColumns(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePromQlValue() throws Exception {
|
||||
// Create Prometheus format test data
|
||||
String prometheusData = """
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "vector",
|
||||
"result": [
|
||||
{
|
||||
"metric": {
|
||||
"__name__": "taos_cluster_info_first_ep",
|
||||
"cluster_id": "590779086215866783",
|
||||
"instance": "host.docker.internal:6043",
|
||||
"job": "tdengine"
|
||||
},
|
||||
"value": [
|
||||
1756233177.048,
|
||||
"1"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}""";
|
||||
List<CollectRep.ValueRow> capturedRows = new ArrayList<>();
|
||||
CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() {
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
|
||||
capturedRows.add(valueRow);
|
||||
return super.addValueRow(valueRow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMetrics() {
|
||||
return "jvm_memory_used_bytes";
|
||||
}
|
||||
};
|
||||
Method parseMethod = HttpCollectImpl.class.getDeclaredMethod(
|
||||
"parseResponseByPromQl",
|
||||
String.class,
|
||||
List.class,
|
||||
HttpProtocol.class,
|
||||
CollectRep.MetricsData.Builder.class);
|
||||
parseMethod.setAccessible(true);
|
||||
|
||||
parseMethod.invoke(httpCollectImpl, prometheusData, Lists.newArrayList("cluster_id", "value"), new HttpProtocol(), builder);
|
||||
|
||||
// Verify the results
|
||||
assertEquals(1, capturedRows.size());
|
||||
CollectRep.ValueRow firstRow = capturedRows.get(0);
|
||||
assertEquals("590779086215866783", firstRow.getColumns(0));
|
||||
assertEquals("1", firstRow.getColumns(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePrometheusLabelValue() throws Exception {
|
||||
// Create Prometheus format test data
|
||||
String prometheusData = """
|
||||
# HELP jvm_memory_used_bytes The amount of used memory in bytes
|
||||
# TYPE jvm_memory_used_bytes gauge
|
||||
jvm_memory_used_bytes{area="heap",value="G1 Survivor Space"} 1048576
|
||||
""";
|
||||
InputStream inputStream = new ByteArrayInputStream(prometheusData.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
List<CollectRep.ValueRow> capturedRows = new ArrayList<>();
|
||||
CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() {
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
|
||||
capturedRows.add(valueRow);
|
||||
return super.addValueRow(valueRow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMetrics() {
|
||||
return "jvm_memory_used_bytes";
|
||||
}
|
||||
};
|
||||
Method parseMethod = HttpCollectImpl.class.getDeclaredMethod(
|
||||
"parseResponseByPrometheusExporter",
|
||||
InputStream.class,
|
||||
List.class,
|
||||
CollectRep.MetricsData.Builder.class);
|
||||
parseMethod.setAccessible(true);
|
||||
|
||||
parseMethod.invoke(httpCollectImpl, inputStream, Lists.newArrayList("area", "value"), builder);
|
||||
|
||||
// Verify the results
|
||||
assertEquals(1, capturedRows.size());
|
||||
CollectRep.ValueRow firstRow = capturedRows.get(0);
|
||||
assertEquals("heap", firstRow.getColumns(0));
|
||||
assertEquals("G1 Survivor Space", firstRow.getColumns(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParsePrometheus() throws Exception {
|
||||
// Create Prometheus format test data
|
||||
String prometheusData = """
|
||||
# HELP jvm_memory_used_bytes The amount of used memory
|
||||
# TYPE jvm_memory_used_bytes gauge
|
||||
jvm_memory_used_bytes{area="heap",id="G1 Eden Space"} 1.63577856E8
|
||||
jvm_memory_used_bytes{area="heap",id="G1 Old Gen"} 2.7874304E7
|
||||
jvm_memory_used_bytes{area="heap",id="G1 Survivor Space"} 512032.0
|
||||
jvm_memory_used_bytes{area="nonheap",id="CodeCache"} 1.460288E7
|
||||
jvm_memory_used_bytes{area="nonheap",id="Compressed Class Space"} 5844504.0
|
||||
jvm_memory_used_bytes{area="nonheap",id="Metaspace"} 4.1576344E7
|
||||
""";
|
||||
InputStream inputStream = new ByteArrayInputStream(prometheusData.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
List<CollectRep.ValueRow> capturedRows = new ArrayList<>();
|
||||
CollectRep.MetricsData.Builder builder = new CollectRep.MetricsData.Builder() {
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder addValueRow(CollectRep.ValueRow valueRow) {
|
||||
capturedRows.add(valueRow);
|
||||
return super.addValueRow(valueRow);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMetrics() {
|
||||
return "jvm_memory_used_bytes";
|
||||
}
|
||||
};
|
||||
Method parseMethod = HttpCollectImpl.class.getDeclaredMethod(
|
||||
"parseResponseByPrometheusExporter",
|
||||
InputStream.class,
|
||||
List.class,
|
||||
CollectRep.MetricsData.Builder.class);
|
||||
parseMethod.setAccessible(true);
|
||||
|
||||
parseMethod.invoke(httpCollectImpl, inputStream, Lists.newArrayList("area", "id"), builder);
|
||||
|
||||
// Verify the results
|
||||
assertEquals(6, capturedRows.size());
|
||||
CollectRep.ValueRow firstRow = capturedRows.get(0);
|
||||
assertEquals("heap", firstRow.getColumns(0));
|
||||
assertEquals("G1 Eden Space", firstRow.getColumns(1));
|
||||
capturedRows.forEach(t -> assertEquals(2, t.getColumnsList().size()));
|
||||
}
|
||||
}
|
||||
|
||||
+61
-78
@@ -17,108 +17,91 @@
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.mqtt;
|
||||
|
||||
import com.hivemq.client.mqtt.MqttVersion;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.MqttProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* Test case for {@link MqttCollectImpl}
|
||||
*/
|
||||
public class MqttCollectTest {
|
||||
class MqttCollectTest {
|
||||
private MqttCollectImpl mqttCollect;
|
||||
private Metrics metrics;
|
||||
private CollectRep.MetricsData.Builder builder;
|
||||
private MqttProtocol.MqttProtocolBuilder mqttBuilder;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
void setup() {
|
||||
mqttCollect = new MqttCollectImpl();
|
||||
MqttProtocol mqtt = MqttProtocol.builder().build();
|
||||
metrics = Metrics.builder()
|
||||
.mqtt(mqtt)
|
||||
.build();
|
||||
builder = CollectRep.MetricsData.newBuilder();
|
||||
metrics = new Metrics();
|
||||
|
||||
// Initialize base MQTT parameters for test cases
|
||||
mqttBuilder = MqttProtocol.builder()
|
||||
.host("example.com")
|
||||
.port("1883")
|
||||
.protocol("mqtt")
|
||||
.timeout("5000")
|
||||
.keepalive("60");
|
||||
}
|
||||
|
||||
// Region: preCheck validation tests
|
||||
|
||||
@Test
|
||||
// Verify preCheck throws exception when host is missing
|
||||
void preCheckShouldThrowWhenHostMissing() {
|
||||
metrics.setMqtt(mqttBuilder.host("").build());
|
||||
assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preCheck() {
|
||||
// host is empty
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
mqttCollect.preCheck(metrics);
|
||||
});
|
||||
|
||||
// port is empty
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
MqttProtocol mqtt = MqttProtocol.builder().build();
|
||||
mqtt.setHost("example.com");
|
||||
metrics.setMqtt(mqtt);
|
||||
mqttCollect.preCheck(metrics);
|
||||
});
|
||||
|
||||
// protocol version is empty
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
MqttProtocol mqtt = MqttProtocol.builder().build();
|
||||
mqtt.setHost("example.com");
|
||||
mqtt.setPort("1883");
|
||||
metrics.setMqtt(mqtt);
|
||||
mqttCollect.preCheck(metrics);
|
||||
});
|
||||
|
||||
// everything is ok
|
||||
assertDoesNotThrow(() -> {
|
||||
MqttProtocol mqtt = MqttProtocol.builder().build();
|
||||
mqtt.setHost("example.com");
|
||||
mqtt.setPort("1883");
|
||||
metrics.setMqtt(mqtt);
|
||||
mqtt.setProtocolVersion("3.1.1");
|
||||
mqttCollect.preCheck(metrics);
|
||||
});
|
||||
// Verify preCheck throws exception when port is missing
|
||||
void preCheckShouldThrowWhenPortMissing() {
|
||||
metrics.setMqtt(mqttBuilder.port("").build());
|
||||
assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics));
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportProtocol() {
|
||||
Assertions.assertEquals(DispatchConstants.PROTOCOL_MQTT, mqttCollect.supportProtocol());
|
||||
// Verify preCheck throws exception when MQTTS mutual auth is enabled but CA cert is missing
|
||||
void preCheckShouldThrowWhenMqttsMutualAuthMissingCerts() {
|
||||
metrics.setMqtt(mqttBuilder
|
||||
.protocol("mqtts")
|
||||
.enableMutualAuth("true")
|
||||
.caCert("")
|
||||
.clientCert("client.crt")
|
||||
.clientKey("client.key")
|
||||
.build());
|
||||
assertThrows(IllegalArgumentException.class, () -> mqttCollect.preCheck(metrics));
|
||||
}
|
||||
|
||||
@Test
|
||||
void collect() {
|
||||
// with version 3.1.1
|
||||
assertDoesNotThrow(() -> {
|
||||
MqttProtocol mqtt = MqttProtocol.builder().build();
|
||||
mqtt.setHost("example.com");
|
||||
mqtt.setPort("1883");
|
||||
mqtt.setClientId("clientid");
|
||||
mqtt.setTimeout("1");
|
||||
mqtt.setProtocolVersion(MqttVersion.MQTT_3_1_1.name());
|
||||
// Verify preCheck succeeds with valid standard MQTT parameters
|
||||
void preCheckShouldSucceedWithValidMqttParams() {
|
||||
metrics.setMqtt(mqttBuilder.build());
|
||||
assertDoesNotThrow(() -> mqttCollect.preCheck(metrics));
|
||||
}
|
||||
|
||||
metrics.setMqtt(mqtt);
|
||||
metrics.setAliasFields(new ArrayList<>());
|
||||
@Test
|
||||
// Verify preCheck succeeds with valid MQTTS parameters including mutual authentication
|
||||
void preCheckShouldSucceedWithValidMqttsMutualAuth() {
|
||||
metrics.setMqtt(mqttBuilder
|
||||
.protocol("mqtts")
|
||||
.enableMutualAuth("true")
|
||||
.caCert("ca.pem")
|
||||
.clientCert("client.crt")
|
||||
.clientKey("client.key")
|
||||
.build());
|
||||
assertDoesNotThrow(() -> mqttCollect.preCheck(metrics));
|
||||
}
|
||||
// End region
|
||||
|
||||
mqttCollect.collect(builder, metrics);
|
||||
});
|
||||
|
||||
|
||||
assertDoesNotThrow(() -> {
|
||||
MqttProtocol mqtt = MqttProtocol.builder().build();
|
||||
mqtt.setHost("example.com");
|
||||
mqtt.setPort("1883");
|
||||
mqtt.setClientId("clientid");
|
||||
mqtt.setTimeout("1");
|
||||
mqtt.setProtocolVersion(MqttVersion.MQTT_5_0.name());
|
||||
|
||||
metrics.setMqtt(mqtt);
|
||||
metrics.setAliasFields(new ArrayList<>());
|
||||
|
||||
mqttCollect.collect(builder, metrics);
|
||||
});
|
||||
@Test
|
||||
// Verify supportProtocol method returns correct MQTT constant
|
||||
void supportProtocolShouldReturnMqttConstant() {
|
||||
assertEquals(DispatchConstants.PROTOCOL_MQTT, mqttCollect.supportProtocol());
|
||||
}
|
||||
}
|
||||
|
||||
+354
@@ -43,6 +43,16 @@ class OnlineParserTest {
|
||||
assertNotNull(metricFamilyMap);
|
||||
}
|
||||
|
||||
@Disabled // because unless you have already saved the locally tested files
|
||||
@Test
|
||||
void parseTestFile() throws Exception {
|
||||
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("test_file.txt");
|
||||
assertNotNull(inputStream, "Failed to load test_file.txt resource");
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void parseMetrics2() throws Exception {
|
||||
String str = """
|
||||
@@ -105,4 +115,348 @@ class OnlineParserTest {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithCrLf() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\r\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\r\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\r\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\r\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\r\n";
|
||||
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithLf() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0 1234567890\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139 1234567890\n";
|
||||
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithoutFinalNewline() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\r\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\r\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 1.0\n"
|
||||
+ "jvm_gc_pause_seconds_sum{action=\"end of major GC\",cause=\"Metadata GC Threshold\",} 0.139";
|
||||
|
||||
inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithEmptyLabelsAndCrLf() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\r\n"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\r\n"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithEmptyLabelsAndLf() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\n"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithMixedLineEndings() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\n"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139\r\n";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithLfCrLineEnding() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n\r"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n\r"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\n\r"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139\n\r";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(2, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithMixedLineEndingsIncludingLfCr() throws Exception {
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "jvm_gc_pause_seconds_count 1.0\n\r"
|
||||
+ "jvm_gc_pause_seconds_sum{} 0.139\r\n"
|
||||
+ "jvm_gc_pause_seconds_max{} 0.139\n";
|
||||
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(3, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_count");
|
||||
assertEquals("jvm_gc_pause_seconds_count", metricFamily.getName());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily1 = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily1.getName());
|
||||
assertEquals(0.139, metricFamily1.getMetricList().get(0).getValue());
|
||||
|
||||
MetricFamily metricFamily2 = metricFamilyMap.get("jvm_gc_pause_seconds_max");
|
||||
assertEquals("jvm_gc_pause_seconds_max", metricFamily2.getName());
|
||||
assertEquals(0.139, metricFamily2.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricsWithOnlyLfCr() throws Exception {
|
||||
String str = "jvm_gc_pause_seconds_sum 42.0\n\r";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(1, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("jvm_gc_pause_seconds_sum");
|
||||
assertEquals("jvm_gc_pause_seconds_sum", metricFamily.getName());
|
||||
assertEquals(42.0, metricFamily.getMetricList().get(0).getValue());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testParseMetricEscape1() throws Exception {
|
||||
// test escape '\\'
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "windows_service_info{display_name=\"\\\\Application Layer \nGateway Service\",name=\"alg\\\\\",process_id=\"0\",run_as=\"NT AUTHORITY\\\\LocalService\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(1, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
|
||||
assertEquals("windows_service_info", metricFamily.getName());
|
||||
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
|
||||
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
|
||||
assertEquals("\\Application Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
|
||||
|
||||
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
|
||||
assertEquals("alg\\", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
|
||||
|
||||
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
|
||||
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
|
||||
|
||||
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
|
||||
assertEquals("NT AUTHORITY\\LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricEscape2() throws Exception {
|
||||
// test escape '\"'
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "windows_service_info{display_name=\"\\\"Application Layer \\\"Gateway Service\",name=\"alg\\\"\",process_id=\"0\",run_as=\"NT AUTHORITY\\\"LocalService\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(1, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
|
||||
assertEquals("windows_service_info", metricFamily.getName());
|
||||
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
|
||||
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
|
||||
assertEquals("\"Application Layer \"Gateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
|
||||
|
||||
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
|
||||
assertEquals("alg\"", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
|
||||
|
||||
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
|
||||
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
|
||||
|
||||
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
|
||||
assertEquals("NT AUTHORITY\"LocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseMetricEscape3() throws Exception {
|
||||
// test escape '\n'
|
||||
String str = "# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\r\n"
|
||||
+ "# TYPE go_gc_duration_seconds summary\n"
|
||||
+ "windows_service_info{display_name=\"\nApplication Layer \nGateway Service\",name=\"alg\n\",process_id=\"0\",run_as=\"NT AUTHORITY\nLocalService\"} 1\n";
|
||||
InputStream inputStream = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
Map<String, MetricFamily> metricFamilyMap = OnlineParser.parseMetrics(inputStream);
|
||||
|
||||
assertNotNull(metricFamilyMap);
|
||||
assertEquals(1, metricFamilyMap.values().size());
|
||||
|
||||
MetricFamily metricFamily = metricFamilyMap.get("windows_service_info");
|
||||
assertEquals("windows_service_info", metricFamily.getName());
|
||||
assertNotNull(metricFamily.getMetricList().get(0).getLabels());
|
||||
assertEquals(4, metricFamily.getMetricList().get(0).getLabels().size());
|
||||
assertEquals(1.0, metricFamily.getMetricList().get(0).getValue());
|
||||
|
||||
assertEquals("display_name", metricFamily.getMetricList().get(0).getLabels().get(0).getName());
|
||||
assertEquals("\nApplication Layer \nGateway Service", metricFamily.getMetricList().get(0).getLabels().get(0).getValue());
|
||||
|
||||
assertEquals("name", metricFamily.getMetricList().get(0).getLabels().get(1).getName());
|
||||
assertEquals("alg\n", metricFamily.getMetricList().get(0).getLabels().get(1).getValue());
|
||||
|
||||
assertEquals("process_id", metricFamily.getMetricList().get(0).getLabels().get(2).getName());
|
||||
assertEquals("0", metricFamily.getMetricList().get(0).getLabels().get(2).getValue());
|
||||
|
||||
assertEquals("run_as", metricFamily.getMetricList().get(0).getLabels().get(3).getName());
|
||||
assertEquals("NT AUTHORITY\nLocalService", metricFamily.getMetricList().get(0).getLabels().get(3).getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,14 +70,11 @@
|
||||
<version>${hertzbeat.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<!-- spring -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
@@ -87,7 +84,15 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- metrics -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -340,4 +345,4 @@
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
+43
-12
@@ -23,6 +23,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
|
||||
import org.apache.hertzbeat.collector.metrics.HertzBeatMetricsCollector;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
import org.apache.hertzbeat.collector.timer.TimerDispatch;
|
||||
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
|
||||
@@ -33,6 +34,7 @@ import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -90,6 +92,9 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
|
||||
private final String collectorIdentity;
|
||||
|
||||
@Autowired
|
||||
private HertzBeatMetricsCollector metricsCollector;
|
||||
|
||||
public CommonDispatcher(MetricsCollectorQueue jobRequestQueue,
|
||||
TimerDispatch timerDispatch,
|
||||
CommonDataQueue commonDataQueue,
|
||||
@@ -153,12 +158,23 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
for (Map.Entry<String, MetricsTime> entry : metricsTimeoutMonitorMap.entrySet()) {
|
||||
MetricsTime metricsTime = entry.getValue();
|
||||
if (metricsTime.getStartTime() < deadline) {
|
||||
// Metrics collection timeout
|
||||
// Metrics collection timeout
|
||||
MetricsTime removedMetricsTime = metricsTimeoutMonitorMap.remove(entry.getKey());
|
||||
if (removedMetricsTime == null) {
|
||||
continue;
|
||||
}
|
||||
WheelTimerTask timerJob = (WheelTimerTask) metricsTime.getTimeout().task();
|
||||
Job job = timerJob.getJob();
|
||||
// timeout metrics
|
||||
if (metricsCollector != null) {
|
||||
long duration = System.currentTimeMillis() - removedMetricsTime.getStartTime();
|
||||
metricsCollector.recordCollectMetrics(job, duration, "timeout");
|
||||
}
|
||||
|
||||
CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder()
|
||||
.setId(timerJob.getJob().getMonitorId())
|
||||
.setTenantId(timerJob.getJob().getTenantId())
|
||||
.setApp(timerJob.getJob().getApp())
|
||||
.setId(job.getMonitorId())
|
||||
.setTenantId(job.getTenantId())
|
||||
.setApp(job.getApp())
|
||||
.setMetrics(metricsTime.getMetrics().getName())
|
||||
.setPriority(metricsTime.getMetrics().getPriority())
|
||||
.setTime(System.currentTimeMillis())
|
||||
@@ -167,7 +183,6 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
if (metricsData.getPriority() == 0) {
|
||||
dispatchCollectData(metricsTime.timeout, metricsTime.getMetrics(), metricsData);
|
||||
}
|
||||
metricsTimeoutMonitorMap.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -177,7 +192,7 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
|
||||
@Override
|
||||
public void dispatchMetricsTask(Timeout timeout) {
|
||||
// Divide the collection task of a single application into corresponding collection tasks of the metrics according to the metrics under it.
|
||||
// Divide the collection task of a single application into corresponding collection tasks of the metrics under it.
|
||||
// Put each collect task into the thread pool for scheduling
|
||||
WheelTimerTask timerTask = (WheelTimerTask) timeout.task();
|
||||
Job job = timerTask.getJob();
|
||||
@@ -201,16 +216,27 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) {
|
||||
WheelTimerTask timerJob = (WheelTimerTask) timeout.task();
|
||||
Job job = timerJob.getJob();
|
||||
String monitorKey;
|
||||
if (metrics.isHasSubTask()) {
|
||||
monitorKey = job.getId() + "-" + metrics.getName() + "-sub-" + metrics.getSubTaskId();
|
||||
} else {
|
||||
monitorKey = job.getId() + "-" + metrics.getName();
|
||||
}
|
||||
MetricsTime metricsTime = metricsTimeoutMonitorMap.remove(monitorKey);
|
||||
|
||||
// job completed metrics
|
||||
if (metricsTime != null && metricsCollector != null) {
|
||||
long duration = System.currentTimeMillis() - metricsTime.getStartTime();
|
||||
String status = metricsData.getCode() == CollectRep.Code.SUCCESS ? "success" : "fail";
|
||||
metricsCollector.recordCollectMetrics(job, duration, status);
|
||||
}
|
||||
if (metrics.isHasSubTask()) {
|
||||
metricsTimeoutMonitorMap.remove(job.getId() + "-" + metrics.getName() + "-sub-" + metrics.getSubTaskId());
|
||||
boolean isLastTask = metrics.consumeSubTaskResponse(metricsData);
|
||||
if (isLastTask) {
|
||||
metricsData = metrics.getSubTaskDataRef().get().build();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
metricsTimeoutMonitorMap.remove(job.getId() + "-" + metrics.getName());
|
||||
}
|
||||
Set<Metrics> metricsSet = job.getNextCollectMetrics(metrics, false);
|
||||
if (job.isCyclic()) {
|
||||
@@ -322,7 +348,13 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
public void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList) {
|
||||
WheelTimerTask timerJob = (WheelTimerTask) timeout.task();
|
||||
Job job = timerJob.getJob();
|
||||
metricsTimeoutMonitorMap.remove(String.valueOf(job.getId()));
|
||||
MetricsTime metricsTime = metricsTimeoutMonitorMap.remove(String.valueOf(job.getId()));
|
||||
if (metricsTime != null && metricsCollector != null) {
|
||||
long duration = System.currentTimeMillis() - metricsTime.getStartTime();
|
||||
// For a list, we consider it a success if at least one item is successful.
|
||||
boolean isSuccess = metricsDataList.stream().anyMatch(item -> item.getCode() == CollectRep.Code.SUCCESS);
|
||||
metricsCollector.recordCollectMetrics(job, duration, isSuccess ? "success" : "fail");
|
||||
}
|
||||
if (job.isCyclic()) {
|
||||
// The collection and execution of all task of this job are completed.
|
||||
// The periodic task pushes the task to the time wheel again.
|
||||
@@ -340,7 +372,6 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
// and the result listener is notified of the combination of all metrics data
|
||||
timerDispatch.responseSyncJobData(job.getId(), metricsDataList);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -354,4 +385,4 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
private Metrics metrics;
|
||||
private Timeout timeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -422,6 +422,9 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
|
||||
*/
|
||||
private Object[] transformCal(String cal, Map<String, String> fieldAliasMap) {
|
||||
int splitIndex = cal.indexOf("=");
|
||||
if (splitIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
String field = cal.substring(0, splitIndex).trim();
|
||||
String expressionStr = cal.substring(splitIndex + 1).trim().replace("\\#", "#");
|
||||
JexlExpression expression;
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service for managing and recording Micrometer metrics.
|
||||
* This service uses Micrometer which integrates natively with Spring Boot Actuator.
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class HertzBeatMetricsCollector {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
public HertzBeatMetricsCollector(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
log.info("MetricsService initialized with MeterRegistry: {}", meterRegistry.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the metrics for a completed collection sub-task.
|
||||
*
|
||||
* @param job The parent job containing monitor info.
|
||||
* @param durationMillis The duration of the collection task in milliseconds.
|
||||
* @param status The final status of the collection ("success", "fail", "timeout").
|
||||
*/
|
||||
public void recordCollectMetrics(Job job, long durationMillis, String status) {
|
||||
if (job == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> metadata = job.getMetadata();
|
||||
String monitorName = metadata != null ? metadata.get("instancename") : "unknown";
|
||||
String monitorTarget = metadata != null ? metadata.get("instancehost") : "unknown";
|
||||
|
||||
// Record collection count
|
||||
Counter.builder("hertzbeat.collect.total")
|
||||
.description("The total number of collection tasks executed")
|
||||
.tag("status", status)
|
||||
.tag("monitor_type", job.getApp())
|
||||
.tag("monitor_id", String.valueOf(job.getMonitorId()))
|
||||
.tag("monitor_name", monitorName)
|
||||
.tag("monitor_target", monitorTarget)
|
||||
.register(meterRegistry)
|
||||
.increment();
|
||||
|
||||
// Record collection duration
|
||||
Timer.builder("hertzbeat.collect.duration")
|
||||
.description("The duration of collection task executions")
|
||||
.tag("status", status)
|
||||
.tag("monitor_type", job.getApp())
|
||||
.tag("monitor_id", String.valueOf(job.getMonitorId()))
|
||||
.tag("monitor_name", monitorName)
|
||||
.tag("monitor_target", monitorTarget)
|
||||
.register(meterRegistry)
|
||||
.record(Duration.ofMillis(durationMillis));
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Recorded metrics for monitor [{}] ({}): status={}, duration={}ms",
|
||||
monitorName, job.getMonitorId(), status, durationMillis);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -30,7 +30,23 @@ spring:
|
||||
# need to disable spring boot mongodb auto config, or default mongodb connection tried and failed...
|
||||
autoconfigure:
|
||||
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include:
|
||||
- 'prometheus'
|
||||
endpoint:
|
||||
prometheus:
|
||||
access: read_only
|
||||
metrics:
|
||||
tags:
|
||||
application: ${spring.application.name}
|
||||
environment: ${spring.profiles.active}
|
||||
prometheus:
|
||||
metrics:
|
||||
export:
|
||||
enabled: true
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
@@ -39,7 +55,7 @@ spring:
|
||||
|
||||
collector:
|
||||
info:
|
||||
version: ${COLLECTOR_VERSION:1.7.2}
|
||||
version: ${COLLECTOR_VERSION:1.7.3}
|
||||
ip: ${COLLECTOR_IP:}
|
||||
dispatch:
|
||||
entrance:
|
||||
|
||||
+13
@@ -22,8 +22,11 @@ import io.netty.channel.ChannelHandlerContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.timer.TimerDispatch;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.dto.ServerInfo;
|
||||
import org.apache.hertzbeat.common.entity.message.ClusterMsg;
|
||||
import org.apache.hertzbeat.common.support.SpringContextHolder;
|
||||
import org.apache.hertzbeat.common.util.AesUtil;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.hertzbeat.remoting.netty.NettyRemotingProcessor;
|
||||
|
||||
/**
|
||||
@@ -40,6 +43,16 @@ public class GoOnlineProcessor implements NettyRemotingProcessor {
|
||||
if (this.timerDispatch == null) {
|
||||
this.timerDispatch = SpringContextHolder.getBean(TimerDispatch.class);
|
||||
}
|
||||
if (message.getMsg().isEmpty()) {
|
||||
log.warn("The message that server response to collector is empty, please upgrade server");
|
||||
} else {
|
||||
ServerInfo serverInfo = JsonUtil.fromJson(message.getMsg().toStringUtf8(), ServerInfo.class);
|
||||
if (serverInfo == null || serverInfo.getAesSecret() == null) {
|
||||
log.warn("The message that server response to collector has not secret empty, please check");
|
||||
} else {
|
||||
AesUtil.setDefaultSecretKey(serverInfo.getAesSecret());
|
||||
}
|
||||
}
|
||||
timerDispatch.goOnline();
|
||||
log.info("receive online message and handle success");
|
||||
return ClusterMsg.Message.newBuilder()
|
||||
|
||||
+10
-1
@@ -171,7 +171,16 @@ public final class CollectUtil {
|
||||
* assert prom field
|
||||
*/
|
||||
public static Boolean assertPromRequireField(String aliasField) {
|
||||
return CommonConstants.PROM_TIME.equals(aliasField) || CommonConstants.PROM_VALUE.equals(aliasField);
|
||||
return CommonConstants.PROM_TIME.equals(aliasField)
|
||||
|| CommonConstants.PROM_VALUE.equals(aliasField)
|
||||
|| CommonConstants.PROM_METRIC_VALUE.equals(aliasField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if "value" field should be treated as a label instead of special field
|
||||
*/
|
||||
public static Boolean isValueAsLabel(String aliasField, JsonObject metric) {
|
||||
return CommonConstants.PROM_VALUE.equals(aliasField) && null != metric && metric.has(aliasField);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -83,13 +83,10 @@
|
||||
<groupId>org.apache.hertzbeat</groupId>
|
||||
<artifactId>hertzbeat-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>apache-${project.artifactId}-${hzb.version}</finalName>
|
||||
</build>
|
||||
|
||||
|
||||
|
||||
</project>
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.apache.hertzbeat.common.config;
|
||||
|
||||
import org.apache.hertzbeat.common.constants.ConfigConstants;
|
||||
import org.apache.hertzbeat.common.constants.SignConstants;
|
||||
import org.apache.hertzbeat.common.util.AesUtil;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
@@ -34,10 +33,4 @@ import org.springframework.context.annotation.ComponentScan;
|
||||
+ ConfigConstants.FunctionModuleConstants.COMMON)
|
||||
@EnableConfigurationProperties(CommonProperties.class)
|
||||
public class CommonConfig {
|
||||
|
||||
public CommonConfig(CommonProperties commonProperties) {
|
||||
if (commonProperties != null && commonProperties.getSecret() != null) {
|
||||
AesUtil.setDefaultSecretKey(commonProperties.getSecret());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -87,6 +87,11 @@ public interface CommonConstants {
|
||||
*/
|
||||
String LABEL_INSTANCE = "instance";
|
||||
|
||||
/**
|
||||
* label key: defineid
|
||||
*/
|
||||
String LABEL_DEFINE_ID = "defineid";
|
||||
|
||||
/**
|
||||
* label key: alert name
|
||||
*/
|
||||
@@ -192,6 +197,11 @@ public interface CommonConstants {
|
||||
*/
|
||||
String PROM_VALUE = "value";
|
||||
|
||||
/**
|
||||
* prometheus metric value alias
|
||||
*/
|
||||
String PROM_METRIC_VALUE = "metric_value";
|
||||
|
||||
/**
|
||||
* Parameter Type Number
|
||||
*/
|
||||
@@ -367,4 +377,9 @@ public interface CommonConstants {
|
||||
* status page incident state resolved
|
||||
*/
|
||||
byte MONITOR_TYPE_DISCOVERY_AUTO_CREATE = 2;
|
||||
|
||||
/**
|
||||
* JEXL custom function `json`
|
||||
*/
|
||||
String JEXL_CUSTOM_JSON_FUNCTION = "json";
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user