Compare commits

...
Author SHA1 Message Date
tomsun28 fcb8beed50 [doc] update 2025-03-22 15:42:12 +08:00
tomsun28 39582f34e1 [release] update release version 1.7.0
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 15:36:00 +08:00
tomsun28 7cd302ca19 Add @sarthakeash as a contributor 2025-03-22 15:33:05 +08:00
tomsun28 d14bfa227d Add @bigcyy as a contributor 2025-03-22 15:32:55 +08:00
tomsun28 265ca270b1 Add @LL-LIN as a contributor 2025-03-22 15:32:40 +08:00
tomsun28 56a4d41fd4 Add @JuJinPark as a contributor 2025-03-22 15:32:25 +08:00
tomsun28 f3050662f6 [release] update release version 1.7.0
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 15:27:38 +08:00
tomsun28 6c2710bf66 [release] update release version 1.7.0
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 15:21:05 +08:00
tomsun28 56ef35c4f9 [release] update release version 1.7.0
Signed-off-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 15:14:27 +08:00
811a07553b [doc] Add alert integration japanese i18 (#3164)
Co-authored-by: yinyijun <yinyijun6@mgtv.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 14:16:28 +08:00
9fd58ced4a [feature] add twilio sms client support (#3159)
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 14:12:32 +08:00
LL-LINandtomsun28 a331c045ae [bugfix] retain sorting state after monitor list auto-refresh (#3156)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-03-22 11:48:34 +08:00
yunfan24andaias00 63d3d18428 [bugfix] Fix http header being incorrectly encoded. (#3108)
Co-authored-by: aias00 <rokkki@163.com>
2025-03-22 11:44:16 +08:00
Yang Chen cec8d8b396 [bugfix]: fix incomplete class documentation in AppServiceImpl (#3162) 2025-03-21 17:34:43 +08:00
9d68d16a78 [feature] Support TencentCloud alert source (#3149)
Co-authored-by: yunfan24 <yunfan24@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
2025-03-17 20:55:56 +08:00
Kerwin BryantandLogic 290474a715 Abstract Redundant Input Components into ConfigurableFieldComponent for Unified Management (#3152)
Co-authored-by: Logic <zqr10159@dromara.org>
2025-03-17 18:50:00 +08:00
Yang Chenandyunfan24 0dbc7347b4 [feature] Supports sending messages to a specific Telegram group topic.(#3079) (#3143)
Co-authored-by: yunfan24 <yunfan24@outlook.com>
2025-03-17 14:56:02 +08:00
tomsun28 e2a2d5eaec [bugfix] fix nightly docker build github action (#3153) 2025-03-17 10:00:41 +08:00
93 changed files with 1571 additions and 630 deletions
+36
View File
@@ -2325,6 +2325,42 @@
"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,
+2
View File
@@ -31,6 +31,8 @@ name: "CodeQL"
on:
schedule:
- cron: '21 13 * * 4'
push:
branches: [ action* ]
jobs:
analyze:
+1 -1
View File
@@ -19,7 +19,7 @@ name: DOC CI
on:
push:
branches: [ master, dev ]
branches: [ master, dev, action* ]
paths:
- 'home/**'
pull_request:
+2 -1
View File
@@ -17,7 +17,8 @@
name: 'HertzBeat Doc Pdf Builder'
on:
page_build
push:
branches: [ page_build* ]
jobs:
build:
+1 -1
View File
@@ -22,7 +22,7 @@ name: Frontend CI
on:
push:
branches: [ master, dev ]
branches: [ master, dev, action* ]
paths:
- 'web-app/**'
pull_request:
+1 -1
View File
@@ -22,7 +22,7 @@ name: License Checker
on:
push:
branches: [ master, dev ]
branches: [ master, dev, action* ]
pull_request:
branches: [ master, dev ]
+1 -1
View File
@@ -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
+6
View File
@@ -510,6 +510,12 @@ Thanks to these wonderful people, welcome to join us:
<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>
+6
View File
@@ -509,6 +509,12 @@ Thanks these wonderful people, welcome to join us:
<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>
+5
View File
@@ -73,6 +73,11 @@
"field": "successCode",
"type": 4,
"paramValue": "200, 201"
},
{
"field": "enableUrlEncoding",
"type": 1,
"paramValue": true
}
]
}
@@ -64,6 +64,12 @@ public class SmsConfig {
* Aws configuration
*/
private AwsSmsProperties aws;
/**
* Twilio SMS configuration
*/
private TwilioSmsProperties twilio;
/**
* Smslocal SMS configuration
*/
@@ -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;
}
@@ -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";
}
@@ -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
@@ -23,6 +23,7 @@ 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;
@@ -35,6 +36,7 @@ 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;
@@ -141,9 +143,12 @@ public class SmsClientFactory {
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;
}
}
}
}
@@ -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);
}
}
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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);
@@ -39,7 +39,7 @@ spring:
collector:
info:
version: ${COLLECTOR_VERSION:1.6.1}
version: ${COLLECTOR_VERSION:1.7.0}
ip: ${COLLECTOR_IP:}
dispatch:
entrance:
@@ -35,4 +35,7 @@ public interface SmsConstants {
// Aws cloud SMS
String AWS = "aws";
// Twilio SMS
String TWILIO = "twilio";
}
@@ -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)
@@ -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
*/
@@ -75,7 +75,8 @@ 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)
@@ -72,14 +72,22 @@ warehouse:
rpc-port: 6667
username: root
password: root
redis:
enabled: false
host: 127.0.0.1
port: 6379
password: 123456
# store real-time metrics data, enable only one below
real-time:
memory:
enabled: true
init-size: 16
redis:
enabled: false
# redis mode: single, sentinel, cluster. Default is single
mode: single
# separate each address with comma when using cluster mode, eg: 127.0.0.1:6379,127.0.0.1:6380
address: 127.0.0.1:6379
# enter master name when using sentinel mode
masterName: mymaster
password: 123456
# redis db index, default: DB0
db: 0
scheduler:
server:
@@ -102,4 +110,4 @@ ai:
# api key
api-key:
#At present, only IFLYTEK large model needs to be filled in
api-secret:
api-secret:
@@ -231,6 +231,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -246,6 +246,20 @@ params:
defaultValue: '200, 201'
# hide param-true or false
hide: true
# field-param field key
- field: enableUrlEncoding
# name-param field display i18n name
name:
zh-CN: 启用URL编码(UTF-8
en-US: Enable URL Encoding(UTF-8)
# type-param field type(most mapping the html input type)
type: boolean
# required-true or false
required: true
# default value
defaultValue: true
# hide param-true or false
hide: true
# collect metrics config list
metrics:
# metrics - summary
@@ -286,6 +300,8 @@ metrics:
method: ^_^httpMethod^_^
# if enabled https
ssl: ^_^ssl^_^
# enable url encoding
enableUrlEncoding: ^_^enableUrlEncoding^_^
# http request payload
payload: ^_^payload^_^
# http request header content
@@ -344,6 +360,8 @@ metrics:
method: GET
# if enabled https
ssl: ^_^ssl^_^
# enable url encoding
enableUrlEncoding: ^_^enableUrlEncoding^_^
authorization:
# http auth type: Basic Auth, Digest Auth, Bearer Token
type: ^_^authType^_^
+46
View File
@@ -225,6 +225,52 @@ alerter:
Now you can configure this information in your hertzbeat application.
### Twilio SMS Configuration
To activate and use the Twilio SMS service, refer to the official Twilio documentation: [SMS Getting Started Guide](https://www.twilio.com/docs/sms/quickstart)
You can configure the Twilio SMS service either through the graphical interface or in the `application.yml` file.
To use `application.yml`, add/fill in the following Twilio SMS configuration (replace parameters with your own SMS server configuration):
```yaml
alerter:
sms:
enable: true # Whether to enable
type: twilio # SMS provider type, supports "twilio"
twilio: # Twilio SMS configuration
account-sid: # Your Twilio Account SID
auth-token: # Your Twilio Auth Token
twilio-phone-number: # Your Twilio Phone Number
```
1. Create a Twilio account
- If you don't have a Twilio account, sign up at [Twilio Console](https://www.twilio.com/console)
- You'll get a free trial account that allows you to test the SMS service
2. Obtain Twilio credentials
- After signing in, locate your Account SID and Auth Token on the dashboard
- These credentials will be used to authenticate API requests
3. Get a Twilio Phone Number
- In the Twilio Console, navigate to "Phone Numbers" > "Manage" > "Active Numbers"
- Click "Buy a Number" or use the trial number provided by Twilio
- Make sure the number has SMS capabilities enabled
- This number will be used as the sender for your SMS alerts
- A phone number is required for the Twilio SMS service to work
4. Testing Your Configuration on Twilio
- Twilio provides a sandbox environment for testing
- If you are in the trial period, you will only be able to send SMS to verified phone numbers
- To verify a phone number, add it to your verified phone numbers list in the Twilio Console
> The message follows the format: "Instance: {}, Priority: {}, Content: {}"
This information can be configured in the Hertzbeat application.
## Operation steps
1. **【Alarm notification】->【Add new recipient】 ->【Select SMS notification method】**
+7
View File
@@ -157,6 +157,13 @@
"help/windows_script"
]
},
{
"type": "category",
"label": "server",
"items": [
"help/ipmi"
]
},
{
"type": "category",
"label": "mid",
+6
View File
@@ -448,6 +448,12 @@ export default function () {
<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>
+1 -1
View File
@@ -93,7 +93,7 @@
</modules>
<properties>
<hzb.version>1.6.1</hzb.version>
<hzb.version>1.7.0</hzb.version>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
+4
View File
@@ -230,6 +230,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -34,6 +34,10 @@ spring:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
freemarker:
enabled: false
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
@@ -197,6 +201,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -63,7 +63,7 @@ services:
- hertzbeat
hertzbeat:
image: apache/hertzbeat
image: apache/hertzbeat:1.7.0
container_name: compose-hertzbeat
hostname: hertzbeat
restart: always
@@ -34,6 +34,10 @@ spring:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
freemarker:
enabled: false
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
@@ -193,6 +197,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -62,7 +62,7 @@ services:
- hertzbeat
hertzbeat:
image: apache/hertzbeat
image: apache/hertzbeat:1.7.0
container_name: compose-hertzbeat
hostname: hertzbeat
restart: always
@@ -34,6 +34,10 @@ spring:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
freemarker:
enabled: false
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
@@ -191,6 +195,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -62,7 +62,7 @@ services:
- hertzbeat
hertzbeat:
image: apache/hertzbeat
image: apache/hertzbeat:1.7.0
container_name: compose-hertzbeat
hostname: hertzbeat
restart: always
@@ -34,6 +34,10 @@ spring:
exclude: org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
freemarker:
enabled: false
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
management:
health:
@@ -190,6 +194,10 @@ alerter:
access-key-id: YOUR_ACCESS_KEY_ID
access-key-secret: YOUR_ACCESS_KEY_SECRET
region: AWS_REGION_FOR_END_USER_MESSAGING
twilio:
account-sid: YOUR_ACCOUNT_SID
auth-token: YOUR_AUTH_TOKEN
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
scheduler:
server:
enabled: true
@@ -64,7 +64,7 @@ services:
- hertzbeat
hertzbeat:
image: apache/hertzbeat
image: apache/hertzbeat:1.7.0
container_name: compose-hertzbeat
hostname: hertzbeat
restart: always
+2 -2
View File
@@ -19,11 +19,11 @@ $ docker buildx use mybuilder
```shell
# Use the --platform flag to specify the target platforms for the build --push flag to push the image to a registry
$ docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:v1.0.0 . --push
$ docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:1.0.0 . --push
$ docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat-collector:latest . --push
# Inspect the image info
$ docker buildx imagetools inspect apache/hertzbeat-collector:v1.0.0
$ docker buildx imagetools inspect apache/hertzbeat-collector:1.0.0
```
+2 -2
View File
@@ -31,11 +31,11 @@ fi
# docker compile context
CONTEXT_DIR=`pwd`
COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:v$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat-collector:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat-collector:v$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat-collector:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat-collector:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --build-arg VERSION="$VERSION" --push"
+2 -2
View File
@@ -19,10 +19,10 @@ $ docker buildx use mybuilder
```shell
# Use the --platform flag to specify the target platforms for the build --push flag to push the image to a registry
$ docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat:v1.0.0 . --push
$ docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat:1.0.0 . --push
$ docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat:latest . --push
# Inspect the image info
$ docker buildx imagetools inspect apache/hertzbeat:v1.0.0
$ docker buildx imagetools inspect apache/hertzbeat:1.0.0
```
+2 -2
View File
@@ -27,11 +27,11 @@ VERSION=`ls apache-hertzbeat-*-incubating-docker-compose.tar.gz| awk -F"-" '{pri
# compile context dir
CONTEXT_DIR=`pwd`
COMMAND="docker buildx build --platform ${IMAGE_PLATFORM:-linux/arm64,linux/amd64} -t apache/hertzbeat:v$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push=${IMAGE_PUSH:-true} --load=${IMAGE_LOAD:-false}"
COMMAND="docker buildx build --platform ${IMAGE_PLATFORM:-linux/arm64,linux/amd64} -t apache/hertzbeat:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push=${IMAGE_PUSH:-true} --load=${IMAGE_LOAD:-false}"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t apache/hertzbeat:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat:v$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat:$VERSION -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push"
#COMMAND="docker buildx build --platform linux/arm64,linux/amd64 -t quay.io/tancloud/hertzbeat:latest -f $CURRENT_DIR/Dockerfile $CONTEXT_DIR --push"
+1 -1
View File
@@ -17,7 +17,7 @@ $gpgKeyId = "********"
# ********************************************************
# Set version and release candidate number
$version = "1.6.1"
$version = "1.7.0"
$rcNumber = "rc1"
# ********************************************************
+1
View File
@@ -27,6 +27,7 @@ export class NoticeReceiver {
email!: string;
tgBotToken!: string;
tgUserId!: string;
tgMessageThreadId!: string;
userId!: string;
slackWebHookUrl!: string;
discordChannelId!: string;
+2
View File
@@ -21,6 +21,7 @@ import { AlibabaSmsConfig } from './AlibabaSmsConfig';
import { AwsSmsConfig } from './AwsSmsConfig';
import { SmslocalSmsConfig } from './SmslocalSmsConfig';
import { TencentSmsConfig } from './TencentSmsConfig';
import { TwilioSmsConfig } from './TwilioSmsConfig';
import { UniSmsConfig } from './UniSmsConfig';
import { SmsType } from './enums/sms-type.enum';
@@ -32,6 +33,7 @@ export class SmsNoticeSender {
unisms: UniSmsConfig = new UniSmsConfig();
smslocal: SmslocalSmsConfig = new SmslocalSmsConfig();
aws: AwsSmsConfig = new AwsSmsConfig();
twilio: TwilioSmsConfig = new TwilioSmsConfig();
enable: boolean = false;
creator!: string;
modifier!: string;
@@ -17,25 +17,8 @@
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LabelsInputComponent } from './labels-input.component';
describe('LabelsInputComponent', () => {
let component: LabelsInputComponent;
let fixture: ComponentFixture<LabelsInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LabelsInputComponent]
}).compileComponents();
fixture = TestBed.createComponent(LabelsInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
export class TwilioSmsConfig {
accountSid!: string;
authToken!: string;
twilioPhoneNumber!: string;
}
+2 -1
View File
@@ -22,7 +22,8 @@ export enum SmsType {
ALIBABA = 'alibaba',
UNISMS = 'unisms',
SMSLOCAL = 'smslocal',
AWS = 'aws'
AWS = 'aws',
TWILIO = 'twilio'
}
export enum UniSmsAuthMode {
@@ -163,7 +163,15 @@
{{ 'alert.inhibit.source_labels' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<app-labels-input [(ngModel)]="alertInhibit.sourceLabels" name="sourceLabels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'sourceLabels'"
[(ngModel)]="alertInhibit.sourceLabels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -171,7 +179,15 @@
{{ 'alert.inhibit.target_labels' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<app-labels-input [(ngModel)]="alertInhibit.targetLabels" name="targetLabels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'targetLabels'"
[(ngModel)]="alertInhibit.targetLabels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -67,12 +67,12 @@ export class AlertIntegrationComponent implements OnInit {
id: 'skywalking',
name: this.i18nSvc.fanyi('alert.integration.source.skywalking'),
icon: 'assets/img/integration/skywalking.svg'
},
{
id: 'tencent',
name: this.i18nSvc.fanyi('alert.integration.source.tencent'),
icon: 'assets/img/integration/tencent.svg'
}
// {
// id: 'tencent',
// name: this.i18nSvc.fanyi('alert.integration.source.tencent'),
// icon: 'assets/img/integration/tencent.svg'
// }
];
selectedSource: DataSource | null = null;
@@ -344,6 +344,12 @@
<input [(ngModel)]="receiver.tgUserId" [required]="receiver.type === 7" name="tgUserId" nz-input type="text" />
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="receiver.type === 7">
<nz-form-label [nzSpan]="7" nzFor="tgMessageThreadId">{{ 'alert.notice.type.telegram-message-thread-id' | i18n }}</nz-form-label>
<nz-form-control [nzErrorTip]="'validation.required' | i18n" [nzSpan]="12">
<input [(ngModel)]="receiver.tgMessageThreadId" name="tgMessageThreadId" nz-input type="text" />
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="receiver.type === 8">
<nz-form-label [nzRequired]="receiver.type === 8" [nzSpan]="7" nzFor="slackWebHookUrl"
>{{ 'alert.notice.type.slack-webHook-url' | i18n }}
@@ -186,7 +186,15 @@
<nz-form-item *ngIf="!rule.filterAll">
<nz-form-label nzFor="labels" nzSpan="7" nzRequired="true">{{ 'alert.notice.rule.tag' | i18n }} </nz-form-label>
<nz-form-control nzSpan="12" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input [(ngModel)]="rule.labels" name="labels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'labels'"
[(ngModel)]="rule.labels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -538,7 +538,14 @@
<nz-form-item>
<nz-form-label nzSpan="7" [nzFor]="'labels'" [nzTooltipTitle]="'label.bind.tip' | i18n">{{ 'label.bind' | i18n }} </nz-form-label>
<nz-form-control nzSpan="12">
<app-labels-input [(ngModel)]="define.labels" name="labels"></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
}"
[name]="'labels'"
[(ngModel)]="define.labels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -546,7 +553,14 @@
>{{ 'common.annotation.bind' | i18n }}
</nz-form-label>
<nz-form-control nzSpan="12">
<app-labels-input [(ngModel)]="define.annotations" name="annotations"></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
}"
[name]="'annotations'"
[(ngModel)]="define.annotations"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -179,7 +179,15 @@
<nz-form-item *ngIf="!silence.matchAll">
<nz-form-label nzSpan="7" nzFor="labels" nzRequired="true">{{ 'alert.silence.labels' | i18n }}</nz-form-label>
<nz-form-control nzSpan="12" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input [(ngModel)]="silence.labels" name="labels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'labels'"
[(ngModel)]="silence.labels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
@@ -131,12 +131,14 @@
<nz-form-item>
<nz-form-label nzSpan="7" [nzFor]="'labels'" [nzTooltipTitle]="'label.bind.tip' | i18n">{{ 'label.bind' | i18n }} </nz-form-label>
<nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input
<app-form-field
[item]="{
field: 'labels',
type: 'labels'
}"
[name]="'labels'"
[(ngModel)]="monitor.labels"
name="labels"
[keyAlias]="'placeholder.key' | i18n"
[valueAlias]="'placeholder.value' | i18n"
></app-labels-input>
/>
</nz-form-control>
</nz-form-item>
@@ -145,12 +147,14 @@
>{{ 'common.annotation.bind' | i18n }}
</nz-form-label>
<nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input
<app-form-field
[item]="{
field: 'annotations',
type: 'labels'
}"
[name]="'annotations'"
[(ngModel)]="monitor.annotations"
name="annotation"
[keyAlias]="'placeholder.key' | i18n"
[valueAlias]="'placeholder.value' | i18n"
></app-labels-input>
/>
</nz-form-control>
</nz-form-item>
@@ -72,6 +72,9 @@ export class MonitorListComponent implements OnInit, OnDestroy {
appSearchOrigin: any[] = [];
appSearchLoading = false;
intervalId: any;
// save the current sorting status
currentSortField: string | null = null;
currentSortOrder: string | null = null;
switchExportTypeModalFooter: ModalButtonOptions[] = [
{ label: this.i18nSvc.fanyi('common.button.cancel'), type: 'default', onClick: () => (this.isSwitchExportTypeModalVisible = false) }
@@ -149,7 +152,7 @@ export class MonitorListComponent implements OnInit, OnDestroy {
}
sync() {
this.loadMonitorTable();
this.loadMonitorTable(this.currentSortField, this.currentSortOrder);
}
getAppIconName(app: string | undefined): string {
@@ -499,9 +502,9 @@ export class MonitorListComponent implements OnInit, OnDestroy {
this.pageIndex = pageIndex;
this.pageSize = pageSize;
const currentSort = sort.find(item => item.value !== null);
const sortField = (currentSort && currentSort.key) || null;
const sortOrder = (currentSort && currentSort.value) || null;
this.changeMonitorTable(sortField, sortOrder);
this.currentSortField = (currentSort && currentSort.key) || null;
this.currentSortOrder = (currentSort && currentSort.value) || null;
this.changeMonitorTable(this.currentSortField, this.currentSortOrder);
}
// begin: app type search filter
@@ -72,6 +72,11 @@
<br />
{{ 'alert.notice.sender.sms.unisms.authMode' | i18n }}: {{ smsNoticeSender.unisms.authMode }}
</ng-container>
<ng-container *ngSwitchCase="SmsType.TWILIO">
{{ 'alert.notice.sender.sms.twilio.accountSid' | i18n }}: {{ smsNoticeSender.twilio.accountSid }}
<br />
{{ 'alert.notice.sender.sms.twilio.twilioPhoneNumber' | i18n }}: {{ smsNoticeSender.twilio.twilioPhoneNumber }}
</ng-container>
</ng-container>
<br />
{{ 'common.enable' | i18n }}: {{ smsNoticeSender.enable ? ('common.yes' | i18n) : ('common.no' | i18n) }}
@@ -160,6 +165,7 @@
<nz-option [nzValue]="SmsType.UNISMS" nzLabel="{{ 'alert.notice.sender.sms.type.unisms' | i18n }}"></nz-option>
<nz-option [nzValue]="SmsType.SMSLOCAL" nzLabel="{{ 'alert.notice.sender.sms.type.smslocal' | i18n }}"></nz-option>
<nz-option [nzValue]="SmsType.AWS" nzLabel="{{ 'alert.notice.sender.sms.type.aws' | i18n }}"></nz-option>
<nz-option [nzValue]="SmsType.TWILIO" nzLabel="{{ 'alert.notice.sender.sms.type.twilio' | i18n }}"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
@@ -373,6 +379,48 @@
</nz-form-item>
</ng-container>
<!-- Twilio SMS -->
<ng-container *ngIf="smsType === SmsType.TWILIO">
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="accountSid" nzRequired="true">
{{ 'alert.notice.sender.sms.twilio.accountSid' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input
[(ngModel)]="smsNoticeSender.twilio.accountSid"
nz-input
required
name="accountSid"
type="password"
id="twilioAccountSid"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="authToken" nzRequired="true">
{{ 'alert.notice.sender.sms.twilio.authToken' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input [(ngModel)]="smsNoticeSender.twilio.authToken" nz-input required name="authToken" type="password" id="twilioAuthToken" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="twilioPhoneNumber" nzRequired="true">
{{ 'alert.notice.sender.sms.twilio.twilioPhoneNumber' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input
[(ngModel)]="smsNoticeSender.twilio.twilioPhoneNumber"
nz-input
required
name="twilioPhoneNumber"
type="text"
id="twilioPhoneNumber"
/>
</nz-form-control>
</nz-form-item>
</ng-container>
<nz-form-item>
<nz-form-label nzSpan="7" nzFor="smsEnable" nzRequired="true">{{ 'common.enable' | i18n }}</nz-form-label>
<nz-form-control nzSpan="12">
@@ -33,6 +33,7 @@ import { SmsType, UniSmsAuthMode } from 'src/app/pojo/enums/sms-type.enum';
import { AwsSmsConfig } from '../../../../pojo/AwsSmsConfig';
import { EmailNoticeSender } from '../../../../pojo/EmailNoticeSender';
import { SmslocalSmsConfig } from '../../../../pojo/SmslocalSmsConfig';
import { TwilioSmsConfig } from '../../../../pojo/TwilioSmsConfig';
import { GeneralConfigService } from '../../../../service/general-config.service';
@Component({
@@ -146,6 +147,7 @@ export class MessageServerComponent implements OnInit {
this.smsNoticeSender.unisms = { ...new UniSmsConfig(), ...message.data.unisms };
this.smsNoticeSender.smslocal = { ...new SmslocalSmsConfig(), ...message.data.smslocal };
this.smsNoticeSender.aws = { ...new AwsSmsConfig(), ...message.data.aws };
this.smsNoticeSender.twilio = { ...new TwilioSmsConfig(), ...message.data.twilio };
this.smsType = message.data.type || 'tencent';
} else {
this.smsNoticeSender = new SmsNoticeSender();
@@ -171,7 +173,8 @@ export class MessageServerComponent implements OnInit {
alibaba: { ...this.smsNoticeSender.alibaba },
unisms: { ...this.smsNoticeSender.unisms },
smslocal: { ...this.smsNoticeSender.smslocal },
aws: { ...this.smsNoticeSender.aws }
aws: { ...this.smsNoticeSender.aws },
twilio: { ...this.smsNoticeSender.twilio }
};
this.isSmsServerModalVisible = true;
}
@@ -184,7 +187,8 @@ export class MessageServerComponent implements OnInit {
alibaba: { ...this.tempSmsNoticeSender.alibaba },
unisms: { ...this.tempSmsNoticeSender.unisms },
smslocal: { ...this.tempSmsNoticeSender.smslocal },
aws: { ...this.tempSmsNoticeSender.aws }
aws: { ...this.tempSmsNoticeSender.aws },
twilio: { ...this.tempSmsNoticeSender.twilio }
};
this.isSmsServerModalVisible = false;
}
@@ -391,7 +391,15 @@
'status.component.tag' | i18n
}}</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<app-labels-input [(ngModel)]="currentStatusComponent.labels" name="sourceLabels" required></app-labels-input>
<app-form-field
[item]="{
field: 'labels',
type: 'labels',
required: true
}"
[name]="'sourceLabels'"
[(ngModel)]="currentStatusComponent.labels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item *ngIf="currentStatusComponent.method == 1">
@@ -0,0 +1,43 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<div class="container">
<div *ngFor="let obj of value; let i = index" class="row">
<ng-container *ngFor="let cfg of configs">
<app-multi-func-input
[style]="cfg.style"
[name]="(name || '') + '-' + i + '-' + cfg.key"
[id]="(id || '') + '-' + i + '-' + cfg.key"
[required]="cfg.required ?? false"
[placeholder]="cfg.placeholder ?? ''"
[(value)]="value[i][cfg.key]"
(valueChange)="onChange(i, cfg.key, $event)"
/>
<span *ngIf="cfg.separatorAfter" class="separator">{{ cfg.separatorAfter }}</span>
</ng-container>
<div class="btns">
<button nz-button nzSize="small" *ngIf="value.length > 1" (click)="del(i, $event)">
<i nz-icon nzType="minus" nzTheme="outline"></i>
</button>
<button nz-button nzSize="small" (click)="add($event)">
<i nz-icon nzType="plus" nzTheme="outline"></i>
</button>
</div>
</div>
</div>
@@ -0,0 +1,31 @@
.container {
display: flex;
flex-direction: column;
gap: 5px;
}
.row {
flex: 1;
display: flex;
gap: 5px;
app-multi-func-input {
flex: 1;
}
}
.separator {
flex-basis: min-content;
text-align: center;
align-content: center;
}
.btns {
display: flex;
align-items: center;
gap: 5px;
button {
margin-left: 0px!important;
}
}
@@ -19,20 +19,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MetricsFieldInputComponent } from './metrics-field-input.component';
import { ConfigurableFieldComponent } from './configurable-field.component';
describe('MetricsFieldInputComponent', () => {
let component: MetricsFieldInputComponent;
let fixture: ComponentFixture<MetricsFieldInputComponent>;
describe('ConfigurableFieldComponent', () => {
let component: ConfigurableFieldComponent;
let fixture: ComponentFixture<ConfigurableFieldComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [MetricsFieldInputComponent]
declarations: [ConfigurableFieldComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MetricsFieldInputComponent);
fixture = TestBed.createComponent(ConfigurableFieldComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -0,0 +1,115 @@
/*
* 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.
*/
import { Component, Input } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
export interface FieldConfig {
key: string;
style?: string;
required?: boolean;
placeholder?: string;
separatorAfter?: string;
validators?: any[];
}
@Component({
selector: 'app-configurable-field',
templateUrl: './configurable-field.component.html',
styleUrls: ['./configurable-field.component.less'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: ConfigurableFieldComponent
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: ConfigurableFieldComponent
}
]
})
export class ConfigurableFieldComponent implements ControlValueAccessor, Validator {
constructor() {}
@Input() configs!: FieldConfig[];
@Input() name!: string;
@Input() id!: string;
value: any = [{}];
validateStatus!: string;
_onChange: Function = () => {};
_onTouched: Function = () => {};
writeValue(value: any): void {
this.value = value && value.length > 0 ? value : [{}];
}
registerOnChange(fn: Function): void {
this._onChange = fn;
}
registerOnTouched(fn: Function): void {
this._onTouched = fn;
}
validate(control: AbstractControl): ValidationErrors | null {
let value = control?.value ?? [];
for (let o of value) {
for (let c of this.configs) {
let v = o[c.key];
if (c.required && (v === null || v === undefined || (typeof v === 'string' && v.trim() === ''))) {
this.validateStatus = 'error';
return {
required: {
valid: false
}
};
}
}
}
this.validateStatus = '';
return null;
}
onChange(index: number, key: string, value: any) {
this.value[index][key] = value;
this._onChange(this.value);
this._onTouched();
}
add(e?: MouseEvent) {
if (e) {
e.preventDefault();
}
this.value.push({});
this._onChange(this.value);
}
del(index: number, e?: MouseEvent) {
if (e) {
e.preventDefault();
}
if (this.value.length > 1) {
this.value.splice(index, 1);
}
this._onChange(this.value);
}
}
@@ -89,23 +89,70 @@
</label>
</nz-radio-group>
<app-key-value-input
*ngIf="item.type === 'key-value'"
<app-configurable-field
*ngIf="item.type === 'labels'"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
[keyAlias]="'monitor.headerName.tip' | i18n"
[valueAlias]="'monitor.headerValue.tip' | i18n"
[configs]="[
{
key: 'key',
required: true,
separatorAfter: ':',
placeholder: 'placeholder.key' | i18n
},
{
key: 'value',
required: true,
placeholder: 'placeholder.value' | i18n
}
]"
[name]="item.field"
[id]="item.field"
/>
<app-metrics-field-input
<app-configurable-field
*ngIf="item.type === 'key-value'"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
[configs]="[
{
key: 'key',
required: true,
placeholder: 'monitor.headerName.tip' | i18n
},
{
key: 'value',
required: true,
placeholder: 'monitor.headerValue.tip' | i18n
}
]"
[name]="item.field"
[id]="item.field"
/>
<app-configurable-field
*ngIf="item.type === 'metrics-field'"
[(value)]="value"
(valueChange)="onChange($event)"
[FieldAlias]="'Field'"
[UnitAlias]="'Unit'"
[TypeAlias]="'Type'"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
[configs]="[
{
key: 'field',
required: true,
placeholder: 'Field'
},
{
key: 'unit',
required: true,
placeholder: 'Unit',
style: 'flex: 0; flex-basis: min-content; min-width: 50px'
},
{
key: 'type',
required: true,
placeholder: 'Type'
}
]"
[name]="item.field"
[id]="item.field"
/>
@@ -17,9 +17,11 @@
* under the License.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, Input, ViewChild } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { ConfigurableFieldComponent } from '../configurable-field/configurable-field.component';
@Component({
selector: 'app-form-field',
templateUrl: './form-field.component.html',
@@ -41,15 +43,39 @@ export class FormFieldComponent implements ControlValueAccessor, Validator {
constructor() {}
@Input() item!: any;
@Input() extra: any = {};
@ViewChild(ConfigurableFieldComponent) configurableField: ConfigurableFieldComponent | undefined;
value: any;
validateStatus!: string;
_onChange: Function = () => {};
_onTouched: Function = () => {};
writeValue(value: any): void {
this.value = value;
if (this.item.type === 'key-value' && value && typeof value === 'string') {
// It is compatible with the key-value type data structure of existing data. For the conversion from the old to the new:
// Old structure: "{"1":"2","3":"4"}"
// New structure: [{key: "1", value: "2"}, {key: "3", value: "4"}]
let tmpValue = JSON.parse(value);
let newValue = [];
for (let k in tmpValue) {
newValue[newValue.length] = {
key: k,
value: tmpValue[k]
};
}
this.value = newValue;
} else if (this.item.type === 'labels' && value && !(value instanceof Array)) {
// It is compatible with the lables type data structure of existing data. For the conversion from the old to the new:
// Old structure: {1:"2", 3:"4"}
// New structure: [{key: "1", value: "2"}, {key: "3", value: "4"}]
let newValue = Object.entries(value).map(([key, value]) => ({
key: String(key),
value: String(value)
}));
this.value = newValue;
} else {
this.value = value;
}
}
registerOnChange(fn: Function): void {
@@ -63,19 +89,44 @@ export class FormFieldComponent implements ControlValueAccessor, Validator {
validate(control: AbstractControl): ValidationErrors | null {
// if (!(control.dirty) && !(control.touched)) return null;
let { value } = control;
if (this.item.required && (value === null || value === undefined || value === '')) {
this.validateStatus = 'error';
if (
this.item.required &&
(value === null ||
value === undefined ||
(typeof value === 'string' && value.trim() === '') ||
this.configurableField?.validateStatus === 'error')
) {
return {
required: {
valid: false
}
};
}
this.validateStatus = '';
return null;
}
onChange(value: any) {
this._onChange(value);
if (this.item.type === 'key-value' && value) {
// It is compatible with the key-value type data structure of existing data. For the conversion from the new to the old:
// Old structure: "{"1":"2","3":"4"}"
// New structure: [{key: "1", value: "2"}, {key: "3", value: "4"}]
const newValue: any = {};
value.forEach((item: any) => {
newValue[item.key] = item.value;
});
this._onChange(JSON.stringify(newValue));
} else if (this.item.type === 'labels' && value) {
// It is compatible with the lables type data structure of existing data. For the conversion from the new to the old:
// Old structure: {1:"2", 3:"4"}
// New structure: [{key: "1", value: "2"}, {key: "3", value: "4"}]
const newValue: any = {};
value.forEach((item: any) => {
newValue[item.key] = item.value;
});
this._onChange(newValue);
} else {
this._onChange(value);
}
this._onTouched();
}
}
@@ -1,31 +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.
-->
<div style="margin-bottom: 2px; margin-top: 2px" *ngFor="let item of keyValues; let i = index" nz-row>
<div nz-col nzSpan="10">
<input nz-input [placeholder]="keyAlias" [(ngModel)]="item.key" (ngModelChange)="emitChange()" />
</div>
<div nz-col nzSpan="10">
<input nz-input [placeholder]="valueAlias" [(ngModel)]="item.value" (ngModelChange)="emitChange()" />
</div>
<div nz-col nzSpan="4">
<i nz-icon nzType="plus-circle" class="dynamic-button" *ngIf="i === 0" (click)="addNew($event)"></i>
<i nz-icon nzType="minus-circle" class="dynamic-button" (click)="removeCurrent(i, $event)"></i>
</div>
</div>
@@ -1,12 +0,0 @@
.dynamic-button {
position: relative;
top: 20%;
margin-left: 12%;
font-size: 15px;
cursor: pointer;
transition: all 0.3s;
}
.dynamic-button:hover {
transform: scale(1.2);
}
@@ -1,43 +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.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { KeyValueInputComponent } from './key-value-input.component';
describe('KeyValueInputComponent', () => {
let component: KeyValueInputComponent;
let fixture: ComponentFixture<KeyValueInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [KeyValueInputComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(KeyValueInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,119 +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.
*/
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-key-value-input',
templateUrl: './key-value-input.component.html',
styleUrls: ['./key-value-input.component.less'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyValueInputComponent),
multi: true
}
]
})
export class KeyValueInputComponent implements OnInit, ControlValueAccessor {
constructor() {}
@Input()
keyAlias: string = 'Key';
@Input()
valueAlias: string = 'Value';
keyValues: any[] = [];
// ControlValueAccessor
private onChange: any = () => {};
private onTouched: any = () => {};
ngOnInit(): void {
if (this.keyValues.length === 0) {
this.addNew();
}
}
writeValue(obj: string): void {
if (obj != undefined) {
let value = JSON.parse(obj);
if (value != null) {
this.keyValues = [];
Object.keys(value).map(item => {
this.keyValues.push({
key: item,
value: value[item]
});
});
}
}
if (this.keyValues.length === 0) {
this.addNew();
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// set disabled state here
}
addNew(e?: MouseEvent) {
if (e) {
e.preventDefault();
}
this.keyValues.push({
key: '',
value: ''
});
}
removeCurrent(index: number, e?: MouseEvent) {
if (e) {
e.preventDefault();
}
if (this.keyValues.length > 1) {
this.keyValues.splice(index, 1);
this.emitChange();
}
}
emitChange() {
let value: Record<string, string> = {};
this.keyValues.forEach(item => {
if (item != null && item.key != null) {
value[item.key] = item.value;
}
});
if (Object.keys(value).length === 0 || Object.values(value).every(v => v === '')) {
this.onChange(null);
} else {
this.onChange(JSON.stringify(value));
}
this.onTouched();
}
}
@@ -1,34 +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.
-->
<div style="margin-bottom: 2px; margin-top: 2px" *ngFor="let item of keyValues; let i = index" nz-row>
<div nz-col nzSpan="10">
<input nz-input [placeholder]="keyAlias" [(ngModel)]="item.key" (ngModelChange)="emitChange()" />
</div>
<div style="text-align: center; font-weight: bolder" nz-col nzSpan="1">
<span>:</span>
</div>
<div nz-col nzSpan="10">
<input nz-input [placeholder]="valueAlias" [(ngModel)]="item.value" (ngModelChange)="emitChange()" />
</div>
<div nz-col nzSpan="3">
<i nz-icon nzType="plus-circle" class="dynamic-button" *ngIf="i === 0" (click)="addNew($event)"></i>
<i nz-icon nzType="minus-circle" class="dynamic-button" (click)="removeCurrent(i, $event)"></i>
</div>
</div>
@@ -1,12 +0,0 @@
.dynamic-button {
position: relative;
top: 20%;
margin-left: 12%;
font-size: 15px;
cursor: pointer;
transition: all 0.3s;
}
.dynamic-button:hover {
transform: scale(1.2);
}
@@ -1,115 +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.
*/
import { Component, Input, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
interface KeyValuePair {
key: string;
value: string;
}
@Component({
selector: 'app-labels-input',
templateUrl: './labels-input.component.html',
styleUrl: './labels-input.component.less',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => LabelsInputComponent),
multi: true
}
]
})
export class LabelsInputComponent implements OnInit, ControlValueAccessor {
constructor() {}
@Input() keyAlias: string = 'Key';
@Input() valueAlias: string = 'Value';
keyValues: KeyValuePair[] = [];
// ControlValueAccessor
private onChange: any = () => {};
private onTouched: any = () => {};
writeValue(value: Record<string, string>): void {
this.keyValues = Object.entries(value || {}).map(([key, value]) => ({
key,
value
}));
if (this.keyValues.length === 0) {
this.addNew();
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
// set disabled state here
}
ngOnInit(): void {
if (this.keyValues.length === 0) {
this.addNew();
}
}
addNew(e?: MouseEvent) {
if (e) {
e.preventDefault();
}
this.keyValues.push({
key: '',
value: ''
});
}
removeCurrent(index: number, e?: MouseEvent) {
if (e) {
e.preventDefault();
}
if (this.keyValues.length > 1) {
this.keyValues.splice(index, 1);
this.emitChange();
}
}
emitChange() {
const result: Record<string, string> = {};
this.keyValues.forEach(item => {
if (item.key?.trim()) {
result[item.key] = item.value || '';
}
});
if (Object.keys(result).length === 0 || Object.values(result).every(v => v === '')) {
this.onChange(null);
} else {
this.onChange(result);
}
this.onTouched();
}
}
@@ -1,34 +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.
-->
<div *ngFor="let item of fields; let i = index" nz-row>
<div nz-col nzSpan="7">
<input nz-input [placeholder]="FieldAlias" [(ngModel)]="item.field" (ngModelChange)="onChange()" />
</div>
<div nz-col nzSpan="7">
<input nz-input [placeholder]="UnitAlias" [(ngModel)]="item.unit" (ngModelChange)="onChange()" />
</div>
<div nz-col nzSpan="7">
<input nz-input [placeholder]="TypeAlias" [(ngModel)]="item.type" (ngModelChange)="onChange()" />
</div>
<div nz-col nzSpan="3">
<i nz-icon nzType="plus-circle" class="dynamic-button" *ngIf="i === 0" (click)="addNew($event)"></i>
<i nz-icon nzType="minus-circle" class="dynamic-button" (click)="removeCurrent(i, $event)"></i>
</div>
</div>
@@ -1,12 +0,0 @@
.dynamic-button {
position: relative;
top: 20%;
margin-left: 12%;
font-size: 15px;
cursor: pointer;
transition: all 0.3s;
}
.dynamic-button:hover {
transform: scale(1.7);
}
@@ -1,87 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
@Component({
selector: 'app-metrics-field-input',
templateUrl: './metrics-field-input.component.html',
styleUrls: ['./metrics-field-input.component.less']
})
export class MetricsFieldInputComponent implements OnInit {
constructor() {}
@Input() value!: any;
@Output() readonly valueChange = new EventEmitter<string>();
@Input()
FieldAlias: string = 'field';
@Input()
UnitAlias: string = 'unit';
@Input()
TypeAlias: string = 'type';
fields: any[] = [];
ngOnInit(): void {
if (this.value == undefined) {
this.fields.push({
field: '',
unit: '',
type: ''
});
} else {
this.value = JSON.parse(this.value);
}
if (this.value) {
for (let item of this.value) {
this.fields.push({
field: item.field,
unit: item.unit,
type: item.type
});
}
}
}
addNew(e?: MouseEvent) {
if (e) {
e.preventDefault();
}
this.fields.push({
field: '',
unit: '',
type: ''
});
}
removeCurrent(index: number, e?: MouseEvent) {
if (e) {
e.preventDefault();
}
if (this.fields.length > 1) {
this.fields.splice(index, 1);
}
}
onChange() {
this.value = this.fields;
this.valueChange.emit(JSON.stringify(this.value));
}
}
@@ -1,5 +1,8 @@
:host {
::ng-deep {
.ant-input-affix-wrapper {
width: 100%!important;
}
.ant-input-prefix, .ant-input-suffix {
display: flex;
gap: 4px;
+1 -1
View File
@@ -18,5 +18,5 @@
*/
export const CONSTANTS = {
VERSION: 'v1.6.1'
VERSION: 'v1.7.0'
};
+3 -7
View File
@@ -11,11 +11,9 @@ import { NzRadioComponent, NzRadioGroupComponent } from 'ng-zorro-antd/radio';
import { NzSwitchComponent } from 'ng-zorro-antd/switch';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { ConfigurableFieldComponent } from './components/configurable-field/configurable-field.component';
import { FormFieldComponent } from './components/form-field/form-field.component';
import { HelpMessageShowComponent } from './components/help-message-show/help-message-show.component';
import { KeyValueInputComponent } from './components/key-value-input/key-value-input.component';
import { LabelsInputComponent } from './components/labels-input/labels-input.component';
import { MetricsFieldInputComponent } from './components/metrics-field-input/metrics-field-input.component';
import { MonitorSelectMenuComponent } from './components/monitor-select-menu/monitor-select-menu.component';
import { MultiFuncInputComponent } from './components/multi-func-input/multi-func-input.component';
import { ToolbarComponent } from './components/toolbar/toolbar.component';
@@ -27,14 +25,12 @@ import { SHARED_ZORRO_MODULES } from './shared-zorro.module';
const ThirdModules: Array<Type<void>> = [];
const COMPONENTS: Array<Type<void>> = [
KeyValueInputComponent,
MultiFuncInputComponent,
HelpMessageShowComponent,
MetricsFieldInputComponent,
ToolbarComponent,
ConfigurableFieldComponent,
FormFieldComponent,
MonitorSelectMenuComponent,
LabelsInputComponent
MonitorSelectMenuComponent
];
const DIRECTIVES: Array<Type<void>> = [TimezonePipe, I18nElsePipe, ElapsedTimePipe];
@@ -0,0 +1,46 @@
> Prometheus AlertManager のアラートを HertzBeat アラートプラットフォームに送信します。
### Alertmanager の Webhook 設定
1. Alertmanager の設定ファイルで webhook を設定する。
```yaml
receivers:
- name: 'webhook'
webhook_configs:
- url: 'http://{hertzbeat_host}:1157/api/alerts/report/alertmanager'
send_resolved: true
http_config:
authorization:
type: 'Bearer'
credentials: '{token}'
```
- `http://{hertzbeat_host}:1157/api/alerts/report/alertmanager` は、HertzBeat の webhook URLです。
- `send_resolved: true` は、アラート解決情報を送信することを意味します。
- `credentials``{token}` はHertzBeatが提供するtokenです。
2. Alertmanager のサービスを再起動する。
### 設定の検証
- Prometheus AlertManager のアラートをトリガーする。
- HertzBeat アラートプラットフォームでアラートデータを閲覧し、正しいか検証する。
### データフロー
```mermaid
graph LR
A[Prometheus Alertmanager] --> B[Webhook]
B --> C[HertzBeatアラートプラットフォーム]
C --> D[グループ収束]
D --> E[アラート抑制]
E --> F[アラームサイレンス]
F --> G[アラームセンター]
F --> H[メッセージ配信]
```
### FAQ
- Alertmanager の設定ファイルで webhook アドレスが正しく、ネットワークが接続されていることを確認してください。
- Alertmanagerのアラートがトリガーされたかどうか、またHertzBeatアラートプラットフォームに送信されたかどうかを確認してください。
@@ -0,0 +1,40 @@
> Prometheus ServerのAlertmanager設定でHertzBeatのサービスアドレスを設定し、HertzBeatを使用してAlertmanagerを置き換え、Prometheus Serverのアラート情報を受信および処理します。
### Prometheus サービスの設定
- Prometheusの設定ファイル prometheus.yml を編集し、HertzBeatをアラートの受信先として追加します。
```yaml
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
- {hertzbeat_host}:1157
authorization:
type: 'Bearer'
credentials: '{token}'
```
- `{hertzbeat_host}:1157` は、HertzBeat Server のアドレスとポートです。実際の環境に合わせて設定を修正し、ネットワーク接続が確立されていることを確認してください。
- `{token}` は、HertzBeat Server の認証トークンです。新しいトークンを申請した後、置き換えてください。
- Prometheus Server を再起動してください。
## 設定の検証
1. Prometheus の設定が正しいことを確認し、設定を再読み込みしてください。
```bash
curl -X POST http://localhost:9090/-/reload
```
2. Prometheusアラートルールの状態を確認する。
```bash
curl http://localhost:9090/api/v1/rules
```
3. テストアラートをトリガーし、HertzBeatアラートセンターで表示を確認する 。
## FAQ
- PrometheusサーバーからHertzBeatのURLへアクセス可能であること。
- Prometheusログ内にアラート送信失敗のエラーメッセージがないか確認する。
- アラートルールの式が正しいことを検証する。
詳細情報は [Prometheusアラート設定ドキュメント](https://prometheus.io/docs/alerting/latest/configuration/) を参照してください
@@ -0,0 +1,30 @@
>SkyWalking のアラートを Webhook 方式で HertzBeat アラートプラットフォームに送信します。
### SkyWalking の設定
- SkyWalking 設定ファイル `alarm-settings.yml` を編集、HertzBeat をアラート受信先として設定します。
```yaml
hooks:
webhook:
default:
is-default: true
urls:
- http://{hertzbeat_host}:1157/api/alerts/report/skywalking
```
- `http://{hertzbeat_host}:1157/api/alerts/report/skywalking` は、 HertzBeat が提供する Webhook インターフェースです。
- SkyWalking OAP Server を再起動してください。
### 設定の検証
1. SkyWalking の設定が正しいことを確認し、設定を再読み込みしてください。
2. SkyWalkingアラートルールの状態を確認する。
3. テストアラートをトリガーし、HertzBeatアラートセンターで表示を確認する。
### FAQ
- SkyWalkingサーバーからHertzBeatのURLへアクセス可能であること。
- SkyWalkingログ内にアラート送信失敗のエラーメッセージがないか確認する。
- アラートルールの式が正しいことを検証する。
詳細情報は [SkyWalkingアラート設定ドキュメント](https://skywalking.apache.org/docs/main/latest/en/setup/backend/backend-alarm/)
@@ -18,7 +18,7 @@ hooks:
1. 确保 SkyWalking 配置正确并重新加载配置
2. 检查 SkyWalking 告警规则状态
3. 触发测试告警并在 SkyWalking 告警中心查看
3. 触发测试告警并在 HertzBeat 告警中心查看
### 常见问题
@@ -0,0 +1,34 @@
>Send Tencent Cloud alerts to the HertzBeat alert platform via Webhook.
### Configure Tencent Cloud Alert Callback
#### Configure Notification Template
1. Log in to [Tencent Cloud Observability Platform](https://console.cloud.tencent.com/monitorv2)
2. Navigate to **Alarm Management** > **Alarm Configuration** > **Notification Templates**
3. Click **Create Notification Template**
4. On the new notification template page, fill in the basic information
5. In the **API Callback** module, enter the HertzBeat Webhook API URL:
```
http://{your_system_host}/api/alerts/tencent
```
6. Save the notification template
#### Bind Alert Policy
1. Go to **Alert Policy List**
2. Select the alert policy that needs to be bound with the Webhook callback, click the policy name to enter the management page
3. In the notification template configuration, bind the notification template created in the previous step
4. Save the policy configuration
### Common Issues
#### Not Receiving Alerts
- Ensure the Webhook URL is accessible from the public network
- Check if there are request records in the server logs
- Test if the Webhook is available on the Tencent Cloud notification template page
#### Alerts Not Triggered
- Ensure the alert policy conditions are correct and the notification template is bound
- Check the alert history on the Tencent Cloud monitoring page to confirm if the policy was triggered
For more information, please refer to [Tencent Cloud Alert Configuration Documentation](https://cloud.tencent.com/document/product/248/50409)
@@ -0,0 +1,34 @@
>TencentCloudのアラートをWebhookでHertzBeatアラートプラットフォームに送信します。
### テンセントクラウドアラートコールバックの設定
#### 通知テンプレートの設定
1. [TencentCloud Observability Platform](https://console.cloud.tencent.com/monitorv2)にログインします。
2. **アラート管理** > **アラート設定** > **通知テンプレート** に移動します。
4. **新しい通知テンプレート** をクリックします。
4. 新しい通知テンプレートページで、基本情報を入力します。
5. **インターフェースコールバック** モジュールで、HertzBeat が提供する Webhook インターフェースアドレス URL を入力します。
```
http://{your_system_host}/api/alerts/tencent
```
6. 通知テンプレートを保存します。
#### アラートポリシーを関連付ける
1. **アラートポリシー一覧画面** に移動します。
2. Webhookコールバックを関連付けるアラートポリシーを選択し、ポリシー名をクリックして管理ページにアクセスします。
3. 通知テンプレート設定項目で、前の手順で作成した通知テンプレートを関連付けます。
4. ポリシー設定を保存します。
### FAQ
#### アラートが受信されない場合
- Webhook URLがパブリックネットワークからアクセス可能か確認してください。
- サーバーログにリクエスト記録があるかチェックしてください。
- Tencent Cloudの通知テンプレートページでWebhookが使用可能かテストしてください。
#### アラートがトリガーされない場合
- アラートポリシーの条件が正しく設定されていること、および通知テンプレートが関連付けられていることを確認してください 。
- Tencent Cloud監視ページのアラート履歴を確認し、ポリシーがトリガーされていることを確認してください。
詳細情報は [TencentCloud アラート設定ドキュメント](https://cloud.tencent.com/document/product/248/50409)
@@ -0,0 +1,34 @@
>将腾讯云的告警通过 Webhook 方式发送到 HertzBeat 告警平台。
### 配置腾讯云告警回调
#### 进入通知模板配置
1. 登录 [腾讯云可观测平台](https://console.cloud.tencent.com/monitorv2)
2. 进入 **告警管理** > **告警配置** > **通知模板**
3. 单击 **新建通知模板**
4. 在新建通知模板页面,填写基础信息
5.**接口回调** 模块中,填写 HertzBeat 提供的 Webhook 接口地址 URL
```
http://{your_system_host}/api/alerts/tencent
```
6. 保存通知模板
#### 绑定告警策略
1. 进入 **告警策略列表**
2. 选择需要绑定 Webhook 回调的告警策略,点击策略名称进入管理页面
3. 在通知模板配置项中,绑定上一步创建的通知模板
4. 保存策略配置
### 常见问题
#### 未收到告警
- 确保 Webhook URL 可以被公网访问
- 检查服务器日志是否有请求记录
- 在腾讯云通知模板页面测试 Webhook 是否可用
#### 告警未触发
- 确保告警策略的条件正确,并已绑定通知模板
- 在腾讯云监控页面查看告警历史,确保策略被触发
更多信息请参考 [腾讯云告警配置文档](https://cloud.tencent.com/document/product/248/50409)
@@ -1,9 +1,34 @@
## 验证配置
>將騰訊雲的告警通過 Webhook 方式發送到 HertzBeat 告警平台。
1. 确保 Prometheus 配置正确并重新加载配置
2. 触发一个测试告警
3. 在 HertzBeat 告警中心查看是否收到告警
### 配置騰訊雲告警回調
更多信息请参考 [Prometheus 官方文档](https://prometheus.io/docs/alerting/latest/configuration/)
#### 進入通知模板配置
1. 登錄 [騰訊雲可觀測平台](https://console.cloud.tencent.com/monitorv2)
2. 進入 **告警管理** > **告警配置** > **通知模板**
3. 單擊 **新建通知模板**
4. 在新建通知模板頁面,填寫基礎信息
5.**接口回調** 模塊中,填寫 HertzBeat 提供的 Webhook 接口地址 URL
```
http://{your_system_host}/api/alerts/tencent
```
6. 保存通知模板
#### 綁定告警策略
1. 進入 **告警策略列表**
2. 選擇需要綁定 Webhook 回調的告警策略,點擊策略名稱進入管理頁面
3. 在通知模板配置項中,綁定上一步創建的通知模板
4. 保存策略配置
### 常見問題
#### 未收到告警
- 確保 Webhook URL 可以被公網訪問
- 檢查服務器日誌是否有請求記錄
- 在騰訊雲通知模板頁面測試 Webhook 是否可用
#### 告警未觸發
- 確保告警策略的條件正確,並已綁定通知模板
- 在騰訊雲監控頁面查看告警歷史,確保策略被觸發
更多信息請參考 [騰訊雲告警配置文檔](https://cloud.tencent.com/document/product/248/50409)
## Alert Rule Example
@@ -0,0 +1,70 @@
> HertzBeat は外部システムに対してAPIインターフェースを提供し、外部システムはWebhook方式でこのインターフェースを呼び出して、アラートデータをHertzBeatアラートプラットフォームにプッシュすることができます。
### インターフェースエンドポイント
`POST /api/alerts/report`
### リクエストヘッダ
- `Content-Type`: `application/json`
- `Authorization`: `Bearer {token}`
### リクエストボディ
```json
{
"labels": {
"alertname": "HighCPUUsage",
"priority": "critical",
"instance": "343483943"
},
"annotations": {
"summary": "High CPU usage detected"
},
"content": "The CPU usage on instance 343483943 is critically high.",
"status": "firing",
"triggerTimes": 3,
"startAt": 1736580031832,
"activeAt": 1736580039832,
"endAt": null
}
```
フィールド説明
- `labels`: アラートラベル
- `alertname`: アラートルール名
- `priority`: アラートレベル (warning, critical)
- `instance`: アラートインスタンス
- `annotations`: アラート注釈情報
- `summary`: アラート概要
- `description`: アラート詳細説明
- `content`: アラート内容
- `status`: アラートステータス (firing, resolved)
- `triggerTimes`: アラートトリガー回数
- `startAt`: アラート開始時間
- `activeAt`: アラートアクティブ時間
- `endAt`: アラート終了時間
### 設定検証
サードパーティシステムがアラートをトリガーした後、Webhookを使用してHertzBeatの `/api/alerts/report` インターフェースをコールバックし、アラートデータをHertzBeatアラートプラットフォームにプッシュします。
HertzBeatアラートプラットフォームでアラートデータを処理し、アラートデータが正しいかどうかを確認します。
### データフロー
```mermaid
graph LR
A[外部システムのアラーム] --> B[Webhook]
B --> C[HertzBeatアラートプラットフォーム]
C --> D[グループ収束]
D --> E[アラート抑制]
E --> F[アラームサイレンス]
F --> G[アラームセンター]
F --> H[メッセージ配信]
```
### FAQ
- HertzBeatのURLがサードパーティシステムのサーバーからアクセス可能であることを確認してください。
- サードパーティシステムのログにアラート送信が成功または失敗したメッセージがないか確認してください。
+5
View File
@@ -158,12 +158,16 @@
"alert.notice.sender.sms.aws.accessKeyId": "Aws SMS AccessKeyId",
"alert.notice.sender.sms.aws.accessKeySecret": "Aws SMS AccessKeySecret",
"alert.notice.sender.sms.aws.region": "Aws Region",
"alert.notice.sender.sms.twilio.accountSid": "Twilio Account SID",
"alert.notice.sender.sms.twilio.authToken": "Twilio Auth Token",
"alert.notice.sender.sms.twilio.twilioPhoneNumber": "Twilio Phone Number",
"alert.notice.sender.sms.type": "Sms Type",
"alert.notice.sender.sms.type.alibaba": "Alibaba Sms",
"alert.notice.sender.sms.type.tencent": "Tencent Sms",
"alert.notice.sender.sms.type.unisms": "UniSMS",
"alert.notice.sender.sms.type.smslocal": "Smslocal Sms",
"alert.notice.sender.sms.type.aws": "Aws Sms",
"alert.notice.sender.sms.type.twilio": "Twilio Sms",
"alert.notice.template": "Notice Template",
"alert.notice.template.content": "Template Content",
"alert.notice.template.delete": "Delete Template",
@@ -211,6 +215,7 @@
"alert.notice.type.telegram-bot": "Telegram Bot",
"alert.notice.type.telegram-bot-token": "Telegram Bot Token",
"alert.notice.type.telegram-bot-user-id": "Telegram User Id",
"alert.notice.type.telegram-message-thread-id": "Message Thread Id",
"alert.notice.type.url": "URL",
"alert.notice.type.userId": "User ID",
"alert.notice.type.wechat": "Open WeChat",
+5
View File
@@ -158,12 +158,16 @@
"alert.notice.sender.sms.aws.accessKeyId": "Aws SMS AccessKeyId",
"alert.notice.sender.sms.aws.accessKeySecret": "Aws SMS AccessKeySecret",
"alert.notice.sender.sms.aws.region": "Aws Region",
"alert.notice.sender.sms.twilio.accountSid": "Twilio Account SID",
"alert.notice.sender.sms.twilio.authToken": "Twilio Auth Token",
"alert.notice.sender.sms.twilio.twilioPhoneNumber": "Twilio Phone Number",
"alert.notice.sender.sms.type": "SMSタイプ",
"alert.notice.sender.sms.type.alibaba": "Alibaba Sms",
"alert.notice.sender.sms.type.tencent": "Tencent Sms",
"alert.notice.sender.sms.type.unisms": "UniSMS",
"alert.notice.sender.sms.type.smslocal": "Smslocal Sms",
"alert.notice.sender.sms.type.aws": "Aws Sms",
"alert.notice.sender.sms.type.twilio": "Twilio Sms",
"alert.notice.template": "通知テンプレート",
"alert.notice.template.content": "テンプレート内容",
"alert.notice.template.delete": "テンプレートを削除",
@@ -211,6 +215,7 @@
"alert.notice.type.telegram-bot": "Telegram ボット",
"alert.notice.type.telegram-bot-token": "Telegram ボットトークン",
"alert.notice.type.telegram-bot-user-id": "Telegram ユーザーID",
"alert.notice.type.telegram-message-thread-id": "Telegram メッセージスレッドID",
"alert.notice.type.url": "URL",
"alert.notice.type.userId": "ユーザーID",
"alert.notice.type.wechat": "Open WeChat",
+1
View File
@@ -305,6 +305,7 @@
"alert.notice.type.telegram-bot": "Bot do Telegram",
"alert.notice.type.telegram-bot-token": "Token do Bot do Telegram",
"alert.notice.type.telegram-bot-user-id": "ID do Usuário do Telegram",
"alert.notice.type.telegram-message-thread-id": "ID da Thread do Telegram",
"alert.notice.type.slack": "WebHook do Slack",
"alert.notice.type.slack-webHook-url": "URL do WebHook do Slack",
"alert.notice.type.discord": "Bot do Discord",
+5
View File
@@ -158,12 +158,16 @@
"alert.notice.sender.sms.aws.accessKeyId": "Aws SMS AccessKeyId",
"alert.notice.sender.sms.aws.accessKeySecret": "Aws SMS AccessKeySecret",
"alert.notice.sender.sms.aws.region": "Aws Region",
"alert.notice.sender.sms.twilio.accountSid": "Twilio Account SID",
"alert.notice.sender.sms.twilio.authToken": "Twilio Auth Token",
"alert.notice.sender.sms.twilio.twilioPhoneNumber": "Twilio Phone Number",
"alert.notice.sender.sms.type": "短信类型",
"alert.notice.sender.sms.type.alibaba": "阿里短信",
"alert.notice.sender.sms.type.tencent": "腾讯短信",
"alert.notice.sender.sms.type.unisms": "合一短信(UniSMS",
"alert.notice.sender.sms.type.smslocal": "当地短信(Smslocal",
"alert.notice.sender.sms.type.aws": "Aws Sms",
"alert.notice.sender.sms.type.twilio": "Twilio Sms",
"alert.notice.template": "通知模板",
"alert.notice.template.content": "模板内容",
"alert.notice.template.delete": "删除通知模板",
@@ -211,6 +215,7 @@
"alert.notice.type.telegram-bot": "Telegram机器人",
"alert.notice.type.telegram-bot-token": "Telegram Bot Token",
"alert.notice.type.telegram-bot-user-id": "Telegram User Id",
"alert.notice.type.telegram-message-thread-id": "Message Thread Id",
"alert.notice.type.url": "URL地址",
"alert.notice.type.userId": "用户ID",
"alert.notice.type.wechat": "微信公众号",
+4
View File
@@ -158,6 +158,9 @@
"alert.notice.sender.sms.aws.accessKeyId": "Aws SMS AccessKeyId",
"alert.notice.sender.sms.aws.accessKeySecret": "Aws SMS AccessKeySecret",
"alert.notice.sender.sms.aws.region": "Aws Region",
"alert.notice.sender.sms.twilio.accountSid": "Twilio Account SID",
"alert.notice.sender.sms.twilio.authToken": "Twilio Auth Token",
"alert.notice.sender.sms.twilio.twilioPhoneNumber": "Twilio Phone Number",
"alert.notice.sender.sms.type": "騰訊類型",
"alert.notice.sender.sms.type.alibaba": "阿裏短訊",
"alert.notice.sender.sms.type.tencent": "騰訊短訊",
@@ -211,6 +214,7 @@
"alert.notice.type.telegram-bot": "Telegram機器人",
"alert.notice.type.telegram-bot-token": "Telegram Bot Token",
"alert.notice.type.telegram-bot-user-id": "Telegram User Id",
"alert.notice.type.telegram-message-thread-id": "Message Thread Id",
"alert.notice.type.url": "URL地址",
"alert.notice.type.userId": "用戶ID",
"alert.notice.type.wechat": "微信公衆號",