mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
132
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99ccf6cd48 | ||
|
|
986e833798 | ||
|
|
13b87b6c51 | ||
|
|
292f03045b | ||
|
|
2746615f63 | ||
|
|
195e7e95fa | ||
|
|
d0b3c2dba2 | ||
|
|
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 |
@@ -6,6 +6,7 @@
|
||||
"files": [
|
||||
"README.md",
|
||||
"README_CN.md",
|
||||
"README_JP.md"
|
||||
"home/src/pages/team/index.jsx"
|
||||
],
|
||||
"imageSize": 100,
|
||||
@@ -2504,6 +2505,24 @@
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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
|
||||
|
||||
@@ -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
|
||||
|
||||
+8
-6
@@ -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,7 +28,7 @@
|
||||
|
||||
## 🎡 <font color="green">介绍</font>
|
||||
|
||||
[Apache HertzBeat](https://github.com/apache/hertzbeat) (incubating)是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
|
||||
[Apache HertzBeat™](https://github.com/apache/hertzbeat) 是一个易用友好的开源实时监控告警系统,无需 Agent,高性能集群,兼容 Prometheus,提供强大的自定义监控和状态页构建能力。
|
||||
|
||||
### 特点
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
- 将 `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,85 @@
|
||||
<?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-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>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>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 java.util.List;
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
}
|
||||
+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.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.support.SpringContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.config;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Configuration class for Large Language Model (LLM) settings.
|
||||
*/
|
||||
|
||||
@Configuration
|
||||
public class LlmConfig {
|
||||
@Bean
|
||||
public ChatClient openAiChatClient(OpenAiChatModel chatModel) {
|
||||
return ChatClient.create(chatModel);
|
||||
}
|
||||
|
||||
}
|
||||
+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();
|
||||
}
|
||||
}
|
||||
+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 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_MONITORING_PROMPT = """
|
||||
You are an AI assistant specialized in monitoring infrastructure and applications with HertzBeat.
|
||||
Your role is to help users manage and analyze their monitoring data using the available tools.
|
||||
You have access to the following HertzBeat monitoring tools:
|
||||
- list_monitors: Query monitor information with flexible filtering and pagination
|
||||
- add_monitor: Add a new monitor to the system
|
||||
When users ask questions about their monitoring setup or data, identify which tool would be most helpful
|
||||
and use it to provide relevant information. Always provide clear explanations of the monitoring data and
|
||||
suggest next steps or insights based on the results.
|
||||
For monitoring-related queries:
|
||||
1. If users want to see their monitors, use list_monitors with appropriate filters
|
||||
2. If users want to add a new monitor, use add_monitor with the necessary details
|
||||
3. If the monitoring information shows potential issues, highlight them and suggest troubleshooting steps
|
||||
For parameters that accept specific values:
|
||||
- Monitor status values: 0 (no monitor), 1 (usable), 2 (disabled), 9 (all)
|
||||
- Sort fields typically include: name, host, app, gmtCreate
|
||||
- Sort order should be 'asc' or 'desc'
|
||||
Keep responses focused on monitoring topics and HertzBeat capabilities.
|
||||
If you're unsure about specific monitoring details, ask clarifying questions before using the tools.
|
||||
""";
|
||||
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
import org.apache.hertzbeat.ai.agent.service.ChatClientProviderService;
|
||||
|
||||
|
||||
/**
|
||||
* Controller class for handling chat-related HTTP requests.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/chat")
|
||||
public class ChatController {
|
||||
|
||||
private final ChatClientProviderService chatClientProviderService;
|
||||
|
||||
@Autowired
|
||||
public ChatController(@Qualifier("openAiChatClient") ChatClient openAiChatClient,
|
||||
ChatClientProviderService chatClientProviderService) {
|
||||
this.chatClientProviderService = chatClientProviderService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message and get a streaming response
|
||||
*
|
||||
* @param context The chat request context containing message and optional
|
||||
* conversationId
|
||||
* @return SSE emitter for streaming response
|
||||
*/
|
||||
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamChat(@RequestBody ChatRequestContext context) {
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String aiResponse = chatClientProviderService.streamChat(context);
|
||||
emitter.send(aiResponse);
|
||||
emitter.complete();
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}).start();
|
||||
return emitter;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Controller for managing conversations.
|
||||
*/
|
||||
public class ConversationController {
|
||||
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for Conversation entities.
|
||||
*/
|
||||
public interface ConversationDao {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for Message entities.
|
||||
*/
|
||||
public interface MessageDao {
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Data Access Object interface for UserPreference entities.
|
||||
*/
|
||||
public interface UserPreferenceDao {
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Chat request context for AI chat endpoint.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChatRequestContext {
|
||||
/**
|
||||
* The user's message (required)
|
||||
*/
|
||||
private String message;
|
||||
/**
|
||||
* Optional conversation ID for context
|
||||
*/
|
||||
private String conversationId;
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Service interface for agent operations.
|
||||
*/
|
||||
public interface AgentService {
|
||||
|
||||
}
|
||||
+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.service;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.pojo.dto.ChatRequestContext;
|
||||
|
||||
/**
|
||||
* Service for interacting with LLM providers (like OpenAI, Anthropic, etc.)
|
||||
*/
|
||||
public interface ChatClientProviderService {
|
||||
|
||||
String complete(String message);
|
||||
|
||||
String streamChat(ChatRequestContext context);
|
||||
}
|
||||
+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.service;
|
||||
|
||||
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service for managing chat conversations and interactions with LLM providers.
|
||||
*/
|
||||
public interface ConversationService {
|
||||
|
||||
/**
|
||||
* Send a message and receive a streaming response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return SseEmitter for streaming the response
|
||||
*/
|
||||
SseEmitter streamChat(String message, String conversationId);
|
||||
|
||||
/**
|
||||
* Send a message and get a complete response
|
||||
*
|
||||
* @param message The user's message
|
||||
* @param conversationId Optional conversation ID for continuing a chat
|
||||
* @return Response object containing the AI's response and conversation metadata
|
||||
*/
|
||||
Map<String, Object> chat(String message, String conversationId);
|
||||
|
||||
/**
|
||||
* Get conversation history for a specific conversation
|
||||
*
|
||||
* @param conversationId Conversation ID
|
||||
* @return Conversation data including messages
|
||||
*/
|
||||
Map<String, Object> getConversation(String conversationId);
|
||||
|
||||
/**
|
||||
* Get all conversations for the current user
|
||||
*
|
||||
* @return List of conversations
|
||||
*/
|
||||
List<Map<String, Object>> getAllConversations();
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
*
|
||||
* @param conversationId Conversation ID to delete
|
||||
*/
|
||||
void deleteConversation(String conversationId);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service;
|
||||
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
|
||||
/**
|
||||
* Service interface for MCP server operations.
|
||||
*/
|
||||
public interface McpServerService {
|
||||
ToolCallbackProvider hertzbeatTools();
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.service.impl;
|
||||
|
||||
import org.apache.hertzbeat.ai.agent.service.AgentService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Implementation of the AgentService interface.
|
||||
* This service provides functionality for handling AI agent operations.
|
||||
*/
|
||||
@Service
|
||||
public class AgentServiceImpl implements AgentService {
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.PromptProvider;
|
||||
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.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
|
||||
/**
|
||||
* Implementation of the {@link ChatClientProviderService}.
|
||||
* Provides functionality to interact with the ChatClient for handling chat
|
||||
* messages.
|
||||
*/
|
||||
@Service
|
||||
public class ChatClientProviderServiceImpl implements ChatClientProviderService {
|
||||
|
||||
private final ChatClient chatClient;
|
||||
|
||||
@Qualifier("hertzbeatTools")
|
||||
@Autowired
|
||||
private ToolCallbackProvider toolCallbackProvider;
|
||||
|
||||
@Autowired
|
||||
public ChatClientProviderServiceImpl(@Qualifier("openAiChatClient") ChatClient openAiChatClient) {
|
||||
this.chatClient = openAiChatClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String complete(String message) {
|
||||
return this.chatClient.prompt()
|
||||
.user(message)
|
||||
.call()
|
||||
.content();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String streamChat(ChatRequestContext context) {
|
||||
try {
|
||||
return this.chatClient.prompt(PromptProvider.HERTZBEAT_MONITORING_PROMPT)
|
||||
.user(context.getMessage())
|
||||
.toolCallbacks(toolCallbackProvider)
|
||||
.call()
|
||||
.content();
|
||||
} catch (Exception e) {
|
||||
return "Error: " + e.getMessage();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Implementation of the ConversationService interface for managing chat conversations.
|
||||
*/
|
||||
@Service
|
||||
public class ConversationServiceImpl {
|
||||
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.apache.hertzbeat.ai.agent.tools.impl.MonitorToolsImpl;
|
||||
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 org.springframework.beans.factory.annotation.Autowired;
|
||||
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 MonitorToolsImpl monitorTools;
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider hertzbeatTools() {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(monitorTools).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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
public interface AlertTools {
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 operations
|
||||
*/
|
||||
public interface MetricsTools {
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package org.apache.hertzbeat.ai.agent.tools;
|
||||
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for Monitoring Tools
|
||||
*/
|
||||
public interface MonitorTools {
|
||||
|
||||
String addMonitor(String name, ToolContext context);
|
||||
|
||||
/**
|
||||
* Query monitor information with flexible filtering and pagination.
|
||||
* Supports filtering by monitor IDs, type, status, host, labels, sorting, and
|
||||
* pagination.
|
||||
* Returns results as plain JSON.
|
||||
*/
|
||||
String listMonitors(
|
||||
List<Long> ids,
|
||||
String app,
|
||||
Byte status,
|
||||
String search,
|
||||
String labels,
|
||||
String sort,
|
||||
String order,
|
||||
Integer pageIndex,
|
||||
Integer pageSize,
|
||||
ToolContext context);
|
||||
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Implementation of Alert Tools functionality
|
||||
*/
|
||||
public class AlertToolsImpl {
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Implementation of Metrics Tools functionality
|
||||
*/
|
||||
public class MetricsToolsImpl {
|
||||
}
|
||||
+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.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.springframework.ai.chat.model.ToolContext;
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* 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 monitor names as string.
|
||||
*/
|
||||
@Override
|
||||
@Tool(name = "list_monitors", returnDirect = true, description = """
|
||||
Query monitor information with flexible filtering and pagination.
|
||||
Supports filtering by monitor IDs, type, status, host, labels, sorting, and pagination.
|
||||
Returns results as String. When no parameters are available, pass the default value as mentioned below. If the user doesn't provide any specific parameter, the default value will be used.
|
||||
""")
|
||||
public String listMonitors(
|
||||
@ToolParam(description = "List of monitor IDs to filter (default: empty list)", required = false) List<Long> ids,
|
||||
@ToolParam(description = "Monitor type, e.g., 'linux' (default: null)", required = false) String app,
|
||||
@ToolParam(description = "Monitor status (0: no monitor, 1: usable, 2: disabled, 9: all) (default: null)", required = false) Byte status,
|
||||
@ToolParam(description = "Fuzzy search for host or name (default: null)", required = false) String search,
|
||||
@ToolParam(description = "Monitor labels, e.g., 'env:prod,instance:22' (default: null)", required = false) String labels,
|
||||
@ToolParam(description = "Sort field, e.g., 'name' (default: gmtCreate)", required = false) String sort,
|
||||
@ToolParam(description = "Sort order, 'asc' or 'desc' (default: desc)", required = false) String order,
|
||||
@ToolParam(description = "Page index (default: 0)", required = false) Integer pageIndex,
|
||||
@ToolParam(description = "Page size (default: 8)", required = false) Integer pageSize,
|
||||
ToolContext context) {
|
||||
try {
|
||||
Page<Monitor> result = monitorServiceAdapter.getMonitors(ids, app, search, status, sort, order, pageIndex, pageSize, labels);
|
||||
log.debug("MonitorServiceAdapter.getMonitors result: {}", result);
|
||||
return result.getContent().stream().map(Monitor::getName).toList().toString();
|
||||
} catch (Exception e) {
|
||||
return "error is" + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Tool(name = "add_monitor", description = "Add a new monitor")
|
||||
public String addMonitor(@ToolParam(description = "Name of the monitor") String name, ToolContext context) {
|
||||
log.debug("Adding monitor with name: {}", name);
|
||||
SubjectSum subjectSum = McpContextHolder.getSubject();
|
||||
log.debug("Current subject in tool: {}", subjectSum);
|
||||
return "Monitor added: " + name;
|
||||
}
|
||||
|
||||
}
|
||||
+5
@@ -109,4 +109,9 @@ public class AlarmCacheManager {
|
||||
private String getCustomKey(String fingerPrint) {
|
||||
return CUSTOM_FIRING_ROW_KEY + fingerPrint;
|
||||
}
|
||||
|
||||
|
||||
public SingleAlert removeFiring(String 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());
|
||||
}
|
||||
}
|
||||
|
||||
+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);
|
||||
|
||||
+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 : '(' ;
|
||||
|
||||
+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());
|
||||
}
|
||||
}
|
||||
+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());
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
-308
@@ -1,308 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.prometheus;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.collect.common.http.CommonHttpClient;
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.collector.util.CollectUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.constants.NetworkConstants;
|
||||
import org.apache.hertzbeat.common.constants.SignConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.PrometheusProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.util.Base64Util;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.hertzbeat.common.util.IpDomainUtil;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.AuthCache;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.CredentialsProvider;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.auth.DigestScheme;
|
||||
import org.apache.http.impl.client.BasicAuthCache;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.SignConstants.RIGHT_DASH;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class PrometheusProxyCollectImpl implements PrometheusCollect {
|
||||
|
||||
private final Set<Integer> defaultSuccessStatusCodes = Stream.of(HttpStatus.SC_OK, HttpStatus.SC_CREATED,
|
||||
HttpStatus.SC_ACCEPTED, HttpStatus.SC_MULTIPLE_CHOICES, HttpStatus.SC_MOVED_PERMANENTLY,
|
||||
HttpStatus.SC_MOVED_TEMPORARILY).collect(Collectors.toSet());
|
||||
|
||||
public static final String RAW_TEXT_CONTENT_FIELD_NAME = "raw_text_content";
|
||||
|
||||
@Override
|
||||
public List<CollectRep.MetricsData> collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
|
||||
PrometheusProtocol prometheusProtocol = metrics.getPrometheus();
|
||||
HttpUriRequest request;
|
||||
try {
|
||||
validateParams(metrics);
|
||||
} catch (Exception e) {
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(e.getMessage());
|
||||
return Collections.singletonList(builder.build());
|
||||
}
|
||||
|
||||
HttpContext httpContext = createHttpContext(prometheusProtocol);
|
||||
request = createHttpRequest(prometheusProtocol);
|
||||
|
||||
try (CloseableHttpResponse response = CommonHttpClient.getHttpClient().execute(request, httpContext)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
log.debug("Prometheus proxy collect, response status: {}", statusCode);
|
||||
|
||||
if (!defaultSuccessStatusCodes.contains(statusCode)) {
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(NetworkConstants.STATUS_CODE + SignConstants.BLANK + statusCode);
|
||||
return Collections.singletonList(builder.build());
|
||||
}
|
||||
|
||||
String rawTextContent = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
|
||||
|
||||
builder.clearFields();
|
||||
builder.clearValues();
|
||||
|
||||
CollectRep.Field rawDataField = CollectRep.Field.newBuilder()
|
||||
.setName(RAW_TEXT_CONTENT_FIELD_NAME)
|
||||
.setType(CommonConstants.TYPE_STRING)
|
||||
.build();
|
||||
builder.addField(rawDataField);
|
||||
|
||||
CollectRep.ValueRow.Builder valueRowBuilder = CollectRep.ValueRow.newBuilder();
|
||||
valueRowBuilder.addColumn(rawTextContent);
|
||||
builder.addValueRow(valueRowBuilder.build());
|
||||
|
||||
builder.setCode(CollectRep.Code.SUCCESS);
|
||||
} catch (ClientProtocolException e1) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(e1);
|
||||
log.error("Prometheus proxy collect error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e1);
|
||||
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
|
||||
builder.setMsg(errorMsg);
|
||||
} catch (UnknownHostException e2) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(e2);
|
||||
log.info("Prometheus proxy collect unknown host: {}. Host: {}", errorMsg, prometheusProtocol.getHost(), e2);
|
||||
builder.setCode(CollectRep.Code.UN_REACHABLE);
|
||||
builder.setMsg("unknown host:" + errorMsg);
|
||||
} catch (InterruptedIOException | ConnectException | SSLException e3) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(e3);
|
||||
log.info("Prometheus proxy collect connect error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e3);
|
||||
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
|
||||
builder.setMsg(errorMsg);
|
||||
} catch (IOException e4) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(e4);
|
||||
log.info("Prometheus proxy collect IO error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e4);
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(errorMsg);
|
||||
} catch (Exception e) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(e);
|
||||
log.error("Prometheus proxy collect unknown error: {}. Host: {}, Port: {}", errorMsg, prometheusProtocol.getHost(), prometheusProtocol.getPort(), e);
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg(errorMsg);
|
||||
} finally {
|
||||
if (request != null) {
|
||||
request.abort();
|
||||
}
|
||||
}
|
||||
return Collections.singletonList(builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String supportProtocol() {
|
||||
return DispatchConstants.PROTOCOL_PROMETHEUS;
|
||||
}
|
||||
|
||||
private void validateParams(Metrics metrics) throws Exception {
|
||||
if (metrics == null || metrics.getPrometheus() == null) {
|
||||
throw new Exception("Prometheus collect must has prometheus params");
|
||||
}
|
||||
PrometheusProtocol protocol = metrics.getPrometheus();
|
||||
if (!StringUtils.hasText(protocol.getHost())
|
||||
|| !StringUtils.hasText(protocol.getPort())) {
|
||||
throw new Exception("Prometheus collect must has host and port params");
|
||||
}
|
||||
if (protocol.getPath() == null
|
||||
|| !StringUtils.hasText(protocol.getPath())
|
||||
|| !protocol.getPath().startsWith(RIGHT_DASH)) {
|
||||
protocol.setPath(protocol.getPath() == null ? RIGHT_DASH : RIGHT_DASH + protocol.getPath().trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create httpContext
|
||||
* This method is adapted from PrometheusAutoCollectImpl
|
||||
* @param protocol prometheus protocol
|
||||
* @return context
|
||||
*/
|
||||
public HttpContext createHttpContext(PrometheusProtocol protocol) {
|
||||
PrometheusProtocol.Authorization auth = protocol.getAuthorization();
|
||||
if (auth != null && DispatchConstants.DIGEST_AUTH.equals(auth.getType())) {
|
||||
HttpClientContext clientContext = new HttpClientContext();
|
||||
if (StringUtils.hasText(auth.getDigestAuthUsername())
|
||||
&& StringUtils.hasText(auth.getDigestAuthPassword())) {
|
||||
CredentialsProvider provider = new BasicCredentialsProvider();
|
||||
UsernamePasswordCredentials credentials =
|
||||
new UsernamePasswordCredentials(auth.getDigestAuthUsername(), auth.getDigestAuthPassword());
|
||||
provider.setCredentials(AuthScope.ANY, credentials);
|
||||
AuthCache authCache = new BasicAuthCache();
|
||||
HttpHost targetHost = new HttpHost(protocol.getHost(), Integer.parseInt(protocol.getPort()));
|
||||
authCache.put(targetHost, new DigestScheme());
|
||||
clientContext.setCredentialsProvider(provider);
|
||||
clientContext.setAuthCache(authCache);
|
||||
return clientContext;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* create http request
|
||||
* This method is adapted from PrometheusAutoCollectImpl
|
||||
* @param protocol http params
|
||||
* @return http uri request
|
||||
*/
|
||||
public HttpUriRequest createHttpRequest(PrometheusProtocol protocol) {
|
||||
RequestBuilder requestBuilder = RequestBuilder.get();
|
||||
// params
|
||||
Map<String, String> params = protocol.getParams();
|
||||
if (params != null && !params.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : params.entrySet()) {
|
||||
if (StringUtils.hasText(param.getValue())) {
|
||||
requestBuilder.addParameter(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
|
||||
requestBuilder.addHeader(HttpHeaders.USER_AGENT, NetworkConstants.USER_AGENT);
|
||||
// headers The custom request header is overwritten here
|
||||
Map<String, String> headers = protocol.getHeaders();
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
if (StringUtils.hasText(header.getValue())) {
|
||||
requestBuilder.addHeader(CollectUtil.replaceUriSpecialChar(header.getKey()),
|
||||
CollectUtil.replaceUriSpecialChar(header.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (headers == null || headers.keySet().stream().noneMatch(HttpHeaders.ACCEPT::equalsIgnoreCase)) {
|
||||
requestBuilder.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE + ";version=0.0.4,*/*;q=0.1");
|
||||
}
|
||||
|
||||
if (protocol.getAuthorization() != null) {
|
||||
PrometheusProtocol.Authorization authorization = protocol.getAuthorization();
|
||||
if (DispatchConstants.BEARER_TOKEN.equalsIgnoreCase(authorization.getType())) {
|
||||
if (StringUtils.hasText(authorization.getBearerTokenToken())) {
|
||||
String value = DispatchConstants.BEARER + " " + authorization.getBearerTokenToken();
|
||||
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, value);
|
||||
}
|
||||
} else if (DispatchConstants.BASIC_AUTH.equals(authorization.getType())) {
|
||||
if (StringUtils.hasText(authorization.getBasicAuthUsername())
|
||||
&& StringUtils.hasText(authorization.getBasicAuthPassword())) {
|
||||
String authStr = authorization.getBasicAuthUsername() + ":" + authorization.getBasicAuthPassword();
|
||||
String encodedAuth = Base64Util.encode(authStr);
|
||||
requestBuilder.addHeader(HttpHeaders.AUTHORIZATION, DispatchConstants.BASIC + " " + encodedAuth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.hasLength(protocol.getPayload())) {
|
||||
requestBuilder.setEntity(new StringEntity(protocol.getPayload(), StandardCharsets.UTF_8));
|
||||
if (headers == null || headers.keySet().stream().noneMatch(HttpHeaders.CONTENT_TYPE::equalsIgnoreCase)) {
|
||||
requestBuilder.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
String uriPath = CollectUtil.replaceUriSpecialChar(protocol.getPath());
|
||||
if (IpDomainUtil.isHasSchema(protocol.getHost())) {
|
||||
requestBuilder.setUri(protocol.getHost() + SignConstants.DOUBLE_MARK + protocol.getPort() + uriPath);
|
||||
} else {
|
||||
String ipAddressType = IpDomainUtil.checkIpAddressType(protocol.getHost());
|
||||
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
|
||||
? String.format("[%s]:%s%s", protocol.getHost(), protocol.getPort(), uriPath)
|
||||
: String.format("%s:%s%s", protocol.getHost(), protocol.getPort(), uriPath);
|
||||
boolean ssl = Boolean.parseBoolean(protocol.getSsl());
|
||||
if (ssl) {
|
||||
requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri);
|
||||
} else {
|
||||
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
|
||||
}
|
||||
}
|
||||
|
||||
// custom timeout
|
||||
int timeout = CollectUtil.getTimeout(protocol.getTimeout());
|
||||
if (timeout > 0) {
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(timeout)
|
||||
.setSocketTimeout(timeout)
|
||||
.setConnectionRequestTimeout(timeout)
|
||||
.setRedirectsEnabled(true)
|
||||
.build();
|
||||
requestBuilder.setConfig(requestConfig);
|
||||
} else {
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setRedirectsEnabled(true)
|
||||
.build();
|
||||
requestBuilder.setConfig(requestConfig);
|
||||
}
|
||||
return requestBuilder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* get collect instance
|
||||
* @return instance
|
||||
*/
|
||||
public static PrometheusProxyCollectImpl getInstance() {
|
||||
return PrometheusProxyCollectImpl.SingleInstance.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* static instance
|
||||
*/
|
||||
private static class SingleInstance {
|
||||
private static final PrometheusProxyCollectImpl INSTANCE = new PrometheusProxyCollectImpl();
|
||||
}
|
||||
}
|
||||
+8
@@ -185,6 +185,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 +197,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);
|
||||
}
|
||||
|
||||
|
||||
+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());
|
||||
}
|
||||
}
|
||||
|
||||
+182
@@ -105,4 +105,186 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* 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.dispatch;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.metrics.HertzBeatMetricsCollector;
|
||||
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class CollectTaskTimeoutMonitor {
|
||||
/**
|
||||
* Collection task timeout value
|
||||
*/
|
||||
private static final long DURATION_TIME = 240_000L;
|
||||
/**
|
||||
* Metrics task and start time mapping map
|
||||
*/
|
||||
private final Map<String, MetricsTime> metricsTimeoutMonitorMap = new ConcurrentHashMap<>(16);
|
||||
|
||||
@Autowired
|
||||
private HertzBeatMetricsCollector metricsCollector;
|
||||
private CommonDispatcher commonDispatcher;
|
||||
|
||||
public void start(CommonDispatcher commonDispatcher) {
|
||||
this.commonDispatcher = commonDispatcher;
|
||||
|
||||
// monitoring metrics collection task execution timeout
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setNameFormat("metrics-task-timeout-monitor-%d")
|
||||
.setDaemon(true)
|
||||
.build();
|
||||
ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(1, threadFactory);
|
||||
scheduledExecutor.scheduleWithFixedDelay(this::monitorCollectTaskTimeout, 2, 20, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void putMetrics(String key, MetricsTime value) {
|
||||
this.metricsTimeoutMonitorMap.put(key, value);
|
||||
}
|
||||
|
||||
public MetricsTime removeMetrics(String key) {
|
||||
return this.metricsTimeoutMonitorMap.remove(key);
|
||||
}
|
||||
|
||||
private void monitorCollectTaskTimeout() {
|
||||
try {
|
||||
// Detect whether the collection unit of each metrics has timed out for 4 minutes,
|
||||
// and if it times out, it will be discarded and an exception will be returned.
|
||||
long deadline = System.currentTimeMillis() - DURATION_TIME;
|
||||
for (Map.Entry<String, MetricsTime> entry : metricsTimeoutMonitorMap.entrySet()) {
|
||||
MetricsTime metricsTime = entry.getValue();
|
||||
if (metricsTime.getStartTime() < deadline) {
|
||||
// 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(job.getMonitorId())
|
||||
.setTenantId(job.getTenantId())
|
||||
.setApp(job.getApp())
|
||||
.setMetrics(metricsTime.getMetrics().getName())
|
||||
.setPriority(metricsTime.getMetrics().getPriority())
|
||||
.setTime(System.currentTimeMillis())
|
||||
.setCode(CollectRep.Code.TIMEOUT)
|
||||
.setMsg("collect timeout")
|
||||
.build();
|
||||
log.error("[Collect Timeout]: \n{}", metricsData);
|
||||
if (metricsData.getPriority() == CommonConstants.AVAILABLE_METRICS) {
|
||||
//todo 使用chain bootstrap
|
||||
// commonDispatcher.dispatchCollectData(metricsTime.timeout, metricsTime.getMetrics(), metricsData);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[Task Timeout Monitor]-{}.", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Metrics times.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class MetricsTime {
|
||||
private long startTime;
|
||||
private Metrics metrics;
|
||||
private Timeout timeout;
|
||||
}
|
||||
}
|
||||
+154
-163
@@ -17,12 +17,23 @@
|
||||
|
||||
package org.apache.hertzbeat.collector.dispatch;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.google.gson.Gson;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.context.impl.DefaultContext;
|
||||
import org.apache.hertzbeat.collector.dispatch.entrance.internal.CollectJobService;
|
||||
import org.apache.hertzbeat.collector.handler.ChainBootstrap;
|
||||
import org.apache.hertzbeat.collector.handler.CollectMetricsDataDataStream;
|
||||
import org.apache.hertzbeat.collector.listener.CalculateFieldsListener;
|
||||
import org.apache.hertzbeat.collector.listener.MetricsDataDeliveryListener;
|
||||
import org.apache.hertzbeat.collector.listener.RemoveTimeoutMonitorListener;
|
||||
import org.apache.hertzbeat.collector.listener.RerunDataStream;
|
||||
import org.apache.hertzbeat.collector.listener.ResponseJobDataListener;
|
||||
import org.apache.hertzbeat.collector.listener.ValidateResponseListener;
|
||||
import org.apache.hertzbeat.collector.metrics.HertzBeatMetricsCollector;
|
||||
import org.apache.hertzbeat.collector.handler.impl.BatchExecuteTaskChain;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.collector.CollectorMetaData;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
import org.apache.hertzbeat.collector.timer.TimerDispatch;
|
||||
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
|
||||
@@ -33,19 +44,21 @@ 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.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Collection task and response data scheduler
|
||||
@@ -54,10 +67,6 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
@Slf4j
|
||||
public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatch {
|
||||
|
||||
/**
|
||||
* Collection task timeout value
|
||||
*/
|
||||
private static final long DURATION_TIME = 240_000L;
|
||||
/**
|
||||
* Trigger sub task max num
|
||||
*/
|
||||
@@ -79,33 +88,41 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
* collection data exporter
|
||||
*/
|
||||
private final CommonDataQueue commonDataQueue;
|
||||
/**
|
||||
* Metrics task and start time mapping map
|
||||
*/
|
||||
private final Map<String, MetricsTime> metricsTimeoutMonitorMap;
|
||||
|
||||
private final List<UnitConvert> unitConvertList;
|
||||
|
||||
private final WorkerPool workerPool;
|
||||
|
||||
private final String collectorIdentity;
|
||||
private final CollectorMetaData metaData;
|
||||
|
||||
private final CollectTaskTimeoutMonitor collectTaskTimeoutMonitor;
|
||||
|
||||
@Autowired
|
||||
private HertzBeatMetricsCollector metricsCollector;
|
||||
|
||||
public CommonDispatcher(MetricsCollectorQueue jobRequestQueue,
|
||||
TimerDispatch timerDispatch,
|
||||
CommonDataQueue commonDataQueue,
|
||||
WorkerPool workerPool,
|
||||
CollectJobService collectJobService,
|
||||
List<UnitConvert> unitConvertList) {
|
||||
List<UnitConvert> unitConvertList,
|
||||
CollectTaskTimeoutMonitor collectTaskTimeoutMonitor) {
|
||||
this.commonDataQueue = commonDataQueue;
|
||||
this.jobRequestQueue = jobRequestQueue;
|
||||
this.timerDispatch = timerDispatch;
|
||||
this.unitConvertList = unitConvertList;
|
||||
this.workerPool = workerPool;
|
||||
this.collectorIdentity = collectJobService.getCollectorIdentity();
|
||||
this.metricsTimeoutMonitorMap = new ConcurrentHashMap<>(16);
|
||||
this.start();
|
||||
this.metaData = CollectorMetaData.builder()
|
||||
.identity(collectJobService.getCollectorIdentity())
|
||||
.mode(collectJobService.getCollectorMode())
|
||||
.startTime(new Date())
|
||||
.build();
|
||||
this.collectTaskTimeoutMonitor = collectTaskTimeoutMonitor;
|
||||
// this.start();
|
||||
this.collectTaskTimeoutMonitor.start(this);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void start() {
|
||||
try {
|
||||
// Pull the collection task from the task queue and put it into the thread pool for execution
|
||||
@@ -133,110 +150,141 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
}
|
||||
log.info("Thread Interrupted, Shutdown the [metrics-task-dispatcher]");
|
||||
});
|
||||
// monitoring metrics collection task execution timeout
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder()
|
||||
.setNameFormat("metrics-task-timeout-monitor-%d")
|
||||
.setDaemon(true)
|
||||
.build();
|
||||
ScheduledThreadPoolExecutor scheduledExecutor = new ScheduledThreadPoolExecutor(1, threadFactory);
|
||||
scheduledExecutor.scheduleWithFixedDelay(this::monitorCollectTaskTimeout, 2, 20, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
log.error("Common Dispatcher error: {}.", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void monitorCollectTaskTimeout() {
|
||||
try {
|
||||
// Detect whether the collection unit of each metrics has timed out for 4 minutes,
|
||||
// and if it times out, it will be discarded and an exception will be returned.
|
||||
long deadline = System.currentTimeMillis() - DURATION_TIME;
|
||||
for (Map.Entry<String, MetricsTime> entry : metricsTimeoutMonitorMap.entrySet()) {
|
||||
MetricsTime metricsTime = entry.getValue();
|
||||
if (metricsTime.getStartTime() < deadline) {
|
||||
// Metrics collection timeout
|
||||
WheelTimerTask timerJob = (WheelTimerTask) metricsTime.getTimeout().task();
|
||||
CollectRep.MetricsData metricsData = CollectRep.MetricsData.newBuilder()
|
||||
.setId(timerJob.getJob().getMonitorId())
|
||||
.setTenantId(timerJob.getJob().getTenantId())
|
||||
.setApp(timerJob.getJob().getApp())
|
||||
.setMetrics(metricsTime.getMetrics().getName())
|
||||
.setPriority(metricsTime.getMetrics().getPriority())
|
||||
.setTime(System.currentTimeMillis())
|
||||
.setCode(CollectRep.Code.TIMEOUT).setMsg("collect timeout").build();
|
||||
log.error("[Collect Timeout]: \n{}", metricsData);
|
||||
if (metricsData.getPriority() == 0) {
|
||||
dispatchCollectData(metricsTime.timeout, metricsTime.getMetrics(), metricsData);
|
||||
}
|
||||
metricsTimeoutMonitorMap.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[Task Timeout Monitor]-{}.", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
job.constructPriorMetrics();
|
||||
Set<Metrics> metricsSet = job.getNextCollectMetrics(null, true);
|
||||
metricsSet.forEach(metrics -> {
|
||||
MetricsCollect metricsCollect = new MetricsCollect(metrics, timeout, this,
|
||||
collectorIdentity, unitConvertList);
|
||||
jobRequestQueue.addJob(metricsCollect);
|
||||
if (metrics.getPrometheus() != null) {
|
||||
metricsTimeoutMonitorMap.put(String.valueOf(job.getId()),
|
||||
new MetricsTime(System.currentTimeMillis(), metrics, timeout));
|
||||
} else {
|
||||
metricsTimeoutMonitorMap.put(job.getId() + "-" + metrics.getName(),
|
||||
new MetricsTime(System.currentTimeMillis(), metrics, timeout));
|
||||
}
|
||||
});
|
||||
|
||||
ChainBootstrap bootstrap = constructMetricsCollectTaskChain(job);
|
||||
//todo context需要划分作用域
|
||||
bootstrap.addContext(ContextKey.META_DATA, metaData)
|
||||
.addContext(ContextKey.JOB, job)
|
||||
.addContext(ContextKey.TIMEOUT, timeout)
|
||||
.addListener(new CalculateFieldsListener(unitConvertList))
|
||||
.addListener(new ValidateResponseListener())
|
||||
.onEachDataStreamComplete(new RemoveTimeoutMonitorListener(collectTaskTimeoutMonitor));
|
||||
|
||||
if (job.isCyclic()) {
|
||||
bootstrap.withWorkerPool(workerPool)
|
||||
.addListener(new MetricsDataDeliveryListener(commonDataQueue))
|
||||
.onComplete(new RerunDataStream(timerDispatch));
|
||||
} else {
|
||||
bootstrap.addListener(new ResponseJobDataListener(timerDispatch));
|
||||
}
|
||||
|
||||
|
||||
bootstrap.start();
|
||||
}
|
||||
|
||||
private ChainBootstrap constructMetricsCollectTaskChain(Job job) {
|
||||
long now = System.currentTimeMillis();
|
||||
Map<Byte, List<Metrics>> currentCollectMetrics = job.getMetrics().stream()
|
||||
.filter(metrics -> (now >= metrics.getCollectTime() + metrics.getInterval() * 1000L))
|
||||
.peek(metric -> {
|
||||
metric.setCollectTime(now);
|
||||
// Determine whether to configure aliasFields If not, configure the default
|
||||
if ((metric.getAliasFields() == null || metric.getAliasFields().isEmpty()) && metric.getFields() != null) {
|
||||
metric.setAliasFields(metric.getFields().stream().map(Metrics.Field::getField).collect(Collectors.toList()));
|
||||
}
|
||||
// Set the default metrics execution priority, if not filled, the default last priority
|
||||
if (metric.getPriority() == null) {
|
||||
metric.setPriority(Byte.MAX_VALUE);
|
||||
}
|
||||
})
|
||||
.collect(Collectors.groupingBy(Metrics::getPriority));
|
||||
|
||||
// the current collect metrics can not empty, if empty, add a default availability metrics
|
||||
// due the metric collect is trigger by the previous metric collect
|
||||
if (currentCollectMetrics.isEmpty()) {
|
||||
Optional<Metrics> defaultMetricOption = job.getMetrics().stream()
|
||||
.filter(metric -> metric.getPriority() == CommonConstants.AVAILABLE_METRICS).findFirst();
|
||||
if (defaultMetricOption.isPresent()) {
|
||||
Metrics defaultMetric = defaultMetricOption.get();
|
||||
defaultMetric.setCollectTime(now);
|
||||
currentCollectMetrics.put(CommonConstants.AVAILABLE_METRICS, Collections.singletonList(defaultMetric));
|
||||
} else {
|
||||
log.error("metrics must has one priority 0 metrics at least.");
|
||||
}
|
||||
}
|
||||
|
||||
ChainBootstrap chainBootstrap = ChainBootstrap.withContext(DefaultContext.newInstance())
|
||||
.withChain(new BatchExecuteTaskChain<Metrics>());
|
||||
// order by priority
|
||||
currentCollectMetrics.keySet().stream()
|
||||
.sorted()
|
||||
.forEach(priority -> {
|
||||
if (job.isCyclic() || isOneTimeJobAndIsAvailableMetrics(job, priority)) {
|
||||
List<Metrics> metricsList = currentCollectMetrics.get(priority);
|
||||
CollectMetricsDataDataStream collectHandler = CollectMetricsDataDataStream.builder()
|
||||
.collectTaskTimeoutMonitor(collectTaskTimeoutMonitor)
|
||||
.build();
|
||||
collectHandler.setSourceDataList(metricsList);
|
||||
|
||||
chainBootstrap.addDataStream(collectHandler);
|
||||
}
|
||||
});
|
||||
|
||||
return chainBootstrap;
|
||||
}
|
||||
|
||||
private boolean isOneTimeJobAndIsAvailableMetrics(Job job, byte priority) {
|
||||
return (!job.isCyclic()) && priority == CommonConstants.AVAILABLE_METRICS;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData) {
|
||||
WheelTimerTask timerJob = (WheelTimerTask) timeout.task();
|
||||
Job job = timerJob.getJob();
|
||||
String monitorKey = job.getId() + "-" + metrics.getName();
|
||||
if (metrics.isHasSubTask()) {
|
||||
monitorKey = monitorKey + "-sub-" + metrics.getSubTaskId();
|
||||
}
|
||||
CollectTaskTimeoutMonitor.MetricsTime metricsTime = this.collectTaskTimeoutMonitor.removeMetrics(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()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Cyclic Job: {} - {} - {}", job.getMonitorId(), job.getApp(), metricsData.getMetrics());
|
||||
for (CollectRep.ValueRow valueRow : metricsData.getValues()) {
|
||||
for (CollectRep.Field field : metricsData.getFields()) {
|
||||
log.debug("Field-->{},Value-->{}", field.getName(), valueRow.getColumns(metricsData.getFields().indexOf(field)));
|
||||
}
|
||||
}
|
||||
}
|
||||
cyclicJobDebugLog(job, metricsData);
|
||||
|
||||
// If metricsSet is null, it means that the execution is completed or whether the priority of the collection metrics is 0, that is, the availability collection metrics.
|
||||
// If the availability collection fails, the next metrics scheduling will be cancelled and the next round of scheduling will be entered directly.
|
||||
boolean isAvailableCollectFailed = metricsSet != null && !metricsSet.isEmpty()
|
||||
&& metrics.getPriority() == (byte) 0 && metricsData.getCode() != CollectRep.Code.SUCCESS;
|
||||
&& metrics.getPriority() == CommonConstants.AVAILABLE_METRICS && metricsData.getCode() != CollectRep.Code.SUCCESS;
|
||||
if (metricsSet == null || isAvailableCollectFailed || job.isSd()) {
|
||||
// The collection and execution task of this job are completed.
|
||||
// The periodic task pushes the task to the time wheel again.
|
||||
// First, determine the execution time of the task and the task collection interval.
|
||||
if (!timeout.isCancelled()) {
|
||||
long spendTime = System.currentTimeMillis() - job.getDispatchTime();
|
||||
long interval = job.getInterval() - spendTime / 1000;
|
||||
long interval = job.getInterval() - spendTime / 1000L;
|
||||
interval = interval <= 0 ? 0 : interval;
|
||||
timerDispatch.cyclicJob(timerJob, interval, TimeUnit.SECONDS);
|
||||
}
|
||||
} else if (!metricsSet.isEmpty()) {
|
||||
}
|
||||
else if (!metricsSet.isEmpty()) {
|
||||
// The execution of the current level metrics is completed, and the execution of the next level metrics starts
|
||||
// use pre collect metrics data to replace next metrics config params
|
||||
List<Map<String, Configmap>> configmapList = CollectUtil.getConfigmapFromPreCollectData(metricsData);
|
||||
@@ -247,12 +295,15 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
Set<String> cryPlaceholderFields = CollectUtil.matchCryPlaceholderField(GSON.toJsonTree(metricItem));
|
||||
if (cryPlaceholderFields.isEmpty()) {
|
||||
MetricsCollect metricsCollect = new MetricsCollect(metricItem, timeout, this,
|
||||
collectorIdentity, unitConvertList);
|
||||
metaData.getIdentity(), unitConvertList);
|
||||
jobRequestQueue.addJob(metricsCollect);
|
||||
metricsTimeoutMonitorMap.put(job.getId() + "-" + metricItem.getName(),
|
||||
new MetricsTime(System.currentTimeMillis(), metricItem, timeout));
|
||||
|
||||
this.collectTaskTimeoutMonitor.putMetrics(job.getId() + "-" + metricItem.getName(),
|
||||
new CollectTaskTimeoutMonitor.MetricsTime(System.currentTimeMillis(), metricItem, timeout));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
boolean isSubTask = configmapList.stream().anyMatch(map -> map.keySet().stream().anyMatch(cryPlaceholderFields::contains));
|
||||
int subTaskNum = isSubTask ? Math.min(configmapList.size(), MAX_SUB_TASK_NUM) : 1;
|
||||
AtomicInteger subTaskNumAtomic = new AtomicInteger(subTaskNum);
|
||||
@@ -268,90 +319,30 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
metric.setSubTaskId(index);
|
||||
metric.setSubTaskDataRef(metricsDataReference);
|
||||
MetricsCollect metricsCollect = new MetricsCollect(metric, timeout, this,
|
||||
collectorIdentity, unitConvertList);
|
||||
metaData.getIdentity(), unitConvertList);
|
||||
jobRequestQueue.addJob(metricsCollect);
|
||||
metricsTimeoutMonitorMap.put(job.getId() + "-" + metric.getName() + "-sub-" + index,
|
||||
new MetricsTime(System.currentTimeMillis(), metric, timeout));
|
||||
|
||||
this.collectTaskTimeoutMonitor.putMetrics(job.getId() + "-" + metric.getName() + "-sub-" + index,
|
||||
new CollectTaskTimeoutMonitor.MetricsTime(System.currentTimeMillis(), metric, timeout));
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
// The list of metrics at the current execution level has not been fully executed.
|
||||
// It needs to wait for the execution of other metrics task of the same level to complete the execution and enter the next level for execution.
|
||||
}
|
||||
// If it is an asynchronous periodic cyclic task, directly response the collected data
|
||||
if (job.isSd()) {
|
||||
CollectRep.MetricsData sdMetricsData = CollectRep.MetricsData.newBuilder(metricsData).build();
|
||||
commonDataQueue.sendServiceDiscoveryData(sdMetricsData);
|
||||
}
|
||||
commonDataQueue.sendMetricsData(metricsData);
|
||||
} else {
|
||||
// If it is a temporary one-time task, you need to wait for the collected data of all metrics task to be packaged and returned.
|
||||
// Insert the current metrics data into the job for unified assembly
|
||||
job.addCollectMetricsData(metricsData);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("One-time Job: {}", metricsData.getMetrics());
|
||||
for (CollectRep.ValueRow valueRow : metricsData.getValues()) {
|
||||
for (CollectRep.Field field : metricsData.getFields()) {
|
||||
log.debug("Field-->{},Value-->{}", field.getName(), valueRow.getColumns(metricsData.getFields().indexOf(field)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (job.isSd() || metricsSet == null) {
|
||||
// The collection and execution of all metrics of this job are completed
|
||||
// and the result listener is notified of the combination of all metrics data
|
||||
timerDispatch.responseSyncJobData(job.getId(), job.getResponseDataTemp());
|
||||
} else if (!metricsSet.isEmpty()) {
|
||||
// The execution of the current level metrics is completed, and the execution of the next level metrics starts
|
||||
metricsSet.forEach(metricItem -> {
|
||||
MetricsCollect metricsCollect = new MetricsCollect(metricItem, timeout, this,
|
||||
collectorIdentity, unitConvertList);
|
||||
jobRequestQueue.addJob(metricsCollect);
|
||||
metricsTimeoutMonitorMap.put(job.getId() + "-" + metricItem.getName(),
|
||||
new MetricsTime(System.currentTimeMillis(), metricItem, timeout));
|
||||
});
|
||||
} else {
|
||||
// The list of metrics task at the current execution level has not been fully executed.
|
||||
// It needs to wait for the execution of other metrics task of the same level to complete the execution and enter the next level for execution.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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()));
|
||||
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.
|
||||
// First, determine the execution time of the task and the task collection interval.
|
||||
if (!timeout.isCancelled()) {
|
||||
long spendTime = System.currentTimeMillis() - job.getDispatchTime();
|
||||
long interval = job.getInterval() - spendTime / 1000;
|
||||
interval = interval <= 0 ? 0 : interval;
|
||||
timerDispatch.cyclicJob(timerJob, interval, TimeUnit.SECONDS);
|
||||
}
|
||||
// it is an asynchronous periodic cyclic task, directly response the collected data
|
||||
metricsDataList.forEach(commonDataQueue::sendMetricsData);
|
||||
} else {
|
||||
// The collection and execution of all metrics of this job are completed
|
||||
// and the result listener is notified of the combination of all metrics data
|
||||
timerDispatch.responseSyncJobData(job.getId(), metricsDataList);
|
||||
private void cyclicJobDebugLog(Job job, CollectRep.MetricsData metricsData) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Cyclic Job: {} - {} - {}", job.getMonitorId(), job.getApp(), metricsData.getMetrics());
|
||||
metricsDataDebugLog(metricsData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Metrics times.
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
protected static class MetricsTime {
|
||||
private long startTime;
|
||||
private Metrics metrics;
|
||||
private Timeout timeout;
|
||||
private void metricsDataDebugLog(CollectRep.MetricsData metricsData) {
|
||||
for (CollectRep.ValueRow valueRow : metricsData.getValues()) {
|
||||
for (CollectRep.Field field : metricsData.getFields()) {
|
||||
log.debug("Field-->{},Value-->{}", field.getName(), valueRow.getColumns(metricsData.getFields().indexOf(field)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-317
@@ -129,7 +129,6 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
|
||||
this.newTime = System.currentTimeMillis();
|
||||
this.timeout = timeout;
|
||||
this.metrics = metrics;
|
||||
this.collectorIdentity = collectorIdentity;
|
||||
WheelTimerTask timerJob = (WheelTimerTask) timeout.task();
|
||||
Job job = timerJob.getJob();
|
||||
this.id = job.getMonitorId();
|
||||
@@ -153,324 +152,9 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.startTime = System.currentTimeMillis();
|
||||
setNewThreadName(id, app, startTime, metrics);
|
||||
CollectRep.MetricsData.Builder response = CollectRep.MetricsData.newBuilder();
|
||||
response.setApp(app).setId(id).setTenantId(tenantId)
|
||||
.setLabels(labels).setAnnotations(annotations).addMetadataAll(metadata);
|
||||
// for prometheus auto or proxy mode
|
||||
if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(metrics.getProtocol())) {
|
||||
List<CollectRep.MetricsData> metricsData;
|
||||
|
||||
// TODO: Refactor Prometheus metrics collection logic.
|
||||
// The current implementation for proxy mode and auto mode needs review and potential simplification.
|
||||
// Consider a more unified approach or clarify the conditions for each mode.
|
||||
/*
|
||||
// TODO USE PROXY MODE
|
||||
if (prometheusProxyMode) {
|
||||
List<CollectRep.MetricsData> proxyData = PrometheusProxyCollectImpl.getInstance().collect(response, metrics);
|
||||
List<CollectRep.MetricsData> autoData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics);
|
||||
metricsData = new LinkedList<>();
|
||||
if (proxyData != null) {
|
||||
metricsData.addAll(proxyData);
|
||||
}
|
||||
if (autoData != null) {
|
||||
metricsData.addAll(autoData);
|
||||
}
|
||||
} else {
|
||||
metricsData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics);
|
||||
}
|
||||
*/
|
||||
metricsData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics);
|
||||
validateResponse(metricsData == null ? null : metricsData.stream().findFirst().orElse(null));
|
||||
collectDataDispatch.dispatchCollectData(timeout, metrics, metricsData);
|
||||
return;
|
||||
}
|
||||
response.setMetrics(metrics.getName());
|
||||
// According to the metrics collection protocol, application type, etc.,
|
||||
// dispatch to the real application metrics collection implementation class
|
||||
AbstractCollect abstractCollect = CollectStrategyFactory.invoke(metrics.getProtocol());
|
||||
if (abstractCollect == null) {
|
||||
log.error("[Dispatcher] - not support this: app: {}, metrics: {}, protocol: {}.",
|
||||
app, metrics.getName(), metrics.getProtocol());
|
||||
response.setCode(CollectRep.Code.FAIL);
|
||||
response.setMsg("not support " + app + ", "
|
||||
+ metrics.getName() + ", " + metrics.getProtocol());
|
||||
} else {
|
||||
try {
|
||||
abstractCollect.preCheck(metrics);
|
||||
abstractCollect.collect(response, metrics);
|
||||
} catch (Exception e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg == null && e.getCause() != null) {
|
||||
msg = e.getCause().getMessage();
|
||||
}
|
||||
if (e instanceof IllegalArgumentException) {
|
||||
log.error("[Metrics PreCheck]: {}.", msg, e);
|
||||
} else {
|
||||
log.error("[Metrics Collect]: {}.", msg, e);
|
||||
}
|
||||
response.setCode(CollectRep.Code.FAIL);
|
||||
if (msg != null) {
|
||||
response.setMsg(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Alias attribute expression replacement calculation
|
||||
if (fastFailed()) {
|
||||
return;
|
||||
}
|
||||
calculateFields(metrics, response);
|
||||
CollectRep.MetricsData metricsData = validateResponse(response);
|
||||
collectDataDispatch.dispatchCollectData(timeout, metrics, metricsData);
|
||||
// collectDataDispatch.dispatchCollectData(timeout, metrics, metricsData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the real metrics value according to the calculates and aliasFields configuration
|
||||
*
|
||||
* @param metrics Metrics configuration
|
||||
* @param collectData Data collection
|
||||
*/
|
||||
public void calculateFields(Metrics metrics, CollectRep.MetricsData.Builder collectData) {
|
||||
collectData.setPriority(metrics.getPriority());
|
||||
List<CollectRep.Field> fieldList = new LinkedList<>();
|
||||
for (Metrics.Field field : metrics.getFields()) {
|
||||
CollectRep.Field.Builder fieldBuilder = CollectRep.Field.newBuilder();
|
||||
fieldBuilder.setName(field.getField()).setType(field.getType()).setLabel(field.isLabel());
|
||||
if (field.getUnit() != null) {
|
||||
fieldBuilder.setUnit(field.getUnit());
|
||||
}
|
||||
fieldList.add(fieldBuilder.build());
|
||||
}
|
||||
collectData.addAllFields(fieldList);
|
||||
List<CollectRep.ValueRow> aliasRowList = collectData.getValuesList();
|
||||
if (aliasRowList == null || aliasRowList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
collectData.clearValues();
|
||||
// Preprocess calculates first
|
||||
if (metrics.getCalculates() == null) {
|
||||
metrics.setCalculates(Collections.emptyList());
|
||||
}
|
||||
// eg: database_pages=Database pages unconventional mapping
|
||||
Map<String, String> fieldAliasMap = new HashMap<>(8);
|
||||
Map<String, JexlExpression> fieldExpressionMap = metrics.getCalculates()
|
||||
.stream()
|
||||
.map(cal -> transformCal(cal, fieldAliasMap))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(arr -> (String) arr[0], arr -> (JexlExpression) arr[1], (oldValue, newValue) -> newValue));
|
||||
|
||||
if (metrics.getUnits() == null) {
|
||||
metrics.setUnits(Collections.emptyList());
|
||||
}
|
||||
Map<String, Pair<String, String>> fieldUnitMap = metrics.getUnits()
|
||||
.stream()
|
||||
.map(this::transformUnit)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(arr -> (String) arr[0], arr -> (Pair<String, String>) arr[1], (oldValue, newValue) -> newValue));
|
||||
|
||||
List<Metrics.Field> fields = metrics.getFields();
|
||||
List<String> aliasFields = Optional.ofNullable(metrics.getAliasFields()).orElseGet(Collections::emptyList);
|
||||
Map<String, String> aliasFieldValueMap = new HashMap<>(8);
|
||||
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||
Map<String, Object> stringTypefieldValueMap = new HashMap<>(8);
|
||||
Map<String, String> aliasFieldUnitMap = new HashMap<>(8);
|
||||
CollectRep.ValueRow.Builder realValueRowBuilder = CollectRep.ValueRow.newBuilder();
|
||||
for (CollectRep.ValueRow aliasRow : aliasRowList) {
|
||||
for (int aliasIndex = 0; aliasIndex < aliasFields.size(); aliasIndex++) {
|
||||
String aliasFieldValue = aliasRow.getColumns(aliasIndex);
|
||||
String aliasField = aliasFields.get(aliasIndex);
|
||||
if (!CommonConstants.NULL_VALUE.equals(aliasFieldValue)) {
|
||||
aliasFieldValueMap.put(aliasField, aliasFieldValue);
|
||||
// whether the alias field is a number
|
||||
CollectUtil.DoubleAndUnit doubleAndUnit = CollectUtil
|
||||
.extractDoubleAndUnitFromStr(aliasFieldValue);
|
||||
if (doubleAndUnit != null && doubleAndUnit.getValue() != null) {
|
||||
fieldValueMap.put(aliasField, doubleAndUnit.getValue());
|
||||
if (doubleAndUnit.getUnit() != null) {
|
||||
aliasFieldUnitMap.put(aliasField, doubleAndUnit.getUnit());
|
||||
}
|
||||
} else {
|
||||
fieldValueMap.put(aliasField, aliasFieldValue);
|
||||
}
|
||||
stringTypefieldValueMap.put(aliasField, aliasFieldValue);
|
||||
} else {
|
||||
fieldValueMap.put(aliasField, null);
|
||||
stringTypefieldValueMap.put(aliasField, null);
|
||||
}
|
||||
}
|
||||
|
||||
for (Metrics.Field field : fields) {
|
||||
String realField = field.getField();
|
||||
JexlExpression expression = fieldExpressionMap.get(realField);
|
||||
String value = null;
|
||||
String aliasFieldUnit = null;
|
||||
if (expression != null) {
|
||||
try {
|
||||
Map<String, Object> context;
|
||||
if (CommonConstants.TYPE_STRING == field.getType()) {
|
||||
context = stringTypefieldValueMap;
|
||||
} else {
|
||||
for (Map.Entry<String, String> unitEntry : aliasFieldUnitMap.entrySet()) {
|
||||
if (expression.getSourceText().contains(unitEntry.getKey())) {
|
||||
aliasFieldUnit = unitEntry.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
context = fieldValueMap;
|
||||
}
|
||||
|
||||
// Also executed when valueList is empty, covering pure string assignment expressions
|
||||
Object objValue = JexlExpressionRunner.evaluate(expression, context);
|
||||
|
||||
if (objValue != null) {
|
||||
value = String.valueOf(objValue);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[calculates execute warning, use original value.] {}", e.getMessage());
|
||||
value = Optional.ofNullable(fieldValueMap.get(expression.getSourceText()))
|
||||
.map(String::valueOf)
|
||||
.orElse(null);
|
||||
}
|
||||
} else {
|
||||
// does not exist then map the alias value
|
||||
String aliasField = fieldAliasMap.get(realField);
|
||||
if (aliasField != null) {
|
||||
value = aliasFieldValueMap.get(aliasField);
|
||||
} else {
|
||||
value = aliasFieldValueMap.get(realField);
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
final byte fieldType = field.getType();
|
||||
if (fieldType == CommonConstants.TYPE_NUMBER) {
|
||||
CollectUtil.DoubleAndUnit doubleAndUnit = CollectUtil
|
||||
.extractDoubleAndUnitFromStr(value);
|
||||
final Double tempValue = doubleAndUnit == null ? null : doubleAndUnit.getValue();
|
||||
value = tempValue == null ? null : String.valueOf(tempValue);
|
||||
aliasFieldUnit = doubleAndUnit == null ? null : doubleAndUnit.getUnit();
|
||||
} else if (fieldType == CommonConstants.TYPE_TIME) {
|
||||
final int tempValue;
|
||||
value = (tempValue = CommonUtil.parseTimeStrToSecond(value)) == -1 ? null : String.valueOf(tempValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Pair<String, String> unitPair = fieldUnitMap.get(realField);
|
||||
if (aliasFieldUnit != null) {
|
||||
if (unitPair != null) {
|
||||
unitPair.setLeft(aliasFieldUnit);
|
||||
} else if (field.getUnit() != null && !aliasFieldUnit.equalsIgnoreCase(field.getUnit())) {
|
||||
unitPair = Pair.of(aliasFieldUnit, field.getUnit());
|
||||
}
|
||||
}
|
||||
if (value != null && unitPair != null) {
|
||||
for (UnitConvert unitConvert : unitConvertList) {
|
||||
if (unitConvert.checkUnit(unitPair.getLeft()) && unitConvert.checkUnit(unitPair.getRight())) {
|
||||
value = unitConvert.convert(value, unitPair.getLeft(), unitPair.getRight());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle metrics values that may have units such as 34%, 34Mb, and limit values to 4 decimal places
|
||||
if (CommonConstants.TYPE_NUMBER == field.getType()) {
|
||||
value = CommonUtil.parseDoubleStr(value, field.getUnit());
|
||||
}
|
||||
if (value == null) {
|
||||
value = CommonConstants.NULL_VALUE;
|
||||
}
|
||||
realValueRowBuilder.addColumn(value);
|
||||
}
|
||||
aliasFieldValueMap.clear();
|
||||
fieldValueMap.clear();
|
||||
aliasFieldUnitMap.clear();
|
||||
stringTypefieldValueMap.clear();
|
||||
CollectRep.ValueRow realValueRow = realValueRowBuilder.build();
|
||||
realValueRowBuilder.clear();
|
||||
// apply filter calculation to the real value row
|
||||
if (!CollectionUtils.isEmpty(metrics.getFilters())) {
|
||||
Map<String, Object> contextMap = new HashMap<>(8);
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
Metrics.Field field = fields.get(i);
|
||||
String value = realValueRow.getColumns(i);
|
||||
contextMap.put(field.getField(), value);
|
||||
}
|
||||
boolean isMatch = false;
|
||||
for (String filterExpr : metrics.getFilters()) {
|
||||
try {
|
||||
JexlExpression expression = JexlExpressionRunner.compile(filterExpr);
|
||||
if ((Boolean) JexlExpressionRunner.evaluate(expression, contextMap)) {
|
||||
isMatch = true;
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[metrics data row filters execute warning] {}.", e.getMessage());
|
||||
}
|
||||
}
|
||||
if (!isMatch) {
|
||||
// ignore this data row
|
||||
continue;
|
||||
}
|
||||
}
|
||||
collectData.addValueRow(realValueRow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cal cal
|
||||
* @param fieldAliasMap field alias map
|
||||
* @return expr
|
||||
*/
|
||||
private Object[] transformCal(String cal, Map<String, String> fieldAliasMap) {
|
||||
int splitIndex = cal.indexOf("=");
|
||||
String field = cal.substring(0, splitIndex).trim();
|
||||
String expressionStr = cal.substring(splitIndex + 1).trim().replace("\\#", "#");
|
||||
JexlExpression expression;
|
||||
try {
|
||||
expression = JexlExpressionRunner.compile(expressionStr);
|
||||
} catch (Exception e) {
|
||||
fieldAliasMap.put(field, expressionStr);
|
||||
return null;
|
||||
}
|
||||
return new Object[]{field, expression};
|
||||
}
|
||||
|
||||
/**
|
||||
* transform unit
|
||||
*
|
||||
* @param unit unit
|
||||
* @return units
|
||||
*/
|
||||
private Object[] transformUnit(String unit) {
|
||||
int equalIndex = unit.indexOf("=");
|
||||
int arrowIndex = unit.indexOf("->");
|
||||
if (equalIndex < 0 || arrowIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
String field = unit.substring(0, equalIndex).trim();
|
||||
String originUnit = unit.substring(equalIndex + 1, arrowIndex).trim();
|
||||
String newUnit = unit.substring(arrowIndex + 2).trim();
|
||||
return new Object[]{field, Pair.of(originUnit, newUnit)};
|
||||
}
|
||||
|
||||
private boolean fastFailed() {
|
||||
return this.timeout == null || this.timeout.isCancelled();
|
||||
}
|
||||
|
||||
private CollectRep.MetricsData validateResponse(CollectRep.MetricsData.Builder builder) {
|
||||
long endTime = System.currentTimeMillis();
|
||||
builder.setTime(endTime);
|
||||
long runningTime = endTime - startTime;
|
||||
long allTime = endTime - newTime;
|
||||
if (startTime - newTime >= WARN_DISPATCH_TIME) {
|
||||
log.warn("[Collector Dispatch Warn, Dispatch Use {}ms.", startTime - newTime);
|
||||
}
|
||||
if (builder.getCode() != CollectRep.Code.SUCCESS) {
|
||||
log.info("[Collect Failed, Run {}ms, All {}ms] Reason: {}", runningTime, allTime, builder.getMsg());
|
||||
} else {
|
||||
log.info("[Collect Success, Run {}ms, All {}ms].", runningTime, allTime);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void validateResponse(CollectRep.MetricsData metricsData) {
|
||||
if (metricsData == null) {
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.handler;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.collect.AbstractCollect;
|
||||
import org.apache.hertzbeat.collector.collect.strategy.CollectStrategyFactory;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.constants.ContextStatus;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.dispatch.CollectTaskTimeoutMonitor;
|
||||
import org.apache.hertzbeat.collector.handler.impl.AbstractBatchDataStream;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
|
||||
|
||||
@Data
|
||||
@Slf4j
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CollectMetricsDataDataStream extends AbstractBatchDataStream<Metrics, CollectRep.MetricsData.Builder> {
|
||||
private CollectTaskTimeoutMonitor collectTaskTimeoutMonitor;
|
||||
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder executeWithResponse(Context context, Metrics data) {
|
||||
// preset start info
|
||||
context.put(ContextKey.METRICS, data);
|
||||
Job job = context.get(ContextKey.JOB);
|
||||
long startTime = context.get(ContextKey.METRICS_COLLECT_START_TIME);
|
||||
setNewThreadName(job.getMonitorId(), job.getApp(), startTime, data);
|
||||
|
||||
|
||||
Timeout timeout = context.get(ContextKey.TIMEOUT);
|
||||
String key = data.getPrometheus() != null ? String.valueOf(job.getId()) : job.getId() + "-" + data.getName();
|
||||
context.put(ContextKey.METRICS_KEY, key);
|
||||
this.collectTaskTimeoutMonitor.putMetrics(key, new CollectTaskTimeoutMonitor.MetricsTime(startTime, data, timeout));
|
||||
|
||||
|
||||
CollectRep.MetricsData.Builder fetchedData = this.fetchData(job, data);
|
||||
if (fetchedData.getCode() != CollectRep.Code.SUCCESS && CommonConstants.AVAILABLE_METRICS == data.getPriority()) {
|
||||
context.setStatus(ContextStatus.TRUNCATE_HANDLER);
|
||||
}
|
||||
return fetchedData;
|
||||
}
|
||||
|
||||
private void setNewThreadName(long monitorId, String app, long startTime, Metrics metrics) {
|
||||
String builder = monitorId + "-" + app + "-" + metrics.getName() + "-" + String.valueOf(startTime).substring(9);
|
||||
Thread.currentThread().setName(builder);
|
||||
}
|
||||
|
||||
private CollectRep.MetricsData.Builder fetchData(Job job, Metrics metrics) {
|
||||
CollectRep.MetricsData.Builder response = CollectRep.MetricsData.newBuilder();
|
||||
response.setApp(job.getApp())
|
||||
.setId(job.getMonitorId())
|
||||
.setTenantId(job.getTenantId())
|
||||
.setLabels(job.getLabels())
|
||||
.setAnnotations(job.getAnnotations())
|
||||
.addMetadataAll(job.getMetadata());
|
||||
|
||||
//todo transcribe Prometheus to different chain
|
||||
// for prometheus auto or proxy mode
|
||||
// if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(metrics.getProtocol())) {
|
||||
// List<CollectRep.MetricsData> metricsData = PrometheusAutoCollectImpl.getInstance().collect(response, metrics);
|
||||
// validateResponse(metricsData == null ? null : metricsData.stream().findFirst().orElse(null));
|
||||
// collectDataDispatch.dispatchCollectData(timeout, metrics, metricsData);
|
||||
// return null;
|
||||
// }
|
||||
|
||||
response.setMetrics(metrics.getName());
|
||||
// According to the metrics collection protocol, application type, etc.,
|
||||
// dispatch to the real application metrics collection implementation class
|
||||
AbstractCollect abstractCollect = CollectStrategyFactory.invoke(metrics.getProtocol());
|
||||
if (abstractCollect == null) {
|
||||
log.error("[Dispatcher] - not support this: app: {}, metrics: {}, protocol: {}.", job.getApp(), metrics.getName(), metrics.getProtocol());
|
||||
response.setCode(CollectRep.Code.FAIL);
|
||||
response.setMsg("not support " + job.getApp() + ", " + metrics.getName() + ", " + metrics.getProtocol());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
abstractCollect.preCheck(metrics);
|
||||
abstractCollect.collect(response, metrics);
|
||||
} catch (Exception e) {
|
||||
String msg = e.getMessage();
|
||||
if (msg == null && e.getCause() != null) {
|
||||
msg = e.getCause().getMessage();
|
||||
}
|
||||
if (e instanceof IllegalArgumentException) {
|
||||
log.error("[Metrics PreCheck]: {}.", msg, e);
|
||||
} else {
|
||||
log.error("[Metrics Collect]: {}.", msg, e);
|
||||
}
|
||||
response.setCode(CollectRep.Code.FAIL);
|
||||
if (msg != null) {
|
||||
response.setMsg(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.handler;
|
||||
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.handler.impl.AbstractBatchDataStream;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class DynamicSubTaskCollectMetricsDataDataStream extends AbstractBatchDataStream<Metrics, CollectRep.MetricsData.Builder> {
|
||||
@Override
|
||||
public CollectRep.MetricsData.Builder executeWithResponse(Context context, Metrics data) {
|
||||
//todo 动态拆分
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* 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.listener;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.jexl3.JexlExpression;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.dispatch.unit.UnitConvert;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundListener;
|
||||
import org.apache.hertzbeat.collector.util.CollectUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.hertzbeat.common.util.JexlExpressionRunner;
|
||||
import org.apache.hertzbeat.common.util.Pair;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class CalculateFieldsListener implements ContextBoundListener<CollectRep.MetricsData.Builder> {
|
||||
private List<UnitConvert> unitConvertList;
|
||||
|
||||
@Override
|
||||
public void execute(Context context, CollectRep.MetricsData.Builder data) {
|
||||
Metrics metrics = context.get(ContextKey.METRICS);
|
||||
|
||||
this.calculateFields(metrics, unitConvertList, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the real metrics value according to the calculates and aliasFields configuration
|
||||
*
|
||||
* @param metrics Metrics configuration
|
||||
* @param collectData Data collection
|
||||
*/
|
||||
private void calculateFields(Metrics metrics, List<UnitConvert> unitConvertList, CollectRep.MetricsData.Builder collectData) {
|
||||
collectData.setPriority(metrics.getPriority());
|
||||
List<CollectRep.Field> fieldList = new LinkedList<>();
|
||||
for (Metrics.Field field : metrics.getFields()) {
|
||||
CollectRep.Field.Builder fieldBuilder = CollectRep.Field.newBuilder();
|
||||
fieldBuilder.setName(field.getField()).setType(field.getType()).setLabel(field.isLabel());
|
||||
if (field.getUnit() != null) {
|
||||
fieldBuilder.setUnit(field.getUnit());
|
||||
}
|
||||
fieldList.add(fieldBuilder.build());
|
||||
}
|
||||
collectData.addAllFields(fieldList);
|
||||
List<CollectRep.ValueRow> aliasRowList = collectData.getValuesList();
|
||||
if (aliasRowList == null || aliasRowList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
collectData.clearValues();
|
||||
// Preprocess calculates first
|
||||
if (metrics.getCalculates() == null) {
|
||||
metrics.setCalculates(Collections.emptyList());
|
||||
}
|
||||
// eg: database_pages=Database pages unconventional mapping
|
||||
Map<String, String> fieldAliasMap = new HashMap<>(8);
|
||||
Map<String, JexlExpression> fieldExpressionMap = metrics.getCalculates()
|
||||
.stream()
|
||||
.map(cal -> transformCal(cal, fieldAliasMap))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(arr -> (String) arr[0], arr -> (JexlExpression) arr[1], (oldValue, newValue) -> newValue));
|
||||
|
||||
if (metrics.getUnits() == null) {
|
||||
metrics.setUnits(Collections.emptyList());
|
||||
}
|
||||
Map<String, Pair<String, String>> fieldUnitMap = metrics.getUnits()
|
||||
.stream()
|
||||
.map(this::transformUnit)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(arr -> (String) arr[0], arr -> (Pair<String, String>) arr[1], (oldValue, newValue) -> newValue));
|
||||
|
||||
List<Metrics.Field> fields = metrics.getFields();
|
||||
List<String> aliasFields = Optional.ofNullable(metrics.getAliasFields()).orElseGet(Collections::emptyList);
|
||||
Map<String, String> aliasFieldValueMap = new HashMap<>(8);
|
||||
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||
Map<String, Object> stringTypefieldValueMap = new HashMap<>(8);
|
||||
Map<String, String> aliasFieldUnitMap = new HashMap<>(8);
|
||||
CollectRep.ValueRow.Builder realValueRowBuilder = CollectRep.ValueRow.newBuilder();
|
||||
for (CollectRep.ValueRow aliasRow : aliasRowList) {
|
||||
for (int aliasIndex = 0; aliasIndex < aliasFields.size(); aliasIndex++) {
|
||||
String aliasFieldValue = aliasRow.getColumns(aliasIndex);
|
||||
String aliasField = aliasFields.get(aliasIndex);
|
||||
if (!CommonConstants.NULL_VALUE.equals(aliasFieldValue)) {
|
||||
aliasFieldValueMap.put(aliasField, aliasFieldValue);
|
||||
// whether the alias field is a number
|
||||
CollectUtil.DoubleAndUnit doubleAndUnit = CollectUtil
|
||||
.extractDoubleAndUnitFromStr(aliasFieldValue);
|
||||
if (doubleAndUnit != null && doubleAndUnit.getValue() != null) {
|
||||
fieldValueMap.put(aliasField, doubleAndUnit.getValue());
|
||||
if (doubleAndUnit.getUnit() != null) {
|
||||
aliasFieldUnitMap.put(aliasField, doubleAndUnit.getUnit());
|
||||
}
|
||||
} else {
|
||||
fieldValueMap.put(aliasField, aliasFieldValue);
|
||||
}
|
||||
stringTypefieldValueMap.put(aliasField, aliasFieldValue);
|
||||
} else {
|
||||
fieldValueMap.put(aliasField, null);
|
||||
stringTypefieldValueMap.put(aliasField, null);
|
||||
}
|
||||
}
|
||||
|
||||
for (Metrics.Field field : fields) {
|
||||
String realField = field.getField();
|
||||
JexlExpression expression = fieldExpressionMap.get(realField);
|
||||
String value = null;
|
||||
String aliasFieldUnit = null;
|
||||
if (expression != null) {
|
||||
try {
|
||||
Map<String, Object> context;
|
||||
if (CommonConstants.TYPE_STRING == field.getType()) {
|
||||
context = stringTypefieldValueMap;
|
||||
} else {
|
||||
for (Map.Entry<String, String> unitEntry : aliasFieldUnitMap.entrySet()) {
|
||||
if (expression.getSourceText().contains(unitEntry.getKey())) {
|
||||
aliasFieldUnit = unitEntry.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
context = fieldValueMap;
|
||||
}
|
||||
|
||||
// Also executed when valueList is empty, covering pure string assignment expressions
|
||||
Object objValue = JexlExpressionRunner.evaluate(expression, context);
|
||||
|
||||
if (objValue != null) {
|
||||
value = String.valueOf(objValue);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[calculates execute warning, use original value.] {}", e.getMessage());
|
||||
value = Optional.ofNullable(fieldValueMap.get(expression.getSourceText()))
|
||||
.map(String::valueOf)
|
||||
.orElse(null);
|
||||
}
|
||||
} else {
|
||||
// does not exist then map the alias value
|
||||
String aliasField = fieldAliasMap.get(realField);
|
||||
if (aliasField != null) {
|
||||
value = aliasFieldValueMap.get(aliasField);
|
||||
} else {
|
||||
value = aliasFieldValueMap.get(realField);
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
final byte fieldType = field.getType();
|
||||
if (fieldType == CommonConstants.TYPE_NUMBER) {
|
||||
CollectUtil.DoubleAndUnit doubleAndUnit = CollectUtil
|
||||
.extractDoubleAndUnitFromStr(value);
|
||||
final Double tempValue = doubleAndUnit == null ? null : doubleAndUnit.getValue();
|
||||
value = tempValue == null ? null : String.valueOf(tempValue);
|
||||
aliasFieldUnit = doubleAndUnit == null ? null : doubleAndUnit.getUnit();
|
||||
} else if (fieldType == CommonConstants.TYPE_TIME) {
|
||||
final int tempValue;
|
||||
value = (tempValue = CommonUtil.parseTimeStrToSecond(value)) == -1 ? null : String.valueOf(tempValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Pair<String, String> unitPair = fieldUnitMap.get(realField);
|
||||
if (aliasFieldUnit != null) {
|
||||
if (unitPair != null) {
|
||||
unitPair.setLeft(aliasFieldUnit);
|
||||
} else if (field.getUnit() != null && !aliasFieldUnit.equalsIgnoreCase(field.getUnit())) {
|
||||
unitPair = Pair.of(aliasFieldUnit, field.getUnit());
|
||||
}
|
||||
}
|
||||
if (value != null && unitPair != null) {
|
||||
for (UnitConvert unitConvert : unitConvertList) {
|
||||
if (unitConvert.checkUnit(unitPair.getLeft()) && unitConvert.checkUnit(unitPair.getRight())) {
|
||||
value = unitConvert.convert(value, unitPair.getLeft(), unitPair.getRight());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle metrics values that may have units such as 34%, 34Mb, and limit values to 4 decimal places
|
||||
if (CommonConstants.TYPE_NUMBER == field.getType()) {
|
||||
value = CommonUtil.parseDoubleStr(value, field.getUnit());
|
||||
}
|
||||
if (value == null) {
|
||||
value = CommonConstants.NULL_VALUE;
|
||||
}
|
||||
realValueRowBuilder.addColumn(value);
|
||||
}
|
||||
aliasFieldValueMap.clear();
|
||||
fieldValueMap.clear();
|
||||
aliasFieldUnitMap.clear();
|
||||
stringTypefieldValueMap.clear();
|
||||
CollectRep.ValueRow realValueRow = realValueRowBuilder.build();
|
||||
realValueRowBuilder.clear();
|
||||
// apply filter calculation to the real value row
|
||||
if (!CollectionUtils.isEmpty(metrics.getFilters())) {
|
||||
Map<String, Object> contextMap = new HashMap<>(8);
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
Metrics.Field field = fields.get(i);
|
||||
String value = realValueRow.getColumns(i);
|
||||
contextMap.put(field.getField(), value);
|
||||
}
|
||||
boolean isMatch = false;
|
||||
for (String filterExpr : metrics.getFilters()) {
|
||||
try {
|
||||
JexlExpression expression = JexlExpressionRunner.compile(filterExpr);
|
||||
if ((Boolean) JexlExpressionRunner.evaluate(expression, contextMap)) {
|
||||
isMatch = true;
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[metrics data row filters execute warning] {}.", e.getMessage());
|
||||
}
|
||||
}
|
||||
if (!isMatch) {
|
||||
// ignore this data row
|
||||
continue;
|
||||
}
|
||||
}
|
||||
collectData.addValueRow(realValueRow);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cal cal
|
||||
* @param fieldAliasMap field alias map
|
||||
* @return expr
|
||||
*/
|
||||
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;
|
||||
try {
|
||||
expression = JexlExpressionRunner.compile(expressionStr);
|
||||
} catch (Exception e) {
|
||||
fieldAliasMap.put(field, expressionStr);
|
||||
return null;
|
||||
}
|
||||
return new Object[]{field, expression};
|
||||
}
|
||||
|
||||
/**
|
||||
* transform unit
|
||||
*
|
||||
* @param unit unit
|
||||
* @return units
|
||||
*/
|
||||
private Object[] transformUnit(String unit) {
|
||||
int equalIndex = unit.indexOf("=");
|
||||
int arrowIndex = unit.indexOf("->");
|
||||
if (equalIndex < 0 || arrowIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
String field = unit.substring(0, equalIndex).trim();
|
||||
String originUnit = unit.substring(equalIndex + 1, arrowIndex).trim();
|
||||
String newUnit = unit.substring(arrowIndex + 2).trim();
|
||||
return new Object[]{field, Pair.of(originUnit, newUnit)};
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.listener;
|
||||
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.constants.ContextStatus;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundListener;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class CommonMetricsDataListener<T> implements ContextBoundListener<T> {
|
||||
@Override
|
||||
public void execute(Context context, T data) {
|
||||
Timeout timeout = context.get(ContextKey.TIMEOUT);
|
||||
|
||||
if (timeout == null || timeout.isCancelled()) {
|
||||
context.setStatus(ContextStatus.STOP);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.collector.listener;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundListener;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.queue.CommonDataQueue;
|
||||
|
||||
/**
|
||||
* 周期任务专用
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class MetricsDataDeliveryListener implements ContextBoundListener<CollectRep.MetricsData.Builder> {
|
||||
private CommonDataQueue commonDataQueue;
|
||||
|
||||
@Override
|
||||
public void execute(Context context, CollectRep.MetricsData.Builder data) {
|
||||
Job job = context.get(ContextKey.JOB);
|
||||
CollectRep.MetricsData metricsData = data.build();
|
||||
|
||||
cyclicJobDebugLog(job, metricsData);
|
||||
|
||||
sendToQueue(job, metricsData);
|
||||
}
|
||||
|
||||
private void sendToQueue(Job job, CollectRep.MetricsData metricsData) {
|
||||
if (job.isSd()) {
|
||||
CollectRep.MetricsData sdMetricsData = CollectRep.MetricsData.newBuilder(metricsData).build();
|
||||
commonDataQueue.sendServiceDiscoveryData(sdMetricsData);
|
||||
}
|
||||
commonDataQueue.sendMetricsData(metricsData);
|
||||
}
|
||||
|
||||
private void cyclicJobDebugLog(Job job, CollectRep.MetricsData metricsData) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Cyclic Job: {} - {} - {}", job.getMonitorId(), job.getApp(), metricsData.getMetrics());
|
||||
metricsDataDebugLog(metricsData);
|
||||
}
|
||||
}
|
||||
|
||||
private void metricsDataDebugLog(CollectRep.MetricsData metricsData) {
|
||||
for (CollectRep.ValueRow valueRow : metricsData.getValues()) {
|
||||
for (CollectRep.Field field : metricsData.getFields()) {
|
||||
log.debug("Field-->{},Value-->{}", field.getName(), valueRow.getColumns(metricsData.getFields().indexOf(field)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.collector.listener;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.dispatch.CollectTaskTimeoutMonitor;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundListener;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class RemoveTimeoutMonitorListener implements ContextBoundListener<Object> {
|
||||
private CollectTaskTimeoutMonitor collectTaskTimeoutMonitor;
|
||||
|
||||
@Override
|
||||
public void execute(Context context, Object data) {
|
||||
String metricsKey = context.get(ContextKey.METRICS_KEY);
|
||||
if (StringUtils.isBlank(metricsKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
collectTaskTimeoutMonitor.removeMetrics(metricsKey);
|
||||
}
|
||||
}
|
||||
+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.collector.listener;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundDataStream;
|
||||
import org.apache.hertzbeat.collector.timer.TimerDispatch;
|
||||
import org.apache.hertzbeat.collector.timer.WheelTimerTask;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 周期任务专用
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class RerunDataStream implements ContextBoundDataStream<Object> {
|
||||
private TimerDispatch timerDispatch;
|
||||
|
||||
@Override
|
||||
public void execute(Context context, Object data) {
|
||||
Job job = context.get(ContextKey.JOB);
|
||||
Timeout timeout = context.get(ContextKey.TIMEOUT);
|
||||
|
||||
if (!timeout.isCancelled()) {
|
||||
long spendTime = System.currentTimeMillis() - job.getDispatchTime();
|
||||
long interval = job.getInterval() - spendTime / 1000L;
|
||||
interval = interval <= 0 ? 0 : interval;
|
||||
timerDispatch.cyclicJob((WheelTimerTask) timeout.task(), interval, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whenException(Context context, Object data, Throwable throwable) {
|
||||
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.listener;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundListener;
|
||||
import org.apache.hertzbeat.collector.timer.TimerDispatch;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
|
||||
/**
|
||||
* 一次性任务专用
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class ResponseJobDataListener implements ContextBoundListener<CollectRep.MetricsData.Builder> {
|
||||
private TimerDispatch timerDispatch;
|
||||
|
||||
@Override
|
||||
public void execute(Context context, CollectRep.MetricsData.Builder data) {
|
||||
Job job = context.get(ContextKey.JOB);
|
||||
CollectRep.MetricsData metricsData = data.build();
|
||||
|
||||
oneTimeJobDebugLog(metricsData);
|
||||
timerDispatch.responseSyncJobData(job.getId(), Lists.newArrayList(metricsData));
|
||||
}
|
||||
|
||||
private void oneTimeJobDebugLog(CollectRep.MetricsData metricsData) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("One-time Job: {}", metricsData.getMetrics());
|
||||
metricsDataDebugLog(metricsData);
|
||||
}
|
||||
}
|
||||
|
||||
private void metricsDataDebugLog(CollectRep.MetricsData metricsData) {
|
||||
for (CollectRep.ValueRow valueRow : metricsData.getValues()) {
|
||||
for (CollectRep.Field field : metricsData.getFields()) {
|
||||
log.debug("Field-->{},Value-->{}", field.getName(), valueRow.getColumns(metricsData.getFields().indexOf(field)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.collector.listener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundListener;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Calvin
|
||||
* @date 9/1/2025
|
||||
*/
|
||||
@Slf4j
|
||||
public class ValidateResponseListener implements ContextBoundListener<CollectRep.MetricsData.Builder> {
|
||||
|
||||
@Override
|
||||
public void execute(Context context, CollectRep.MetricsData.Builder data) {
|
||||
long startTime = context.get(ContextKey.METRICS_COLLECT_START_TIME);
|
||||
Metrics metrics = context.get(ContextKey.METRICS);
|
||||
|
||||
this.validateResponse(startTime, metrics, data);
|
||||
}
|
||||
|
||||
private void validateResponse(long startTime, Metrics metrics, CollectRep.MetricsData.Builder builder) {
|
||||
long endTime = System.currentTimeMillis();
|
||||
builder.setTime(endTime);
|
||||
long allTime = endTime - startTime;
|
||||
if (builder.getCode() != CollectRep.Code.SUCCESS) {
|
||||
log.info("[Metrics: {}][Collect Failed, Run {}ms] Reason: {}", metrics.getName(), allTime, builder.getMsg());
|
||||
} else {
|
||||
log.info("[Metrics: {}][Collect Success, Run {}ms].", metrics.getName(), allTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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:
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.constants;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.apache.hertzbeat.common.entity.collector.CollectorMetaData;
|
||||
import org.apache.hertzbeat.common.entity.job.Job;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.timer.Timeout;
|
||||
|
||||
public enum ContextKey {
|
||||
META_DATA(CollectorMetaData.class),
|
||||
JOB(Job.class),
|
||||
TIMEOUT(Timeout.class),
|
||||
METRICS_COLLECT_START_TIME(Long.class),
|
||||
METRICS(Metrics.class),
|
||||
METRICS_KEY(String.class),
|
||||
;
|
||||
|
||||
@Getter
|
||||
private final Class<?> clazz;
|
||||
|
||||
ContextKey(Class<?> clazz) {
|
||||
this.clazz = clazz;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.constants;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public enum ContextStatus {
|
||||
WAITING, RUNNING, STOP, TRUNCATE_HANDLER
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.constants;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public enum HandlerType {
|
||||
NORMAL, ON_COMPLETE
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.context;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.hertzbeat.collector.constants.ContextStatus;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractInmemoryContext implements Context {
|
||||
protected final AtomicReference<ContextStatus> contextStatus = new AtomicReference<>(ContextStatus.WAITING);
|
||||
protected final Map<Object, Object> map = new ConcurrentHashMap<>();
|
||||
@Getter
|
||||
@Setter
|
||||
private Throwable error;
|
||||
|
||||
|
||||
@Override
|
||||
public <T> void put(Object key, T value) {
|
||||
this.map.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T remove(Object key) {
|
||||
return (T) this.map.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T get(Object key) {
|
||||
return (T) this.map.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getOrDefault(Object key, T defaultValue) {
|
||||
return (T) this.map.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
return this.map.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContextStatus getStatus() {
|
||||
return contextStatus.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(ContextStatus status) {
|
||||
contextStatus.set(status);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
import org.apache.hertzbeat.collector.constants.ContextStatus;
|
||||
|
||||
/**
|
||||
* 只维护与上下文的元数据
|
||||
*/
|
||||
public interface Context extends ContextView, ContextOperation {
|
||||
ContextStatus getStatus();
|
||||
|
||||
void setStatus(ContextStatus status);
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
import io.micrometer.common.lang.Nullable;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface ContextOperation {
|
||||
<T> void put(Object key, T value);
|
||||
|
||||
<T> T remove(Object key);
|
||||
|
||||
@Nullable
|
||||
Throwable getError();
|
||||
|
||||
void setError(Throwable error);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
/**
|
||||
* 对上下文内容的查询操作
|
||||
*/
|
||||
public interface ContextView {
|
||||
<T> T get(Object key);
|
||||
|
||||
<T> T getOrDefault(Object key, T defaultValue);
|
||||
|
||||
boolean hasKey(Object key);
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.context.impl;
|
||||
|
||||
import org.apache.hertzbeat.collector.context.AbstractInmemoryContext;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class DefaultContext extends AbstractInmemoryContext {
|
||||
private DefaultContext() {
|
||||
}
|
||||
|
||||
public static DefaultContext newInstance() {
|
||||
return new DefaultContext();
|
||||
}
|
||||
}
|
||||
-9
@@ -36,13 +36,4 @@ public interface CollectDataDispatch {
|
||||
*/
|
||||
void dispatchCollectData(Timeout timeout, Metrics metrics, CollectRep.MetricsData metricsData);
|
||||
|
||||
/**
|
||||
* Processing and distributing collection result data
|
||||
*
|
||||
* @param timeout time wheel timeout
|
||||
* @param metrics The following metrics collection tasks
|
||||
* @param metricsDataList Collect result data
|
||||
*/
|
||||
void dispatchCollectData(Timeout timeout, Metrics metrics, List<CollectRep.MetricsData> metricsDataList);
|
||||
|
||||
}
|
||||
|
||||
+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()
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.handler;
|
||||
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.hertzbeat.collector.constants.HandlerType;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.dispatch.WorkerPool;
|
||||
import org.apache.hertzbeat.collector.handler.impl.AbstractListenerBoundDataStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@Slf4j
|
||||
public class ChainBootstrap {
|
||||
@Setter
|
||||
private Context context;
|
||||
private TaskChain<?> taskChain;
|
||||
private WorkerPool workerPool;
|
||||
private final List<ContextBoundDataStream> contextBoundDataStreamList = new ArrayList<>();
|
||||
private final List<ContextBoundDataStream> onCompleteContextBoundDataStreamList = new ArrayList<>();
|
||||
private final List<ContextBoundListener> dataListenerList = new ArrayList<>();
|
||||
private final List<ContextBoundListener> onCompleteListenerList = new ArrayList<>();
|
||||
|
||||
public static ChainBootstrap withContext(Context context) {
|
||||
ChainBootstrap bootstrap = new ChainBootstrap();
|
||||
bootstrap.setContext(context);
|
||||
return bootstrap;
|
||||
}
|
||||
|
||||
public ChainBootstrap withChain(TaskChain<?> taskChain) {
|
||||
this.taskChain = taskChain;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChainBootstrap withWorkerPool(WorkerPool workerPool) {
|
||||
this.workerPool = workerPool;
|
||||
return this;
|
||||
}
|
||||
|
||||
public <T> ChainBootstrap addContext(Object key, T value) {
|
||||
context.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChainBootstrap addDataStream(ContextBoundDataStream contextBoundDataStream) {
|
||||
contextBoundDataStreamList.add(contextBoundDataStream);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChainBootstrap onComplete(ContextBoundDataStream contextBoundDataStream) {
|
||||
onCompleteContextBoundDataStreamList.add(contextBoundDataStream);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChainBootstrap addListener(ContextBoundListener dataListener) {
|
||||
dataListenerList.add(dataListener);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChainBootstrap onEachDataStreamComplete(ContextBoundListener dataListener) {
|
||||
onCompleteListenerList.add(dataListener);
|
||||
return this;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (taskChain == null || context == null) {
|
||||
log.error("Failed to start chain boostrap due to null value of Context or TaskChain");
|
||||
return;
|
||||
}
|
||||
|
||||
for (ContextBoundDataStream contextBoundDataStream : contextBoundDataStreamList) {
|
||||
if (contextBoundDataStream instanceof AbstractListenerBoundDataStream listenerBoundHandler) {
|
||||
if (CollectionUtils.isNotEmpty(dataListenerList)) {
|
||||
listenerBoundHandler.getDataListenerList().addAll(dataListenerList);
|
||||
}
|
||||
if (CollectionUtils.isNotEmpty(onCompleteListenerList)) {
|
||||
listenerBoundHandler.getOnCompleteListenerList().addAll(onCompleteListenerList);
|
||||
}
|
||||
}
|
||||
|
||||
taskChain.addLast(HandlerType.NORMAL, contextBoundDataStream);
|
||||
}
|
||||
|
||||
onCompleteContextBoundDataStreamList.forEach(handler -> taskChain.addLast(HandlerType.ON_COMPLETE, handler));
|
||||
|
||||
if (workerPool != null) {
|
||||
workerPool.executeJob(() -> taskChain.execute(context));
|
||||
} else {
|
||||
taskChain.execute(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.handler;
|
||||
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface ContextBoundDataStream<T> {
|
||||
void execute(Context context, T data);
|
||||
|
||||
void whenException(Context context, T data, Throwable throwable);
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.handler;
|
||||
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
|
||||
/**
|
||||
*/
|
||||
public interface ContextBoundListener<T> {
|
||||
void execute(Context context, T data);
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.handler;
|
||||
|
||||
import org.apache.hertzbeat.collector.constants.HandlerType;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public interface TaskChain<T> {
|
||||
void execute(Context context);
|
||||
|
||||
void execute(Context context, T data);
|
||||
|
||||
void addLast(HandlerType handlerType, ContextBoundDataStream<T> handler);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.handler.impl;
|
||||
|
||||
import lombok.Setter;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractBatchDataStream<T, R> extends AbstractListenerBoundDataStream<T, R> {
|
||||
@Setter
|
||||
protected List<T> sourceDataList;
|
||||
|
||||
@Override
|
||||
public void execute(Context context, T data) {
|
||||
for (T t : sourceDataList) {
|
||||
super.execute(context, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.collector.handler.impl;
|
||||
|
||||
import org.apache.hertzbeat.collector.constants.HandlerType;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundDataStream;
|
||||
import org.apache.hertzbeat.collector.handler.TaskChain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractContextBoundTaskChain<T> implements TaskChain<T> {
|
||||
protected final Map<HandlerType, List<ContextBoundDataStream<T>>> contextBoundHandlerMap = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void addLast(HandlerType handlerType, ContextBoundDataStream<T> handler) {
|
||||
if (!contextBoundHandlerMap.containsKey(handlerType)) {
|
||||
contextBoundHandlerMap.put(handlerType, new ArrayList<>());
|
||||
}
|
||||
|
||||
contextBoundHandlerMap.get(handlerType).add(handler);
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.handler.impl;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.hertzbeat.collector.constants.ContextKey;
|
||||
import org.apache.hertzbeat.collector.constants.ContextStatus;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundDataStream;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractListenerBoundDataStream<T, R> implements ContextBoundDataStream<T> {
|
||||
@Getter
|
||||
private final List<? extends ContextBoundListener<R>> dataListenerList = new ArrayList<>();
|
||||
@Getter
|
||||
private final List<? extends ContextBoundListener<R>> onCompleteListenerList = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void execute(Context context, T data) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
context.put(ContextKey.METRICS_COLLECT_START_TIME, startTime);
|
||||
|
||||
R executeResult = executeWithResponse(context, data);
|
||||
|
||||
runListener(context, executeResult);
|
||||
|
||||
runOnCompleteListener(context, executeResult);
|
||||
}
|
||||
|
||||
public R executeWithResponse(Context context, T data) {
|
||||
// no-op
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void whenException(Context context, T data, Throwable throwable) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void runListener(Context context, R executeResult) {
|
||||
if (CollectionUtils.isEmpty(dataListenerList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContextStatus.STOP.equals(context.getStatus())) {
|
||||
return;
|
||||
}
|
||||
|
||||
//todo 异常处理
|
||||
for (ContextBoundListener<R> listener : dataListenerList) {
|
||||
listener.execute(context, executeResult);
|
||||
|
||||
if (ContextStatus.STOP.equals(context.getStatus())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void runOnCompleteListener(Context context, R executeResult) {
|
||||
if (CollectionUtils.isEmpty(onCompleteListenerList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ContextBoundListener<R> listener : onCompleteListenerList) {
|
||||
listener.execute(context, executeResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.handler.impl;
|
||||
|
||||
import org.apache.hertzbeat.collector.constants.ContextStatus;
|
||||
import org.apache.hertzbeat.collector.constants.HandlerType;
|
||||
import org.apache.hertzbeat.collector.context.Context;
|
||||
import org.apache.hertzbeat.collector.handler.ContextBoundDataStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class BatchExecuteTaskChain<T> extends AbstractContextBoundTaskChain<T> {
|
||||
@Override
|
||||
public void execute(Context context) {
|
||||
this.execute(context, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Context context, T data) {
|
||||
context.setStatus(ContextStatus.RUNNING);
|
||||
|
||||
for (ContextBoundDataStream<T> contextBoundDataStream : contextBoundHandlerMap.getOrDefault(HandlerType.NORMAL, new ArrayList<>())) {
|
||||
runHandler(context, data, contextBoundDataStream);
|
||||
|
||||
if (ContextStatus.TRUNCATE_HANDLER.equals(context.getStatus()) || ContextStatus.STOP.equals(context.getStatus())) {
|
||||
break;
|
||||
}
|
||||
|
||||
// in order to init error info for the next loop
|
||||
context.setError(null);
|
||||
}
|
||||
|
||||
contextBoundHandlerMap.getOrDefault(HandlerType.ON_COMPLETE, new ArrayList<>()).forEach(handler -> runHandler(context, data, handler));
|
||||
}
|
||||
|
||||
private static <T> void runHandler(Context context, T data, ContextBoundDataStream<T> contextBoundDataStream) {
|
||||
try {
|
||||
contextBoundDataStream.execute(context, data);
|
||||
} catch (Exception exception) {
|
||||
context.setError(exception);
|
||||
contextBoundDataStream.whenException(context, data, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user