mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 18:19:02 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b77222765c | ||
|
|
aeb4188f83 | ||
|
|
311cb131fd | ||
|
|
21558e9b4f | ||
|
|
8d1f306433 | ||
|
|
e98af9527e | ||
|
|
811a07553b | ||
|
|
9fd58ced4a | ||
|
|
a331c045ae | ||
|
|
63d3d18428 | ||
|
|
cec8d8b396 | ||
|
|
9d68d16a78 | ||
|
|
290474a715 | ||
|
|
0dbc7347b4 | ||
|
|
e2a2d5eaec | ||
|
|
356cca295d | ||
|
|
5e09c6d9ae | ||
|
|
54caa84fc1 | ||
|
|
d2a332f533 | ||
|
|
dc2efdf1c2 | ||
|
|
4defb733bf | ||
|
|
f8e85ec4f1 | ||
|
|
3468739504 | ||
|
|
5a92161cd9 | ||
|
|
4ce6ffd4ee | ||
|
|
9f34d201ae | ||
|
|
4b8ccf2fea | ||
|
|
adf27d2544 | ||
|
|
033b9b0706 | ||
|
|
01be0c7a1d | ||
|
|
0fdd76c0de | ||
|
|
1a38fcf776 | ||
|
|
268e111024 | ||
|
|
484dc5f70f | ||
|
|
b7bd9eb305 | ||
|
|
a5f874e682 | ||
|
|
fcf52b383a | ||
|
|
e93c3a9054 | ||
|
|
84f222b516 | ||
|
|
53bb58da5f | ||
|
|
3688974610 | ||
|
|
af35043663 | ||
|
|
84c28229b2 | ||
|
|
1a3e614209 | ||
|
|
3df0e56da9 | ||
|
|
b5b4771e1a | ||
|
|
b8c5ae4d40 | ||
|
|
207f2d958c | ||
|
|
398a40bba4 | ||
|
|
bb14681621 | ||
|
|
d3a080fbaf | ||
|
|
ce6f9feac6 | ||
|
|
7d18118bbd | ||
|
|
d5d2b6bc71 | ||
|
|
e11a4da2ac | ||
|
|
58360442ca | ||
|
|
4d76c0abf4 | ||
|
|
f761e18739 | ||
|
|
6ac073a54e | ||
|
|
65a8d19c5d | ||
|
|
060b4d875d |
@@ -2298,6 +2298,69 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yasminvo",
|
||||
"name": "yasminvo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/107528848?v=4",
|
||||
"profile": "https://github.com/yasminvo",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "notbugggg",
|
||||
"name": "不关银渐层的事哦",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/147966331?v=4",
|
||||
"profile": "https://github.com/notbugggg",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yyahang",
|
||||
"name": "yyahang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/90464876?v=4",
|
||||
"profile": "https://github.com/yyahang",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JuJinPark",
|
||||
"name": "jujin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/44892459?v=4",
|
||||
"profile": "https://github.com/JuJinPark",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "LL-LIN",
|
||||
"name": "LL-LIN",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/43002118?v=4",
|
||||
"profile": "https://github.com/LL-LIN",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bigcyy",
|
||||
"name": "Yang Chen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/73413979?v=4",
|
||||
"profile": "https://bigcyy.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sarthakeash",
|
||||
"name": "Sarthak Arora",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/74091160?v=4",
|
||||
"profile": "https://github.com/sarthakeash",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
|
||||
/home export-ignore
|
||||
/hip export-ignore
|
||||
/template-marketplace export-ignore
|
||||
/.github export-ignore
|
||||
/.idea export-ignore
|
||||
/.devcontainer export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
@@ -31,6 +31,8 @@ name: "CodeQL"
|
||||
on:
|
||||
schedule:
|
||||
- cron: '21 13 * * 4'
|
||||
push:
|
||||
branches: [ action* ]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
|
||||
@@ -19,7 +19,7 @@ name: DOC CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, dev ]
|
||||
branches: [ master, dev, action* ]
|
||||
paths:
|
||||
- 'home/**'
|
||||
pull_request:
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
|
||||
name: 'HertzBeat Doc Pdf Builder'
|
||||
on:
|
||||
page_build
|
||||
push:
|
||||
branches: [ page_build* ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -22,7 +22,7 @@ name: Frontend CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, dev ]
|
||||
branches: [ master, dev, action* ]
|
||||
paths:
|
||||
- 'web-app/**'
|
||||
pull_request:
|
||||
|
||||
@@ -22,7 +22,7 @@ name: License Checker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, dev ]
|
||||
branches: [ master, dev, action* ]
|
||||
pull_request:
|
||||
branches: [ master, dev ]
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
run: |
|
||||
mvn clean install
|
||||
mvn clean package -Prelease -DskipTests
|
||||
cd collector
|
||||
cd hertzbeat-collector
|
||||
mvn clean package -Pcluster -DskipTests
|
||||
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -13,6 +13,7 @@ mvnw.bat
|
||||
package-lock.json
|
||||
*.zip
|
||||
jdk/
|
||||
data/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
@@ -54,3 +55,5 @@ node_modules
|
||||
application-dev.yml
|
||||
application-mysql.yml
|
||||
application-pg.yml
|
||||
/package.json
|
||||
/yarn.lock
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
FROM gitpod/workspace-full
|
||||
|
||||
USER gitpod
|
||||
|
||||
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh && \
|
||||
sdk install java 17.0.3-ms && \
|
||||
sdk default java 17.0.3-ms"
|
||||
+3
-2
@@ -1,3 +1,6 @@
|
||||
image:
|
||||
file: .gitpod.Dockerfile
|
||||
|
||||
tasks:
|
||||
|
||||
- name: Run backend
|
||||
@@ -15,8 +18,6 @@ tasks:
|
||||
|
||||
- name: Setup backend
|
||||
init: |
|
||||
sdk install java 17.0.11.fx-zulu < /dev/null
|
||||
sdk default java 17.0.11.fx-zulu < /dev/null
|
||||
mvn clean install -DskipTests
|
||||
command: |
|
||||
gp sync-done setup-backend
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Apache HertzBeat (incubating)
|
||||
Copyright 2024 The Apache Software Foundation
|
||||
Copyright 2024-2025 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
[](https://twitter.com/hertzbeat1024)
|
||||
[](https://x.com/hertzbeat1024)
|
||||
[](https://www.bestpractices.dev/projects/8139)
|
||||
[](https://app.codecov.io/gh/apache/hertzbeat)
|
||||
[](https://hub.docker.com/r/apache/hertzbeat)
|
||||
@@ -507,6 +507,15 @@ Thanks to these wonderful people, welcome to join us:
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -534,7 +543,7 @@ WeChat Public : Search ID `usthecom`.
|
||||
|
||||
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
|
||||
|
||||
[Follow Us Twitter](https://twitter.com/hertzbeat1024)
|
||||
[Follow Us Twitter](https://x.com/hertzbeat1024)
|
||||
|
||||
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||
|
||||
|
||||
+11
-2
@@ -12,7 +12,7 @@
|
||||
|
||||
[](https://discord.gg/Fb6M73htGr)
|
||||
[](https://www.reddit.com/r/hertzbeat/)
|
||||
[](https://twitter.com/hertzbeat1024)
|
||||
[](https://x.com/hertzbeat1024)
|
||||
[](https://www.bestpractices.dev/projects/8139)
|
||||
[](https://app.codecov.io/gh/apache/hertzbeat)
|
||||
[](https://hub.docker.com/r/apache/hertzbeat)
|
||||
@@ -506,6 +506,15 @@ Thanks these wonderful people, welcome to join us:
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/myangle1120"><img src="https://avatars.githubusercontent.com/u/19237013?v=4?s=100" width="100px;" alt="myangle1120"/><br /><sub><b>myangle1120</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=myangle1120" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yasminvo"><img src="https://avatars.githubusercontent.com/u/107528848?v=4?s=100" width="100px;" alt="yasminvo"/><br /><sub><b>yasminvo</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yasminvo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/notbugggg"><img src="https://avatars.githubusercontent.com/u/147966331?v=4?s=100" width="100px;" alt="不关银渐层的事哦"/><br /><sub><b>不关银渐层的事哦</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=notbugggg" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yyahang"><img src="https://avatars.githubusercontent.com/u/90464876?v=4?s=100" width="100px;" alt="yyahang"/><br /><sub><b>yyahang</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=yyahang" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JuJinPark"><img src="https://avatars.githubusercontent.com/u/44892459?v=4?s=100" width="100px;" alt="jujin"/><br /><sub><b>jujin</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=JuJinPark" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/LL-LIN"><img src="https://avatars.githubusercontent.com/u/43002118?v=4?s=100" width="100px;" alt="LL-LIN"/><br /><sub><b>LL-LIN</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=LL-LIN" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bigcyy.github.io/"><img src="https://avatars.githubusercontent.com/u/73413979?v=4?s=100" width="100px;" alt="Yang Chen"/><br /><sub><b>Yang Chen</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=bigcyy" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sarthakeash"><img src="https://avatars.githubusercontent.com/u/74091160?v=4?s=100" width="100px;" alt="Sarthak Arora"/><br /><sub><b>Sarthak Arora</b></sub></a><br /><a href="https://github.com/apache/hertzbeat/commits?author=sarthakeash" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -533,7 +542,7 @@ Thanks these wonderful people, welcome to join us:
|
||||
|
||||
[Reddit Community](https://www.reddit.com/r/hertzbeat/)
|
||||
|
||||
[Follow Us Twitter](https://twitter.com/hertzbeat1024)
|
||||
[Follow Us Twitter](https://x.com/hertzbeat1024)
|
||||
|
||||
[Subscribe YouTube](https://www.youtube.com/channel/UCri75zfWX0GHqJFPENEbLow)
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@
|
||||
"field": "successCode",
|
||||
"type": 4,
|
||||
"paramValue": "200, 201"
|
||||
},
|
||||
{
|
||||
"field": "enableUrlEncoding",
|
||||
"type": 1,
|
||||
"paramValue": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -98,25 +98,6 @@
|
||||
<version>${easy-poi.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<!-- sms -->
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.squareup.okhttp</groupId>
|
||||
<artifactId>logging-interceptor</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.squareup.okhttp</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.huaweicloud.sdk</groupId>
|
||||
<artifactId>huaweicloud-sdk-smn</artifactId>
|
||||
|
||||
+43
-18
@@ -55,6 +55,8 @@ public class RealTimeAlertCalculator {
|
||||
private static final int CALCULATE_THREADS = 3;
|
||||
|
||||
private static final String KEY_INSTANCE = "__instance__";
|
||||
private static final String KEY_INSTANCE_NAME = "__instancename__";
|
||||
private static final String KEY_INSTANCE_HOST = "__instancehost__";
|
||||
private static final String KEY_APP = "__app__";
|
||||
private static final String KEY_METRICS = "__metrics__";
|
||||
private static final String KEY_PRIORITY = "__priority__";
|
||||
@@ -125,10 +127,14 @@ public class RealTimeAlertCalculator {
|
||||
private void calculate(CollectRep.MetricsData metricsData) {
|
||||
long currentTimeMilli = System.currentTimeMillis();
|
||||
String instance = String.valueOf(metricsData.getId());
|
||||
String instanceName = metricsData.getInstanceName();
|
||||
String instanceHost = metricsData.getInstanceHost();
|
||||
String app = metricsData.getApp();
|
||||
String metrics = metricsData.getMetrics();
|
||||
int priority = metricsData.getPriority();
|
||||
int code = metricsData.getCode().getNumber();
|
||||
Map<String, String> labels = metricsData.getLabels();
|
||||
Map<String, String> annotations = metricsData.getAnnotations();
|
||||
List<AlertDefine> thresholds = this.alertDefineService.getRealTimeAlertDefines();
|
||||
// Filter thresholds by app, metrics and instance
|
||||
thresholds = filterThresholdsByAppAndMetrics(thresholds, app, metrics, instance, priority);
|
||||
@@ -137,6 +143,8 @@ public class RealTimeAlertCalculator {
|
||||
}
|
||||
Map<String, Object> commonContext = new HashMap<>(8);
|
||||
commonContext.put(KEY_INSTANCE, instance);
|
||||
commonContext.put(KEY_INSTANCE_NAME, instanceName);
|
||||
commonContext.put(KEY_INSTANCE_HOST, instanceHost);
|
||||
commonContext.put(KEY_APP, app);
|
||||
commonContext.put(KEY_PRIORITY, priority);
|
||||
commonContext.put(KEY_CODE, code);
|
||||
@@ -144,6 +152,9 @@ public class RealTimeAlertCalculator {
|
||||
if (priority == 0) {
|
||||
commonContext.put(KEY_AVAILABLE, metricsData.getCode() == CollectRep.Code.SUCCESS ? UP : DOWN);
|
||||
}
|
||||
if (labels != null) {
|
||||
commonContext.putAll(labels);
|
||||
}
|
||||
List<CollectRep.Field> fields = metricsData.getFields();
|
||||
Map<String, Object> fieldValueMap = new HashMap<>(8);
|
||||
int valueRowCount = metricsData.getValuesCount();
|
||||
@@ -160,21 +171,26 @@ public class RealTimeAlertCalculator {
|
||||
if (StringUtils.isBlank(expr)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> commonFingerPrints = new HashMap<>(8);
|
||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
|
||||
// here use the alert name as finger, not care the alert name may be changed
|
||||
commonFingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_NAME, instanceName);
|
||||
commonFingerPrints.put(CommonConstants.LABEL_INSTANCE_HOST, instanceHost);
|
||||
commonFingerPrints.putAll(define.getLabels());
|
||||
if (labels != null) {
|
||||
commonFingerPrints.putAll(labels);
|
||||
}
|
||||
{
|
||||
// trigger the expr before the metrics data, due the available up down or others
|
||||
try {
|
||||
boolean match = execAlertExpression(fieldValueMap, expr, true);
|
||||
try {
|
||||
Map<String, String> fingerPrints = new HashMap<>(8);
|
||||
fingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
|
||||
// here use the alert name as finger, not care the alert name may be changed
|
||||
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||
fingerPrints.putAll(define.getLabels());
|
||||
if (match) {
|
||||
// If the threshold rule matches, the number of times the threshold has been triggered is determined and an alarm is triggered
|
||||
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
|
||||
afterThresholdRuleMatch(currentTimeMilli, commonFingerPrints, fieldValueMap, define, annotations);
|
||||
} else {
|
||||
handleRecoveredAlert(fingerPrints);
|
||||
handleRecoveredAlert(commonFingerPrints);
|
||||
}
|
||||
// if this threshold pre compile success, ignore blew
|
||||
continue;
|
||||
@@ -192,9 +208,7 @@ public class RealTimeAlertCalculator {
|
||||
fieldValueMap.put(KEY_ROW, valueRowCount);
|
||||
fieldValueMap.putAll(commonContext);
|
||||
fingerPrints.clear();
|
||||
fingerPrints.put(CommonConstants.LABEL_INSTANCE, instance);
|
||||
fingerPrints.put(CommonConstants.LABEL_ALERT_NAME, define.getName());
|
||||
fingerPrints.putAll(define.getLabels());
|
||||
fingerPrints.putAll(commonFingerPrints);
|
||||
for (int index = 0; index < valueRow.getColumnsList().size(); index++) {
|
||||
String valueStr = valueRow.getColumns(index);
|
||||
if (CommonConstants.NULL_VALUE.equals(valueStr)) {
|
||||
@@ -228,7 +242,7 @@ public class RealTimeAlertCalculator {
|
||||
boolean match = execAlertExpression(fieldValueMap, expr, false);
|
||||
try {
|
||||
if (match) {
|
||||
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define);
|
||||
afterThresholdRuleMatch(currentTimeMilli, fingerPrints, fieldValueMap, define, annotations);
|
||||
} else {
|
||||
handleRecoveredAlert(fingerPrints);
|
||||
}
|
||||
@@ -311,20 +325,31 @@ public class RealTimeAlertCalculator {
|
||||
pendingAlertMap.remove(fingerprint);
|
||||
}
|
||||
|
||||
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
|
||||
Map<String, Object> fieldValueMap, AlertDefine define) {
|
||||
private void afterThresholdRuleMatch(long currentTimeMilli, Map<String, String> fingerPrints,
|
||||
Map<String, Object> fieldValueMap, AlertDefine define, Map<String, String> annotations) {
|
||||
String fingerprint = calculateFingerprint(fingerPrints);
|
||||
SingleAlert existingAlert = pendingAlertMap.get(fingerprint);
|
||||
Map<String, String> labels = new HashMap<>(8);
|
||||
fieldValueMap.putAll(define.getLabels());
|
||||
labels.putAll(fingerPrints);
|
||||
int requiredTimes = define.getTimes() == null ? 1 : define.getTimes();
|
||||
if (existingAlert == null) {
|
||||
// First time triggering alert, create new alert and set to pending status
|
||||
Map<String, String> alertLabels = new HashMap<>(8);
|
||||
alertLabels.putAll(fingerPrints);
|
||||
Map<String, String> alertAnnotations = new HashMap<>(8);
|
||||
if (annotations != null) {
|
||||
alertAnnotations.putAll(annotations);
|
||||
}
|
||||
if (define.getAnnotations() != null) {
|
||||
alertAnnotations.putAll(define.getAnnotations());
|
||||
}
|
||||
// render var content in annotations
|
||||
for (Map.Entry<String, String> entry : alertAnnotations.entrySet()) {
|
||||
entry.setValue(AlertTemplateUtil.render(entry.getValue(), fieldValueMap));
|
||||
}
|
||||
SingleAlert newAlert = SingleAlert.builder()
|
||||
.labels(labels)
|
||||
// todo render var content in annotations
|
||||
.annotations(define.getAnnotations())
|
||||
.labels(alertLabels)
|
||||
.annotations(alertAnnotations)
|
||||
// render var content in content
|
||||
.content(AlertTemplateUtil.render(define.getTemplate(), fieldValueMap))
|
||||
.status(CommonConstants.ALERT_STATUS_PENDING)
|
||||
.triggerTimes(1)
|
||||
|
||||
+9
-2
@@ -19,6 +19,7 @@
|
||||
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
@@ -30,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
/**
|
||||
* SSE manager for alert
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AlertSseManager {
|
||||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
|
||||
@@ -38,6 +40,7 @@ public class AlertSseManager {
|
||||
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
|
||||
emitter.onCompletion(() -> removeEmitter(clientId));
|
||||
emitter.onTimeout(() -> removeEmitter(clientId));
|
||||
emitter.onError((ex) -> removeEmitter(clientId));
|
||||
emitters.put(clientId, emitter);
|
||||
return emitter;
|
||||
}
|
||||
@@ -50,7 +53,11 @@ public class AlertSseManager {
|
||||
.id(String.valueOf(System.currentTimeMillis()))
|
||||
.name("ALERT_EVENT")
|
||||
.data(data));
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
} catch (Exception exception) {
|
||||
log.error("Failed to broadcast alert data to client: {}", exception.getMessage());
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
}
|
||||
@@ -60,4 +67,4 @@ public class AlertSseManager {
|
||||
private void removeEmitter(Long clientId) {
|
||||
emitters.remove(clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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.alert.config;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Alibaba Cloud SMS configuration properties
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AlibabaSmsProperties {
|
||||
/**
|
||||
* Alibaba Cloud access key id
|
||||
*/
|
||||
@NotBlank(message = "AccessKeyId cannot be empty")
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
* Alibaba Cloud access key secret
|
||||
*/
|
||||
@NotBlank(message = "AccessKeySecret cannot be empty")
|
||||
private String accessKeySecret;
|
||||
|
||||
/**
|
||||
* SMS signature
|
||||
*/
|
||||
@NotBlank(message = "SignName cannot be empty")
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* SMS template Code
|
||||
*/
|
||||
@NotBlank(message = "TemplateCode cannot be null")
|
||||
private String templateCode;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* AWS Cloud SMS configuration properties
|
||||
*/
|
||||
@Data
|
||||
public class AwsSmsProperties {
|
||||
/**
|
||||
* AWS Cloud access key id
|
||||
*/
|
||||
@NotBlank(message = "AccessKeyId cannot be empty")
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
* AWS Cloud access key secret
|
||||
*/
|
||||
@NotBlank(message = "AccessKeySecret cannot be empty")
|
||||
private String accessKeySecret;
|
||||
|
||||
/**
|
||||
* AWS Cloud region
|
||||
*/
|
||||
@NotBlank(message = "region cannot be empty")
|
||||
private String region;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* SMS configuration
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "alerter.sms")
|
||||
public class SmsConfig {
|
||||
|
||||
/**
|
||||
* whether to enable SMS, default is false
|
||||
*/
|
||||
private boolean enable = false;
|
||||
|
||||
/**
|
||||
* sms service provider
|
||||
*/
|
||||
@NotBlank(message = "Type cannot be empty")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* Tencent cloud SMS configuration
|
||||
*/
|
||||
private TencentSmsProperties tencent;
|
||||
|
||||
/**
|
||||
* Aliyun SMS configuration
|
||||
*/
|
||||
private AlibabaSmsProperties alibaba;
|
||||
|
||||
/**
|
||||
* UniSMS configuration
|
||||
*/
|
||||
private UniSmsProperties unisms;
|
||||
|
||||
/**
|
||||
* Aws configuration
|
||||
*/
|
||||
private AwsSmsProperties aws;
|
||||
|
||||
/**
|
||||
* Twilio SMS configuration
|
||||
*/
|
||||
private TwilioSmsProperties twilio;
|
||||
|
||||
/**
|
||||
* Smslocal SMS configuration
|
||||
*/
|
||||
private SmslocalSmsProperties smslocal;
|
||||
}
|
||||
+8
-12
@@ -15,27 +15,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.manager.pojo.dto;
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Sms Sender configuration dto
|
||||
* Smslocal SMS Properties
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SmsNoticeSender {
|
||||
|
||||
@NotBlank(message = "Type cannot be empty")
|
||||
private String type;
|
||||
|
||||
private SmsTencentConfig tencent;
|
||||
|
||||
private SmsAlibabaConfig alibaba;
|
||||
public class SmslocalSmsProperties {
|
||||
/**
|
||||
* SmsLocal account api key
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
private boolean enable = true;
|
||||
}
|
||||
+22
-9
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.manager.pojo.dto;
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -23,26 +23,39 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Tencent Sms Sender configuration dto
|
||||
* Tencent Cloud SMS properties
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SmsTencentConfig {
|
||||
|
||||
public class TencentSmsProperties {
|
||||
/**
|
||||
* Tencent cloud account secret id
|
||||
*/
|
||||
@NotBlank(message = "SecretId cannot be empty")
|
||||
private String secretId;
|
||||
|
||||
/**
|
||||
* Tencent cloud account secret key
|
||||
*/
|
||||
@NotBlank(message = "SecretKey cannot be empty")
|
||||
private String secretKey;
|
||||
|
||||
@NotBlank(message = "SignName cannot be empty")
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* SMS app id
|
||||
*/
|
||||
@NotBlank(message = "AppId cannot be null")
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* SMS signature
|
||||
*/
|
||||
@NotBlank(message = "SignName cannot be empty")
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* SMS template ID
|
||||
*/
|
||||
@NotBlank(message = "templateId cannot be null")
|
||||
private String templateId;
|
||||
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Twilio SMS configuration properties
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class TwilioSmsProperties {
|
||||
/**
|
||||
* Twilio Account SID
|
||||
*/
|
||||
@NotBlank(message = "Account SID cannot be empty")
|
||||
private String accountSid;
|
||||
|
||||
/**
|
||||
* Twilio Auth Token
|
||||
*/
|
||||
@NotBlank(message = "Auth Token cannot be empty")
|
||||
private String authToken;
|
||||
|
||||
/**
|
||||
* Twilio Issued Phone Number
|
||||
*/
|
||||
@NotBlank(message = "Twilio Phone Number cannot be empty")
|
||||
private String twilioPhoneNumber;
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.config;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* UniSMS properties
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UniSmsProperties {
|
||||
/**
|
||||
* UniSMS access key id
|
||||
*/
|
||||
@NotBlank(message = "accessKeyId cannot be empty")
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
* UniSMS access key secret, required for HMAC mode
|
||||
*/
|
||||
private String accessKeySecret;
|
||||
|
||||
/**
|
||||
* SMS signature
|
||||
*/
|
||||
@NotBlank(message = "signature cannot be null")
|
||||
private String signature;
|
||||
|
||||
/**
|
||||
* SMS template ID
|
||||
*/
|
||||
@NotBlank(message = "templateId cannot be null")
|
||||
private String templateId;
|
||||
|
||||
/**
|
||||
* Authentication mode: simple or hmac, default is simple
|
||||
*/
|
||||
@NotBlank(message = "authMode cannot be null")
|
||||
private String authMode = "simple";
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* SkyWalking Alert Content Entity
|
||||
* @see <a href="https://skywalking.apache.org/docs/main/latest/en/setup/backend/backend-alarm/">backend-alarm</a>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SkyWalkingExternAlert {
|
||||
|
||||
private Long scopeId;
|
||||
|
||||
private String scope;
|
||||
|
||||
private String name;
|
||||
|
||||
private String id0;
|
||||
|
||||
private String id1;
|
||||
|
||||
private String ruleName;
|
||||
|
||||
private String alarmMessage;
|
||||
|
||||
private Long startTime;
|
||||
|
||||
private List<Tag> tags;
|
||||
|
||||
/**
|
||||
* SkyWalking Tag Entity
|
||||
*/
|
||||
@Data
|
||||
public static class Tag {
|
||||
|
||||
private String key;
|
||||
|
||||
private String value;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -118,11 +118,11 @@ public class TencentCloudExternAlert {
|
||||
/**
|
||||
* Transaction alarm
|
||||
*/
|
||||
private static final String EVENT = "event";
|
||||
public static final String EVENT = "event";
|
||||
|
||||
/**
|
||||
* Indicator alarm
|
||||
*/
|
||||
private static final String METRIC = "metric";
|
||||
public static final String METRIC = "metric";
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ public class AlertNoticeDispatch {
|
||||
if (noticeTemplate == null) {
|
||||
noticeTemplate = noticeConfigService.getDefaultNoticeTemplateByType(alertNotifyHandler.type());
|
||||
}
|
||||
if (noticeTemplate == null) {
|
||||
if (noticeTemplate == null && alertNotifyHandler.type() != 0) {
|
||||
log.error("alert does not have mapping default notice template. type: {}.", alertNotifyHandler.type());
|
||||
throw new NullPointerException(alertNotifyHandler.type() + " does not have mapping default notice template");
|
||||
}
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.notice.impl;
|
||||
|
||||
import java.util.ResourceBundle;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Send alarm information through Alibaba Cloud SMS
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@ConditionalOnProperty("common.sms.aliyun.app-id")
|
||||
final class AliYunAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
||||
|
||||
private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
|
||||
|
||||
@Override
|
||||
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
// SMS notification
|
||||
try {
|
||||
// todo send aliyun sms
|
||||
} catch (Exception e) {
|
||||
throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte type() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+10
-23
@@ -21,12 +21,12 @@ import java.util.ResourceBundle;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.notice.AlertNoticeException;
|
||||
import org.apache.hertzbeat.alert.service.TencentSmsClient;
|
||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||
import org.apache.hertzbeat.alert.service.SmsClientFactory;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.util.ResourceBundleUtil;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
@@ -35,35 +35,22 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@ConditionalOnProperty("common.sms.tencent.app-id")
|
||||
@Deprecated
|
||||
final class SmsAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl {
|
||||
|
||||
private final TencentSmsClient tencentSmsClient;
|
||||
|
||||
private final SmsClientFactory smsFactory;
|
||||
private final ResourceBundle bundle = ResourceBundleUtil.getBundle("alerter");
|
||||
|
||||
@Override
|
||||
public void send(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
// SMS notification todo use the rest api not sdk
|
||||
try {
|
||||
String instance = null;
|
||||
String priority = null;
|
||||
String content = null;
|
||||
if (alert.getCommonLabels() != null) {
|
||||
instance = alert.getCommonLabels().get("instance");
|
||||
priority = alert.getCommonLabels().get("priority");
|
||||
content = alert.getCommonAnnotations().get("summary");
|
||||
content = content == null ? alert.getCommonAnnotations().get("description") : content;
|
||||
if (content == null) {
|
||||
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
|
||||
}
|
||||
SmsClient smsClient = smsFactory.getSmsClient();
|
||||
if (smsClient == null) {
|
||||
throw new AlertNoticeException("No SMS Service available, please check the configuration");
|
||||
}
|
||||
String[] params = new String[3];
|
||||
params[0] = instance == null ? alert.getGroupKey() : instance;
|
||||
params[1] = priority == null ? "unknown" : priority;
|
||||
params[2] = content;
|
||||
tencentSmsClient.sendMessage(params, new String[]{receiver.getPhone()});
|
||||
if (!smsClient.checkConfig()) {
|
||||
throw new AlertNoticeException(smsClient.getType() + " SMS Service configuration is invalid, please check the configuration");
|
||||
}
|
||||
smsClient.sendMessage(receiver, noticeTemplate, alert);
|
||||
} catch (Exception e) {
|
||||
throw new AlertNoticeException("[Sms Notify Error] " + e.getMessage());
|
||||
}
|
||||
|
||||
+3
@@ -48,6 +48,7 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
||||
TelegramBotNotifyDTO notifyBody = TelegramBotNotifyDTO.builder()
|
||||
.chatId(receiver.getTgUserId())
|
||||
.text(renderContent(noticeTemplate, alert))
|
||||
.messageThreadId(receiver.getTgMessageThreadId())
|
||||
.disableWebPagePreview(true)
|
||||
.build();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
@@ -84,6 +85,8 @@ final class TelegramBotAlertNotifyHandlerImpl extends AbstractAlertNotifyHandler
|
||||
private String text;
|
||||
@JsonProperty("disable_web_page_preview")
|
||||
private Boolean disableWebPagePreview;
|
||||
@JsonProperty("message_thread_id")
|
||||
private String messageThreadId;
|
||||
}
|
||||
|
||||
@NoArgsConstructor
|
||||
|
||||
-1
@@ -80,7 +80,6 @@ final class WeChatAlertNotifyHandlerImpl extends AbstractAlertNotifyHandlerImpl
|
||||
JsonObject textContent = new JsonObject();
|
||||
|
||||
// Here you can construct the message content based on the NoticeTemplate and Alert information
|
||||
// String alertMessage = String.format("警告:%s\n详情:%s", alert.getAlertDefineId(), alert.getContent());
|
||||
String alertMessage = "Alert message content";
|
||||
textContent.addProperty("content", alertMessage);
|
||||
messageContent.add("text", textContent);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service;
|
||||
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
|
||||
/**
|
||||
* SMS client interface
|
||||
*/
|
||||
public interface SmsClient {
|
||||
/**
|
||||
* send SMS
|
||||
*/
|
||||
void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert);
|
||||
|
||||
/**
|
||||
* get SMS provider type
|
||||
*/
|
||||
String getType();
|
||||
|
||||
/**
|
||||
* check SMS configuration, return true if the configuration is correct
|
||||
*/
|
||||
boolean checkConfig();
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.config.SmsConfig;
|
||||
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
|
||||
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
|
||||
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
|
||||
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
|
||||
import org.apache.hertzbeat.alert.service.impl.UniSmsClientImpl;
|
||||
import org.apache.hertzbeat.alert.service.impl.AlibabaSmsClientImpl;
|
||||
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
|
||||
import org.apache.hertzbeat.common.constants.GeneralConfigTypeEnum;
|
||||
import org.apache.hertzbeat.common.entity.manager.GeneralConfig;
|
||||
import org.apache.hertzbeat.common.support.event.SmsConfigChangeEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA;
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.AWS;
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT;
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.TWILIO;
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS;
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.SMSLOCAL;
|
||||
|
||||
/**
|
||||
* SMS client factory
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SmsClientFactory {
|
||||
|
||||
private static final String TYPE = GeneralConfigTypeEnum.sms.name();
|
||||
|
||||
private final GeneralConfigDao generalConfigDao;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SmsConfig yamlSmsConfig;
|
||||
|
||||
private volatile SmsClient currentSmsClient;
|
||||
|
||||
public SmsClientFactory(GeneralConfigDao generalConfigDao, ObjectMapper objectMapper, SmsConfig yamlSmsConfig) {
|
||||
this.generalConfigDao = generalConfigDao;
|
||||
this.objectMapper = objectMapper;
|
||||
this.yamlSmsConfig = yamlSmsConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* SMS configuration change event listener
|
||||
*/
|
||||
@EventListener(SmsConfigChangeEvent.class)
|
||||
public void onSmsConfigChange(SmsConfigChangeEvent event) {
|
||||
log.info("[SmsClientFactory] SMS configuration change event received");
|
||||
synchronized (this) {
|
||||
currentSmsClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
public SmsClient getSmsClient() {
|
||||
if (currentSmsClient != null) {
|
||||
return currentSmsClient;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (currentSmsClient != null) {
|
||||
return currentSmsClient;
|
||||
}
|
||||
loadConfig();
|
||||
return currentSmsClient;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadConfig() {
|
||||
try {
|
||||
// 1. try to load database configuration
|
||||
SmsConfig dbConfig = loadDatabaseConfig();
|
||||
if (dbConfig != null && !dbConfig.getType().isBlank() && dbConfig.isEnable()) {
|
||||
createSmsClient(dbConfig);
|
||||
if (currentSmsClient != null) {
|
||||
log.info("[SmsClientFactory] Using database SMS configuration, provider: {}", dbConfig.getType());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. try to load YAML configuration
|
||||
if (yamlSmsConfig != null && !yamlSmsConfig.getType().isBlank() && yamlSmsConfig.isEnable()) {
|
||||
createSmsClient(yamlSmsConfig);
|
||||
if (currentSmsClient != null) {
|
||||
log.info("[SmsClientFactory] Using YAML SMS configuration, provider: {}", yamlSmsConfig.getType());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("[SmsClientFactory] No valid SMS configuration found");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[SmsClientFactory] Failed to load SMS configuration", e);
|
||||
currentSmsClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private SmsConfig loadDatabaseConfig() {
|
||||
try {
|
||||
GeneralConfig config = generalConfigDao.findByType(TYPE);
|
||||
if (config != null && config.getContent() != null) {
|
||||
return objectMapper.readValue(config.getContent(), SmsConfig.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[SmsClientFactory] Failed to load database configuration", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void createSmsClient(SmsConfig smsConfig) {
|
||||
switch (smsConfig.getType()) {
|
||||
case TENCENT:
|
||||
currentSmsClient = new TencentSmsClientImpl(smsConfig.getTencent());
|
||||
break;
|
||||
case UNISMS:
|
||||
currentSmsClient = new UniSmsClientImpl(smsConfig.getUnisms());
|
||||
break;
|
||||
case ALIBABA:
|
||||
currentSmsClient = new AlibabaSmsClientImpl(smsConfig.getAlibaba());
|
||||
break;
|
||||
case SMSLOCAL:
|
||||
currentSmsClient = new SmsLocalSmsClientImpl(smsConfig.getSmslocal());
|
||||
break;
|
||||
case AWS:
|
||||
currentSmsClient = new AwsSmsClientImpl(smsConfig.getAws());
|
||||
break;
|
||||
case TWILIO:
|
||||
currentSmsClient = new TwilioSmsClientImpl(smsConfig.getTwilio());
|
||||
break;
|
||||
default:
|
||||
log.warn("[SmsClientFactory] Unsupported SMS provider type: {}", smsConfig.getType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service;
|
||||
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.config.CommonProperties;
|
||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* sms service client for tencent cloud
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty("common.sms.tencent.app-id")
|
||||
@Slf4j
|
||||
public class TencentSmsClient {
|
||||
|
||||
private static final String RESPONSE_OK = "Ok";
|
||||
private static final String REGION = "ap-guangzhou";
|
||||
|
||||
private SmsClient smsClient;
|
||||
private String appId;
|
||||
private String signName;
|
||||
private String templateId;
|
||||
|
||||
public TencentSmsClient(CommonProperties properties) {
|
||||
if (properties == null || properties.getSms() == null || properties.getSms().getTencent() == null) {
|
||||
log.error("init error, please config TencentSmsClient props in application.yml");
|
||||
throw new IllegalArgumentException("please config TencentSmsClient props");
|
||||
}
|
||||
initSmsClient(properties.getSms().getTencent());
|
||||
}
|
||||
|
||||
private void initSmsClient(CommonProperties.TencentSmsProperties tencent) {
|
||||
this.appId = tencent.getAppId();
|
||||
this.signName = tencent.getSignName();
|
||||
this.templateId = tencent.getTemplateId();
|
||||
Credential cred = new Credential(tencent.getSecretId(), tencent.getSecretKey());
|
||||
smsClient = new SmsClient(cred, REGION);
|
||||
}
|
||||
|
||||
/**
|
||||
* send text message
|
||||
* @param appId appId
|
||||
* @param signName sign name
|
||||
* @param templateId template id
|
||||
* @param templateValues template values
|
||||
* @param phones phones num
|
||||
*/
|
||||
public void sendMessage(String appId, String signName, String templateId,
|
||||
String[] templateValues, String[] phones) {
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
req.setSmsSdkAppId(appId);
|
||||
req.setSignName(signName);
|
||||
req.setTemplateId(templateId);
|
||||
req.setTemplateParamSet(templateValues);
|
||||
req.setPhoneNumberSet(phones);
|
||||
try {
|
||||
SendSmsResponse smsResponse = this.smsClient.SendSms(req);
|
||||
SendStatus sendStatus = smsResponse.getSendStatusSet()[0];
|
||||
if (!RESPONSE_OK.equals(sendStatus.getCode())) {
|
||||
throw new SendMessageException(sendStatus.getCode() + ":" + sendStatus.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* send text message
|
||||
* @param templateValues template values
|
||||
* @param phones phones num
|
||||
*/
|
||||
public void sendMessage(String[] templateValues, String[] phones) {
|
||||
sendMessage(this.appId, this.signName, this.templateId, templateValues, phones);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.config.AlibabaSmsProperties;
|
||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.SimpleTimeZone;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA;
|
||||
|
||||
/**
|
||||
* Alibaba Cloud SMS Client Implementation<br>
|
||||
* API doc: <a href="https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms">https://next.api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms</a><br>
|
||||
* Singnature doc: <a href="https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature">https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature</a>
|
||||
*/
|
||||
@Slf4j
|
||||
public class AlibabaSmsClientImpl implements SmsClient {
|
||||
|
||||
private static final String API_VERSION = "2017-05-25";
|
||||
private static final String ACTION = "SendSms";
|
||||
private static final String HOST = "dysmsapi.aliyuncs.com";
|
||||
private static final String ALGORITHM = "ACS3-HMAC-SHA256";
|
||||
|
||||
private final String accessKeyId;
|
||||
private final String accessKeySecret;
|
||||
private final String signName;
|
||||
private final String templateCode;
|
||||
|
||||
public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
|
||||
if (config != null) {
|
||||
this.accessKeyId = config.getAccessKeyId();
|
||||
this.accessKeySecret = config.getAccessKeySecret();
|
||||
this.signName = config.getSignName();
|
||||
this.templateCode = config.getTemplateCode();
|
||||
} else {
|
||||
this.accessKeyId = "";
|
||||
this.accessKeySecret = "";
|
||||
this.signName = "";
|
||||
this.templateCode = "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
// Extract alert info
|
||||
String instance = null;
|
||||
String priority = null;
|
||||
String content = null;
|
||||
if (alert.getCommonLabels() != null) {
|
||||
instance = alert.getCommonLabels().get("instance");
|
||||
priority = alert.getCommonLabels().get("priority");
|
||||
content = alert.getCommonAnnotations().get("summary");
|
||||
content = content == null ? alert.getCommonAnnotations().get("description") : content;
|
||||
if (content == null) {
|
||||
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Build template parameters
|
||||
Map<String, String> templateParam = new HashMap<>();
|
||||
templateParam.put("instance", instance == null ? alert.getGroupKey() : instance);
|
||||
templateParam.put("priority", priority == null ? "unknown" : priority);
|
||||
templateParam.put("content", content);
|
||||
|
||||
sendSms(receiver.getPhone(), JsonUtil.toJson(templateParam));
|
||||
}
|
||||
|
||||
private void sendSms(String phoneNumber, String templateParam) {
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
// Build query parameters
|
||||
Map<String, String> queryParams = new TreeMap<>();
|
||||
queryParams.put("PhoneNumbers", phoneNumber);
|
||||
queryParams.put("SignName", signName);
|
||||
queryParams.put("TemplateCode", templateCode);
|
||||
queryParams.put("TemplateParam", templateParam);
|
||||
|
||||
// Build canonical query string
|
||||
StringBuilder canonicalQueryString = new StringBuilder();
|
||||
queryParams.forEach((key, value) -> {
|
||||
if (canonicalQueryString.length() > 0) {
|
||||
canonicalQueryString.append("&");
|
||||
}
|
||||
canonicalQueryString.append(percentEncode(key))
|
||||
.append("=")
|
||||
.append(percentEncode(value));
|
||||
});
|
||||
|
||||
// Generate timestamp and nonce
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
sdf.setTimeZone(new SimpleTimeZone(0, "GMT"));
|
||||
String timestamp = sdf.format(new Date());
|
||||
String nonce = UUID.randomUUID().toString();
|
||||
|
||||
// Calculate signature
|
||||
String authorization = calculateAuthorization(
|
||||
canonicalQueryString.toString(),
|
||||
timestamp,
|
||||
nonce
|
||||
);
|
||||
|
||||
// Build URL
|
||||
String url = "https://" + HOST + "/?" + canonicalQueryString;
|
||||
|
||||
// Build HTTP request
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setHeader("Content-Type", "application/json");
|
||||
httpPost.setHeader("Host", HOST);
|
||||
httpPost.setHeader("Authorization", authorization);
|
||||
httpPost.setHeader("x-acs-action", ACTION);
|
||||
httpPost.setHeader("x-acs-version", API_VERSION);
|
||||
httpPost.setHeader("x-acs-date", timestamp);
|
||||
httpPost.setHeader("x-acs-signature-nonce", nonce);
|
||||
httpPost.setHeader("x-acs-content-sha256",
|
||||
CryptoUtils.sha256Hex(""));
|
||||
|
||||
log.info("Sending Alibaba SMS request to {}", url + ", params: " + templateParam + "headers: " + Arrays.toString(httpPost.getAllHeaders()));
|
||||
|
||||
// Send request and handle response
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity());
|
||||
|
||||
log.info("SMS response status: {}, body: {}", statusCode, responseBody);
|
||||
|
||||
if (statusCode != 200) {
|
||||
throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
|
||||
}
|
||||
|
||||
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
|
||||
String code = jsonResponse.get("Code").asText();
|
||||
if (!"OK".equals(code)) {
|
||||
String message = jsonResponse.get("Message").asText();
|
||||
throw new SendMessageException(code + ":" + message);
|
||||
}
|
||||
|
||||
log.info("Successfully sent SMS to phone: {}", phoneNumber);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to send SMS: {}", e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String calculateAuthorization(String canonicalQueryString, String timestamp, String nonce) {
|
||||
try {
|
||||
// Step 1: Build canonical request
|
||||
String canonicalRequest = buildCanonicalRequest(canonicalQueryString, timestamp, nonce);
|
||||
|
||||
// Step 2: Build string to sign
|
||||
String stringToSign = ALGORITHM + "\n" + CryptoUtils.sha256Hex(canonicalRequest);
|
||||
|
||||
// Step 3: Calculate signature
|
||||
String signature = CryptoUtils.hmacSha256Hex(accessKeySecret, stringToSign);
|
||||
|
||||
// Step 4: Build authorization header
|
||||
return ALGORITHM + " Credential=" + accessKeyId + ",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + "x-acs-signature-nonce;x-acs-version,Signature=" + signature;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to calculate authorization", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildCanonicalRequest(String canonicalQueryString, String timestamp, String nonce) {
|
||||
return "POST\n"
|
||||
+ "/\n"
|
||||
+ canonicalQueryString + "\n"
|
||||
+ "host:" + HOST + "\n"
|
||||
+ "x-acs-action:" + ACTION + "\n"
|
||||
+ "x-acs-content-sha256:" + CryptoUtils.sha256Hex("") + "\n"
|
||||
+ "x-acs-date:" + timestamp + "\n"
|
||||
+ "x-acs-signature-nonce:" + nonce + "\n"
|
||||
+ "x-acs-version:" + API_VERSION + "\n\n"
|
||||
+ "host;x-acs-action;x-acs-content-sha256;x-acs-date;"
|
||||
+ "x-acs-signature-nonce;x-acs-version\n"
|
||||
+ CryptoUtils.sha256Hex("");
|
||||
}
|
||||
|
||||
private String percentEncode(String value) {
|
||||
try {
|
||||
return java.net.URLEncoder.encode(value, StandardCharsets.UTF_8)
|
||||
.replace("+", "%20")
|
||||
.replace("*", "%2A")
|
||||
.replace("%7E", "~");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to encode value", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return ALIBABA;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkConfig() {
|
||||
return !(accessKeyId.isBlank() || accessKeySecret.isBlank() || signName.isBlank() || templateCode.isBlank());
|
||||
}
|
||||
}
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
|
||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.AWS;
|
||||
|
||||
/**
|
||||
* AWS Cloud SMS Client Implementation<br>
|
||||
* API doc:<a href="https://docs.aws.amazon.com/pinpoint/latest/apireference_smsvoicev2/API_SendTextMessage.html">https://docs.aws.amazon.com/pinpoint/latest/apireference_smsvoicev2/API_SendTextMessage.html</a><br>
|
||||
* Signature doc:<a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html"</a>https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html</a>
|
||||
*/
|
||||
@Slf4j
|
||||
public class AwsSmsClientImpl implements SmsClient {
|
||||
|
||||
|
||||
private static final String SERVICE = "sms-voice";
|
||||
private static final String MESSAGE_TEMPLATE = "Instance: %s, Priority: %s, Content: %s";
|
||||
|
||||
private final String accessKey;
|
||||
private final String secretKey;
|
||||
private final String region;
|
||||
|
||||
public AwsSmsClientImpl(AwsSmsProperties config) {
|
||||
if (config != null) {
|
||||
this.accessKey = config.getAccessKeyId();
|
||||
this.secretKey = config.getAccessKeySecret();
|
||||
this.region = config.getRegion();
|
||||
} else {
|
||||
this.accessKey = "";
|
||||
this.secretKey = "";
|
||||
this.region = "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
// Extract alert info
|
||||
String instance = null;
|
||||
String priority = null;
|
||||
String content = null;
|
||||
if (alert.getCommonLabels() != null) {
|
||||
instance = alert.getCommonLabels().get("instance") == null ? alert.getGroupKey() : alert.getCommonLabels().get("instance");
|
||||
priority = alert.getCommonLabels().get("priority") == null ? "unknown" : alert.getCommonLabels().get("priority");
|
||||
content = alert.getCommonAnnotations().get("summary");
|
||||
content = content == null ? alert.getCommonAnnotations().get("description") : content;
|
||||
if (content == null) {
|
||||
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
this.send(receiver.getPhone(), createMessage(instance, priority, content));
|
||||
}
|
||||
|
||||
private String createMessage(String instance, String priority, String content) {
|
||||
return String.format(MESSAGE_TEMPLATE, instance, priority, content);
|
||||
}
|
||||
|
||||
private void send(String phoneNumber, String message) {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("MessageBody", message);
|
||||
payload.put("DestinationPhoneNumber", phoneNumber);
|
||||
payload.put("MessageType", "TRANSACTIONAL");
|
||||
|
||||
Instant now = Instant.now();
|
||||
String amzDate = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").withZone(ZoneOffset.UTC).format(now);
|
||||
String endpoint = "https://" + SERVICE + "." + region + ".amazonaws.com";
|
||||
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
String payloadInString = new ObjectMapper().writeValueAsString(payload);
|
||||
URI requestUri = new URI(endpoint);
|
||||
|
||||
HttpPost httpPost = createHttpPost(requestUri, amzDate, payloadInString);
|
||||
log.info("Sending AWS SMS request to {}", requestUri + "," + "headers: " + Arrays.toString(httpPost.getAllHeaders()));
|
||||
executeRequest(httpClient, httpPost, phoneNumber);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to send SMS: {}", e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private HttpPost createHttpPost(URI requestUri, String amzDate, String payloadInString) throws Exception {
|
||||
// Define headers once to avoid repetition (Order of headers must be maintained)
|
||||
Map<String, String> headers = new LinkedHashMap<>();
|
||||
headers.put("content-type", "application/x-amz-json-1.0");
|
||||
headers.put("host", SERVICE + "." + this.region + ".amazonaws.com");
|
||||
headers.put("x-amz-date", amzDate);
|
||||
headers.put("x-amz-target", "PinpointSMSVoiceV2.SendTextMessage");
|
||||
|
||||
// Build authorization header using the same headers
|
||||
AwsAuthenticationBuilder authBuilder = new AwsAuthenticationBuilder()
|
||||
.credential(this.accessKey, this.secretKey)
|
||||
.region(this.region);
|
||||
headers.forEach(authBuilder::header);
|
||||
String authorizationHeader = authBuilder.build(requestUri, "POST", SERVICE, payloadInString);
|
||||
|
||||
// Create HTTP post and set all headers
|
||||
HttpPost httpPost = new HttpPost(requestUri);
|
||||
httpPost.setHeader("Authorization", authorizationHeader);
|
||||
headers.forEach(httpPost::setHeader);
|
||||
|
||||
httpPost.setEntity(new StringEntity(payloadInString, ContentType.APPLICATION_JSON));
|
||||
return httpPost;
|
||||
}
|
||||
|
||||
private void executeRequest(CloseableHttpClient httpClient, HttpPost httpPost, String phoneNumber) throws Exception {
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity());
|
||||
log.info("SMS response status: {}, body: {}", statusCode, responseBody);
|
||||
|
||||
if (statusCode != 200) {
|
||||
throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
|
||||
}
|
||||
|
||||
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
|
||||
if (jsonResponse == null) {
|
||||
throw new SendMessageException(statusCode + ":" + responseBody);
|
||||
}
|
||||
|
||||
JsonNode responseNode = jsonResponse.get("MessageId");
|
||||
if (responseNode == null) {
|
||||
throw new SendMessageException(statusCode + ":" + responseBody);
|
||||
}
|
||||
|
||||
String messageId = responseNode.asText();
|
||||
log.info("Successfully sent SMS to phone: {}, messageId: {}", phoneNumber, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return AWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkConfig() {
|
||||
return !(accessKey.isBlank() || secretKey.isBlank() || region.isBlank());
|
||||
}
|
||||
|
||||
|
||||
private static class AwsAuthenticationBuilder {
|
||||
private String accessKey;
|
||||
private String secretKey;
|
||||
private String region;
|
||||
private String service;
|
||||
private URI requestUri;
|
||||
private String method;
|
||||
private String payload;
|
||||
private final Map<String, String> headers = new LinkedHashMap<>();
|
||||
|
||||
public AwsAuthenticationBuilder region(String region) {
|
||||
this.region = region;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AwsAuthenticationBuilder credential(String accessKey, String secretKey) {
|
||||
this.accessKey = accessKey;
|
||||
this.secretKey = secretKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AwsAuthenticationBuilder header(String header, String value) {
|
||||
this.headers.put(header, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String build(URI requestUri, String method, String service, String payload) throws Exception {
|
||||
this.requestUri = requestUri;
|
||||
this.method = method;
|
||||
this.service = service;
|
||||
this.payload = payload;
|
||||
return "%s Credential=%s/%s, SignedHeaders=%s, Signature=%s".formatted(
|
||||
getAlgorithm(),
|
||||
this.accessKey,
|
||||
getCredentialScope(),
|
||||
getSignedHeaders(),
|
||||
getSignature()
|
||||
);
|
||||
}
|
||||
|
||||
private String getCredentialScope() {
|
||||
return getDateStamp() + "/" + region + "/" + service + "/aws4_request";
|
||||
}
|
||||
|
||||
private String getSignedHeaders() {
|
||||
return String.join(";", headers.keySet());
|
||||
}
|
||||
|
||||
private String getDateTime() {
|
||||
return headers.get("x-amz-date");
|
||||
}
|
||||
|
||||
private String getDateStamp() {
|
||||
return getDateTime().substring(0, 8);
|
||||
}
|
||||
|
||||
private String getSignature() {
|
||||
String canonicalRequest = getCanonicalRequest();
|
||||
String stringToSign = String.join("\n",
|
||||
getAlgorithm(),
|
||||
getDateTime(),
|
||||
getCredentialScope(),
|
||||
CryptoUtils.sha256Hex(canonicalRequest));
|
||||
|
||||
byte[] signingKey = getSignatureKey(secretKey, getDateStamp(), region, service);
|
||||
return CryptoUtils.hmacSha256Hex(signingKey, stringToSign);
|
||||
}
|
||||
|
||||
private String getCanonicalRequest() {
|
||||
return String.join("\n",
|
||||
method,
|
||||
requestUri.getPath().isEmpty() ? "/" : requestUri.getPath(),
|
||||
requestUri.getQuery() != null ? requestUri.getQuery() : "",
|
||||
getCanonicalHeaders(),
|
||||
getSignedHeaders(),
|
||||
CryptoUtils.sha256Hex(payload)
|
||||
);
|
||||
}
|
||||
|
||||
private String getCanonicalHeaders() {
|
||||
return headers.entrySet().stream()
|
||||
.map(entry -> entry.getKey() + ":" + entry.getValue())
|
||||
.collect(Collectors.joining("\n")) + "\n";
|
||||
}
|
||||
|
||||
private String getAlgorithm() {
|
||||
return "AWS4-HMAC-SHA256";
|
||||
}
|
||||
|
||||
private byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) {
|
||||
byte[] secret = ("AWS4" + key).getBytes(StandardCharsets.UTF_8);
|
||||
byte[] date = CryptoUtils.hmac256(secret, dateStamp);
|
||||
byte[] region = CryptoUtils.hmac256(date, regionName);
|
||||
byte[] service = CryptoUtils.hmac256(region, serviceName);
|
||||
return CryptoUtils.hmac256(service, "aws4_request");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -305,7 +305,7 @@ public class NoticeConfigServiceImpl implements NoticeConfigService, CommandLine
|
||||
Map<String, String> labels = new HashMap<>(8);
|
||||
labels.put(CommonConstants.LABEL_INSTANCE, "1000000");
|
||||
labels.put(CommonConstants.LABEL_ALERT_NAME, "CPU Usage Alert");
|
||||
labels.put(CommonConstants.LABEL_HOST, "127.0.0.1");
|
||||
labels.put(CommonConstants.LABEL_INSTANCE_HOST, "127.0.0.1");
|
||||
Map<String, String> annotations = new HashMap<>(8);
|
||||
annotations.put("suggest", "Please check the CPU usage of the server");
|
||||
SingleAlert singleAlert1 = SingleAlert.builder()
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.dto.SkyWalkingExternAlert;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.apache.hertzbeat.alert.service.ExternAlertService;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SkyWalking external alarm service impl
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SkyWalkingExternAlertService implements ExternAlertService {
|
||||
|
||||
@Autowired
|
||||
private AlarmCommonReduce alarmCommonReduce;
|
||||
|
||||
|
||||
@Override
|
||||
public void addExternAlert(String content) {
|
||||
TypeReference<List<SkyWalkingExternAlert>> typeReference = new TypeReference<>() {};
|
||||
List<SkyWalkingExternAlert> alerts = JsonUtil.fromJson(content, typeReference);
|
||||
if (alerts == null || alerts.isEmpty()) {
|
||||
log.warn("Parse SkyWalking extern alert content failed! content: {}", content);
|
||||
return;
|
||||
}
|
||||
for (SkyWalkingExternAlert alert : alerts) {
|
||||
SingleAlert singleAlert = SingleAlert.builder()
|
||||
.content(alert.getAlarmMessage())
|
||||
.status(CommonConstants.ALERT_STATUS_FIRING)
|
||||
.activeAt(Instant.now().toEpochMilli())
|
||||
.startAt(alert.getStartTime() != null ? alert.getStartTime() : Instant.now().toEpochMilli())
|
||||
.labels(acquireAlertLabels(alert))
|
||||
.annotations(acquireAlertAnnotations(alert))
|
||||
.triggerTimes(1)
|
||||
.build();
|
||||
alarmCommonReduce.reduceAndSendAlarm(singleAlert);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String supportSource() {
|
||||
return "skywalking";
|
||||
}
|
||||
|
||||
private Map<String, String> acquireAlertLabels(SkyWalkingExternAlert externAlert){
|
||||
Map<String, String> labels = new HashMap<>(8);
|
||||
labels.put("__source__", "skywalking");
|
||||
List<SkyWalkingExternAlert.Tag> tags = externAlert.getTags();
|
||||
if (tags == null || tags.isEmpty()){
|
||||
return labels;
|
||||
}
|
||||
tags.forEach(tag -> labels.put(tag.getKey(), tag.getValue()));
|
||||
return labels;
|
||||
}
|
||||
|
||||
private Map<String, String> acquireAlertAnnotations(SkyWalkingExternAlert externAlert){
|
||||
Map<String, String> annotations = new HashMap<>(8);
|
||||
annotations.putIfAbsent("scope", externAlert.getScope());
|
||||
annotations.putIfAbsent("name", externAlert.getName());
|
||||
annotations.putIfAbsent("id0", externAlert.getId0());
|
||||
annotations.putIfAbsent("id1", externAlert.getId1());
|
||||
annotations.putIfAbsent("ruleName", externAlert.getRuleName());
|
||||
return annotations;
|
||||
}
|
||||
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.config.SmslocalSmsProperties;
|
||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||
import org.apache.hertzbeat.common.constants.SmsConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Smslocal SMS Client Implement
|
||||
*/
|
||||
@Slf4j
|
||||
public class SmsLocalSmsClientImpl implements SmsClient {
|
||||
private static final String HOST = "secure.smslocal.com";
|
||||
|
||||
private static final String PATH = "/api/service/enterprise-service/external/sms";
|
||||
|
||||
private static final String FROM = "Hertzbeat";
|
||||
|
||||
private static final String SUCCESS_CODE = "200";
|
||||
|
||||
private final SmslocalSmsProperties config;
|
||||
|
||||
public SmsLocalSmsClientImpl(SmslocalSmsProperties smslocalSmsProperties) {
|
||||
this.config = smslocalSmsProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
if (Objects.isNull(receiver) || Objects.isNull(alert)) {
|
||||
log.warn("receiver and alert can not be null! receiver: {}, alert:{}", receiver, alert);
|
||||
return;
|
||||
}
|
||||
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
String content = alert.getCommonAnnotations().get("summary");
|
||||
if (Objects.isNull(content) || Objects.isNull(alert.getCommonAnnotations().get("description"))) {
|
||||
content = alert.getAlerts().get(0).getContent();
|
||||
}
|
||||
SmsMessage smsMessage = new SmsMessage(FROM, receiver.getPhone(), content);
|
||||
|
||||
String payload = JsonUtil.toJson(smsMessage);
|
||||
|
||||
HttpPost httpPost = new HttpPost("https://" + HOST + PATH);
|
||||
httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
httpPost.setHeader("Token", config.getApiKey());
|
||||
httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8));
|
||||
|
||||
log.debug("Sending SMS request to {}, payload: {}", httpPost.getURI(), payload);
|
||||
|
||||
// send http request and handle response
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity());
|
||||
|
||||
log.debug("SMS response status: {}, body: {}", statusCode, responseBody);
|
||||
|
||||
if (statusCode != 200) {
|
||||
throw new SendMessageException("HTTP request failed with status code: " + statusCode);
|
||||
}
|
||||
|
||||
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
|
||||
JsonNode jsonNode = jsonResponse.get(0);
|
||||
if (Objects.isNull(jsonNode)) {
|
||||
log.warn("jsonResponse parse errorCode failed: {}", jsonResponse);
|
||||
return;
|
||||
}
|
||||
String errorCode = jsonNode.get("errorCode").asText();
|
||||
if (!SUCCESS_CODE.equals(errorCode)) {
|
||||
String msgid = jsonNode.get("id").asText();
|
||||
throw new SendMessageException(errorCode + ":" + msgid);
|
||||
}
|
||||
|
||||
log.info("Successfully sent SMS to phone: {}", receiver.getPhone());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send SMS: {}", e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return SmsConstants.SMSLOCAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkConfig() {
|
||||
if (Objects.isNull(config) || Objects.isNull(config.getApiKey()) || config.getApiKey().isBlank()) {
|
||||
log.warn("smslocal properties can not be null: {}", config);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private static class SmsMessage {
|
||||
String from;
|
||||
String to;
|
||||
String content;
|
||||
final int datacoding = 0;
|
||||
final String direction = "mt";
|
||||
|
||||
public SmsMessage(String from, String to, String content) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+174
-2
@@ -18,14 +18,22 @@
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hertzbeat.alert.dto.TencentCloudExternAlert;
|
||||
import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
|
||||
import org.apache.hertzbeat.alert.service.ExternAlertService;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Default external alarm service impl
|
||||
*/
|
||||
@@ -43,8 +51,7 @@ public class TencentExternAlertService implements ExternAlertService {
|
||||
log.warn("parse extern alert content failed! content: {}", content);
|
||||
return;
|
||||
}
|
||||
// todo convert TenCloudAlertReport to SingleAlert
|
||||
SingleAlert alert = new SingleAlert();
|
||||
SingleAlert alert = new TencentCloudAlertConverter().convert(report);
|
||||
alarmCommonReduce.reduceAndSendAlarm(alert);
|
||||
}
|
||||
|
||||
@@ -52,4 +59,169 @@ public class TencentExternAlertService implements ExternAlertService {
|
||||
public String supportSource() {
|
||||
return "tencent";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converter: TencentCloud alert to SingleAlert
|
||||
*/
|
||||
public static class TencentCloudAlertConverter {
|
||||
|
||||
/**
|
||||
* Metric alert content template
|
||||
*/
|
||||
private final String metricTemplate = "Tencent Cloud Metric Alert: {metricShowName} {calcType} {calcValue}{calcUnit}, Current: {currentValue}{unit}";
|
||||
|
||||
/**
|
||||
* Event alert content template
|
||||
*/
|
||||
private final String eventTemplate = "Tencent Cloud Event Alert: {productShowName} - {eventShowName}";
|
||||
|
||||
|
||||
/**
|
||||
* Convert TencentCloud alert to SingleAlert
|
||||
*/
|
||||
public SingleAlert convert(TencentCloudExternAlert tencentAlert) {
|
||||
// Building basic information
|
||||
SingleAlert.SingleAlertBuilder builder = SingleAlert.builder()
|
||||
.status(convertStatus(tencentAlert.getAlarmStatus()))
|
||||
.startAt(parseTime(tencentAlert.getFirstOccurTime()))
|
||||
.activeAt(parseTime(tencentAlert.getFirstOccurTime()))
|
||||
.triggerTimes(1);
|
||||
// If it is a recovery state, set the end time.
|
||||
if ("0".equals(tencentAlert.getAlarmStatus())) {
|
||||
builder.endAt(parseTime(tencentAlert.getRecoverTime()));
|
||||
}
|
||||
|
||||
// Build labels
|
||||
Map<String, String> labels = new HashMap<>();
|
||||
buildLabels(labels, tencentAlert);
|
||||
|
||||
// Build annotations
|
||||
Map<String, String> annotations = new HashMap<>();
|
||||
buildAnnotations(annotations, tencentAlert);
|
||||
|
||||
// Build content
|
||||
String content = generateContent(tencentAlert);
|
||||
|
||||
return builder
|
||||
.labels(labels)
|
||||
.annotations(annotations)
|
||||
.content(content)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void buildLabels(Map<String, String> labels, TencentCloudExternAlert alert) {
|
||||
labels.put("__source__", "tencent_cloud");
|
||||
labels.put("alert_type", alert.getAlarmType());
|
||||
if (TencentCloudExternAlert.METRIC.equals(alert.getAlarmType())) {
|
||||
labels.put("metric_name", alert.getAlarmPolicyInfo().getConditions().getMetricName());
|
||||
labels.put("namespace", alert.getAlarmObjInfo().getNamespace());
|
||||
} else {
|
||||
labels.put("event_name", alert.getAlarmPolicyInfo().getConditions().getEventName());
|
||||
labels.put("product_name", alert.getAlarmPolicyInfo().getConditions().getProductName());
|
||||
}
|
||||
}
|
||||
|
||||
private void buildAnnotations(Map<String, String> annotations, TencentCloudExternAlert alert) {
|
||||
TencentCloudExternAlert.AlarmPolicyInfo alarmPolicyInfo = alert.getAlarmPolicyInfo();
|
||||
TencentCloudExternAlert.AlarmObjInfo alarmObjInfo = alert.getAlarmObjInfo();
|
||||
TencentCloudExternAlert.Dimensions dimensions = alert.getAlarmObjInfo().getDimensions();
|
||||
|
||||
putIfNotNull(annotations, "policy_id", alarmPolicyInfo.getPolicyId());
|
||||
putIfNotNull(annotations, "policy_type", alarmPolicyInfo.getPolicyType());
|
||||
putIfNotNull(annotations, "policy_name", alarmPolicyInfo.getPolicyName());
|
||||
putIfNotNull(annotations, "policy_type_cname", alarmPolicyInfo.getPolicyTypeCname());
|
||||
|
||||
putIfNotNull(annotations, "namespace", alarmObjInfo.getNamespace());
|
||||
putIfNotNull(annotations, "region", alarmObjInfo.getRegion());
|
||||
putIfNotNull(annotations, "app_id", alarmObjInfo.getAppId());
|
||||
putIfNotNull(annotations, "uin", alarmObjInfo.getUin());
|
||||
|
||||
putIfNotNull(annotations, "instance_id", dimensions.getUnInstanceId());
|
||||
putIfNotNull(annotations, "obj_id", dimensions.getObjId());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate alert content with template
|
||||
*/
|
||||
private String generateContent(TencentCloudExternAlert alert) {
|
||||
if (TencentCloudExternAlert.METRIC.equals(alert.getAlarmType())) {
|
||||
return generateMetricContent(alert);
|
||||
} else {
|
||||
return generateEventContent(alert);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate metric alert content
|
||||
*/
|
||||
private String generateMetricContent(TencentCloudExternAlert alert) {
|
||||
TencentCloudExternAlert.Conditions conditions = alert.getAlarmPolicyInfo().getConditions();
|
||||
Map<String, String> params = new HashMap<>();
|
||||
|
||||
params.put("metricShowName", conditions.getMetricShowName());
|
||||
|
||||
if (StringUtils.isNotEmpty(conditions.getCalcType())
|
||||
&& StringUtils.isNotEmpty(conditions.getCalcValue())) {
|
||||
params.put("calcType", conditions.getCalcType());
|
||||
params.put("calcValue", conditions.getCalcValue());
|
||||
params.put("calcUnit", Objects.toString(conditions.getCalcUnit(), ""));
|
||||
} else {
|
||||
params.put("calcType", "");
|
||||
params.put("calcValue", "N/A");
|
||||
params.put("calcUnit", "");
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(conditions.getCurrentValue())) {
|
||||
params.put("currentValue", conditions.getCurrentValue());
|
||||
params.put("unit", Objects.toString(conditions.getUnit(), ""));
|
||||
} else {
|
||||
params.put("currentValue", "N/A");
|
||||
params.put("unit", "");
|
||||
}
|
||||
return replacePlaceholders(metricTemplate, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate event alert content
|
||||
*/
|
||||
private String generateEventContent(TencentCloudExternAlert alert) {
|
||||
TencentCloudExternAlert.Conditions conditions = alert.getAlarmPolicyInfo().getConditions();
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("productShowName", conditions.getProductShowName());
|
||||
params.put("eventShowName", conditions.getEventShowName());
|
||||
return replacePlaceholders(eventTemplate, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace placeholders in template with actual values
|
||||
*/
|
||||
private String replacePlaceholders(String template, Map<String, String> params) {
|
||||
String result = template;
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
result = result.replace("{" + entry.getKey() + "}", Objects.toString(entry.getValue(), "NULL"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String convertStatus(String alarmStatus) {
|
||||
return "1".equals(alarmStatus) ? CommonConstants.ALERT_STATUS_FIRING : CommonConstants.ALERT_STATUS_RESOLVED;
|
||||
}
|
||||
|
||||
private Long parseTime(String timeStr) {
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
return sdf.parse(timeStr).getTime();
|
||||
} catch (ParseException e) {
|
||||
log.error("Failed to parse time: {}", timeStr);
|
||||
throw new IllegalArgumentException("Failed to parse time: " + timeStr, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void putIfNotNull(Map<String, String> map, String key, String value){
|
||||
if (StringUtils.isNotEmpty(value)){
|
||||
map.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.config.TencentSmsProperties;
|
||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT;
|
||||
|
||||
/**
|
||||
* sms service client for tencent cloud <br>
|
||||
* doc: <a href="https://cloud.tencent.com/document/api/382/55981">https://cloud.tencent.com/document/api/382/55981</a>
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
public class TencentSmsClientImpl implements SmsClient {
|
||||
|
||||
private static final String RESPONSE_OK = "Ok";
|
||||
private static final String REGION = "ap-guangzhou";
|
||||
private static final String API_VERSION = "2021-01-11";
|
||||
private static final String ACTION = "SendSms";
|
||||
private static final String HOST = "sms.tencentcloudapi.com";
|
||||
private static final Charset UTF8 = StandardCharsets.UTF_8;
|
||||
|
||||
private String appId;
|
||||
private String signName;
|
||||
private String templateId;
|
||||
private String secretId;
|
||||
private String secretKey;
|
||||
|
||||
public TencentSmsClientImpl(TencentSmsProperties config) {
|
||||
if (config != null) {
|
||||
this.appId = config.getAppId();
|
||||
this.signName = config.getSignName();
|
||||
this.templateId = config.getTemplateId();
|
||||
this.secretId = config.getSecretId();
|
||||
this.secretKey = config.getSecretKey();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
// todo limit the number of words
|
||||
String instance = null;
|
||||
String priority = null;
|
||||
String content = null;
|
||||
if (alert.getCommonLabels() != null) {
|
||||
instance = alert.getCommonLabels().get("instance");
|
||||
priority = alert.getCommonLabels().get("priority");
|
||||
content = alert.getCommonAnnotations().get("summary");
|
||||
content = content == null ? alert.getCommonAnnotations().get("description") : content;
|
||||
if (content == null) {
|
||||
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
String[] templateValues = new String[3];
|
||||
templateValues[0] = instance == null ? alert.getGroupKey() : instance;
|
||||
templateValues[1] = priority == null ? "unknown" : priority;
|
||||
templateValues[2] = content;
|
||||
|
||||
String[] phones = new String[1];
|
||||
phones[0] = receiver.getPhone();
|
||||
|
||||
sendSms(this.appId, this.signName, this.templateId, templateValues, phones);
|
||||
}
|
||||
|
||||
public void sendSms(String appId, String signName, String templateId,
|
||||
String[] templateValues, String[] phones) {
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
||||
|
||||
// build request payload
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("SmsSdkAppId", appId);
|
||||
params.put("SignName", signName);
|
||||
params.put("TemplateId", templateId);
|
||||
params.put("TemplateParamSet", templateValues);
|
||||
params.put("PhoneNumberSet", phones);
|
||||
|
||||
String payload = JsonUtil.toJson(params);
|
||||
|
||||
// calculate request signature
|
||||
String authorization = calculateAuthorization(
|
||||
secretId, secretKey, "sms", HOST, REGION,
|
||||
ACTION, API_VERSION, payload);
|
||||
|
||||
// build http request
|
||||
HttpPost httpPost = new HttpPost("https://" + HOST);
|
||||
httpPost.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
httpPost.setHeader("Host", HOST);
|
||||
httpPost.setHeader("X-TC-Action", ACTION);
|
||||
httpPost.setHeader("X-TC-Timestamp", timestamp);
|
||||
httpPost.setHeader("X-TC-Version", API_VERSION);
|
||||
httpPost.setHeader("X-TC-Region", REGION);
|
||||
httpPost.setHeader("Authorization", authorization);
|
||||
httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8));
|
||||
|
||||
log.debug("Sending SMS request to {}, payload: {}", httpPost.getURI(), payload);
|
||||
|
||||
// send http request and handle response
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity());
|
||||
|
||||
log.debug("SMS response status: {}, body: {}", statusCode, responseBody);
|
||||
|
||||
if (statusCode != 200) {
|
||||
throw new SendMessageException("HTTP request failed with status code: " + statusCode);
|
||||
}
|
||||
|
||||
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
|
||||
JsonNode responseNode = jsonResponse.get("Response");
|
||||
JsonNode error = responseNode.get("Error");
|
||||
if (error != null) {
|
||||
String code = error.get("Code").asText();
|
||||
String message = error.get("Message").asText();
|
||||
throw new SendMessageException(code + ":" + message);
|
||||
}
|
||||
JsonNode sendStatusSet = responseNode.get("SendStatusSet");
|
||||
if (sendStatusSet != null && sendStatusSet.isArray() && sendStatusSet.size() > 0) {
|
||||
JsonNode firstStatus = sendStatusSet.get(0);
|
||||
String code = firstStatus.get("Code").asText();
|
||||
String message = firstStatus.get("Message").asText();
|
||||
if (!RESPONSE_OK.equals(code)) {
|
||||
throw new SendMessageException(code + ":" + message);
|
||||
}
|
||||
}
|
||||
log.info("Successfully sent SMS to phones: {}", String.join(",", phones));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to send SMS: {}", e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TENCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkConfig() {
|
||||
if (appId.isBlank() || templateId.isBlank() || secretId.isBlank() || secretKey.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String calculateAuthorization(String secretId, String secretKey,
|
||||
String service, String host, String region,
|
||||
String action, String version, String payload) throws Exception {
|
||||
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
|
||||
|
||||
// Step 1: Construct the canonical request string
|
||||
String httpRequestMethod = "POST";
|
||||
String canonicalUri = "/";
|
||||
String canonicalQueryString = "";
|
||||
String canonicalHeaders = "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n";
|
||||
String signedHeaders = "content-type;host";
|
||||
String hashedRequestPayload = CryptoUtils.sha256Hex(payload);
|
||||
String canonicalRequest = httpRequestMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n"
|
||||
+ canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;
|
||||
|
||||
// Step 2: Construct the string to sign
|
||||
String algorithm = "TC3-HMAC-SHA256";
|
||||
String credentialScope = date + "/" + service + "/" + "tc3_request";
|
||||
String hashedCanonicalRequest = CryptoUtils.sha256Hex(canonicalRequest);
|
||||
String stringToSign = algorithm + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
|
||||
|
||||
// Step 3: Calculate the signature
|
||||
byte[] secretDate = CryptoUtils.hmac256(("TC3" + secretKey).getBytes(UTF8), date);
|
||||
byte[] secretService = CryptoUtils.hmac256(secretDate, service);
|
||||
byte[] secretSigning = CryptoUtils.hmac256(secretService, "tc3_request");
|
||||
String signature = DatatypeConverter.printHexBinary(
|
||||
CryptoUtils.hmac256(secretSigning, stringToSign)).toLowerCase();
|
||||
|
||||
// Step 4: Construct the Authorization header
|
||||
return algorithm + " " + "Credential=" + secretId + "/" + credentialScope + ", "
|
||||
+ "SignedHeaders=" + signedHeaders + ", " + "Signature=" + signature;
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
|
||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.TWILIO;
|
||||
|
||||
/**
|
||||
* Twilio SMS Client Implementation<br>
|
||||
* API doc: <a href=
|
||||
* "https://www.twilio.com/docs/sms/api">https://www.twilio.com/docs/sms/api</a>
|
||||
*/
|
||||
@Slf4j
|
||||
public class TwilioSmsClientImpl implements SmsClient {
|
||||
|
||||
private static final String API_URL_FORMAT = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json";
|
||||
private static final String MESSAGE_TEMPLATE = "Instance: %s, Priority: %s, Content: %s";
|
||||
|
||||
private final String accountSid;
|
||||
private final String authToken;
|
||||
private final String twilioPhoneNumber;
|
||||
|
||||
public TwilioSmsClientImpl(TwilioSmsProperties config) {
|
||||
if (config != null) {
|
||||
this.accountSid = config.getAccountSid();
|
||||
this.authToken = config.getAuthToken();
|
||||
this.twilioPhoneNumber = config.getTwilioPhoneNumber();
|
||||
} else {
|
||||
this.accountSid = "";
|
||||
this.authToken = "";
|
||||
this.twilioPhoneNumber = "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
String instance = null;
|
||||
String priority = null;
|
||||
String content = null;
|
||||
if (alert.getCommonLabels() != null) {
|
||||
instance = alert.getCommonLabels().get("instance") == null ? alert.getGroupKey()
|
||||
: alert.getCommonLabels().get("instance");
|
||||
priority = alert.getCommonLabels().get("priority") == null ? "unknown"
|
||||
: alert.getCommonLabels().get("priority");
|
||||
content = alert.getCommonAnnotations().get("summary");
|
||||
content = content == null ? alert.getCommonAnnotations().get("description") : content;
|
||||
if (content == null) {
|
||||
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
this.send(receiver.getPhone(), createMessage(instance, priority, content));
|
||||
}
|
||||
|
||||
private String createMessage(String instance, String priority, String content) {
|
||||
return String.format(MESSAGE_TEMPLATE, instance, priority, content);
|
||||
}
|
||||
|
||||
private void send(String phoneNumber, String message) {
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
String endpoint = String.format(API_URL_FORMAT, accountSid);
|
||||
URI requestUri = new URI(endpoint);
|
||||
|
||||
HttpPost httpPost = createHttpPost(requestUri, phoneNumber, message);
|
||||
log.info("Sending Twilio SMS request to {}", requestUri);
|
||||
executeRequest(httpClient, httpPost, phoneNumber);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to send SMS: {}", e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private HttpPost createHttpPost(URI requestUri, String toNumber, String message) {
|
||||
HttpPost httpPost = new HttpPost(requestUri);
|
||||
|
||||
String auth = accountSid + ":" + authToken;
|
||||
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
|
||||
httpPost.setHeader("Authorization", "Basic " + encodedAuth);
|
||||
|
||||
List<BasicNameValuePair> parameters = new ArrayList<>();
|
||||
parameters.add(new BasicNameValuePair("To", toNumber));
|
||||
parameters.add(new BasicNameValuePair("From", twilioPhoneNumber));
|
||||
parameters.add(new BasicNameValuePair("Body", message));
|
||||
|
||||
try {
|
||||
httpPost.setEntity(new UrlEncodedFormEntity(parameters));
|
||||
return httpPost;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create HTTP request: {}", e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void executeRequest(CloseableHttpClient httpClient, HttpPost httpPost, String phoneNumber)
|
||||
throws Exception {
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity());
|
||||
log.info("SMS response status: {}, body: {}", statusCode, responseBody);
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
|
||||
if (responseBody.contains("21608")) {
|
||||
throw new SendMessageException(
|
||||
"The Twilio trial account can only send SMS to verified phone numbers");
|
||||
} else {
|
||||
throw new SendMessageException(
|
||||
"HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
|
||||
if (jsonResponse == null) {
|
||||
throw new SendMessageException(statusCode + ":" + responseBody);
|
||||
}
|
||||
|
||||
JsonNode sidNode = jsonResponse.get("sid");
|
||||
if (sidNode == null) {
|
||||
throw new SendMessageException(statusCode + ":" + responseBody);
|
||||
}
|
||||
|
||||
String sid = sidNode.asText();
|
||||
log.info("Successfully sent SMS to phone: {}, sid: {}", phoneNumber, sid);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TWILIO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkConfig() {
|
||||
return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank());
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.alert.config.UniSmsProperties;
|
||||
import org.apache.hertzbeat.alert.service.SmsClient;
|
||||
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
|
||||
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
|
||||
import org.apache.hertzbeat.common.support.exception.SendMessageException;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.apache.hertzbeat.alert.util.CryptoUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS;
|
||||
|
||||
/**
|
||||
* UniSMS client implementation <br/>
|
||||
* doc:<a href="https://unisms.apistd.com/docs/api/send">https://unisms.apistd.com/docs/api/send</a>
|
||||
*/
|
||||
@Slf4j
|
||||
public class UniSmsClientImpl implements SmsClient {
|
||||
|
||||
private static final String API_URL = "https://uni.apistd.com";
|
||||
private static final String ACTION = "sms.message.send";
|
||||
private static final String SUCCESS_CODE = "0";
|
||||
private static final String HMAC_ALGORITHM = "hmac-sha256";
|
||||
|
||||
private final UniSmsProperties config;
|
||||
|
||||
public UniSmsClientImpl(UniSmsProperties config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
|
||||
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
||||
// build request parameters
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("to", receiver.getPhone());
|
||||
params.put("signature", config.getSignature());
|
||||
params.put("templateId", config.getTemplateId());
|
||||
|
||||
// build template data
|
||||
Map<String, String> templateData = new HashMap<>();
|
||||
String instance = alert.getCommonLabels().getOrDefault("instance", alert.getGroupKey());
|
||||
String priority = alert.getCommonLabels().getOrDefault("priority", "unknown");
|
||||
String content = alert.getCommonAnnotations().get("summary");
|
||||
content = content == null ? alert.getCommonAnnotations().get("description") : content;
|
||||
if (content == null) {
|
||||
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
templateData.put("instance", instance);
|
||||
templateData.put("priority", priority);
|
||||
templateData.put("content", content);
|
||||
params.put("templateData", templateData);
|
||||
|
||||
// build URL and request headers
|
||||
String url;
|
||||
if ("hmac".equalsIgnoreCase(config.getAuthMode())) {
|
||||
url = buildHmacUrl();
|
||||
} else {
|
||||
url = buildSimpleUrl();
|
||||
}
|
||||
|
||||
// send HTTP request
|
||||
HttpPost httpPost = new HttpPost(url);
|
||||
httpPost.setHeader("Content-Type", "application/json");
|
||||
httpPost.setHeader("Accept", "application/json");
|
||||
|
||||
String payload = JsonUtil.toJson(params);
|
||||
httpPost.setEntity(new StringEntity(payload, StandardCharsets.UTF_8));
|
||||
|
||||
log.info("Sending SMS request to UniSMS, payload: {}, url: {}", payload, url);
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
|
||||
handleResponse(response, receiver.getPhone());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send SMS via UniSMS: {}", e.getMessage());
|
||||
throw new SendMessageException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String buildSimpleUrl() {
|
||||
return String.format("%s/?action=%s&accessKeyId=%s",
|
||||
API_URL, ACTION, config.getAccessKeyId());
|
||||
}
|
||||
|
||||
private String buildHmacUrl() {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String nonce = generateNonce();
|
||||
|
||||
// build query parameters
|
||||
Map<String, String> params = new TreeMap<>();
|
||||
params.put("accessKeyId", config.getAccessKeyId());
|
||||
params.put("action", ACTION);
|
||||
params.put("algorithm", HMAC_ALGORITHM);
|
||||
params.put("nonce", nonce);
|
||||
params.put("timestamp", String.valueOf(timestamp));
|
||||
|
||||
// build sign text
|
||||
String signText = params.entrySet().stream()
|
||||
.map(entry -> entry.getKey() + "=" + entry.getValue())
|
||||
.collect(Collectors.joining("&"));
|
||||
|
||||
// calculate signature
|
||||
String signature = CryptoUtils.hmacSha256Base64(config.getAccessKeySecret(), signText);
|
||||
return String.format("%s/?action=%s&accessKeyId=%s&algorithm=%s×tamp=%d&nonce=%s&signature=%s",
|
||||
API_URL, ACTION, config.getAccessKeyId(), HMAC_ALGORITHM, timestamp, nonce, signature);
|
||||
}
|
||||
|
||||
private String generateNonce() {
|
||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
||||
}
|
||||
|
||||
private void handleResponse(CloseableHttpResponse response, String phone) throws IOException {
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String responseBody = EntityUtils.toString(response.getEntity());
|
||||
|
||||
log.info("UniSMS response status: {}, body: {}", statusCode, responseBody);
|
||||
|
||||
if (statusCode != 200) {
|
||||
throw new SendMessageException("HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
|
||||
}
|
||||
|
||||
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
|
||||
String code = jsonResponse.get("code").asText();
|
||||
if (!SUCCESS_CODE.equals(code)) {
|
||||
String message = jsonResponse.get("message").asText();
|
||||
throw new SendMessageException(code + ":" + message);
|
||||
}
|
||||
|
||||
log.info("Successfully sent SMS to phone: {}", phone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return UNISMS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkConfig() {
|
||||
if (config == null
|
||||
|| config.getAccessKeyId() == null
|
||||
|| config.getAccessKeyId().isBlank()
|
||||
|| config.getSignature() == null
|
||||
|| config.getSignature().isBlank()
|
||||
|| config.getTemplateId() == null
|
||||
|| config.getTemplateId().isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// HMAC mode requires additional check for accessKeySecret
|
||||
if ("hmac".equalsIgnoreCase(config.getAuthMode())
|
||||
&& (config.getAccessKeySecret() == null || config.getAccessKeySecret().isBlank())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* General encryption utility class
|
||||
*/
|
||||
@Slf4j
|
||||
public class CryptoUtils {
|
||||
|
||||
private static final Charset UTF8 = StandardCharsets.UTF_8;
|
||||
|
||||
private CryptoUtils() {}
|
||||
|
||||
/**
|
||||
* Calculate HMAC-SHA256 signature
|
||||
* @param key secret key
|
||||
* @param msg message to be signed
|
||||
* @return signed byte array
|
||||
*/
|
||||
public static byte[] hmac256(byte[] key, String msg) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
|
||||
mac.init(secretKeySpec);
|
||||
return mac.doFinal(msg.getBytes(UTF8));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to calculate HMAC-SHA256: {}", e.getMessage());
|
||||
throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate SHA256 hash and convert to lowercase hexadecimal string
|
||||
* @param data data to be hashed
|
||||
* @return lowercase hexadecimal string
|
||||
*/
|
||||
public static String sha256Hex(String data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] digest = md.digest(data.getBytes(UTF8));
|
||||
return DatatypeConverter.printHexBinary(digest).toLowerCase();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to calculate SHA256: {}", e.getMessage());
|
||||
throw new RuntimeException("Failed to calculate SHA256", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate HMAC-SHA256 signature and convert to Base64 string
|
||||
* @param key secret key
|
||||
* @param data data to be signed
|
||||
* @return Base64 encoded signature string
|
||||
*/
|
||||
public static String hmacSha256Base64(String key, String data) {
|
||||
byte[] hmacResult = hmac256(key.getBytes(UTF8), data);
|
||||
return Base64.getEncoder().encodeToString(hmacResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate HMAC-SHA256 signature and convert to lowercase hexadecimal string
|
||||
* @param key secret key
|
||||
* @param data data to be signed
|
||||
* @return lowercase hexadecimal string
|
||||
*/
|
||||
public static String hmacSha256Hex(String key, String data) {
|
||||
return hmacSha256Hex(hmac256(key.getBytes(UTF8), data), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate HMAC-SHA256 signature and convert to lowercase hexadecimal string
|
||||
* Note: This is necessary as, converting a String to byte[] and back may yield different results due to encoding differences or data loss.
|
||||
* @param key secret key in byte array
|
||||
* @param data data to be signed
|
||||
* @return lowercase hexadecimal string
|
||||
*/
|
||||
public static String hmacSha256Hex(byte[] key, String data) {
|
||||
byte[] hmacResult = hmac256(key, data);
|
||||
return DatatypeConverter.printHexBinary(hmacResult).toLowerCase();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -99,7 +99,7 @@ class EmailAlertNotifyHandlerImplTest {
|
||||
template.setName("test-template");
|
||||
template.setContent("test content");
|
||||
|
||||
// 设置邮件服务器配置
|
||||
// Set up email server configuration
|
||||
MailServerConfig mailServerConfig = new MailServerConfig();
|
||||
mailServerConfig.setEmailHost("smtp.example.com");
|
||||
mailServerConfig.setEmailPort(587);
|
||||
|
||||
+2
-2
@@ -83,8 +83,8 @@ class AlarmInhibitReduceTest {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
when(alertInhibitDao.findAlertInhibitsByEnableIsTrue())
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
// 正确设置 AlerterProperties mock
|
||||
|
||||
// Correctly set up AlerterProperties mock
|
||||
AlerterProperties.InhibitProperties inhibitProperties = new AlerterProperties.InhibitProperties();
|
||||
inhibitProperties.setTtl(60000);
|
||||
when(alerterProperties.getInhibit()).thenReturn(inhibitProperties);
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service;
|
||||
|
||||
import org.apache.hertzbeat.alert.config.AwsSmsProperties;
|
||||
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Test case for {@link AwsSmsClientImpl}
|
||||
*/
|
||||
class AwsSmsClientImplTest {
|
||||
|
||||
|
||||
@Test
|
||||
void getType() {
|
||||
AwsSmsProperties awsSmsProperties = new AwsSmsProperties();
|
||||
awsSmsProperties.setAccessKeyId("accessKeyId");
|
||||
awsSmsProperties.setAccessKeySecret("accessKeySecret");
|
||||
awsSmsProperties.setRegion("region");
|
||||
AwsSmsClientImpl awsSmsClient = new AwsSmsClientImpl(awsSmsProperties);
|
||||
|
||||
assertEquals("aws", awsSmsClient.getType());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"accessKeyId, accessKeySecret, region, true",
|
||||
"accessKeyId, accessKeySecret, '', false",
|
||||
"accessKeyId, '', region, false",
|
||||
"accessKeyId, accessKeySecret, '', false",
|
||||
"accessKeyId, '', '', false",
|
||||
"'', accessKeySecret, region, false",
|
||||
"'', accessKeySecret, '', false",
|
||||
"'', '', region, false",
|
||||
"'', accessKeySecret, '', false",
|
||||
})
|
||||
void checkConfig(String accessKeyId, String accessKeySecret, String region, boolean expected) {
|
||||
AwsSmsProperties awsSmsProperties = new AwsSmsProperties();
|
||||
awsSmsProperties.setAccessKeyId(accessKeyId);
|
||||
awsSmsProperties.setAccessKeySecret(accessKeySecret);
|
||||
awsSmsProperties.setRegion(region);
|
||||
AwsSmsClientImpl awsSmsClient = new AwsSmsClientImpl(awsSmsProperties);
|
||||
|
||||
assertEquals(expected, awsSmsClient.checkConfig());
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.alert.service;
|
||||
|
||||
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
|
||||
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Test case for {@link TwilioSmsClientImpl}
|
||||
*/
|
||||
class TwilioSmsClientImplTest {
|
||||
|
||||
@Test
|
||||
void getType() {
|
||||
TwilioSmsProperties twilioSmsProperties = new TwilioSmsProperties();
|
||||
twilioSmsProperties.setAccountSid("accountSid");
|
||||
twilioSmsProperties.setAuthToken("authToken");
|
||||
twilioSmsProperties.setTwilioPhoneNumber("twilioPhoneNumber");
|
||||
TwilioSmsClientImpl twilioSmsClient = new TwilioSmsClientImpl(twilioSmsProperties);
|
||||
|
||||
assertEquals("twilio", twilioSmsClient.getType());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"accountSid, authToken, twilioPhoneNumber, true",
|
||||
"accountSid, authToken, '', false",
|
||||
"accountSid, '', twilioPhoneNumber, false",
|
||||
"accountSid, '', '', false",
|
||||
"'', authToken, twilioPhoneNumber, false",
|
||||
"'', authToken, '', false",
|
||||
"'', '', twilioPhoneNumber, false",
|
||||
"'', '', '', false",
|
||||
})
|
||||
void checkConfig(String accountSid, String authToken, String twilioPhoneNumber, boolean expected) {
|
||||
TwilioSmsProperties twilioSmsProperties = new TwilioSmsProperties();
|
||||
twilioSmsProperties.setAccountSid(accountSid);
|
||||
twilioSmsProperties.setAuthToken(authToken);
|
||||
twilioSmsProperties.setTwilioPhoneNumber(twilioPhoneNumber);
|
||||
TwilioSmsClientImpl twilioSmsClient = new TwilioSmsClientImpl(twilioSmsProperties);
|
||||
|
||||
assertEquals(expected, twilioSmsClient.checkConfig());
|
||||
}
|
||||
}
|
||||
+87
-51
@@ -26,20 +26,25 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.collect.AbstractCollect;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.JdbcConnect;
|
||||
import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper;
|
||||
import org.apache.hertzbeat.collector.constants.CollectorConstants;
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.collector.util.CollectUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.SshTunnel;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.JdbcProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.sshd.common.SshException;
|
||||
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
|
||||
import org.postgresql.util.PSQLException;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.jdbc.datasource.init.ScriptUtils;
|
||||
@@ -55,7 +60,7 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
private static final String QUERY_TYPE_MULTI_ROW = "multiRow";
|
||||
private static final String QUERY_TYPE_COLUMNS = "columns";
|
||||
private static final String RUN_SCRIPT = "runScript";
|
||||
|
||||
|
||||
private static final String[] VULNERABLE_KEYWORDS = {"allowLoadLocalInfile", "allowLoadLocalInfileInPath", "useLocalInfile"};
|
||||
|
||||
private final GlobalConnectionCache connectionCommonCache = GlobalConnectionCache.getInstance();
|
||||
@@ -73,18 +78,29 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
}
|
||||
}
|
||||
}
|
||||
SshTunnelHelper.checkTunnelParam(metrics.getJdbc().getSshTunnel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
JdbcProtocol jdbcProtocol = metrics.getJdbc();
|
||||
String databaseUrl = constructDatabaseUrl(jdbcProtocol);
|
||||
SshTunnel sshTunnel = jdbcProtocol.getSshTunnel();
|
||||
|
||||
int timeout = CollectUtil.getTimeout(jdbcProtocol.getTimeout());
|
||||
boolean reuseConnection = Boolean.parseBoolean(jdbcProtocol.getReuseConnection());
|
||||
Statement statement = null;
|
||||
String databaseUrl;
|
||||
try {
|
||||
if (sshTunnel != null && Boolean.parseBoolean(sshTunnel.getEnable())) {
|
||||
int localPort = SshTunnelHelper.localPortForward(sshTunnel, jdbcProtocol.getHost(), jdbcProtocol.getPort());
|
||||
databaseUrl = constructDatabaseUrl(jdbcProtocol, "localhost", String.valueOf(localPort));
|
||||
} else {
|
||||
databaseUrl = constructDatabaseUrl(jdbcProtocol, jdbcProtocol.getHost(), jdbcProtocol.getPort());
|
||||
}
|
||||
|
||||
statement = getConnection(jdbcProtocol.getUsername(),
|
||||
jdbcProtocol.getPassword(), databaseUrl, timeout);
|
||||
jdbcProtocol.getPassword(), databaseUrl, timeout, reuseConnection);
|
||||
switch (jdbcProtocol.getQueryType()) {
|
||||
case QUERY_TYPE_ONE_ROW -> queryOneRow(statement, jdbcProtocol.getSql(), metrics.getAliasFields(), builder, startTime);
|
||||
case QUERY_TYPE_MULTI_ROW -> queryMultiRow(statement, jdbcProtocol.getSql(), metrics.getAliasFields(), builder, startTime);
|
||||
@@ -112,6 +128,14 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
log.warn("Jdbc sql error: {}, code: {}.", sqlException.getMessage(), sqlException.getErrorCode());
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg("Query Error: " + sqlException.getMessage() + " Code: " + sqlException.getErrorCode());
|
||||
} catch (SshException sshException) {
|
||||
Throwable throwable = sshException.getCause();
|
||||
if (throwable instanceof SshChannelOpenException) {
|
||||
log.warn("[Jdbc collect] Remote ssh server no more session channel, please increase sshd_config MaxSessions.");
|
||||
}
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(sshException);
|
||||
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
|
||||
builder.setMsg("Peer ssh connection failed: " + errorMsg);
|
||||
} catch (Exception e) {
|
||||
String errorMessage = CommonUtil.getMessageFromThrowable(e);
|
||||
log.error("Jdbc error: {}.", errorMessage, e);
|
||||
@@ -119,11 +143,20 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
builder.setMsg("Query Error: " + errorMessage);
|
||||
} finally {
|
||||
if (statement != null) {
|
||||
Connection connection = null;
|
||||
try {
|
||||
connection = statement.getConnection();
|
||||
statement.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Jdbc close statement error: {}", e.getMessage());
|
||||
}
|
||||
try {
|
||||
if (!reuseConnection && connection != null) {
|
||||
connection.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Jdbc close connection error: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,38 +167,40 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
}
|
||||
|
||||
|
||||
private Statement getConnection(String username, String password, String url, Integer timeout) throws Exception {
|
||||
private Statement getConnection(String username, String password, String url, Integer timeout, boolean reuseConnection) throws Exception {
|
||||
CacheIdentifier identifier = CacheIdentifier.builder()
|
||||
.ip(url)
|
||||
.username(username).password(password).build();
|
||||
Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true);
|
||||
Statement statement = null;
|
||||
if (cacheOption.isPresent()) {
|
||||
JdbcConnect jdbcConnect = (JdbcConnect) cacheOption.get();
|
||||
try {
|
||||
statement = jdbcConnect.getConnection().createStatement();
|
||||
// set query timeout
|
||||
int timeoutSecond = timeout / 1000;
|
||||
timeoutSecond = timeoutSecond <= 0 ? 1 : timeoutSecond;
|
||||
statement.setQueryTimeout(timeoutSecond);
|
||||
// set query max row number
|
||||
statement.setMaxRows(1000);
|
||||
} catch (Exception e) {
|
||||
log.info("The jdbc connect from cache, create statement error: {}", e.getMessage());
|
||||
if (reuseConnection) {
|
||||
Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true);
|
||||
if (cacheOption.isPresent()) {
|
||||
JdbcConnect jdbcConnect = (JdbcConnect) cacheOption.get();
|
||||
try {
|
||||
if (statement != null) {
|
||||
statement.close();
|
||||
statement = jdbcConnect.getConnection().createStatement();
|
||||
// set query timeout
|
||||
int timeoutSecond = timeout / 1000;
|
||||
timeoutSecond = timeoutSecond <= 0 ? 1 : timeoutSecond;
|
||||
statement.setQueryTimeout(timeoutSecond);
|
||||
// set query max row number
|
||||
statement.setMaxRows(1000);
|
||||
} catch (Exception e) {
|
||||
log.info("The jdbc connect from cache, create statement error: {}", e.getMessage());
|
||||
try {
|
||||
if (statement != null) {
|
||||
statement.close();
|
||||
}
|
||||
jdbcConnect.close();
|
||||
} catch (Exception e2) {
|
||||
log.error(e2.getMessage());
|
||||
}
|
||||
jdbcConnect.close();
|
||||
} catch (Exception e2) {
|
||||
log.error(e2.getMessage());
|
||||
statement = null;
|
||||
connectionCommonCache.removeCache(identifier);
|
||||
}
|
||||
statement = null;
|
||||
connectionCommonCache.removeCache(identifier);
|
||||
}
|
||||
}
|
||||
if (statement != null) {
|
||||
return statement;
|
||||
if (statement != null) {
|
||||
return statement;
|
||||
}
|
||||
}
|
||||
// renew connection when failed
|
||||
Connection connection = DriverManager.getConnection(url, username, password);
|
||||
@@ -175,8 +210,10 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
timeoutSecond = timeoutSecond <= 0 ? 1 : timeoutSecond;
|
||||
statement.setQueryTimeout(timeoutSecond);
|
||||
statement.setMaxRows(1000);
|
||||
JdbcConnect jdbcConnect = new JdbcConnect(connection);
|
||||
connectionCommonCache.addCache(identifier, jdbcConnect);
|
||||
if (reuseConnection) {
|
||||
JdbcConnect jdbcConnect = new JdbcConnect(connection);
|
||||
connectionCommonCache.addCache(identifier, jdbcConnect);
|
||||
}
|
||||
return statement;
|
||||
}
|
||||
|
||||
@@ -185,13 +222,14 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
* eg:
|
||||
* query metrics:one tow three four
|
||||
* query sql:select one, tow, three, four from book limit 1;
|
||||
*
|
||||
* @param statement statement
|
||||
* @param sql sql
|
||||
* @param columns query metrics field list
|
||||
* @param sql sql
|
||||
* @param columns query metrics field list
|
||||
* @throws Exception when error happen
|
||||
*/
|
||||
private void queryOneRow(Statement statement, String sql, List<String> columns,
|
||||
CollectRep.MetricsData.Builder builder, long startTime) throws Exception {
|
||||
CollectRep.MetricsData.Builder builder, long startTime) throws Exception {
|
||||
statement.setMaxRows(1);
|
||||
try (ResultSet resultSet = statement.executeQuery(sql)) {
|
||||
if (resultSet.next()) {
|
||||
@@ -216,14 +254,15 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
* eg:
|
||||
* query metrics:one two three four
|
||||
* query sql:select key, value from book; the key is the query metrics fields
|
||||
* select key, value from book;
|
||||
* select key, value from book;
|
||||
* one - value1
|
||||
* two - value2
|
||||
* three - value3
|
||||
* four - value4
|
||||
*
|
||||
* @param statement statement
|
||||
* @param sql sql
|
||||
* @param columns query metrics field list
|
||||
* @param sql sql
|
||||
* @param columns query metrics field list
|
||||
* @throws Exception when error happen
|
||||
*/
|
||||
private void queryOneRowByMatchTwoColumns(Statement statement, String sql, List<String> columns,
|
||||
@@ -256,9 +295,10 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
* query metrics:one tow three four
|
||||
* query sql:select one, tow, three, four from book;
|
||||
* and return multi row record mapping with the metrics
|
||||
*
|
||||
* @param statement statement
|
||||
* @param sql sql
|
||||
* @param columns query metrics field list
|
||||
* @param sql sql
|
||||
* @param columns query metrics field list
|
||||
* @throws Exception when error happen
|
||||
*/
|
||||
private void queryMultiRow(Statement statement, String sql, List<String> columns,
|
||||
@@ -283,14 +323,16 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
|
||||
/**
|
||||
* construct jdbc url due the jdbc protocol
|
||||
*
|
||||
* @param jdbcProtocol jdbc
|
||||
* @return URL
|
||||
*/
|
||||
private String constructDatabaseUrl(JdbcProtocol jdbcProtocol) {
|
||||
private String constructDatabaseUrl(JdbcProtocol jdbcProtocol, String host, String port) {
|
||||
if (Objects.nonNull(jdbcProtocol.getUrl())
|
||||
&& !Objects.equals("", jdbcProtocol.getUrl())
|
||||
&& jdbcProtocol.getUrl().startsWith("jdbc")) {
|
||||
String url = jdbcProtocol.getUrl().toLowerCase(); // convert the URL to lowercase for case-insensitive checking
|
||||
// convert the URL to lowercase for case-insensitive checking
|
||||
String url = jdbcProtocol.getUrl().toLowerCase();
|
||||
// check whether the parameter is valid
|
||||
if (url.contains("create trigger") || url.contains("create alias") || url.contains("runscript from")
|
||||
|| url.contains("allowloadlocalinfile") || url.contains("allowloadlocalinfileinpath")
|
||||
@@ -302,25 +344,19 @@ public class JdbcCommonCollect extends AbstractCollect {
|
||||
return jdbcProtocol.getUrl();
|
||||
}
|
||||
return switch (jdbcProtocol.getPlatform()) {
|
||||
case "mysql", "mariadb" ->
|
||||
"jdbc:mysql://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
|
||||
case "mysql", "mariadb" -> "jdbc:mysql://" + host + ":" + port
|
||||
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase())
|
||||
+ "?useUnicode=true&characterEncoding=utf-8&useSSL=false";
|
||||
case "postgresql" ->
|
||||
"jdbc:postgresql://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
|
||||
case "postgresql" -> "jdbc:postgresql://" + host + ":" + port
|
||||
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
|
||||
case "clickhouse" ->
|
||||
"jdbc:clickhouse://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
|
||||
case "clickhouse" -> "jdbc:clickhouse://" + host + ":" + port
|
||||
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
|
||||
case "sqlserver" ->
|
||||
"jdbc:sqlserver://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
|
||||
case "sqlserver" -> "jdbc:sqlserver://" + host + ":" + port
|
||||
+ ";" + (jdbcProtocol.getDatabase() == null ? "" : "DatabaseName=" + jdbcProtocol.getDatabase())
|
||||
+ ";trustServerCertificate=true;";
|
||||
case "oracle" ->
|
||||
"jdbc:oracle:thin:@" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort()
|
||||
case "oracle" -> "jdbc:oracle:thin:@" + host + ":" + port
|
||||
+ "/" + (jdbcProtocol.getDatabase() == null ? "" : jdbcProtocol.getDatabase());
|
||||
case "dm" ->
|
||||
"jdbc:dm://" + jdbcProtocol.getHost() + ":" + jdbcProtocol.getPort();
|
||||
case "dm" -> "jdbc:dm://" + host + ":" + port;
|
||||
default -> throw new IllegalArgumentException("Not support database platform: " + jdbcProtocol.getPlatform());
|
||||
};
|
||||
}
|
||||
|
||||
+59
-10
@@ -86,6 +86,7 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
@@ -509,13 +510,37 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
}
|
||||
// params
|
||||
Map<String, String> params = httpProtocol.getParams();
|
||||
boolean enableUrlEncoding = Boolean.parseBoolean(httpProtocol.getEnableUrlEncoding());
|
||||
StringBuilder queryParams = new StringBuilder();
|
||||
|
||||
if (params != null && !params.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : params.entrySet()) {
|
||||
if (StringUtils.hasText(param.getValue())) {
|
||||
requestBuilder.addParameter(param.getKey(), TimeExpressionUtil.calculate(param.getValue()));
|
||||
String key = param.getKey();
|
||||
String value = param.getValue();
|
||||
|
||||
if (!StringUtils.hasText(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (queryParams.length() > 0) {
|
||||
queryParams.append("&");
|
||||
}
|
||||
|
||||
if (enableUrlEncoding) {
|
||||
key = UriUtils.encodeQueryParam(key, "UTF-8");
|
||||
}
|
||||
queryParams.append(key);
|
||||
|
||||
if (StringUtils.hasText(value)) {
|
||||
String calculatedValue = TimeExpressionUtil.calculate(value);
|
||||
if (enableUrlEncoding) {
|
||||
calculatedValue = UriUtils.encodeQueryParam(calculatedValue, "UTF-8");
|
||||
}
|
||||
queryParams.append("=").append(calculatedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The default request header can be overridden if customized
|
||||
// keep-alive
|
||||
requestBuilder.addHeader(HttpHeaders.CONNECTION, NetworkConstants.KEEP_ALIVE);
|
||||
@@ -525,8 +550,7 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
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()));
|
||||
requestBuilder.addHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,11 +584,29 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
requestBuilder.setEntity(new StringEntity(httpProtocol.getPayload(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
// uri
|
||||
String uri = CollectUtil.replaceUriSpecialChar(httpProtocol.getUrl());
|
||||
if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) {
|
||||
// uri encode
|
||||
String uri;
|
||||
if (enableUrlEncoding) {
|
||||
// if the url contains parameters directly
|
||||
if (httpProtocol.getUrl().contains("?")) {
|
||||
String path = httpProtocol.getUrl().substring(0, httpProtocol.getUrl().indexOf("?"));
|
||||
String query = httpProtocol.getUrl().substring(httpProtocol.getUrl().indexOf("?") + 1);
|
||||
uri = UriUtils.encodePath(path, "UTF-8") + "?" + UriUtils.encodeQuery(query, "UTF-8");
|
||||
} else {
|
||||
uri = UriUtils.encodePath(httpProtocol.getUrl(), "UTF-8");
|
||||
}
|
||||
} else {
|
||||
uri = httpProtocol.getUrl();
|
||||
}
|
||||
|
||||
requestBuilder.setUri(httpProtocol.getHost() + ":" + httpProtocol.getPort() + uri);
|
||||
// append query params
|
||||
if (queryParams.length() > 0) {
|
||||
uri += (uri.contains("?") ? "&" : "?") + queryParams.toString();
|
||||
}
|
||||
|
||||
String finalUri;
|
||||
if (IpDomainUtil.isHasSchema(httpProtocol.getHost())) {
|
||||
finalUri = httpProtocol.getHost() + ":" + httpProtocol.getPort() + uri;
|
||||
} else {
|
||||
String ipAddressType = IpDomainUtil.checkIpAddressType(httpProtocol.getHost());
|
||||
String baseUri = NetworkConstants.IPV6.equals(ipAddressType)
|
||||
@@ -572,11 +614,18 @@ public class HttpCollectImpl extends AbstractCollect {
|
||||
: String.format("%s:%s%s", httpProtocol.getHost(), httpProtocol.getPort(), uri);
|
||||
boolean ssl = Boolean.parseBoolean(httpProtocol.getSsl());
|
||||
if (ssl) {
|
||||
requestBuilder.setUri(NetworkConstants.HTTPS_HEADER + baseUri);
|
||||
finalUri = NetworkConstants.HTTPS_HEADER + baseUri;
|
||||
} else {
|
||||
requestBuilder.setUri(NetworkConstants.HTTP_HEADER + baseUri);
|
||||
finalUri = NetworkConstants.HTTP_HEADER + baseUri;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
requestBuilder.setUri(finalUri);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Invalid URI with illegal characters: {}. User has disabled URL encoding, not applying any encoding.", finalUri);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// custom timeout
|
||||
int timeout = CollectUtil.getTimeout(httpProtocol.getTimeout(), 0);
|
||||
|
||||
-1
@@ -173,7 +173,6 @@ public class PrometheusAutoCollectImpl {
|
||||
Map<String, MetricFamily> metricFamilyMap = TextParser.textToMetricFamilies(resp);
|
||||
List<CollectRep.MetricsData> metricsDataList = new LinkedList<>();
|
||||
for (Map.Entry<String, MetricFamily> entry : metricFamilyMap.entrySet()) {
|
||||
builder.clearMetrics();
|
||||
builder.clearFields();
|
||||
builder.clearValues();
|
||||
String metricsName = entry.getKey();
|
||||
|
||||
+56
-18
@@ -28,6 +28,8 @@ import io.lettuce.core.cluster.models.partitions.Partitions;
|
||||
import io.lettuce.core.cluster.models.partitions.RedisClusterNode;
|
||||
import io.lettuce.core.resource.ClientResources;
|
||||
import io.lettuce.core.resource.DefaultClientResources;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -42,6 +44,7 @@ import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.RedisConnect;
|
||||
import org.apache.hertzbeat.collector.collect.common.ssh.SshTunnelHelper;
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.collector.util.CollectUtil;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
@@ -51,6 +54,8 @@ import org.apache.hertzbeat.common.entity.job.protocol.RedisProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.hertzbeat.common.util.MapCapUtil;
|
||||
import org.apache.sshd.common.SshException;
|
||||
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -79,6 +84,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
RedisProtocol redisProtocol = metrics.getRedis();
|
||||
Assert.hasText(redisProtocol.getHost(), "Redis Protocol host is required.");
|
||||
Assert.hasText(redisProtocol.getPort(), "Redis Protocol port is required.");
|
||||
SshTunnelHelper.checkTunnelParam(metrics.getRedis().getSshTunnel());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,6 +102,14 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
log.info("[redis connection] error: {}", errorMsg);
|
||||
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
|
||||
builder.setMsg(errorMsg);
|
||||
} catch (SshException sshException) {
|
||||
Throwable throwable = sshException.getCause();
|
||||
if (throwable instanceof SshChannelOpenException) {
|
||||
log.warn("[redis collect] Remote ssh server no more session channel, please increase sshd_config MaxSessions.");
|
||||
}
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(sshException);
|
||||
builder.setCode(CollectRep.Code.UN_CONNECTABLE);
|
||||
builder.setMsg("Peer ssh connection failed: " + errorMsg);
|
||||
} catch (Exception e) {
|
||||
String errorMsg = CommonUtil.getMessageFromThrowable(e);
|
||||
log.warn("[redis collect] error: {}", e.getMessage(), e);
|
||||
@@ -109,7 +123,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @param metrics metrics config
|
||||
* @return data
|
||||
*/
|
||||
private Map<String, String> getSingleRedisInfo(Metrics metrics) {
|
||||
private Map<String, String> getSingleRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException {
|
||||
StatefulRedisConnection<String, String> connection = getSingleConnection(metrics.getRedis());
|
||||
String info = connection.sync().info(metrics.getName());
|
||||
Map<String, String> valueMap = parseInfo(info, metrics);
|
||||
@@ -125,7 +139,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @param metrics metrics config
|
||||
* @return data
|
||||
*/
|
||||
private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) {
|
||||
private List<Map<String, String>> getClusterRedisInfo(Metrics metrics) throws GeneralSecurityException, IOException {
|
||||
Map<String, StatefulRedisClusterConnection<String, String>> connectionMap = getConnectionList(metrics.getRedis());
|
||||
List<Map<String, String>> list = new ArrayList<>(connectionMap.size());
|
||||
connectionMap.forEach((identity, connection) ->{
|
||||
@@ -179,12 +193,16 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @param redisProtocol protocol
|
||||
* @return connection
|
||||
*/
|
||||
private StatefulRedisConnection<String, String> getSingleConnection(RedisProtocol redisProtocol) {
|
||||
CacheIdentifier identifier = doIdentifier(redisProtocol);
|
||||
private StatefulRedisConnection<String, String> getSingleConnection(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
|
||||
String[] resolvedArr = resolveHostAndPort(redisProtocol);
|
||||
String host = resolvedArr[0];
|
||||
String port = resolvedArr[1];
|
||||
|
||||
CacheIdentifier identifier = doIdentifier(redisProtocol, host, port);
|
||||
StatefulRedisConnection<String, String> connection = (StatefulRedisConnection<String, String>) getStatefulConnection(identifier);
|
||||
if (Objects.isNull(connection)) {
|
||||
// reuse connection failed, new one
|
||||
RedisClient redisClient = buildSingleClient(redisProtocol);
|
||||
RedisClient redisClient = buildSingleClient(redisProtocol, host, port);
|
||||
connection = redisClient.connect();
|
||||
connectionCache.addCache(identifier, new RedisConnect(connection));
|
||||
}
|
||||
@@ -196,7 +214,7 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @param redisProtocol protocol
|
||||
* @return connection map
|
||||
*/
|
||||
private Map<String, StatefulRedisClusterConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) {
|
||||
private Map<String, StatefulRedisClusterConnection<String, String>> getConnectionList(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
|
||||
// first connection
|
||||
StatefulRedisClusterConnection<String, String> connection = getClusterConnection(redisProtocol);
|
||||
Partitions partitions = connection.getPartitions();
|
||||
@@ -217,12 +235,16 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @param redisProtocol redis protocol
|
||||
* @return cluster connection
|
||||
*/
|
||||
private StatefulRedisClusterConnection<String, String> getClusterConnection(RedisProtocol redisProtocol) {
|
||||
CacheIdentifier identifier = doIdentifier(redisProtocol);
|
||||
private StatefulRedisClusterConnection<String, String> getClusterConnection(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
|
||||
String[] resolvedArr = resolveHostAndPort(redisProtocol);
|
||||
String host = resolvedArr[0];
|
||||
String port = resolvedArr[1];
|
||||
|
||||
CacheIdentifier identifier = doIdentifier(redisProtocol, host, port);
|
||||
StatefulRedisClusterConnection<String, String> connection = (StatefulRedisClusterConnection<String, String>) getStatefulConnection(identifier);
|
||||
if (connection == null) {
|
||||
// reuse connection failed, new one
|
||||
RedisClusterClient redisClusterClient = buildClusterClient(redisProtocol);
|
||||
RedisClusterClient redisClusterClient = buildClusterClient(redisProtocol, host, port);
|
||||
connection = redisClusterClient.connect();
|
||||
connectionCache.addCache(identifier, new RedisConnect(connection));
|
||||
}
|
||||
@@ -260,8 +282,8 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @param redisProtocol redis protocol config
|
||||
* @return redis cluster client
|
||||
*/
|
||||
private RedisClusterClient buildClusterClient(RedisProtocol redisProtocol) {
|
||||
return RedisClusterClient.create(defaultClientResources, redisUri(redisProtocol));
|
||||
private RedisClusterClient buildClusterClient(RedisProtocol redisProtocol, String host, String port) {
|
||||
return RedisClusterClient.create(defaultClientResources, redisUri(redisProtocol, host, port));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,12 +292,12 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
* @param redisProtocol redis protocol config
|
||||
* @return redis single client
|
||||
*/
|
||||
private RedisClient buildSingleClient(RedisProtocol redisProtocol) {
|
||||
return RedisClient.create(defaultClientResources, redisUri(redisProtocol));
|
||||
private RedisClient buildSingleClient(RedisProtocol redisProtocol, String host, String port) {
|
||||
return RedisClient.create(defaultClientResources, redisUri(redisProtocol, host, port));
|
||||
}
|
||||
|
||||
private RedisURI redisUri(RedisProtocol redisProtocol) {
|
||||
RedisURI.Builder redisUriBuilder = RedisURI.builder().withHost(redisProtocol.getHost()).withPort(Integer.parseInt(redisProtocol.getPort()));
|
||||
private RedisURI redisUri(RedisProtocol redisProtocol, String host, String port) {
|
||||
RedisURI.Builder redisUriBuilder = RedisURI.builder().withHost(host).withPort(Integer.parseInt(port));
|
||||
if (StringUtils.hasText(redisProtocol.getUsername())) {
|
||||
redisUriBuilder.withClientName(redisProtocol.getUsername());
|
||||
}
|
||||
@@ -295,10 +317,10 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
return ip + SignConstants.DOUBLE_MARK + port;
|
||||
}
|
||||
|
||||
private CacheIdentifier doIdentifier(RedisProtocol redisProtocol) {
|
||||
private CacheIdentifier doIdentifier(RedisProtocol redisProtocol, String host, String port) {
|
||||
return CacheIdentifier.builder()
|
||||
.ip(redisProtocol.getHost())
|
||||
.port(redisProtocol.getPort())
|
||||
.ip(host)
|
||||
.port(port)
|
||||
.username(redisProtocol.getUsername())
|
||||
.password(redisProtocol.getPassword())
|
||||
.customArg(redisProtocol.getPattern())
|
||||
@@ -328,6 +350,22 @@ public class RedisCommonCollectImpl extends AbstractCollect {
|
||||
return result;
|
||||
}
|
||||
|
||||
private String[] resolveHostAndPort(RedisProtocol redisProtocol) throws GeneralSecurityException, IOException {
|
||||
boolean enableSshTunnel = Optional.ofNullable(redisProtocol.getSshTunnel())
|
||||
.map(ssh -> Boolean.parseBoolean(ssh.getEnable()))
|
||||
.orElse(false);
|
||||
String host;
|
||||
String port;
|
||||
if (enableSshTunnel){
|
||||
host = "localhost";
|
||||
port = String.valueOf(SshTunnelHelper.localPortForward(redisProtocol.getSshTunnel(), redisProtocol.getHost(), redisProtocol.getPort()));
|
||||
} else {
|
||||
host = redisProtocol.getHost();
|
||||
port = redisProtocol.getPort();
|
||||
}
|
||||
return new String[]{host, port};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String supportProtocol() {
|
||||
return DispatchConstants.PROTOCOL_REDIS;
|
||||
|
||||
+67
@@ -84,6 +84,8 @@ public class SnmpCollectImpl extends AbstractCollect {
|
||||
+ "{1,choice,0#|1#1 hour, |1<{1,number,integer} hours, }"
|
||||
+ "{2,choice,0#|1#1 minute, |1<{2,number,integer} minutes, }"
|
||||
+ "{3,choice,0#|1#1 second, |1<{3,number,integer} seconds }";
|
||||
private static final String DATE_AND_TIME_PATTERN = "%04d-%02d-%02d,%02d:%02d:%02d.%d";
|
||||
private static final String TIME_ZONE_PATTERN = "%c%02d:%02d";
|
||||
|
||||
private final Map<Integer, Snmp> versionSnmpService = new ConcurrentHashMap<>(3);
|
||||
|
||||
@@ -305,6 +307,9 @@ public class SnmpCollectImpl extends AbstractCollect {
|
||||
CharBuffer res = decoder.decode(ByteBuffer.wrap(bytes));
|
||||
return res.toString();
|
||||
} catch (Exception e) {
|
||||
if (isDateAndTimeOctetString(binding)) {
|
||||
return parseDateAndTime(clearHexStr);
|
||||
}
|
||||
return new String(bytes);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -315,6 +320,68 @@ public class SnmpCollectImpl extends AbstractCollect {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDateAndTimeOctetString(VariableBinding binding) {
|
||||
if (!(binding.getVariable() instanceof OctetString)) {
|
||||
return false;
|
||||
}
|
||||
byte[] bytes = HexFormat.of().parseHex(binding.toValueString().replaceAll(HEX_SPLIT, ""));
|
||||
if (bytes.length != 8 && bytes.length != 11) return false;
|
||||
|
||||
int year = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
|
||||
if (year < 1970 || year > 3000) return false;
|
||||
|
||||
int month = bytes[2] & 0xFF;
|
||||
if (month < 1 || month > 12) return false;
|
||||
|
||||
int day = bytes[3] & 0xFF;
|
||||
if (day < 1 || day > 31) return false;
|
||||
|
||||
int hour = bytes[4] & 0xFF;
|
||||
if (hour > 23) return false;
|
||||
|
||||
int minute = bytes[5] & 0xFF;
|
||||
if (minute > 59) return false;
|
||||
|
||||
int second = bytes[6] & 0xFF;
|
||||
if (second > 59) return false;
|
||||
|
||||
int deciSecond = bytes[7] & 0xFF;
|
||||
if (deciSecond > 99) return false;
|
||||
|
||||
if (bytes.length == 11) {
|
||||
int tzSign = bytes[8] & 0xFF;
|
||||
if (tzSign != 0x2B && tzSign != 0x2D) return false;
|
||||
|
||||
int tzHour = bytes[9] & 0xFF;
|
||||
if (tzHour > 23) return false;
|
||||
|
||||
int tzMinute = bytes[10] & 0xFF;
|
||||
if (tzMinute > 59) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String parseDateAndTime(String hexString) {
|
||||
byte[] bytes = HexFormat.of().parseHex(hexString);
|
||||
int year = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
|
||||
int month = bytes[2] & 0xFF;
|
||||
int day = bytes[3] & 0xFF;
|
||||
int hour = bytes[4] & 0xFF;
|
||||
int minute = bytes[5] & 0xFF;
|
||||
int second = bytes[6] & 0xFF;
|
||||
int deciSeconds = bytes[7] & 0xFF;
|
||||
String dateTime = String.format(DATE_AND_TIME_PATTERN,
|
||||
year, month, day, hour, minute, second, deciSeconds);
|
||||
if (bytes.length == 11) {
|
||||
char sign = (char) bytes[8];
|
||||
int tzHour = bytes[9] & 0xFF;
|
||||
int tzMinute = bytes[10] & 0xFF;
|
||||
dateTime += String.format(" " + TIME_ZONE_PATTERN, sign, tzHour, tzMinute);
|
||||
}
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
|
||||
private void snmpClose(Snmp snmp, int version) throws IOException {
|
||||
snmp.close();
|
||||
versionSnmpService.remove(version);
|
||||
|
||||
+6
-65
@@ -18,7 +18,6 @@
|
||||
package org.apache.hertzbeat.collector.collect.ssh;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.SocketTimeoutException;
|
||||
@@ -30,35 +29,27 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.collect.AbstractCollect;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.SshConnect;
|
||||
import org.apache.hertzbeat.collector.collect.common.ssh.CommonSshBlacklist;
|
||||
import org.apache.hertzbeat.collector.collect.common.ssh.CommonSshClient;
|
||||
import org.apache.hertzbeat.collector.collect.common.ssh.SshHelper;
|
||||
import org.apache.hertzbeat.collector.constants.CollectorConstants;
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.collector.util.CollectUtil;
|
||||
import org.apache.hertzbeat.collector.util.PrivateKeyUtils;
|
||||
import org.apache.hertzbeat.common.constants.CommonConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol;
|
||||
import org.apache.hertzbeat.common.entity.message.CollectRep;
|
||||
import org.apache.hertzbeat.common.util.CommonUtil;
|
||||
import org.apache.sshd.client.SshClient;
|
||||
import org.apache.sshd.client.channel.ClientChannel;
|
||||
import org.apache.sshd.client.channel.ClientChannelEvent;
|
||||
import org.apache.sshd.client.session.ClientSession;
|
||||
import org.apache.sshd.common.SshException;
|
||||
import org.apache.sshd.common.channel.exception.SshChannelOpenException;
|
||||
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
||||
import org.apache.sshd.common.util.io.output.NoCloseOutputStream;
|
||||
import org.apache.sshd.common.util.security.SecurityUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -88,11 +79,12 @@ public class SshCollectImpl extends AbstractCollect {
|
||||
long startTime = System.currentTimeMillis();
|
||||
SshProtocol sshProtocol = metrics.getSsh();
|
||||
boolean reuseConnection = Boolean.parseBoolean(sshProtocol.getReuseConnection());
|
||||
boolean useProxy = Boolean.parseBoolean(sshProtocol.getUseProxy());
|
||||
int timeout = CollectUtil.getTimeout(sshProtocol.getTimeout(), DEFAULT_TIMEOUT);
|
||||
ClientChannel channel = null;
|
||||
ClientSession clientSession = null;
|
||||
try {
|
||||
clientSession = getConnectSession(sshProtocol, timeout, reuseConnection);
|
||||
clientSession = getConnectSession(sshProtocol, timeout, reuseConnection, useProxy);
|
||||
if (CommonSshBlacklist.isCommandBlacklisted(sshProtocol.getScript())) {
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg("The command is blacklisted: " + sshProtocol.getScript());
|
||||
@@ -165,7 +157,7 @@ public class SshCollectImpl extends AbstractCollect {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
if (clientSession != null && !reuseConnection) {
|
||||
if (clientSession != null && !reuseConnection && !useProxy) {
|
||||
try {
|
||||
clientSession.close();
|
||||
} catch (Exception e) {
|
||||
@@ -289,59 +281,8 @@ public class SshCollectImpl extends AbstractCollect {
|
||||
connectionCommonCache.removeCache(identifier);
|
||||
}
|
||||
|
||||
private ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection)
|
||||
private ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection, boolean useProxy)
|
||||
throws IOException, GeneralSecurityException {
|
||||
CacheIdentifier identifier = CacheIdentifier.builder()
|
||||
.ip(sshProtocol.getHost()).port(sshProtocol.getPort())
|
||||
.username(sshProtocol.getUsername()).password(sshProtocol.getPassword())
|
||||
.build();
|
||||
ClientSession clientSession = null;
|
||||
if (reuseConnection) {
|
||||
Optional<AbstractConnection<?>> cacheOption = connectionCommonCache.getCache(identifier, true);
|
||||
if (cacheOption.isPresent()) {
|
||||
SshConnect sshConnect = (SshConnect) cacheOption.get();
|
||||
clientSession = sshConnect.getConnection();
|
||||
try {
|
||||
if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) {
|
||||
clientSession = null;
|
||||
connectionCommonCache.removeCache(identifier);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
clientSession = null;
|
||||
connectionCommonCache.removeCache(identifier);
|
||||
}
|
||||
}
|
||||
if (clientSession != null) {
|
||||
return clientSession;
|
||||
}
|
||||
}
|
||||
SshClient sshClient = CommonSshClient.getSshClient();
|
||||
clientSession = sshClient.connect(sshProtocol.getUsername(), sshProtocol.getHost(), Integer.parseInt(sshProtocol.getPort()))
|
||||
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
|
||||
if (StringUtils.hasText(sshProtocol.getPassword())) {
|
||||
clientSession.addPasswordIdentity(sshProtocol.getPassword());
|
||||
} else if (StringUtils.hasText(sshProtocol.getPrivateKey())) {
|
||||
var resourceKey = PrivateKeyUtils.writePrivateKey(sshProtocol.getHost(), sshProtocol.getPrivateKey());
|
||||
FilePasswordProvider passwordProvider = (session, resource, index) -> {
|
||||
if (StringUtils.hasText(sshProtocol.getPrivateKeyPassphrase())) {
|
||||
return sshProtocol.getPrivateKeyPassphrase();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
SecurityUtils.loadKeyPairIdentities(null, () -> resourceKey, new FileInputStream(resourceKey), passwordProvider)
|
||||
.forEach(clientSession::addPublicKeyIdentity);
|
||||
} // else auth with localhost private public key certificates
|
||||
|
||||
// auth
|
||||
if (!clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS).isSuccess()) {
|
||||
clientSession.close();
|
||||
throw new IllegalArgumentException("ssh auth failed.");
|
||||
}
|
||||
if (reuseConnection) {
|
||||
SshConnect sshConnect = new SshConnect(clientSession);
|
||||
connectionCommonCache.addCache(identifier, sshConnect);
|
||||
}
|
||||
return clientSession;
|
||||
return SshHelper.getConnectSession(sshProtocol, timeout, reuseConnection, useProxy);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-15
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.collector.collect.database;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
|
||||
import org.apache.hertzbeat.common.entity.job.Metrics;
|
||||
@@ -99,28 +100,27 @@ class JdbcCommonCollectTest {
|
||||
"dm"
|
||||
};
|
||||
for (String platform : platforms) {
|
||||
assertDoesNotThrow(() -> {
|
||||
JdbcProtocol jdbc = new JdbcProtocol();
|
||||
jdbc.setPlatform(platform);
|
||||
|
||||
Metrics metrics = new Metrics();
|
||||
metrics.setJdbc(jdbc);
|
||||
|
||||
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
|
||||
jdbcCommonCollect.collect(builder, metrics);
|
||||
});
|
||||
}
|
||||
// invalid platform
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
JdbcProtocol jdbc = new JdbcProtocol();
|
||||
jdbc.setPlatform("invalid");
|
||||
jdbc.setPlatform(platform);
|
||||
|
||||
Metrics metrics = new Metrics();
|
||||
metrics.setJdbc(jdbc);
|
||||
|
||||
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
|
||||
jdbcCommonCollect.collect(builder, metrics);
|
||||
});
|
||||
assertNotEquals(builder.getMsg(), "Query Error: Not support database platform: " + platform);
|
||||
}
|
||||
// invalid platform
|
||||
JdbcProtocol jdbc = new JdbcProtocol();
|
||||
jdbc.setPlatform("invalid");
|
||||
|
||||
Metrics metrics = new Metrics();
|
||||
metrics.setJdbc(jdbc);
|
||||
|
||||
CollectRep.MetricsData.Builder builder = CollectRep.MetricsData.newBuilder();
|
||||
jdbcCommonCollect.collect(builder, metrics);
|
||||
assertEquals(builder.getCode(), CollectRep.Code.FAIL);
|
||||
assertEquals(builder.getMsg(), "Query Error: Not support database platform: invalid");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+12
-19
@@ -183,21 +183,18 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
Job job = timerTask.getJob();
|
||||
job.constructPriorMetrics();
|
||||
Set<Metrics> metricsSet = job.getNextCollectMetrics(null, true);
|
||||
// Avoid NullPointerException
|
||||
if (null != metricsSet){
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -237,8 +234,6 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
long spendTime = System.currentTimeMillis() - job.getDispatchTime();
|
||||
long interval = job.getInterval() - spendTime / 1000;
|
||||
interval = interval <= 0 ? 0 : interval;
|
||||
// Reset Construction Execution Metrics Task View
|
||||
job.constructPriorMetrics();
|
||||
timerDispatch.cyclicJob(timerJob, interval, TimeUnit.SECONDS);
|
||||
}
|
||||
} else if (!metricsSet.isEmpty()) {
|
||||
@@ -335,8 +330,6 @@ public class CommonDispatcher implements MetricsTaskDispatch, CollectDataDispatc
|
||||
long spendTime = System.currentTimeMillis() - job.getDispatchTime();
|
||||
long interval = job.getInterval() - spendTime / 1000;
|
||||
interval = interval <= 0 ? 0 : interval;
|
||||
// Reset Construction Execution Metrics Task View
|
||||
job.constructPriorMetrics();
|
||||
timerDispatch.cyclicJob(timerJob, interval, TimeUnit.SECONDS);
|
||||
}
|
||||
// it is an asynchronous periodic cyclic task, directly response the collected data
|
||||
|
||||
+17
-3
@@ -75,6 +75,18 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
|
||||
* metrics configuration
|
||||
*/
|
||||
protected Metrics metrics;
|
||||
/**
|
||||
* metadata
|
||||
*/
|
||||
protected Map<String, String> metadata;
|
||||
/**
|
||||
* labels
|
||||
*/
|
||||
protected Map<String, String> labels;
|
||||
/**
|
||||
* annotations
|
||||
*/
|
||||
protected Map<String, String> annotations;
|
||||
/**
|
||||
* time wheel timeout
|
||||
*/
|
||||
@@ -119,6 +131,9 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
|
||||
this.id = job.getMonitorId();
|
||||
this.tenantId = job.getTenantId();
|
||||
this.app = job.getApp();
|
||||
this.metadata = job.getMetadata();
|
||||
this.labels = job.getLabels();
|
||||
this.annotations = job.getAnnotations();
|
||||
this.collectDataDispatch = collectDataDispatch;
|
||||
this.isCyclic = job.isCyclic();
|
||||
this.isSd = job.isSd();
|
||||
@@ -136,9 +151,8 @@ public class MetricsCollect implements Runnable, Comparable<MetricsCollect> {
|
||||
this.startTime = System.currentTimeMillis();
|
||||
setNewThreadName(id, app, startTime, metrics);
|
||||
CollectRep.MetricsData.Builder response = CollectRep.MetricsData.newBuilder();
|
||||
response.setApp(app);
|
||||
response.setId(id);
|
||||
response.setTenantId(tenantId);
|
||||
response.setApp(app).setId(id).setTenantId(tenantId)
|
||||
.setLabels(labels).setAnnotations(annotations).addMetadataAll(metadata);
|
||||
// for prometheus auto
|
||||
if (DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(metrics.getProtocol())) {
|
||||
List<CollectRep.MetricsData> metricsData = PrometheusAutoCollectImpl
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ spring:
|
||||
|
||||
collector:
|
||||
info:
|
||||
version: ${COLLECTOR_VERSION:1.6.1}
|
||||
version: ${COLLECTOR_VERSION:1.7.0}
|
||||
ip: ${COLLECTOR_IP:}
|
||||
dispatch:
|
||||
entrance:
|
||||
|
||||
+2
@@ -25,6 +25,7 @@ import org.apache.sshd.common.NamedFactory;
|
||||
import org.apache.sshd.common.PropertyResolverUtils;
|
||||
import org.apache.sshd.common.kex.BuiltinDHFactories;
|
||||
import org.apache.sshd.core.CoreModuleProperties;
|
||||
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
|
||||
|
||||
/**
|
||||
* common ssh pool client
|
||||
@@ -52,6 +53,7 @@ public class CommonSshClient {
|
||||
BuiltinDHFactories.VALUES,
|
||||
ClientBuilder.DH2KEX
|
||||
));
|
||||
SSH_CLIENT.setForwardingFilter(new AcceptAllForwardingFilter());
|
||||
// todo when connect AlibabaCloud ubuntu server, custom signature factories will cause error, why?
|
||||
// SSH_CLIENT.setSignatureFactories(new ArrayList<>(BuiltinSignatures.VALUES));
|
||||
SSH_CLIENT.start();
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.common.ssh;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyPair;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.AbstractConnection;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.CacheIdentifier;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.GlobalConnectionCache;
|
||||
import org.apache.hertzbeat.collector.collect.common.cache.SshConnect;
|
||||
import org.apache.hertzbeat.collector.util.PrivateKeyUtils;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.SshProtocol;
|
||||
import org.apache.sshd.client.SshClient;
|
||||
import org.apache.sshd.client.config.hosts.HostConfigEntry;
|
||||
import org.apache.sshd.client.session.ClientSession;
|
||||
import org.apache.sshd.common.config.keys.FilePasswordProvider;
|
||||
import org.apache.sshd.common.util.security.SecurityUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* ssh helper
|
||||
*/
|
||||
@Slf4j
|
||||
public class SshHelper {
|
||||
|
||||
private static final GlobalConnectionCache CONNECTION_COMMON_CACHE = GlobalConnectionCache.getInstance();
|
||||
|
||||
public static ClientSession getConnectSession(String host, String port, String username, String password, String privateKey,
|
||||
String privateKeyPassphrase, int timeout, boolean reuseConnection)
|
||||
throws IOException, GeneralSecurityException {
|
||||
CacheIdentifier identifier = CacheIdentifier.builder()
|
||||
.ip(host).port(port)
|
||||
.username(username).password(password)
|
||||
.build();
|
||||
ClientSession clientSession = null;
|
||||
if (reuseConnection) {
|
||||
Optional<AbstractConnection<?>> cacheOption = CONNECTION_COMMON_CACHE.getCache(identifier, true);
|
||||
if (cacheOption.isPresent()) {
|
||||
SshConnect sshConnect = (SshConnect) cacheOption.get();
|
||||
clientSession = sshConnect.getConnection();
|
||||
try {
|
||||
if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) {
|
||||
clientSession = null;
|
||||
CONNECTION_COMMON_CACHE.removeCache(identifier);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
clientSession = null;
|
||||
CONNECTION_COMMON_CACHE.removeCache(identifier);
|
||||
}
|
||||
}
|
||||
if (clientSession != null) {
|
||||
return clientSession;
|
||||
}
|
||||
}
|
||||
SshClient sshClient = CommonSshClient.getSshClient();
|
||||
|
||||
clientSession = sshClient.connect(username, host, Integer.parseInt(port))
|
||||
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
|
||||
if (StringUtils.hasText(password)) {
|
||||
clientSession.addPasswordIdentity(password);
|
||||
} else if (StringUtils.hasText(privateKey)) {
|
||||
var resourceKey = PrivateKeyUtils.writePrivateKey(host, privateKey);
|
||||
FilePasswordProvider passwordProvider = (session, resource, index) -> {
|
||||
if (StringUtils.hasText(privateKeyPassphrase)) {
|
||||
return privateKeyPassphrase;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
SecurityUtils.loadKeyPairIdentities(null, () -> resourceKey, new FileInputStream(resourceKey), passwordProvider)
|
||||
.forEach(clientSession::addPublicKeyIdentity);
|
||||
} // else auth with localhost private public key certificates
|
||||
|
||||
// auth
|
||||
if (!clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS).isSuccess()) {
|
||||
clientSession.close();
|
||||
throw new IllegalArgumentException("ssh auth failed.");
|
||||
}
|
||||
if (reuseConnection) {
|
||||
SshConnect sshConnect = new SshConnect(clientSession);
|
||||
CONNECTION_COMMON_CACHE.addCache(identifier, sshConnect);
|
||||
}
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
public static ClientSession getConnectSession(SshProtocol sshProtocol, int timeout, boolean reuseConnection, boolean useProxy)
|
||||
throws IOException, GeneralSecurityException {
|
||||
CacheIdentifier identifier = CacheIdentifier.builder()
|
||||
.ip(sshProtocol.getHost()).port(sshProtocol.getPort())
|
||||
.username(sshProtocol.getUsername()).password(sshProtocol.getPassword())
|
||||
.build();
|
||||
ClientSession clientSession = null;
|
||||
// When using ProxyJump, force connection reuse:
|
||||
// Apache MINA SSHD will pass the proxy password error to the target host in proxy scenarios, causing the first connection to fail.
|
||||
// Reusing connections can skip duplicate authentication and avoid this problem.
|
||||
if (reuseConnection || useProxy) {
|
||||
Optional<AbstractConnection<?>> cacheOption = CONNECTION_COMMON_CACHE.getCache(identifier, true);
|
||||
if (cacheOption.isPresent()) {
|
||||
SshConnect sshConnect = (SshConnect) cacheOption.get();
|
||||
clientSession = sshConnect.getConnection();
|
||||
try {
|
||||
if (clientSession == null || clientSession.isClosed() || clientSession.isClosing()) {
|
||||
clientSession = null;
|
||||
CONNECTION_COMMON_CACHE.removeCache(identifier);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
clientSession = null;
|
||||
CONNECTION_COMMON_CACHE.removeCache(identifier);
|
||||
}
|
||||
}
|
||||
if (clientSession != null) {
|
||||
return clientSession;
|
||||
}
|
||||
}
|
||||
SshClient sshClient = CommonSshClient.getSshClient();
|
||||
HostConfigEntry proxyConfig = new HostConfigEntry();
|
||||
if (useProxy && StringUtils.hasText(sshProtocol.getProxyHost())) {
|
||||
String proxySpec = String.format("%s@%s:%d", sshProtocol.getProxyUsername(), sshProtocol.getProxyHost(), Integer.parseInt(sshProtocol.getProxyPort()));
|
||||
proxyConfig.setHostName(sshProtocol.getHost());
|
||||
proxyConfig.setHost(sshProtocol.getHost());
|
||||
proxyConfig.setPort(Integer.parseInt(sshProtocol.getPort()));
|
||||
proxyConfig.setUsername(sshProtocol.getUsername());
|
||||
proxyConfig.setProxyJump(proxySpec);
|
||||
|
||||
// Apache SSHD requires the password for the proxy to be preloaded into the sshClient instance before connecting
|
||||
if (StringUtils.hasText(sshProtocol.getProxyPassword())) {
|
||||
sshClient.addPasswordIdentity(sshProtocol.getProxyPassword());
|
||||
log.debug("Loaded proxy server password authentication: {}@{}", sshProtocol.getProxyUsername(), sshProtocol.getProxyHost());
|
||||
}
|
||||
if (StringUtils.hasText(sshProtocol.getProxyPrivateKey())) {
|
||||
proxyConfig.setIdentities(List.of(sshProtocol.getProxyPrivateKey()));
|
||||
log.debug("Proxy private key loaded into HostConfigEntry");
|
||||
}
|
||||
}
|
||||
|
||||
if (useProxy && StringUtils.hasText(sshProtocol.getProxyHost())) {
|
||||
try {
|
||||
clientSession = sshClient.connect(proxyConfig)
|
||||
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
|
||||
}
|
||||
finally {
|
||||
sshClient.removePasswordIdentity(sshProtocol.getProxyPassword());
|
||||
}
|
||||
} else {
|
||||
clientSession = sshClient.connect(sshProtocol.getUsername(), sshProtocol.getHost(), Integer.parseInt(sshProtocol.getPort()))
|
||||
.verify(timeout, TimeUnit.MILLISECONDS).getSession();
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(sshProtocol.getPassword())) {
|
||||
clientSession.addPasswordIdentity(sshProtocol.getPassword());
|
||||
} else if (StringUtils.hasText(sshProtocol.getPrivateKey())) {
|
||||
var resourceKey = PrivateKeyUtils.writePrivateKey(sshProtocol.getHost(), sshProtocol.getPrivateKey());
|
||||
try (InputStream keyStream = new FileInputStream(resourceKey)) {
|
||||
FilePasswordProvider passwordProvider = (session, resource, index) -> {
|
||||
if (StringUtils.hasText(sshProtocol.getPrivateKeyPassphrase())) {
|
||||
return sshProtocol.getPrivateKeyPassphrase();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
Iterable<KeyPair> keyPairs = SecurityUtils.loadKeyPairIdentities(null, () -> resourceKey, keyStream, passwordProvider);
|
||||
if (keyPairs != null) {
|
||||
keyPairs.forEach(clientSession::addPublicKeyIdentity);
|
||||
} else {
|
||||
log.error("Failed to load private key pairs from: {}", resourceKey);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading private key file: {}", e.getMessage());
|
||||
}
|
||||
} // else auth with localhost private public key certificates
|
||||
|
||||
// auth
|
||||
if (!clientSession.auth().verify(timeout, TimeUnit.MILLISECONDS).isSuccess()) {
|
||||
clientSession.close();
|
||||
throw new IllegalArgumentException("ssh auth failed.");
|
||||
}
|
||||
if (reuseConnection || useProxy) {
|
||||
SshConnect sshConnect = new SshConnect(clientSession);
|
||||
CONNECTION_COMMON_CACHE.addCache(identifier, sshConnect);
|
||||
}
|
||||
return clientSession;
|
||||
}
|
||||
}
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.collector.collect.common.ssh;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.RemovalCause;
|
||||
import com.github.benmanes.caffeine.cache.Scheduler;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.entity.job.SshTunnel;
|
||||
import org.apache.sshd.client.session.ClientSession;
|
||||
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
|
||||
import org.apache.sshd.common.util.net.SshdSocketAddress;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.time.Duration;
|
||||
import java.util.Comparator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Ssh Tunnel Helper
|
||||
*/
|
||||
@Slf4j
|
||||
public class SshTunnelHelper {
|
||||
|
||||
private static final long DEFAULT_CACHE_TIMEOUT = 500 * 1000;
|
||||
|
||||
private static final Cache<SshClientSessionWrapper, LocalPortForwardingWrapper> TRACKER_CACHE =
|
||||
Caffeine.newBuilder()
|
||||
.initialCapacity(1)
|
||||
.maximumSize(1000)
|
||||
.expireAfterAccess(Duration.ofMillis(DEFAULT_CACHE_TIMEOUT))
|
||||
.scheduler(Scheduler.systemScheduler())
|
||||
.removalListener((key, value, cause) -> {
|
||||
if (cause == RemovalCause.REPLACED) {
|
||||
return;
|
||||
}
|
||||
if (key != null && value != null) {
|
||||
// 1. try close tunnel
|
||||
SshClientSessionWrapper clientSessionWrapper = (SshClientSessionWrapper) key;
|
||||
LocalPortForwardingWrapper wrapper = (LocalPortForwardingWrapper) value;
|
||||
wrapper.remove(clientSessionWrapper.getClientSession());
|
||||
|
||||
// 2. try close session
|
||||
if (!clientSessionWrapper.isShareConnection()) {
|
||||
try {
|
||||
clientSessionWrapper.close();
|
||||
log.info("[SSH Tunnel] close unshared ssh connection, {}", clientSessionWrapper);
|
||||
} catch (IOException e) {
|
||||
log.error("[SSH Tunnel] close unshared ssh connection error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
|
||||
/**
|
||||
* check ssh tunnel param
|
||||
*
|
||||
* @param sshTunnel ssh tunnel param
|
||||
*/
|
||||
public static void checkTunnelParam(SshTunnel sshTunnel) {
|
||||
if (sshTunnel == null || !Boolean.parseBoolean(sshTunnel.getEnable())) {
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.hasText(sshTunnel.getHost())) {
|
||||
throw new IllegalArgumentException("ssh tunnel must has ssh host param");
|
||||
}
|
||||
if (!StringUtils.hasText(sshTunnel.getPort())) {
|
||||
throw new IllegalArgumentException("ssh tunnel must has ssh port param");
|
||||
}
|
||||
if (!StringUtils.hasText(sshTunnel.getUsername())) {
|
||||
throw new IllegalArgumentException("ssh tunnel must has ssh username param");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* create ssh tunnel
|
||||
*
|
||||
* @param sshTunnel ssh tunnel param
|
||||
* @param remoteHost remote host
|
||||
* @param remotePort remote port
|
||||
* @return local port
|
||||
*/
|
||||
public static int localPortForward(SshTunnel sshTunnel, String remoteHost, String remotePort) throws GeneralSecurityException, IOException {
|
||||
boolean shareConnection = Boolean.parseBoolean(sshTunnel.getShareConnection());
|
||||
// 1. get ssh session
|
||||
ClientSession session = SshHelper.getConnectSession(sshTunnel.getHost(), sshTunnel.getPort(),
|
||||
sshTunnel.getUsername(), sshTunnel.getPassword(), sshTunnel.getPrivateKey(), sshTunnel.getPrivateKeyPassphrase(),
|
||||
Integer.parseInt(sshTunnel.getTimeout()), shareConnection);
|
||||
SshClientSessionWrapper sessionWrapper = new SshClientSessionWrapper(session, shareConnection);
|
||||
|
||||
// 2. get tunnel
|
||||
LocalPortForwardingWrapper forwardingWrapper = selectWrapper(
|
||||
TRACKER_CACHE.getIfPresent(sessionWrapper), sessionWrapper, remoteHost, remotePort);
|
||||
int localPort;
|
||||
if (forwardingWrapper == null) {
|
||||
localPort = getRandomPort();
|
||||
LocalPortForwardingWrapper newForwardingWrapper = sessionWrapper
|
||||
.createLocalPortForwardingTracker(localPort, remoteHost, Integer.parseInt(remotePort));
|
||||
if (TRACKER_CACHE.getIfPresent(sessionWrapper) == null) {
|
||||
TRACKER_CACHE.put(sessionWrapper, newForwardingWrapper);
|
||||
}
|
||||
log.info("[SSH Tunnel] created ssh forwarding tracker ssh:{}, remote:{}, localPort:{}",
|
||||
sshTunnel.getHost() + ":" + sshTunnel.getPort(), remoteHost + ":" + remotePort, localPort);
|
||||
} else {
|
||||
localPort = forwardingWrapper.getTracker().getLocalAddress().getPort();
|
||||
}
|
||||
|
||||
return localPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* get tunnel
|
||||
*
|
||||
* @param wrapper LocalPortForwardingWrapper
|
||||
* @param sessionWrapper SshClientSessionWrapper
|
||||
* @param remoteHost remote host
|
||||
* @param remotePort remote port
|
||||
* @return LocalPortForwardingWrapper
|
||||
*/
|
||||
private static LocalPortForwardingWrapper selectWrapper(LocalPortForwardingWrapper wrapper, SshClientSessionWrapper sessionWrapper,
|
||||
String remoteHost, String remotePort) {
|
||||
if (wrapper == null) {
|
||||
return null;
|
||||
}
|
||||
List<LocalPortForwardingWrapper> selectList = wrapper.select(sessionWrapper.getClientSession(), localPortForwardWrapper -> {
|
||||
if (!localPortForwardWrapper.isOpen()) {
|
||||
return false;
|
||||
}
|
||||
ExplicitPortForwardingTracker tracker = localPortForwardWrapper.getTracker();
|
||||
SshdSocketAddress remoteAddress = tracker.getRemoteAddress();
|
||||
return Objects.equals(remoteAddress.getHostName(), remoteHost)
|
||||
&& Objects.equals(remoteAddress.getPort(), Integer.parseInt(remotePort));
|
||||
});
|
||||
|
||||
if (selectList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
LocalPortForwardingWrapper selected;
|
||||
if (selectList.size() == 1) {
|
||||
selected = selectList.get(0);
|
||||
} else {
|
||||
selected = selectList.stream().min(Comparator.comparing(LocalPortForwardingWrapper::getLastAccessTime)).get();
|
||||
}
|
||||
selected.setLastAccessTime(System.currentTimeMillis());
|
||||
return selected;
|
||||
}
|
||||
|
||||
|
||||
private static int getRandomPort() throws IOException {
|
||||
try (ServerSocket serverSocket = new ServerSocket(0)) {
|
||||
return serverSocket.getLocalPort();
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode
|
||||
private static class SshClientSessionWrapper {
|
||||
private ClientSession clientSession;
|
||||
private boolean shareConnection;
|
||||
|
||||
public SshClientSessionWrapper(ClientSession clientSession, boolean shareConnection) {
|
||||
this.clientSession = clientSession;
|
||||
this.shareConnection = shareConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a local port forwarding
|
||||
* @param localPort local port
|
||||
* @param remoteHost remove host
|
||||
* @param remotePort remote port
|
||||
* @return LocalPortForwardingWrapper
|
||||
*/
|
||||
public LocalPortForwardingWrapper createLocalPortForwardingTracker(Integer localPort, String remoteHost, Integer remotePort) throws IOException {
|
||||
SshdSocketAddress remoteAddress = new SshdSocketAddress(remoteHost, remotePort);
|
||||
SshdSocketAddress localAddress = new SshdSocketAddress("localhost", localPort);
|
||||
ExplicitPortForwardingTracker tracker = clientSession.createLocalPortForwardingTracker(localAddress, remoteAddress);
|
||||
return new LocalPortForwardingWrapper(tracker);
|
||||
}
|
||||
|
||||
/**
|
||||
* close client session
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
clientSession.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{ ssh:%s, shareConnection:%b }".formatted(clientSession, shareConnection);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@EqualsAndHashCode
|
||||
private static class LocalPortForwardingWrapper {
|
||||
private static Map<ClientSession, List<LocalPortForwardingWrapper>> map = new ConcurrentHashMap<>();
|
||||
|
||||
private ExplicitPortForwardingTracker tracker;
|
||||
private Long lastAccessTime;
|
||||
|
||||
public LocalPortForwardingWrapper(ExplicitPortForwardingTracker tracker) {
|
||||
this.tracker = tracker;
|
||||
this.lastAccessTime = System.currentTimeMillis();
|
||||
map.computeIfAbsent(tracker.getClientSession(), (key) -> new ArrayList<>()).add(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* select ClientSession LocalPortForwardingWrapper List
|
||||
* @param session ssh client session
|
||||
* @param predicate condition
|
||||
* @return LocalPortForwardWrapper
|
||||
*/
|
||||
public List<LocalPortForwardingWrapper> select(ClientSession session, Predicate<LocalPortForwardingWrapper> predicate) {
|
||||
List<LocalPortForwardingWrapper> trackerList = map.get(session);
|
||||
if (CollectionUtils.isEmpty(trackerList)) {
|
||||
return trackerList;
|
||||
}
|
||||
List<LocalPortForwardingWrapper> list = new ArrayList<>();
|
||||
long currentTimeMillis = System.currentTimeMillis();
|
||||
Iterator<LocalPortForwardingWrapper> iterator = trackerList.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
LocalPortForwardingWrapper wrapper = iterator.next();
|
||||
// lazy remove
|
||||
if (currentTimeMillis - wrapper.getLastAccessTime() > DEFAULT_CACHE_TIMEOUT) {
|
||||
try {
|
||||
wrapper.getTracker().close();
|
||||
iterator.remove();
|
||||
log.info("[SSH Tunnel] Lazy Remove ssh local port forwarding {}", wrapper);
|
||||
} catch (IOException e) {
|
||||
log.warn("[SSH Tunnel] Lazy Remove ssh local port forwarding Error", e);
|
||||
}
|
||||
} else if (predicate == null || predicate.test(wrapper)) {
|
||||
list.add(wrapper);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove session local port forwarding
|
||||
* @param session ssh client session
|
||||
*/
|
||||
public void remove(ClientSession session) {
|
||||
List<LocalPortForwardingWrapper> trackerList = map.get(session);
|
||||
if (CollectionUtils.isEmpty(trackerList)) {
|
||||
return;
|
||||
}
|
||||
Iterator<LocalPortForwardingWrapper> iterator = trackerList.iterator();
|
||||
while (iterator.hasNext()){
|
||||
try {
|
||||
LocalPortForwardingWrapper next = iterator.next();
|
||||
next.close();
|
||||
iterator.remove();
|
||||
log.info("[SSH Tunnel] Remove ssh local port forwarding, {}", next);
|
||||
} catch (IOException e) {
|
||||
log.error("[SSH Tunnel] Remove ssh session local port forwarding error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
tracker.close();
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return tracker.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{ ssh:%s, remote:%s, localPort:%d }".formatted(
|
||||
tracker.getSession().getConnectAddress(),
|
||||
tracker.getRemoteAddress(),
|
||||
tracker.getLocalAddress().getPort()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-5
@@ -154,13 +154,11 @@ public class DispatchProperties {
|
||||
|
||||
/**
|
||||
* Schedule Data Export Configuration Properties
|
||||
* 调度数据出口配置属性
|
||||
*/
|
||||
public static class ExportProperties {
|
||||
|
||||
/**
|
||||
* kafka configuration information
|
||||
* kafka配置信息
|
||||
*/
|
||||
private KafkaProperties kafka;
|
||||
|
||||
@@ -178,18 +176,15 @@ public class DispatchProperties {
|
||||
public static class KafkaProperties {
|
||||
/**
|
||||
* Whether the kafka data export is started
|
||||
* kafka数据出口是否启动
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* kafka's connection server url
|
||||
* kafka的连接服务器url
|
||||
*/
|
||||
private String servers = "http://127.0.0.1:2379";
|
||||
/**
|
||||
* Topic name to send data to
|
||||
* 发送数据的topic名称
|
||||
*/
|
||||
private String topic;
|
||||
|
||||
|
||||
+4
@@ -223,7 +223,9 @@ public class KafkaCollectImpl extends AbstractCollect {
|
||||
|
||||
@Override
|
||||
public void preCheck(Metrics metrics) throws IllegalArgumentException {
|
||||
Assert.isTrue(metrics != null, "Metrics cannot be null");
|
||||
KafkaProtocol kafkaProtocol = metrics.getKclient();
|
||||
|
||||
// Ensure that metrics and kafkaProtocol are not null
|
||||
Assert.isTrue(metrics != null && kafkaProtocol != null, "Kafka collect must have kafkaProtocol params");
|
||||
// Ensure that host and port are not empty
|
||||
@@ -265,6 +267,8 @@ public class KafkaCollectImpl extends AbstractCollect {
|
||||
break;
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
builder.setCode(CollectRep.Code.FAIL);
|
||||
builder.setMsg("Kafka collect error: " + e.getMessage());
|
||||
log.error("Kafka collect error", e);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-9
@@ -42,7 +42,7 @@ public class KafkaCollectTest {
|
||||
@Test
|
||||
void preCheck() {
|
||||
// metrics is null
|
||||
assertThrows(NullPointerException.class, () -> {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
collect.preCheck(null);
|
||||
});
|
||||
|
||||
@@ -51,22 +51,21 @@ public class KafkaCollectTest {
|
||||
collect.preCheck(Metrics.builder().build());
|
||||
});
|
||||
|
||||
KafkaProtocol kafka = new KafkaProtocol();
|
||||
Metrics metric = Metrics.builder().kclient(kafka).build();
|
||||
// kafka srv host is null
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
KafkaProtocol kafka = new KafkaProtocol();
|
||||
collect.preCheck(Metrics.builder().kclient(kafka).build());
|
||||
collect.preCheck(metric);
|
||||
});
|
||||
|
||||
// kafka port is null
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
KafkaProtocol kafka = KafkaProtocol.builder().host("127.0.0.1").build();
|
||||
collect.preCheck(Metrics.builder().kclient(kafka).build());
|
||||
kafka.setHost("127.0.0.1");
|
||||
collect.preCheck(metric);
|
||||
});
|
||||
|
||||
// no exception throw
|
||||
assertDoesNotThrow(() -> {
|
||||
KafkaProtocol kafka = KafkaProtocol.builder().host("127.0.0.1").port("9092").build();
|
||||
collect.preCheck(Metrics.builder().kclient(kafka).build());
|
||||
kafka.setPort("9092");
|
||||
collect.preCheck(metric);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -173,6 +173,12 @@
|
||||
<artifactId>snappy-java</artifactId>
|
||||
<version>${snappy-java.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.javaparser</groupId>
|
||||
<artifactId>javaparser-core</artifactId>
|
||||
<version>${javaparser.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
-84
@@ -42,11 +42,6 @@ public class CommonProperties {
|
||||
*/
|
||||
private DataQueueProperties queue;
|
||||
|
||||
/**
|
||||
* sms impl properties
|
||||
*/
|
||||
private SmsProperties sms;
|
||||
|
||||
/**
|
||||
* data queue properties
|
||||
*/
|
||||
@@ -146,83 +141,4 @@ public class CommonProperties {
|
||||
*/
|
||||
private String alertsDataTopic;
|
||||
}
|
||||
|
||||
/**
|
||||
* sms properties
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public static class SmsProperties {
|
||||
//Tencent cloud SMS configuration
|
||||
private TencentSmsProperties tencent;
|
||||
//Ali cloud SMS configuration
|
||||
private AliYunSmsProperties aliYun;
|
||||
}
|
||||
|
||||
/**
|
||||
* tencent sms properties
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public static class TencentSmsProperties {
|
||||
|
||||
/**
|
||||
* Tencent cloud account secret id
|
||||
*/
|
||||
private String secretId;
|
||||
|
||||
/**
|
||||
* Tencent cloud account secret key
|
||||
*/
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* SMS app id
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* SMS signature
|
||||
*/
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* SMS template ID
|
||||
*/
|
||||
private String templateId;
|
||||
}
|
||||
|
||||
/**
|
||||
* aliYun sms properties
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public static class AliYunSmsProperties {
|
||||
|
||||
/**
|
||||
* Aliyun account access key id
|
||||
*/
|
||||
private String secretId;
|
||||
|
||||
/**
|
||||
* Ali Cloud account access key
|
||||
*/
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* SMS app id
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* SMS signature
|
||||
*/
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* ID of the SMS template
|
||||
*/
|
||||
private String templateId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-2
@@ -93,9 +93,14 @@ public interface CommonConstants {
|
||||
String LABEL_ALERT_NAME = "alertname";
|
||||
|
||||
/**
|
||||
* label key: host
|
||||
* label key: instance host
|
||||
*/
|
||||
String LABEL_HOST = "host";
|
||||
String LABEL_INSTANCE_HOST = "instancehost";
|
||||
|
||||
/**
|
||||
* label key: instance name
|
||||
*/
|
||||
String LABEL_INSTANCE_NAME = "instancename";
|
||||
|
||||
/**
|
||||
* Alarm severity label key
|
||||
|
||||
+8
-13
@@ -15,21 +15,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.manager.pojo.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
package org.apache.hertzbeat.common.constants;
|
||||
|
||||
/**
|
||||
* Alibaba Sms Sender configuration dto
|
||||
* Import/Export task constants
|
||||
*/
|
||||
@Data
|
||||
public class SmsAlibabaConfig {
|
||||
|
||||
@NotBlank(message = "SecretId cannot be empty")
|
||||
private String secretId;
|
||||
|
||||
@NotBlank(message = "SecretKey cannot be empty")
|
||||
private String secretKey;
|
||||
|
||||
public interface ImExportTaskConstant {
|
||||
|
||||
/**
|
||||
* If the number of tasks exceeds 100, progress information will broadcast
|
||||
*/
|
||||
Integer IMPORT_TASK_PROCESS_THRESHOLD = 100;
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.constants;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Import/Export task status
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public enum ImportTaskStatusEnum {
|
||||
|
||||
/**
|
||||
* In progress
|
||||
*/
|
||||
IN_PROGRESS("IN_PROGRESS"),
|
||||
|
||||
/**
|
||||
* Completed
|
||||
*/
|
||||
COMPLETED("COMPLETED"),
|
||||
|
||||
/**
|
||||
* Failed
|
||||
*/
|
||||
FAILED("FAILED");
|
||||
|
||||
private final String value;
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.constants;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Manager Event Type Enum
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public enum ManagerEventTypeEnum {
|
||||
|
||||
/**
|
||||
* IMPORT_TASK_EVENT
|
||||
*/
|
||||
IMPORT_TASK_EVENT("IMPORT_TASK_EVENT");
|
||||
|
||||
private final String value;
|
||||
}
|
||||
+5
-1
@@ -27,12 +27,16 @@ public interface MetricDataConstants {
|
||||
String UNIT = "unit";
|
||||
|
||||
// key in Schema metadata
|
||||
String MONITOR_ID = "id";
|
||||
String ID = "id";
|
||||
String TENANT_ID = "tenantId";
|
||||
String APP = "app";
|
||||
String METRICS = "metrics";
|
||||
String LABELS = "labels";
|
||||
String ANNOTATIONS = "annotations";
|
||||
String PRIORITY = "priority";
|
||||
String TIME = "time";
|
||||
String CODE = "code";
|
||||
String MSG = "msg";
|
||||
String INSTANCE_NAME = "instancename";
|
||||
String INSTANCE_HOST = "instancehost";
|
||||
}
|
||||
|
||||
+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.common.constants;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Notify Level Enum
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
public enum NotifyLevelEnum {
|
||||
|
||||
SUCCESS("SUCCESS"),
|
||||
ERROR("ERROR"),
|
||||
INFO("INFO"),
|
||||
WARNING("WARNING"),
|
||||
BLANK("BLANK");
|
||||
|
||||
private final String value;
|
||||
}
|
||||
+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.common.constants;
|
||||
|
||||
/**
|
||||
* SMS provider constants
|
||||
*/
|
||||
public interface SmsConstants {
|
||||
// Tencent cloud SMS
|
||||
String TENCENT = "tencent";
|
||||
|
||||
// Alibaba Cloud SMS
|
||||
String ALIBABA = "alibaba";
|
||||
|
||||
// UniSMS
|
||||
String UNISMS = "unisms";
|
||||
|
||||
// Smslocal SMS
|
||||
String SMSLOCAL = "smslocal";
|
||||
|
||||
// Aws cloud SMS
|
||||
String AWS = "aws";
|
||||
|
||||
// Twilio SMS
|
||||
String TWILIO = "twilio";
|
||||
}
|
||||
+1
@@ -63,6 +63,7 @@ public class GroupAlert {
|
||||
private Long id;
|
||||
|
||||
@Schema(title = "Group Key", example = "HighCPUUsage{alertname=\"HighCPUUsage\", instance=\"server1\"}")
|
||||
@Column(length = 2048)
|
||||
private String groupKey;
|
||||
|
||||
@Schema(title = "Status", example = "resolved")
|
||||
|
||||
+5
@@ -122,6 +122,11 @@ public class NoticeReceiver {
|
||||
example = "779294123", accessMode = READ_WRITE)
|
||||
private String tgUserId;
|
||||
|
||||
@Schema(title = "Telegram message thread id: The notification method is valid for Telegram Bot",
|
||||
description = "TTelegram message thread id: The notification method is valid for Telegram Bot",
|
||||
example = "779294123", accessMode = READ_WRITE)
|
||||
private String tgMessageThreadId;
|
||||
|
||||
@Schema(title = "DingTalk,FeiShu,WeWork user id: The notification method is valid for DingTalk,FeiShu,WeWork Bot",
|
||||
description = "DingTalk,FeiShu,WeWork user id: The notification method is valid for DingTalk,FeiShu,WeWork Bot",
|
||||
example = "779294123", accessMode = READ_WRITE)
|
||||
|
||||
+4
-2
@@ -59,8 +59,9 @@ public class SingleAlert {
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Schema(title = "Threshold Id", example = "87584674384", accessMode = READ_ONLY)
|
||||
private Long id;
|
||||
|
||||
@Schema(title = "Fingerprint", example = "dxsdfdsf")
|
||||
|
||||
@Schema(title = "Fingerprint", example = "alertname:demo")
|
||||
@Column(length = 2048)
|
||||
private String fingerprint;
|
||||
|
||||
@Schema(title = "Labels", example = "{\"alertname\": \"HighCPUUsage\", \"priority\": \"critical\", \"instance\": \"343483943\"}")
|
||||
@@ -74,6 +75,7 @@ public class SingleAlert {
|
||||
private Map<String, String> annotations;
|
||||
|
||||
@Schema(title = "Content", example = "CPU usage is above 80% for the last 5 minutes on instance server1.example.com.")
|
||||
@Column(length = 4096)
|
||||
private String content;
|
||||
|
||||
@Schema(title = "Status", example = "firing|resolved")
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.apache.hertzbeat.common.constants.ImportTaskStatusEnum;
|
||||
import org.apache.hertzbeat.common.constants.ManagerEventTypeEnum;
|
||||
import org.apache.hertzbeat.common.constants.NotifyLevelEnum;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Import task message
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class ImportTaskMessage extends ManagerMessage {
|
||||
|
||||
/**
|
||||
* Task name
|
||||
*/
|
||||
@NotNull
|
||||
private String taskName;
|
||||
|
||||
/**
|
||||
* Progress, expressed as a percentage
|
||||
*/
|
||||
@Nullable
|
||||
private Integer progress;
|
||||
|
||||
/**
|
||||
* Task Status,
|
||||
* @see ImportTaskStatusEnum
|
||||
*/
|
||||
@NotNull
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* If Fail, the error message
|
||||
*/
|
||||
@Nullable
|
||||
private String errMsg;
|
||||
|
||||
public ImportTaskMessage(String notifyLevel, String managerEventType, String taskName, @Nullable Integer progress, String status, @Nullable String errMsg){
|
||||
super(notifyLevel, managerEventType);
|
||||
this.taskName = taskName;
|
||||
this.progress = progress;
|
||||
this.status = status;
|
||||
this.errMsg = errMsg;
|
||||
}
|
||||
|
||||
public static ManagerMessage createInProgressMessage(String taskName, Integer process){
|
||||
return new ImportTaskMessage(NotifyLevelEnum.INFO.getValue(), ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), taskName, process, ImportTaskStatusEnum.IN_PROGRESS.getValue(), null);
|
||||
}
|
||||
|
||||
public static ManagerMessage createCompletedMessage(String taskName){
|
||||
return new ImportTaskMessage(NotifyLevelEnum.SUCCESS.getValue(), ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), taskName, null, ImportTaskStatusEnum.COMPLETED.getValue(), null);
|
||||
}
|
||||
|
||||
public static ManagerMessage createFailedMessage(String taskName, String errMsg){
|
||||
return new ImportTaskMessage(NotifyLevelEnum.ERROR.getValue(), ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), taskName, null, ImportTaskStatusEnum.FAILED.getValue(), errMsg);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Manager Message Entity
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ManagerMessage {
|
||||
/**
|
||||
* Notify Level
|
||||
* @see org.apache.hertzbeat.common.constants.NotifyLevelEnum
|
||||
*/
|
||||
private String notifyLevel;
|
||||
|
||||
/**
|
||||
* Manager Event Type
|
||||
* @see org.apache.hertzbeat.common.constants.ManagerEventTypeEnum
|
||||
*/
|
||||
private String managerEventType;
|
||||
}
|
||||
+1
-1
@@ -41,7 +41,7 @@ public class Configmap implements Serializable {
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* parameter value 参数value
|
||||
* parameter value
|
||||
*/
|
||||
private Object value;
|
||||
|
||||
|
||||
@@ -61,6 +61,19 @@ public class Job {
|
||||
* Monitoring Task ID
|
||||
*/
|
||||
private long monitorId;
|
||||
/**
|
||||
* metadata info bind with this job
|
||||
* eg: instancename, instancehost
|
||||
*/
|
||||
private Map<String, String> metadata;
|
||||
/**
|
||||
* bind labels
|
||||
*/
|
||||
private Map<String, String> labels;
|
||||
/**
|
||||
* bind annotations
|
||||
*/
|
||||
private Map<String, String> annotations;
|
||||
/**
|
||||
* Is hide this app in main menus layout, only for app type, default true.
|
||||
*/
|
||||
@@ -159,23 +172,36 @@ public class Job {
|
||||
* collector use - construct to initialize metrics execution view
|
||||
*/
|
||||
public synchronized void constructPriorMetrics() {
|
||||
Map<Byte, List<Metrics>> map = metrics.stream()
|
||||
.filter(metrics -> (System.currentTimeMillis() >= metrics.getCollectTime() + metrics.getInterval() * 1000))
|
||||
.peek(metric -> {
|
||||
metric.setCollectTime(System.currentTimeMillis());
|
||||
// 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));
|
||||
long now = System.currentTimeMillis();
|
||||
Map<Byte, List<Metrics>> currentCollectMetrics = metrics.stream()
|
||||
.filter(metrics -> (now >= metrics.getCollectTime() + metrics.getInterval() * 1000))
|
||||
.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 = metrics.stream().filter(metric -> metric.getPriority() == 0).findFirst();
|
||||
if (defaultMetricOption.isPresent()) {
|
||||
Metrics defaultMetric = defaultMetricOption.get();
|
||||
defaultMetric.setCollectTime(now);
|
||||
currentCollectMetrics.put((byte) 0, Collections.singletonList(defaultMetric));
|
||||
} else {
|
||||
log.error("metrics must has one priority 0 metrics at least.");
|
||||
}
|
||||
}
|
||||
// Construct a linked list of task execution order of the metrics
|
||||
priorMetrics = new LinkedList<>();
|
||||
map.values().forEach(metric -> {
|
||||
currentCollectMetrics.values().forEach(metric -> {
|
||||
Set<Metrics> metricsSet = Collections.synchronizedSet(new HashSet<>(metric));
|
||||
priorMetrics.add(metricsSet);
|
||||
});
|
||||
@@ -260,7 +286,7 @@ public class Job {
|
||||
public void initIntervals() {
|
||||
List<Long> metricsIntervals = new LinkedList<>();
|
||||
for (Metrics metrics: getMetrics()) {
|
||||
metrics.setCollectTime(System.currentTimeMillis());
|
||||
metrics.setCollectTime(0L);
|
||||
if (metrics.getInterval() <= 0) {
|
||||
metrics.setInterval(defaultInterval);
|
||||
}
|
||||
@@ -287,10 +313,10 @@ public class Job {
|
||||
* The least common multiple
|
||||
*/
|
||||
public static long lcm(List<Long> array) {
|
||||
if (array != null) {
|
||||
if (array != null && !array.isEmpty()) {
|
||||
long result = array.get(0);
|
||||
for (int i = 1; i < array.size(); i++) {
|
||||
result = (result * array.get(i)) / gcd(result, array.get(i));
|
||||
result = result / gcd(result, array.get(i)) * array.get(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.job;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.CommonRequestProtocol;
|
||||
import org.apache.hertzbeat.common.entity.job.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* ssh tunnel
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SshTunnel implements CommonRequestProtocol, Protocol {
|
||||
|
||||
/**
|
||||
* enable ssh tunnel
|
||||
*/
|
||||
private String enable = "false";
|
||||
|
||||
/**
|
||||
* IP ADDRESS OR DOMAIN NAME OF THE PEER HOST
|
||||
*/
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* Peer host port
|
||||
*/
|
||||
private String port = "22";
|
||||
|
||||
/**
|
||||
* TIME OUT PERIOD
|
||||
*/
|
||||
private String timeout = "6000";
|
||||
|
||||
/**
|
||||
* UserName
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* Password (optional)
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Private key (optional)
|
||||
*/
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* private key passphrase (optional)
|
||||
*/
|
||||
private String privateKeyPassphrase;
|
||||
|
||||
/**
|
||||
* share connection session
|
||||
*/
|
||||
private String shareConnection = "true";
|
||||
}
|
||||
+6
@@ -95,6 +95,12 @@ public class HttpProtocol implements CommonRequestProtocol, Protocol {
|
||||
*/
|
||||
private List<String> successCodes;
|
||||
|
||||
/**
|
||||
* Whether to enable URL encoding for the path. Default is true.
|
||||
* When true, the URL path will be encoded. When false, the URL path will not be encoded.
|
||||
*/
|
||||
private String enableUrlEncoding = "true";
|
||||
|
||||
/**
|
||||
* authentication information
|
||||
*/
|
||||
|
||||
+9
@@ -21,6 +21,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.hertzbeat.common.entity.job.SshTunnel;
|
||||
|
||||
/**
|
||||
* Database configuration information implemented by the common jdbc specification
|
||||
@@ -70,4 +71,12 @@ public class JdbcProtocol implements CommonRequestProtocol, Protocol {
|
||||
* DATABASE LINK URL eg: jdbc:mysql://localhost:3306/usthe
|
||||
*/
|
||||
private String url;
|
||||
/**
|
||||
* reuse connection session
|
||||
*/
|
||||
private String reuseConnection = "true";
|
||||
/**
|
||||
* ssh tunnel
|
||||
*/
|
||||
private SshTunnel sshTunnel;
|
||||
}
|
||||
|
||||
+6
@@ -21,6 +21,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.apache.hertzbeat.common.entity.job.SshTunnel;
|
||||
|
||||
/**
|
||||
* Redis Protocol
|
||||
@@ -61,4 +62,9 @@ public class RedisProtocol implements CommonRequestProtocol, Protocol {
|
||||
*/
|
||||
private String timeout;
|
||||
|
||||
/**
|
||||
* SSH TUNNEL
|
||||
*/
|
||||
private SshTunnel sshTunnel;
|
||||
|
||||
}
|
||||
|
||||
+30
@@ -80,4 +80,34 @@ public class SshProtocol implements CommonRequestProtocol, Protocol {
|
||||
* Response data parsing mode:oneRow, multiRow
|
||||
*/
|
||||
private String parseType;
|
||||
|
||||
/**
|
||||
* IP ADDRESS OR DOMAIN NAME OF THE PEER PROXY HOST
|
||||
*/
|
||||
private String proxyHost;
|
||||
|
||||
/**
|
||||
* Peer proxy host port
|
||||
*/
|
||||
private String proxyPort;
|
||||
|
||||
/**
|
||||
* Proxy UserName
|
||||
*/
|
||||
private String proxyUsername;
|
||||
|
||||
/**
|
||||
* Proxy Password (optional)
|
||||
*/
|
||||
private String proxyPassword;
|
||||
|
||||
/**
|
||||
* flag of use proxy
|
||||
*/
|
||||
private String useProxy = "false";
|
||||
|
||||
/**
|
||||
* Proxy private key (optional)
|
||||
*/
|
||||
private String proxyPrivateKey;
|
||||
}
|
||||
|
||||
+137
-61
@@ -43,6 +43,7 @@ import org.apache.arrow.vector.types.pojo.FieldType;
|
||||
import org.apache.arrow.vector.types.pojo.Schema;
|
||||
import org.apache.hertzbeat.common.constants.MetricDataConstants;
|
||||
import org.apache.hertzbeat.common.entity.arrow.RowWrapper;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
|
||||
@SuppressWarnings("all")
|
||||
@Slf4j
|
||||
@@ -138,17 +139,7 @@ public final class CollectRep {
|
||||
|
||||
public static Builder newBuilder(MetricsData metricsData) {
|
||||
Builder builder = new Builder();
|
||||
// get metadata from metricsData
|
||||
Map<String, String> metadata = metricsData.getMetadata();
|
||||
builder.setId(Long.parseLong(metadata.getOrDefault(MetricDataConstants.MONITOR_ID, "0")))
|
||||
.setTenantId(Long.parseLong(metadata.getOrDefault(MetricDataConstants.TENANT_ID, "0")))
|
||||
.setApp(metadata.getOrDefault(MetricDataConstants.APP, ""))
|
||||
.setMetrics(metadata.getOrDefault(MetricDataConstants.METRICS, ""))
|
||||
.setPriority(Integer.parseInt(metadata.getOrDefault(MetricDataConstants.PRIORITY, "0")))
|
||||
.setTime(Long.parseLong(metadata.getOrDefault(MetricDataConstants.TIME, "0")))
|
||||
.setCode(Code.forNumber(Integer.parseInt(metadata.getOrDefault(MetricDataConstants.CODE, "0"))))
|
||||
.setMsg(metadata.getOrDefault(MetricDataConstants.MSG, ""));
|
||||
|
||||
builder.addMetadataAll(metricsData.getMetadata());
|
||||
metricsData.getFields().forEach(builder::addField);
|
||||
metricsData.getValues().forEach(builder::addValueRow);
|
||||
return builder;
|
||||
@@ -191,42 +182,70 @@ public final class CollectRep {
|
||||
|
||||
public long getId() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return Long.parseLong(metadata.getOrDefault("id", "0"));
|
||||
return Long.parseLong(metadata.getOrDefault(MetricDataConstants.ID, "0"));
|
||||
}
|
||||
|
||||
public long getTenantId() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return Long.parseLong(metadata.getOrDefault("tenantId", "0"));
|
||||
return Long.parseLong(metadata.getOrDefault(MetricDataConstants.TENANT_ID, "0"));
|
||||
}
|
||||
|
||||
public String getApp() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return metadata.getOrDefault("app", "");
|
||||
return metadata.getOrDefault(MetricDataConstants.APP, "");
|
||||
}
|
||||
|
||||
public String getMetrics() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return metadata.getOrDefault("metrics", "");
|
||||
return metadata.getOrDefault(MetricDataConstants.METRICS, "");
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return Integer.parseInt(metadata.getOrDefault("priority", "0"));
|
||||
return Integer.parseInt(metadata.getOrDefault(MetricDataConstants.PRIORITY, "0"));
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return Long.parseLong(metadata.getOrDefault("time", "0"));
|
||||
return Long.parseLong(metadata.getOrDefault(MetricDataConstants.TIME, "0"));
|
||||
}
|
||||
|
||||
public Code getCode() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return Code.forNumber(Integer.parseInt(metadata.getOrDefault("code", "0")));
|
||||
return Code.forNumber(Integer.parseInt(metadata.getOrDefault(MetricDataConstants.CODE, "0")));
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return metadata.getOrDefault("msg", "");
|
||||
return metadata.getOrDefault(MetricDataConstants.MSG, "");
|
||||
}
|
||||
|
||||
public String getInstanceName() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return metadata.getOrDefault(MetricDataConstants.INSTANCE_NAME, null);
|
||||
}
|
||||
|
||||
public String getInstanceHost() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
return metadata.getOrDefault(MetricDataConstants.INSTANCE_HOST, null);
|
||||
}
|
||||
|
||||
public Map<String, String> getLabels() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
String labelStr = metadata.getOrDefault(MetricDataConstants.LABELS, "");
|
||||
if (labelStr == null || "".equals(labelStr)) {
|
||||
return null;
|
||||
}
|
||||
return JsonUtil.fromJson(labelStr, Map.class);
|
||||
}
|
||||
|
||||
public Map<String, String> getAnnotations() {
|
||||
Map<String, String> metadata = getMetadata();
|
||||
String annotationStr = metadata.getOrDefault(MetricDataConstants.ANNOTATIONS, "");
|
||||
if (annotationStr == null || "".equals(annotationStr)) {
|
||||
return null;
|
||||
}
|
||||
return JsonUtil.fromJson(annotationStr, Map.class);
|
||||
}
|
||||
|
||||
private Map<String, String> getMetadata() {
|
||||
@@ -298,54 +317,82 @@ public final class CollectRep {
|
||||
|
||||
// Builder remains mostly the same, but build() method changes
|
||||
public static class Builder {
|
||||
private long id;
|
||||
private long tenantId;
|
||||
private String app = "";
|
||||
private String metrics = "";
|
||||
private int priority;
|
||||
private long time;
|
||||
private Code code = Code.SUCCESS;
|
||||
private String msg = "";
|
||||
|
||||
private Map<String, String> metadata = new HashMap<>();
|
||||
private List<Field> fields = new ArrayList<>();
|
||||
private List<ValueRow> values = new ArrayList<>();
|
||||
|
||||
public Builder setId(long id) {
|
||||
this.id = id;
|
||||
metadata.put(MetricDataConstants.ID, String.valueOf(id));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTenantId(long tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
metadata.put(MetricDataConstants.TENANT_ID, String.valueOf(tenantId));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setApp(String app) {
|
||||
this.app = app;
|
||||
metadata.put(MetricDataConstants.APP, app != null ? app : "");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMetrics(String metrics) {
|
||||
this.metrics = metrics;
|
||||
metadata.put(MetricDataConstants.METRICS, metrics != null ? metrics : "");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLabels(Map<String, String> labels) {
|
||||
if (labels == null || labels.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
String labelStr = JsonUtil.toJson(labels);
|
||||
metadata.put(MetricDataConstants.LABELS, labelStr);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAnnotations(Map<String, String> annotations) {
|
||||
if (annotations == null || annotations.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
String annotationStr = JsonUtil.toJson(annotations);
|
||||
metadata.put(MetricDataConstants.ANNOTATIONS, annotationStr);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPriority(int priority) {
|
||||
this.priority = priority;
|
||||
metadata.put(MetricDataConstants.PRIORITY, String.valueOf(priority));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setTime(long time) {
|
||||
this.time = time;
|
||||
metadata.put(MetricDataConstants.TIME, String.valueOf(time));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCode(Code code) {
|
||||
this.code = code;
|
||||
metadata.put(MetricDataConstants.CODE, String.valueOf(code != null ? code.value : 0));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
metadata.put(MetricDataConstants.MSG, msg != null ? msg : "");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addMetadata(String key, String value) {
|
||||
if (key == null || "".equals(key)) {
|
||||
return this;
|
||||
}
|
||||
metadata.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addMetadataAll(Map<String, String> meta) {
|
||||
if (meta == null || meta.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
this.metadata.putAll(meta);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -360,17 +407,6 @@ public final class CollectRep {
|
||||
}
|
||||
|
||||
public MetricsData build() {
|
||||
// Create metadata
|
||||
Map<String, String> metadata = new HashMap<>();
|
||||
metadata.put(MetricDataConstants.MONITOR_ID, String.valueOf(id));
|
||||
metadata.put(MetricDataConstants.TENANT_ID, String.valueOf(tenantId));
|
||||
metadata.put(MetricDataConstants.APP, app != null ? app : "");
|
||||
metadata.put(MetricDataConstants.METRICS, metrics != null ? metrics : "");
|
||||
metadata.put(MetricDataConstants.PRIORITY, String.valueOf(priority));
|
||||
metadata.put(MetricDataConstants.TIME, String.valueOf(time));
|
||||
metadata.put(MetricDataConstants.CODE, String.valueOf(code != null ? code.value : 0));
|
||||
metadata.put(MetricDataConstants.MSG, msg != null ? msg : "");
|
||||
|
||||
BufferAllocator allocator = new RootAllocator();
|
||||
try {
|
||||
// Create Arrow fields with metadata
|
||||
@@ -407,7 +443,17 @@ public final class CollectRep {
|
||||
fieldIndex < row.getColumnsList().size()) {
|
||||
String value = row.getColumns(fieldIndex);
|
||||
if (value != null) {
|
||||
vector.set(rowIndex, value.getBytes(StandardCharsets.UTF_8));
|
||||
// Check byte array size, Arrow buffer size is 32768 bytes
|
||||
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
if (bytes.length > 32700) {
|
||||
log.warn("Value too large for Arrow buffer ({}), truncating to 32700 bytes. Meta: {}",
|
||||
bytes.length, JsonUtil.toJson(metadata));
|
||||
byte[] truncatedBytes = new byte[32700];
|
||||
System.arraycopy(bytes, 0, truncatedBytes, 0, 32700);
|
||||
vector.set(rowIndex, truncatedBytes);
|
||||
} else {
|
||||
vector.set(rowIndex, bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,38 +471,65 @@ public final class CollectRep {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Add getter methods
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
return Long.parseLong(metadata.getOrDefault(MetricDataConstants.ID, "0"));
|
||||
}
|
||||
|
||||
public long getTenantId() {
|
||||
return tenantId;
|
||||
return Long.parseLong(metadata.getOrDefault(MetricDataConstants.TENANT_ID, "0"));
|
||||
}
|
||||
|
||||
public String getApp() {
|
||||
return app;
|
||||
return metadata.getOrDefault(MetricDataConstants.APP, "");
|
||||
}
|
||||
|
||||
public String getMetrics() {
|
||||
return metrics;
|
||||
return metadata.getOrDefault(MetricDataConstants.METRICS, "");
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
return Integer.parseInt(metadata.getOrDefault(MetricDataConstants.PRIORITY, "0"));
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
return Integer.parseInt(metadata.getOrDefault(MetricDataConstants.TIME, "0"));
|
||||
}
|
||||
|
||||
public Code getCode() {
|
||||
return code;
|
||||
return Code.forNumber(Integer.parseInt(metadata.getOrDefault(MetricDataConstants.CODE, "0")));
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
return metadata.getOrDefault(MetricDataConstants.MSG, "");
|
||||
}
|
||||
|
||||
public String getInstanceName() {
|
||||
return metadata.getOrDefault(MetricDataConstants.INSTANCE_NAME, null);
|
||||
}
|
||||
|
||||
public String getInstanceHost() {
|
||||
return metadata.getOrDefault(MetricDataConstants.INSTANCE_HOST, null);
|
||||
}
|
||||
|
||||
public Map<String, String> getLabels() {
|
||||
String labelStr = metadata.getOrDefault(MetricDataConstants.LABELS, "");
|
||||
if (labelStr == null || "".equals(labelStr)) {
|
||||
return null;
|
||||
}
|
||||
return JsonUtil.fromJson(labelStr, Map.class);
|
||||
}
|
||||
|
||||
public Map<String, String> getAnnotations() {
|
||||
String annotationStr = metadata.getOrDefault(MetricDataConstants.ANNOTATIONS, "");
|
||||
if (annotationStr == null || "".equals(annotationStr)) {
|
||||
return null;
|
||||
}
|
||||
return JsonUtil.fromJson(annotationStr, Map.class);
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public List<Field> getFieldsList() {
|
||||
@@ -491,10 +564,6 @@ public final class CollectRep {
|
||||
values = new LinkedList<>();
|
||||
}
|
||||
|
||||
public void clearMetrics() {
|
||||
metrics = null;
|
||||
}
|
||||
|
||||
public void clearFields() {
|
||||
fields = new LinkedList<>();
|
||||
}
|
||||
@@ -567,6 +636,13 @@ public final class CollectRep {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if(!(o instanceof Field))
|
||||
return false;
|
||||
Field other = (Field)o;
|
||||
return other.name.equals(this.name);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final Field instance;
|
||||
|
||||
|
||||
+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.common.support.event;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* the event for sms config change
|
||||
*/
|
||||
public class SmsConfigChangeEvent extends ApplicationEvent {
|
||||
|
||||
public SmsConfigChangeEvent(Object source) {
|
||||
super(source);
|
||||
}
|
||||
}
|
||||
@@ -142,7 +142,7 @@ public final class AesUtil {
|
||||
/**
|
||||
* Determine whether it is encrypted
|
||||
* @param text text
|
||||
* @return true-是 false-否
|
||||
* @return true false
|
||||
*/
|
||||
public static boolean isCiphertext(String text, String decryptKey) {
|
||||
// First use whether it is base64 to determine whether it has been encrypted
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.entity.message;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Test case for {@link CollectRep}
|
||||
*/
|
||||
public class CollectRepTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(value = {
|
||||
"name, name, true",
|
||||
"name1, name3, false",
|
||||
})
|
||||
void testFieldEquals(String name1, String name2, boolean result) {
|
||||
CollectRep.Field field1 = new CollectRep.Field();
|
||||
field1.setName(name1);
|
||||
CollectRep.Field field2 = new CollectRep.Field();
|
||||
field2.setName(name2);
|
||||
|
||||
assertEquals(field1.equals(field2), result);
|
||||
}
|
||||
|
||||
}
|
||||
+6
-6
@@ -68,19 +68,19 @@ class KafkaCommonDataQueueTest {
|
||||
when(commonProperties.getQueue()).thenReturn(dataQueueProperties);
|
||||
when(dataQueueProperties.getKafka()).thenReturn(kafkaProperties);
|
||||
|
||||
// 设置所有必需的 topic
|
||||
// Set all required topics
|
||||
when(kafkaProperties.getMetricsDataTopic()).thenReturn("metricsDataTopic");
|
||||
when(kafkaProperties.getAlertsDataTopic()).thenReturn("alertsDataTopic");
|
||||
when(kafkaProperties.getMetricsDataToStorageTopic()).thenReturn("metricsDataToStorageTopic");
|
||||
when(kafkaProperties.getServiceDiscoveryDataTopic()).thenReturn("serviceDiscoveryDataTopic");
|
||||
when(kafkaProperties.getServers()).thenReturn("localhost:9092");
|
||||
|
||||
// 模拟 consumer 的 subscribe 方法
|
||||
// Simulate the subscribe method for consumers
|
||||
doNothing().when(metricsDataToAlertConsumer).subscribe(anyCollection());
|
||||
|
||||
kafkaCommonDataQueue = new KafkaCommonDataQueue(commonProperties);
|
||||
|
||||
// 使用反射设置私有字段
|
||||
// Use reflection to set private fields
|
||||
setPrivateField(kafkaCommonDataQueue, "metricsDataProducer", metricsDataProducer);
|
||||
setPrivateField(kafkaCommonDataQueue, "metricsDataToAlertConsumer", metricsDataToAlertConsumer);
|
||||
}
|
||||
@@ -98,16 +98,16 @@ class KafkaCommonDataQueueTest {
|
||||
|
||||
@Test
|
||||
void testPollMetricsDataToAlerter() throws InterruptedException {
|
||||
// 创建一个测试数据
|
||||
// Create a test data
|
||||
CollectRep.MetricsData expectedData = CollectRep.MetricsData.newBuilder()
|
||||
.setMetrics("test metrics")
|
||||
.build();
|
||||
|
||||
// 创建一个包含测试数据的 ConsumerRecord
|
||||
// Create a ConsumerRecord containing test data
|
||||
ConsumerRecord<Long, CollectRep.MetricsData> record =
|
||||
new ConsumerRecord<>("metricsDataTopic", 0, 0L, 1L, expectedData);
|
||||
|
||||
// 创建一个包含单个记录的 ConsumerRecords
|
||||
// Create a ConsumerRecords containing a single record.
|
||||
Map<TopicPartition, List<ConsumerRecord<Long, CollectRep.MetricsData>>> recordsMap =
|
||||
Collections.singletonMap(
|
||||
new TopicPartition("metricsDataTopic", 0),
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.common.util;
|
||||
|
||||
import com.github.javaparser.JavaParser;
|
||||
import com.github.javaparser.ParseResult;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Test case for checking Chinese characters in Java files
|
||||
*/
|
||||
@Slf4j
|
||||
public class ChineseCharacterCheckTest {
|
||||
|
||||
private static final Pattern CHINESE_CHAR_PATTERN = Pattern.compile("[\u4e00-\u9fa5]");
|
||||
private static final Set<String> EXCLUDED_FILES = new HashSet<>(Collections.singletonList("Metrics"));
|
||||
private static final String MAIN_SOURCE_DIR = "src/main/java";
|
||||
private static final String TEST_SOURCE_DIR = "src/test/java";
|
||||
|
||||
private final JavaParser javaParser = new JavaParser();
|
||||
private final String sourceDir;
|
||||
private final String testDir;
|
||||
|
||||
public ChineseCharacterCheckTest() {
|
||||
boolean isWindowsOs = System.getProperty("os.name").toLowerCase().startsWith("win");
|
||||
String separator = isWindowsOs ? "\\" : "/";
|
||||
this.sourceDir = MAIN_SOURCE_DIR.replace("/", separator);
|
||||
this.testDir = TEST_SOURCE_DIR.replace("/", separator);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotContainChineseInComments() {
|
||||
List<String> violations = scanForChineseCharacters(ScanTarget.COMMENTS);
|
||||
assertNoChineseCharacters(violations);
|
||||
}
|
||||
|
||||
private List<String> scanForChineseCharacters(ScanTarget target) {
|
||||
List<String> violations = new ArrayList<>();
|
||||
try (Stream<Path> paths = Files.walk(Paths.get(".."), FileVisitOption.FOLLOW_LINKS)) {
|
||||
paths.filter(this::isValidJavaFile)
|
||||
.forEach(path -> processFile(path, target, violations));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to scan Java files", e);
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
private boolean isValidJavaFile(Path path) {
|
||||
String pathStr = path.toString();
|
||||
return pathStr.endsWith(".java")
|
||||
&& (pathStr.contains(sourceDir) || pathStr.contains(testDir))
|
||||
&& EXCLUDED_FILES.stream().noneMatch(pathStr::contains);
|
||||
}
|
||||
|
||||
private void processFile(Path path, ScanTarget target, List<String> violations) {
|
||||
try {
|
||||
ParseResult<CompilationUnit> parseResult = javaParser.parse(Files.newInputStream(path));
|
||||
parseResult.getResult().ifPresent(cu -> {
|
||||
if (target.includeComments()) {
|
||||
checkComments(cu, path, violations);
|
||||
}
|
||||
if (target.includeCode()) {
|
||||
checkCode(cu, path, violations);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing file: {}", path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkComments(CompilationUnit cu, Path path, List<String> violations) {
|
||||
cu.getAllContainedComments().stream()
|
||||
.filter(comment -> CHINESE_CHAR_PATTERN.matcher(comment.getContent()).find())
|
||||
.forEach(comment -> violations.add(formatViolation(path, "comment", comment.getContent().trim())));
|
||||
}
|
||||
|
||||
private void checkCode(CompilationUnit cu, Path path, List<String> violations) {
|
||||
cu.findAll(com.github.javaparser.ast.expr.StringLiteralExpr.class).stream()
|
||||
.filter(str -> CHINESE_CHAR_PATTERN.matcher(str.getValue()).find())
|
||||
.forEach(str -> violations.add(formatViolation(path, "code", str.getValue())));
|
||||
}
|
||||
|
||||
private String formatViolation(Path path, String location, String content) {
|
||||
return String.format("Chinese characters found in %s at %s: %s",
|
||||
location, path.toAbsolutePath(), content);
|
||||
}
|
||||
|
||||
private void assertNoChineseCharacters(List<String> violations) {
|
||||
Assertions.assertEquals(0, violations.size(),
|
||||
() -> String.format("Found Chinese characters in files:%n%s",
|
||||
String.join(System.lineSeparator(), violations)));
|
||||
}
|
||||
|
||||
private enum ScanTarget {
|
||||
COMMENTS(true, false),
|
||||
CODE(false, true),
|
||||
ALL(true, true);
|
||||
|
||||
private final boolean checkComments;
|
||||
private final boolean checkCode;
|
||||
|
||||
ScanTarget(boolean checkComments, boolean checkCode) {
|
||||
this.checkComments = checkComments;
|
||||
this.checkCode = checkCode;
|
||||
}
|
||||
|
||||
public boolean includeComments() {
|
||||
return checkComments;
|
||||
}
|
||||
|
||||
public boolean includeCode() {
|
||||
return checkCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -72,7 +72,7 @@ public abstract class AbstractCollectE2eTest {
|
||||
metricsCollect = new MetricsCollect(mock(Metrics.class), timeout, mock(CollectDataDispatch.class), null, List.of());
|
||||
|
||||
// Initialize services and components
|
||||
appService.run();
|
||||
appService.afterPropertiesSet();
|
||||
metrics = new Metrics();
|
||||
}
|
||||
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.apache.hertzbeat.manager.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.constants.ManagerEventTypeEnum;
|
||||
import org.apache.hertzbeat.common.entity.dto.ImportTaskMessage;
|
||||
import org.apache.hertzbeat.common.entity.dto.ManagerMessage;
|
||||
import org.apache.hertzbeat.common.util.JsonUtil;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Manager SSE
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ManagerSseManager {
|
||||
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
|
||||
|
||||
public SseEmitter createEmitter(Long clientId) {
|
||||
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
|
||||
emitter.onCompletion(() -> removeEmitter(clientId));
|
||||
emitter.onTimeout(() -> removeEmitter(clientId));
|
||||
emitters.put(clientId, emitter);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@Async
|
||||
public void broadcast(String eventName, String data) {
|
||||
emitters.forEach((clientId, emitter) -> {
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.id(String.valueOf(System.currentTimeMillis()))
|
||||
.name(eventName)
|
||||
.data(data));
|
||||
} catch (IOException | IllegalStateException e) {
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
} catch (Exception exception) {
|
||||
log.error("Failed to broadcast manager message data to client: {}", exception.getMessage());
|
||||
emitter.complete();
|
||||
removeEmitter(clientId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void broadcastImportTaskInProgress(String taskName, Integer progress){
|
||||
ManagerMessage managerMessage = ImportTaskMessage.createInProgressMessage(taskName, progress);
|
||||
broadcast(ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), JsonUtil.toJson(managerMessage));
|
||||
}
|
||||
|
||||
public void broadcastImportTaskSuccess(String taskName){
|
||||
ManagerMessage managerMessage = ImportTaskMessage.createCompletedMessage(taskName);
|
||||
broadcast(ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), JsonUtil.toJson(managerMessage));
|
||||
}
|
||||
|
||||
public void broadcastImportTaskFail(String taskName, String errMsg){
|
||||
ManagerMessage managerMessage = ImportTaskMessage.createFailedMessage(taskName, errMsg);
|
||||
broadcast(ManagerEventTypeEnum.IMPORT_TASK_EVENT.getValue(), JsonUtil.toJson(managerMessage));
|
||||
}
|
||||
|
||||
private void removeEmitter(Long clientId) {
|
||||
emitters.remove(clientId);
|
||||
}
|
||||
}
|
||||
+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.manager.controller;
|
||||
|
||||
import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
|
||||
import org.apache.hertzbeat.manager.config.ManagerSseManager;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
|
||||
|
||||
|
||||
/**
|
||||
* SSE controller for manager
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping(path = "/api/manager/sse", produces = {TEXT_EVENT_STREAM_VALUE})
|
||||
public class ManagerSseController {
|
||||
|
||||
private final ManagerSseManager emitterManager;
|
||||
|
||||
public ManagerSseController(ManagerSseManager emitterManager) {
|
||||
this.emitterManager = emitterManager;
|
||||
}
|
||||
|
||||
@GetMapping(path = "/subscribe")
|
||||
public SseEmitter subscribe() {
|
||||
Long clientId = SnowFlakeIdGenerator.generateId();
|
||||
return emitterManager.createEmitter(clientId);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ public class AppCount {
|
||||
private String app;
|
||||
|
||||
/**
|
||||
* 任务状态
|
||||
* task status
|
||||
*/
|
||||
private transient byte status;
|
||||
|
||||
+5
-1
@@ -120,7 +120,11 @@ public class SchedulerInit implements CommandLineRunner {
|
||||
appDefine.setDefaultInterval(monitor.getIntervals());
|
||||
appDefine.setCyclic(true);
|
||||
appDefine.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
Map<String, String> metadata = Map.of(CommonConstants.LABEL_INSTANCE_NAME, monitor.getName(),
|
||||
CommonConstants.LABEL_INSTANCE_HOST, monitor.getHost());
|
||||
appDefine.setMetadata(metadata);
|
||||
appDefine.setLabels(monitor.getLabels());
|
||||
appDefine.setAnnotations(monitor.getAnnotations());
|
||||
List<Configmap> configmaps = params.stream()
|
||||
.map(param -> new Configmap(param.getField(), param.getParamValue(),
|
||||
param.getType())).collect(Collectors.toList());
|
||||
|
||||
+2
-1
@@ -29,9 +29,10 @@ public interface ImExportService {
|
||||
|
||||
/**
|
||||
* Import Configuration
|
||||
* @param taskName task name
|
||||
* @param is input stream
|
||||
*/
|
||||
void importConfig(InputStream is);
|
||||
void importConfig(String taskName, InputStream is);
|
||||
|
||||
/**
|
||||
* Export Configuration
|
||||
|
||||
+40
-39
@@ -22,17 +22,12 @@ import cn.afterturn.easypoi.excel.annotation.ExcelTarget;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hertzbeat.common.constants.ImExportTaskConstant;
|
||||
import org.apache.hertzbeat.common.entity.manager.Monitor;
|
||||
import org.apache.hertzbeat.common.entity.manager.Param;
|
||||
import org.apache.hertzbeat.manager.config.ManagerSseManager;
|
||||
import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
|
||||
import org.apache.hertzbeat.manager.service.ImExportService;
|
||||
import org.apache.hertzbeat.manager.service.MonitorService;
|
||||
@@ -41,6 +36,14 @@ import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* class AbstractImExportServiceImpl
|
||||
*/
|
||||
@@ -54,27 +57,30 @@ public abstract class AbstractImExportServiceImpl implements ImExportService {
|
||||
@Resource
|
||||
private TagService tagService;
|
||||
|
||||
@Resource
|
||||
private ManagerSseManager managerSseManager;
|
||||
|
||||
@Override
|
||||
public void importConfig(InputStream is) {
|
||||
var formList = parseImport(is)
|
||||
.stream()
|
||||
.map(this::convert)
|
||||
.toList();
|
||||
public void importConfig(String taskName, InputStream is) {
|
||||
var formList = parseImport(is).stream().map(this::convert).toList();
|
||||
if (!CollectionUtils.isEmpty(formList)) {
|
||||
formList.forEach(monitorDto -> {
|
||||
int totalElements = formList.size();
|
||||
int progressInterval = Math.max(1, totalElements / 10);
|
||||
for (int i = 0; i < totalElements; i++) {
|
||||
MonitorDto monitorDto = formList.get(i);
|
||||
monitorService.validate(monitorDto, false);
|
||||
monitorService.addMonitor(monitorDto.getMonitor(), monitorDto.getParams(), monitorDto.getCollector(), monitorDto.getGrafanaDashboard());
|
||||
});
|
||||
if (totalElements >= ImExportTaskConstant.IMPORT_TASK_PROCESS_THRESHOLD && ((i + 1) % progressInterval == 0) && (i + 1 < totalElements)) {
|
||||
managerSseManager.broadcastImportTaskInProgress(taskName, (int) ((i + 1) * 100.0 / totalElements));
|
||||
}
|
||||
}
|
||||
managerSseManager.broadcastImportTaskSuccess(taskName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportConfig(OutputStream os, List<Long> configList) {
|
||||
var monitorList = configList.stream()
|
||||
.map(it -> monitorService.getMonitorDto(it))
|
||||
.filter(Objects::nonNull)
|
||||
.map(this::convert)
|
||||
.toList();
|
||||
var monitorList = configList.stream().map(it -> monitorService.getMonitorDto(it)).filter(Objects::nonNull).map(this::convert).toList();
|
||||
writeOs(monitorList, os);
|
||||
}
|
||||
|
||||
@@ -99,15 +105,13 @@ public abstract class AbstractImExportServiceImpl implements ImExportService {
|
||||
var monitor = new MonitorDTO();
|
||||
BeanUtils.copyProperties(dto.getMonitor(), monitor);
|
||||
exportMonitor.setMonitor(monitor);
|
||||
exportMonitor.setParams(dto.getParams().stream()
|
||||
.map(it -> {
|
||||
var param = new ParamDTO();
|
||||
param.setField(it.getField());
|
||||
param.setType(it.getType());
|
||||
param.setValue(it.getParamValue());
|
||||
return param;
|
||||
})
|
||||
.toList());
|
||||
exportMonitor.setParams(dto.getParams().stream().map(it -> {
|
||||
var param = new ParamDTO();
|
||||
param.setField(it.getField());
|
||||
param.setType(it.getType());
|
||||
param.setValue(it.getParamValue());
|
||||
return param;
|
||||
}).toList());
|
||||
exportMonitor.getMonitor().setCollector(dto.getCollector());
|
||||
return exportMonitor;
|
||||
}
|
||||
@@ -120,7 +124,7 @@ public abstract class AbstractImExportServiceImpl implements ImExportService {
|
||||
var monitorDto = new MonitorDto();
|
||||
var monitor = new Monitor();
|
||||
log.debug("exportMonitor.monitor{}", exportMonitor.monitor);
|
||||
if (exportMonitor.monitor != null) {
|
||||
if (exportMonitor.monitor != null) {
|
||||
// Add one more null check
|
||||
BeanUtils.copyProperties(exportMonitor.monitor, monitor);
|
||||
}
|
||||
@@ -129,15 +133,13 @@ public abstract class AbstractImExportServiceImpl implements ImExportService {
|
||||
monitorDto.setCollector(exportMonitor.getMonitor().getCollector());
|
||||
}
|
||||
if (exportMonitor.params != null) {
|
||||
monitorDto.setParams(exportMonitor.params.stream()
|
||||
.map(it -> {
|
||||
var param = new Param();
|
||||
param.setField(it.field);
|
||||
param.setType(it.type);
|
||||
param.setParamValue(it.value);
|
||||
return param;
|
||||
})
|
||||
.toList());
|
||||
monitorDto.setParams(exportMonitor.params.stream().map(it -> {
|
||||
var param = new Param();
|
||||
param.setField(it.field);
|
||||
param.setType(it.type);
|
||||
param.setParamValue(it.value);
|
||||
return param;
|
||||
}).toList());
|
||||
} else {
|
||||
monitorDto.setParams(Collections.emptyList());
|
||||
}
|
||||
@@ -203,5 +205,4 @@ public abstract class AbstractImExportServiceImpl implements ImExportService {
|
||||
@Excel(name = "Value")
|
||||
private String value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-4
@@ -63,7 +63,7 @@ import org.apache.hertzbeat.manager.service.AppService;
|
||||
import org.apache.hertzbeat.manager.service.MonitorService;
|
||||
import org.apache.hertzbeat.manager.service.ObjectStoreService;
|
||||
import org.apache.hertzbeat.warehouse.service.WarehouseService;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@@ -75,12 +75,13 @@ import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* Monitoring Type Management Implementation
|
||||
* temporarily stores the monitoring configuration and parameter configuration in memory and then stores it in the
|
||||
* Temporarily stores the monitoring configuration and parameter configuration in memory,
|
||||
* and then persists it to a storage system.
|
||||
*/
|
||||
@Service
|
||||
@Order(value = Ordered.HIGHEST_PRECEDENCE)
|
||||
@Slf4j
|
||||
public class AppServiceImpl implements AppService, CommandLineRunner {
|
||||
public class AppServiceImpl implements AppService, InitializingBean {
|
||||
|
||||
private static final String PUSH_PROTOCOL_METRICS_NAME = "metrics";
|
||||
|
||||
@@ -500,7 +501,7 @@ public class AppServiceImpl implements AppService, CommandLineRunner {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
var objectStoreConfig = objectStoreConfigService.getConfig();
|
||||
refreshStore(objectStoreConfig);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user