This is an automated email from the ASF dual-hosted git repository.
gongchao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/master by this push:
new 99b5460dd1 [feature] Support Huawei Cloud `Cloud Eye` alert source
(#3443)
99b5460dd1 is described below
commit 99b5460dd13c4be85b53264a553c0ad77c2c39e6
Author: Duansg <[email protected]>
AuthorDate: Mon Jun 9 00:22:31 2025 +0800
[feature] Support Huawei Cloud `Cloud Eye` alert source (#3443)
Co-authored-by: tomsun28 <[email protected]>
---
.../alert/dto/HuaweiCloudExternAlert.java | 237 ++++++++++++++
.../impl/HuaweiCloudExternAlertService.java | 343 +++++++++++++++++++++
.../org/apache/hertzbeat/alert/util/DateUtil.java | 16 +
.../service/HuaweiCloudExternAlertServiceTest.java | 154 +++++++++
.../alert-integration.component.ts | 5 +
.../doc/alert-integration/huaweicloud-ces.en-US.md | 77 +++++
.../doc/alert-integration/huaweicloud-ces.zh-CN.md | 80 +++++
.../doc/alert-integration/huaweicloud-ces.zh-TW.md | 77 +++++
web-app/src/assets/i18n/en-US.json | 1 +
web-app/src/assets/i18n/ja-JP.json | 1 +
web-app/src/assets/i18n/pt-BR.json | 1 +
web-app/src/assets/i18n/zh-CN.json | 1 +
web-app/src/assets/i18n/zh-TW.json | 1 +
web-app/src/assets/img/integration/huaweicloud.svg | 1 +
14 files changed, 995 insertions(+)
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/HuaweiCloudExternAlert.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/HuaweiCloudExternAlert.java
new file mode 100644
index 0000000000..a82157ef31
--- /dev/null
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/dto/HuaweiCloudExternAlert.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * Huawei Cloud (CES) alert content entity.
+ *
+ * @see <a
href="https://support.huaweicloud.com/usermanual-ces/ces_01_0218.html"/>
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class HuaweiCloudExternAlert {
+
+ public static final String FIELD_MESSAGE = "message";
+ public static final String FIELD_MESSAGE_ID = "message_id";
+ public static final String FIELD_TIMESTAMP = "timestamp";
+ public static final String FIELD_TOPIC_URN = "topic_urn";
+ public static final String FIELD_TYPE = "type";
+ public static final String FIELD_SUBJECT = "subject";
+ public static final String FIELD_SUBSCRIBE_URL = "subscribe_url";
+
+ /**
+ * Signature information.
+ */
+ private String signature;
+
+ /**
+ * Subject
+ */
+ private String subject;
+
+ /**
+ * The unique identifier of a topic, indicating the topic to which the
message belongs.
+ */
+ @JsonProperty("topic_urn")
+ private String topicUrn;
+
+ /**
+ * Message unique identifier.
+ */
+ @JsonProperty("message_id")
+ private String messageId;
+
+ /**
+ * Message
+ */
+ private String message;
+
+ /**
+ * message types, the message types are respectively:
+ * SubscriptionConfirmation、Notification、UnsubscribeConfirmation
+ */
+ private String type;
+
+ /**
+ * Subscription confirms the URL that needs to be accessed
+ */
+ @JsonProperty("subscribe_url")
+ private String subscribeUrl;
+
+ /**
+ * The certificate URL used for message signing, which does not require
authentication and can be accessed directly.
+ */
+ @JsonProperty("signing_cert_url")
+ private String signingCertUrl;
+
+ /**
+ * The timestamp of when the message was first sent.
+ */
+ private String timestamp;
+
+ /**
+ * Alert message
+ */
+ @Data
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ public static class AlertMessage {
+
+ private String version;
+
+ private AlertData data;
+ }
+
+ /**
+ * Alert data
+ */
+ @Data
+ @Builder
+ @AllArgsConstructor
+ @NoArgsConstructor
+ public static class AlertData {
+
+ /**
+ * Whether an alarm occurs.
+ * Note: Empty and false are both recovery notifications.
+ * Note: There are no recovery notifications for event types
+ */
+ @JsonProperty("IsAlarm")
+ private Boolean alarm;
+
+ /**
+ * Alarm time
+ */
+ @JsonProperty("AlarmTime")
+ private String alarmTime;
+
+ /**
+ * Resource ID
+ */
+ @JsonProperty("ResourceId")
+ private String resourceId;
+
+ /**
+ * The name of the metric
+ */
+ @JsonProperty("MetricName")
+ private String metricName;
+
+ /**
+ * Specifies the alarm severity, which can be Critical, Major, Minor,
or Informational.
+ */
+ @JsonProperty("AlarmLevel")
+ private String alarmLevel;
+
+ /**
+ * Namespace
+ */
+ @JsonProperty("Namespace")
+ private String namespace;
+
+ /**
+ * Region
+ */
+ @JsonProperty("Region")
+ private String region;
+
+ /**
+ * Dimension name
+ */
+ @JsonProperty("DimensionName")
+ private String dimensionName;
+
+ /**
+ * Resource name
+ */
+ @JsonProperty("ResourceName")
+ private String resourceName;
+
+ /**
+ * Alarm record ID
+ */
+ @JsonProperty("AlarmRecordID")
+ private String alarmRecordId;
+
+ /**
+ * Current data
+ */
+ @JsonProperty("CurrentData")
+ private String currentData;
+
+ /**
+ * The comparison conditions for the alarm thresholds can be >, =, <,
>=, <=.
+ */
+ @JsonProperty("ComparisonOperator")
+ private String comparisonOperator;
+
+ /**
+ * Alarm value
+ */
+ @JsonProperty("Value")
+ private String value;
+
+ /**
+ * Number of consecutive occurrences of triggered alarms
+ */
+ @JsonProperty("Count")
+ private int count;
+
+ }
+
+ /**
+ * Huawei cloud alert type
+ */
+ public enum AlertType {
+
+ SUBSCRIPTION("SubscriptionConfirmation"),
+
+ UNSUBSCRIBE("UnsubscribeConfirmation"),
+
+ NOTIFICATION("Notification");
+
+ private final String type;
+
+ AlertType(String type) {
+ this.type = type;
+ }
+
+ public static boolean valid(String type) {
+ if (null == type || type.isEmpty()) {
+ return false;
+ }
+ return Arrays.stream(AlertType.values()).anyMatch(alertType ->
alertType.getType().equals(type));
+ }
+
+ public String getType() {
+ return type;
+ }
+ }
+
+}
\ No newline at end of file
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/HuaweiCloudExternAlertService.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/HuaweiCloudExternAlertService.java
new file mode 100644
index 0000000000..2c538aeb48
--- /dev/null
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/HuaweiCloudExternAlertService.java
@@ -0,0 +1,343 @@
+/*
+ * 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.commons.lang3.StringUtils;
+import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
+import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
+import org.apache.hertzbeat.alert.service.ExternAlertService;
+import org.apache.hertzbeat.alert.util.DateUtil;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.support.exception.IgnoreException;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.springframework.stereotype.Service;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.Signature;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.NOTIFICATION;
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.SUBSCRIPTION;
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.AlertType.UNSUBSCRIBE;
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE;
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_MESSAGE_ID;
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBJECT;
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_SUBSCRIBE_URL;
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TIMESTAMP;
+import static
org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TOPIC_URN;
+import static org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert.FIELD_TYPE;
+
+/**
+ * Huawei cloud external alarm service impl
+ */
+@Slf4j
+@Service
+public class HuaweiCloudExternAlertService implements ExternAlertService {
+
+ private static final String CERTIFICATE_TYPE = "X.509";
+
+ private static final String CHARSET_UTF8 = StandardCharsets.UTF_8.name();
+
+ private final AlarmCommonReduce alarmCommonReduce;
+
+ public HuaweiCloudExternAlertService(AlarmCommonReduce alarmCommonReduce) {
+ this.alarmCommonReduce = alarmCommonReduce;
+ }
+
+ @Override
+ public void addExternAlert(String content) {
+ HuaweiCloudExternAlert externAlert = JsonUtil.fromJson(content,
HuaweiCloudExternAlert.class);
+ if (externAlert == null ||
StringUtils.isBlank(externAlert.getMessage())) {
+ log.warn("Failure to parse external alert content. content: {}",
content);
+ return;
+ }
+ if (!isMessageValid(externAlert)) {
+ log.warn("Huawei cloud alert verify failed. content: {}", content);
+ return;
+ }
+ process(externAlert);
+ }
+
+ /**
+ * Process according to different types
+ *
+ * @param externAlert alert content entity
+ */
+ private void process(HuaweiCloudExternAlert externAlert) {
+ if (NOTIFICATION.getType().equals(externAlert.getType())) {
+
Optional.ofNullable(buildSendAlert(externAlert)).ifPresent(alarmCommonReduce::reduceAndSendAlarm);
+ } else if (SUBSCRIPTION.getType().equals(externAlert.getType())) {
+ autoSubscribeForUrl(externAlert.getSubscribeUrl());
+ } else if (UNSUBSCRIBE.getType().equals(externAlert.getType())) {
+ log.warn("Huawei cloud notifies the recipient of the notification
to cancel the subscription.");
+ }
+ }
+
+ /**
+ * Build single alert.
+ *
+ * @param externAlert alert content entity
+ * @return single alert
+ */
+ private SingleAlert buildSendAlert(HuaweiCloudExternAlert externAlert) {
+ HuaweiCloudExternAlert.AlertMessage message =
JsonUtil.fromJson(externAlert.getMessage(),
HuaweiCloudExternAlert.AlertMessage.class);
+ if (null == message || null == message.getData()) {
+ log.warn("Failure to parse external alert message. message: {}",
externAlert.getMessage());
+ return null;
+ }
+ // Note: Empty and false are both recovery notifications.
+ // Note: There are no recovery notifications for event types
+ boolean isAlarm = null != message.getData().getAlarm() &&
message.getData().getAlarm();
+ Long alarmTime =
DateUtil.getZonedTimeStampFromFormat(message.getData().getAlarmTime(),
"yyyy/MM/dd HH:mm:ss 'GMT'XXX");
+ return SingleAlert.builder()
+ .triggerTimes(1)
+ .status(isAlarm ? CommonConstants.ALERT_STATUS_FIRING :
CommonConstants.ALERT_STATUS_RESOLVED)
+ .startAt(alarmTime)
+ .activeAt(Instant.now().toEpochMilli())
+ .endAt(isAlarm ? null : alarmTime)
+ .labels(buildLabels(message.getData()))
+ .annotations(buildAnnotations(message.getData()))
+ .content(formatContent(externAlert.getSubject(),
message.getData()))
+ .build();
+ }
+
+ /**
+ * Build basic annotations and fill annotations for huawei cloud.
+ *
+ * @param alertData alert content entity
+ * @return annotations
+ */
+ private Map<String, String>
buildAnnotations(HuaweiCloudExternAlert.AlertData alertData) {
+ Map<String, String> annotations = new HashMap<>(8);
+ if (null != alertData) {
+ putIfNotBlank(annotations, "region", alertData.getRegion());
+ putIfNotBlank(annotations, "dimensionName",
alertData.getDimensionName());
+ putIfNotBlank(annotations, "resourceName",
alertData.getResourceName());
+ putIfNotBlank(annotations, "alarmRecordId",
alertData.getAlarmRecordId());
+ }
+ return annotations;
+ }
+
+ /**
+ * Build basic labels and fill labels for huawei cloud.
+ *
+ * @param alertData alert content entity
+ * @return labels
+ */
+ private Map<String, String> buildLabels(HuaweiCloudExternAlert.AlertData
alertData) {
+ Map<String, String> labels = new HashMap<>(8);
+ labels.put("__source__", "huaweicloud-ces");
+ if (null != alertData) {
+ putIfNotBlank(labels, "namespace", alertData.getNamespace());
+ putIfNotBlank(labels, "metricName", alertData.getMetricName());
+ putIfNotBlank(labels, "resourceId", alertData.getResourceId());
+ putIfNotBlank(labels, "level", alertData.getAlarmLevel());
+ }
+ return labels;
+ }
+
+ /**
+ * todo i18n
+ *
+ * @param subject alert subject
+ * @param alertData alert content entity
+ * @return content
+ */
+ private String formatContent(String subject,
HuaweiCloudExternAlert.AlertData alertData) {
+ if (null == alertData) {
+ return subject;
+ }
+ return MessageFormat.format(
+ "{0} threshold:{1}{2}, current:{3}",
+ subject,
+ alertData.getComparisonOperator(),
+ alertData.getValue(),
+ alertData.getCurrentData()
+ );
+ }
+
+ /**
+ * Automatic subscription url.
+ *
+ * @param subscribeUrl subscribeUrl
+ */
+ public void autoSubscribeForUrl(String subscribeUrl) {
+ if (StringUtils.isBlank(subscribeUrl)) {
+ return;
+ }
+ try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+ HttpGet httpGet = new HttpGet(subscribeUrl);
+ try (CloseableHttpResponse response = httpClient.execute(httpGet))
{
+ int statusCode = response.getStatusLine().getStatusCode();
+ String responseBody =
EntityUtils.toString(response.getEntity());
+
+ if (statusCode != 200) {
+ log.error("Subscribe url request failed with status code:
" + statusCode + ", response: " + responseBody);
+ return;
+ }
+ JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
+ if (jsonResponse == null) {
+ throw new IgnoreException("Subscribe url failed with
status code: " + statusCode + ", response: " + responseBody);
+ }
+ JsonNode surnNode = jsonResponse.get("subscription_urn");
+ if (surnNode == null ||
StringUtils.isBlank(surnNode.asText())) {
+ throw new IgnoreException("Subscribe url failed with
status code: " + statusCode + ", response: " + responseBody);
+ }
+ log.info("Successfully subscribed to Huawei Cloud(SMN) url.");
+ }
+ } catch (Exception e) {
+ log.error("Failed to subscribe url request: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * Verifying the signature of huawei cloud alert message.
+ *
+ * @param externAlert alert content entity
+ * @return verification result
+ * @throws SecurityException thrown when validation fails
+ */
+ private boolean isMessageValid(HuaweiCloudExternAlert externAlert) {
+ try {
+ String signMessage = buildSignMessage(externAlert);
+ if (StringUtils.isBlank(signMessage)) {
+ throw new SecurityException("Verify sign message is null");
+ }
+ X509Certificate cert =
getCertificate(externAlert.getSigningCertUrl());
+ return verifySignature(signMessage, cert,
externAlert.getSignature());
+ } catch (Exception e) {
+ log.error("Failed to verify message signature: ", e);
+ return false;
+ }
+ }
+
+ /**
+ * Build sign message.
+ *
+ * @param externAlert alert content entity
+ * @return sign message
+ */
+ private String buildSignMessage(HuaweiCloudExternAlert externAlert) {
+ if (NOTIFICATION.getType().equals(externAlert.getType())) {
+ return buildNotificationMessage(externAlert);
+ } else if (SUBSCRIPTION.getType().equals(externAlert.getType()) ||
UNSUBSCRIBE.getType().equals(externAlert.getType())){
+ return buildSubscriptionMessage(externAlert);
+ }
+ return null;
+ }
+
+ /**
+ * Building sign message of 'Notification' type
+ *
+ * @param externAlert alert content entity
+ * @return sign message
+ */
+ private String buildNotificationMessage(HuaweiCloudExternAlert
externAlert) {
+ StringBuilder message = new StringBuilder();
+ appendField(message, FIELD_MESSAGE, externAlert.getMessage());
+ appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId());
+ if (StringUtils.isNotBlank(externAlert.getSubject())) {
+ appendField(message, FIELD_SUBJECT, externAlert.getSubject());
+ }
+ appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp());
+ appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn());
+ appendField(message, FIELD_TYPE, externAlert.getType());
+ return message.toString();
+ }
+
+ /**
+ * Building sign message of 'SubscriptionConfirmation' or
'UnsubscribeConfirmation' type
+ *
+ * @param externAlert alert content entity
+ * @return sign message
+ */
+ private String buildSubscriptionMessage(HuaweiCloudExternAlert
externAlert) {
+ StringBuilder message = new StringBuilder();
+ appendField(message, FIELD_MESSAGE, externAlert.getMessage());
+ appendField(message, FIELD_MESSAGE_ID, externAlert.getMessageId());
+ appendField(message, FIELD_SUBSCRIBE_URL,
externAlert.getSubscribeUrl());
+ appendField(message, FIELD_TIMESTAMP, externAlert.getTimestamp());
+ appendField(message, FIELD_TOPIC_URN, externAlert.getTopicUrn());
+ appendField(message, FIELD_TYPE, externAlert.getType());
+ return message.toString();
+ }
+
+ /**
+ * Obtain certificate
+ *
+ * @param signCertUrl sign cert url
+ * @return X509 certificate
+ * @throws Exception Thrown when certificate acquisition fails
+ */
+ private X509Certificate getCertificate(String signCertUrl) throws
Exception {
+ URL url = new URL(signCertUrl);
+ try (InputStream in = url.openStream()) {
+ CertificateFactory cf =
CertificateFactory.getInstance(CERTIFICATE_TYPE);
+ return (X509Certificate) cf.generateCertificate(in);
+ }
+ }
+
+ /**
+ * Verify signature
+ *
+ * @param message sign message
+ * @param cert cert
+ * @param signature signature
+ * @return verification result
+ * @throws Exception thrown when an error occurs in the validation process
+ */
+ private boolean verifySignature(String message, X509Certificate cert,
String signature) throws Exception {
+ Signature sig = Signature.getInstance(cert.getSigAlgName());
+ sig.initVerify(cert.getPublicKey());
+ sig.update(message.getBytes(CHARSET_UTF8));
+ return sig.verify(Base64.getDecoder().decode(signature));
+ }
+
+ private void putIfNotBlank(Map<String, String> map, String key, String
value) {
+ if (StringUtils.isNotBlank(value)){
+ map.put(key, value);
+ }
+ }
+
+ private void appendField(StringBuilder builder, String fieldName, String
value) {
+ builder.append(fieldName).append("\n").append(value).append("\n");
+ }
+
+ @Override
+ public String supportSource() {
+ return "huaweicloud-ces";
+ }
+
+}
\ No newline at end of file
diff --git
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java
index 55b0104eb9..13c357369e 100644
---
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java
+++
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/util/DateUtil.java
@@ -19,6 +19,7 @@ package org.apache.hertzbeat.alert.util;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Optional;
@@ -80,4 +81,19 @@ public final class DateUtil {
return Optional.empty();
}
+ /**
+ * convert format data to timestamp
+ */
+ public static Long getZonedTimeStampFromFormat(String dateStr, String
format) {
+ try {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
+ // the parsed zoned date-time, not null
+ return ZonedDateTime.parse(dateStr,
formatter).toInstant().toEpochMilli();
+ } catch (Exception e) {
+ log.error("Error parsing date '{}' with format '{}': {}",
+ dateStr, format, e.getMessage());
+ }
+ return null;
+ }
+
}
diff --git
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java
new file mode 100644
index 0000000000..7167a5dcca
--- /dev/null
+++
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/HuaweiCloudExternAlertServiceTest.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.service;
+
+import org.apache.hertzbeat.alert.dto.HuaweiCloudExternAlert;
+import org.apache.hertzbeat.alert.reduce.AlarmCommonReduce;
+import org.apache.hertzbeat.alert.service.impl.HuaweiCloudExternAlertService;
+import org.apache.hertzbeat.common.entity.alerter.SingleAlert;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * unit test for {@link AlibabaCloudSlsExternAlertServiceTest }
+ */
+@Disabled
+@ExtendWith(MockitoExtension.class)
+public class HuaweiCloudExternAlertServiceTest {
+
+ @Mock
+ private AlarmCommonReduce alarmCommonReduce;
+
+ @InjectMocks
+ private HuaweiCloudExternAlertService externAlertService;
+
+ @BeforeEach
+ void setUp() {
+ assertEquals("huaweicloud-ces", externAlertService.supportSource());
+ }
+
+ @Test
+ void testAddExternAlertWithInvalidContent() {
+ String invalidContent = "invalid json";
+ externAlertService.addExternAlert(invalidContent);
+ verify(alarmCommonReduce,
never()).reduceAndSendAlarm(any(SingleAlert.class));
+ }
+
+ @Test
+ void testMessageValidFailed() {
+ HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
+ externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
+ externAlert.setType("failedType");
+ externAlert.setMessage("failedMessage");
+ externAlert.setTimestamp("2025-06-07T15:12:09Z");
+ externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
+ verify(alarmCommonReduce,
never()).reduceAndSendAlarm(any(SingleAlert.class));
+ }
+
+ @Test
+ void testCertFailed() {
+ HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
+
externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLb"
+ +
"vaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwX"
+ +
"w1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9lYyeL7vVI0e4sfRbuQruz3E"
+ +
"0ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
+ externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例
“dcs-h4tv” 的每秒并发操作数已恢复正常。");
+
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
+ externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
+ externAlert.setType("Notification");
+ externAlert.setMessage("{}");
+
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/failedUrl");
+ externAlert.setTimestamp("2025-06-07T15:12:09Z");
+ externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
+ verify(alarmCommonReduce,
never()).reduceAndSendAlarm(any(SingleAlert.class));
+ }
+
+ @Test
+ void testAddExternAlert() {
+ HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
+
externAlert.setSignature("Igs0bBhzw0JGmlgBH+9ejw2xWfPTXjAatAEsKDkkWcC5bZ/jveckdRZdgp/S0JER9eiJfMF427YDABufIN0sv/vBRXaRQKfRBLTJYbSTl+AQpEbIW5yUfJSRLEG3HNEhUDjASolbrW7zdPCoGkkqjifE23FCvw"
+ +
"+4tewMzqmHnfJHcFBq3W89CJzdPBjwO1UcY9C39moUZgqZk+qDVLpxb4bHSrEYAwPOSrOPR7TZpETJ30UOgFYajJydQk692edfs0NeVutHoQiOJ5/YC83ULHft0aXhichjtfZE4KF69nROAKez0ubk3l"
+ + "Ey/mBIM9Ylbxn5b84OIrzzZQrIWe8Syw==");
+ externAlert.setSubject("[华为云][紧急告警]云监控通知:分布式缓存服务-DCS Redis实例
“dcs-h4tv” 的每秒并发操作数已触发告警。");
+
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
+ externAlert.setMessageId("1565df032a19494590d61e05f7b0dc0e");
+ externAlert.setType("Notification");
+
externAlert.setMessage("{\"version\":\"v1\",\"data\":{\"AccountName\":\"hid_hk6tij5o1v-95zn\",\"Namespace\":\"分布式缓存服务\",\"DimensionName\":\"DCS
Redis实例\",\"ResourceName\""
+ +
":\"dcs-h4tv\",\"MetricName\":\"每秒并发操作数\",\"IsAlarm\":true,\"AlarmLevel\":\"紧急\",\"Region\":\"华东-上海一\",\"RegionId\":\"cn-east-3\",\"ResourceId\":\"3dc7b9ea"
+ +
"-70b4-4c38-942d-e2636e6d844c\",\"PrivateIp\":\"192.168.0.54\",\"CurrentData\":\"6.00
count\",\"AlarmTime\":\"2025/06/02 22:56:15 GMT+08:00\","
+ + "\"AlarmRecordID\":\"ah1748876175242njvndyzMZ\","
+ +
"\"AlarmRuleName\":\"alarm-c5jj\",\"IsOriginalValue\":true,\"Filter\":\"原始值\",\"ComparisonOperator\":\"\\u003e\",\"Value\":\"5
count\",\"Unit\":\"count\",\"Count\":2}}");
+
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
+ externAlert.setTimestamp("2025-06-02T14:56:17Z");
+ externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
+ verify(alarmCommonReduce,
times(1)).reduceAndSendAlarm(any(SingleAlert.class));
+ }
+
+ @Test
+ void testSubscriptionUrl() {
+ HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
+
externAlert.setSubscribeUrl("https://console.huaweicloud.com/smn/subscription/confirm?token=477a784601d744e4ab9ab83986502d31c4b938"
+ +
"0ec0b64392b134e517c3aa17eb7b3a12dc9f3b4ab495e61c4dee654b435d7223ea934345bf8ae8901cef912b1d&topic_urn=urn:smn:cn-north-4"
+ +
":477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX®ion=cn-north-4");
+
externAlert.setSignature("ottf37C/2RdDgqimRQMIBU6i7XjUfPPMU760jJn71wwP3825YPoIT22uw2A9399rkm9Jrt1qUEFrDLuA5yHFLd5n/XoM4FghIgyFn7VIfgpuVM31a+co78s"
+ +
"YBiZ1egOCE/AwFm2oygRhfIceUj9Kw9vmc06el9TXY6RtE5tAEF6qEmICtTh45KwtCO/WRs3DY72dQi5hm0w7/tktS4WFZ1iP4LHt5eCwFvnH0u29Y96cJNI0fLUQxI5MkhgjK"
+ +
"77JkFK7UT6ZYJZhzgSp/B7OQGStOQx+3Duvx4T4CzccZQM3sca81Z0B0GFGWeVXuEHyCPLsayY/Iz+5Tco51elT8w==");
+
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
+ externAlert.setMessageId("242fac183d3a4936b5ead6c725a32ed0");
+ externAlert.setType("SubscriptionConfirmation");
+ externAlert.setMessage("You are invited to subscribe to topic:
urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:"
+ + "CES_notification_group_bngJ2aMpX. To confirm this
subscription, please visit the subscribe_url included in this message. The
subscribe_url is valid only within 48 hours.");
+
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
+ externAlert.setTimestamp("2025-06-07T15:07:14Z");
+ externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
+ verify(alarmCommonReduce,
never()).reduceAndSendAlarm(any(SingleAlert.class));
+ }
+
+ @Test
+ void testUnsubscribe() {
+ HuaweiCloudExternAlert externAlert = new HuaweiCloudExternAlert();
+
externAlert.setSignature("TImrLoeb0tV1JZJSPyA0rpC9mNqH3MmhwQ4tgpuHHa+JztfGVZFvkU//OthKKhzpDAoYiXOYG9DbzXCLbvaGePIRITakoynYyYr9zZIpdx9jXhQNlgF8np"
+ +
"1+t0JxNeoIq0DYWgH52tsodwqOm+OnmkcHwCRo/1rFv85KrKAaX2gy3sNwXw1hKnAwAw0mJlxHHSf/N3+7j6GoxCNV7fN9K4CpJiLMGNvUa7zVmG0U9mPvt/7Lac155kPPQ9l"
+ +
"YyeL7vVI0e4sfRbuQruz3E0+ZP40TKx0afoeR0/Bx/IoZzRP1La7pKlbEISvkcM7TqW/IOGQTkhVsQ32RFRxZWO2snw==");
+ externAlert.setSubject("[华为云][紧急告警恢复]云监控通知:分布式缓存服务-DCS Redis实例
“dcs-h4tv” 的每秒并发操作数已恢复正常。");
+
externAlert.setTopicUrn("urn:smn:cn-north-4:477a784601d744e4ab9ab83986502d31:CES_notification_group_bngJ2aMpX");
+ externAlert.setMessageId("d3672d737bb742cf8c2aa3f0fd72d4d1");
+ externAlert.setType("UnsubscribeConfirmation");
+ externAlert.setMessage("{}");
+
externAlert.setSigningCertUrl("https://smn.cn-north-4.myhuaweicloud.com/smn/SMN_cn-north-4_b98100ca131b4116ab8ee7ccedbaae99.pem");
+ externAlert.setTimestamp("2025-06-07T15:12:09Z");
+ externAlertService.addExternAlert(JsonUtil.toJson(externAlert));
+ verify(alarmCommonReduce,
never()).reduceAndSendAlarm(any(SingleAlert.class));
+ }
+
+
+
+
+}
\ No newline at end of file
diff --git
a/web-app/src/app/routes/alert/alert-integration/alert-integration.component.ts
b/web-app/src/app/routes/alert/alert-integration/alert-integration.component.ts
index efcec7b9b1..baf5bf657f 100644
---
a/web-app/src/app/routes/alert/alert-integration/alert-integration.component.ts
+++
b/web-app/src/app/routes/alert/alert-integration/alert-integration.component.ts
@@ -87,6 +87,11 @@ export class AlertIntegrationComponent implements OnInit {
id: 'alibabacloud-sls',
name: this.i18nSvc.fanyi('alert.integration.source.alibabacloud-sls'),
icon: 'assets/img/integration/alibabacloud.svg'
+ },
+ {
+ id: 'huaweicloud-ces',
+ name: this.i18nSvc.fanyi('alert.integration.source.huaweicloud-ces'),
+ icon: 'assets/img/integration/huaweicloud.svg'
}
];
diff --git a/web-app/src/assets/doc/alert-integration/huaweicloud-ces.en-US.md
b/web-app/src/assets/doc/alert-integration/huaweicloud-ces.en-US.md
new file mode 100644
index 0000000000..b847dd670d
--- /dev/null
+++ b/web-app/src/assets/doc/alert-integration/huaweicloud-ces.en-US.md
@@ -0,0 +1,77 @@
+>Send Huawei Cloud `Cloud Eye` alerts to the HertzBeat alert platform via
Webhook.
+
+### Step 1: Configure Notification Template in `Cloud Eye` Console
+1. Log in to Huawei Cloud `Cloud Eye` Console
+2. Select **Alarm Management** > **Alarm Notifications** > **Notification
Templates** > **Create**
+3. Protocol: Set to HTTP/HTTPS, Notification Type: Choose as needed, Format:
JSON
+4. Ensure the following JSON preview is correct:
+```json
+{
+ "version": "v1",
+ "data": {
+ "AccountName": "RDS_test",
+ "Namespace": "Elastic Cloud Server",
+ "DimensionName": "Cloud Server",
+ "ResourceName": "ecs-test",
+ "MetricName": "CPU Usage",
+ "IsAlarm": true,
+ "AlarmLevel": "Important",
+ "Region": "North China-Ulanqab-203",
+ "RegionId": "cn-north-4",
+ "ResourceId": "xxxx-xxxx",
+ "PrivateIp": "127.0.0.0",
+ "PublicIp": "100.0.0.0",
+ "CurrentData": "1.06%",
+ "AlarmTime": "2024/08/0514:45:16GMT+08:00",
+ "AlarmRecordID": "ah1722xxxxxx",
+ "AlarmRuleName": "test-xxx",
+ "IsOriginalValue": true,
+ "Filter": "Original Value",
+ "ComparisonOperator": "u003e=",
+ "Value": "0%",
+ "Unit": "%",
+ "Count": 1,
+ "EpName": "default"
+ }
+}
+```
+
+### Step 2: Configure Notification Recipients in `Cloud Eye` Console
+1. Log in to Huawei Cloud `Cloud Eye` Console
+2. Select **Alarm Management** > **Alarm Notifications** > **Recipients** >
**Create**
+3. Select Channel: HTTP or HTTPS
+4. Add HertzBeat as the alert receiver configuration
+ - Request URL:
`http://{hertzbeat_host}:1157/api/alerts/report/huaweicloud-ces`
+
+### Step 3: Configure Notification Groups in `Cloud Eye` Console
+1. Log in to Huawei Cloud `Cloud Eye` Console
+2. Select **Alarm Management** > **Alarm Notifications** > **Notification
Groups** > **Create**
+3. Select Recipients: The **Recipients** set in Step 2
+ - You can also add the **NoRecipients** to existing notification groups
+
+### Step 4: Configure Notification Policies in `Cloud Eye` Console
+1. Log in to Huawei Cloud `Cloud Eye` Console
+2. Select **Alarm Management** > **Alarm Notifications** > **Notification
Policies** > **Create**
+3. Select **Notification Scope** > **Recipients** > **Notification Group** >
Choose the **Notification Group** set in Step 3
+4. Select **Notification Templates** > **Templates for Metric Monitoring** and
**Templates for Event Monitoring** -> Choose the **Notification Recipients**
set in Step 1
+5. Configure other settings as needed
+
+### Common Issues
+
+#### Alerts Not Triggering
+- Ensure the Webhook URL is accessible by Huawei Cloud Monitor Service (CES)
notifications
+- Verify the correctness of **Notification Policies**, **Notification
Groups**, **Recipients**, and **Notification Templates** settings
+- Verify the correctness/enablement of **Alarm Management** > **Alarm Rules**,
check **Alarm Records** for triggered alarms
+- Note: After adding a created **Recipients** to a **Notification Groups**,
the **Simple Message Notification (SMN)** will send a subscription confirmation
message to the subscription endpoint, which must be confirmed before receiving
alarm notifications.
+ - After creating a notification group, a topic will be automatically created
in **Simple Message Notification (SMN)** > **Topic Management** > **Topics**,
and a subscription will be created in **Simple Message Notification (SMN)** >
**Topic Management** > **Subscriptions**. HertzBeat has added automatic
subscription functionality. If the status is not (Confirmed), please manually
request the subscription
+- Note: If multiple **Recipientss** are created with different names but the
same notification channel object, you will only receive one subscription
confirmation message.
+
+#### Other Notes
+- HertzBeat has added automatic subscription functionality after being added
to a **Notification Groups**.
+- For security purposes, HertzBeat supports **Message Signature Verification**
to verify the legitimacy of messages through signature strings.
+
+#### For More Information, Please Refer to
+- [Alarm
Management](https://support.huaweicloud.com/intl/en-us/usermanual-ces/ces_01_0067.html)
+- [Message Signature
Verification](https://support.huaweicloud.com/intl/en-us/usermanual-smn/smn_ug_a9003.html)
+- [Requesting Subscription
Confirmation](https://support.huaweicloud.com/intl/en-us/usermanual-smn/smn_ug_0009.html)
+- [HTTP or HTTPS Message
Format](https://support.huaweicloud.com/intl/en-us/usermanual-smn/smn_ug_a9002.html)
diff --git a/web-app/src/assets/doc/alert-integration/huaweicloud-ces.zh-CN.md
b/web-app/src/assets/doc/alert-integration/huaweicloud-ces.zh-CN.md
new file mode 100644
index 0000000000..419ca39651
--- /dev/null
+++ b/web-app/src/assets/doc/alert-integration/huaweicloud-ces.zh-CN.md
@@ -0,0 +1,80 @@
+>将华为云监控服务(CES)的告警通过 Webhook 方式发送到 HertzBeat 告警平台。
+
+### 步骤一: 在云监控服务控制台设置通知模板
+1. 登录华为云监控服务控制台
+2. 选择 **告警** > **告警通知** > **通知内容模板** > **创建通知内容模板**
+3. 渠道类型:设置为 HTTP/HTTPS 、通知类型:按需选择、数据格式: JSON
+4. 确保如下 JSON 正确预览
+```
+{
+ "version": "v1",
+ "data": {
+ "AccountName": "RDS_test",
+ "Namespace": "弹性云服务器",
+ "DimensionName": "云服务器",
+ "ResourceName": "ecs-test",
+ "MetricName": "CPU使用率",
+ "IsAlarm": true,
+ "AlarmLevel": "重要",
+ "Region": "华北-乌兰察布-二零三",
+ "RegionId": "cn-north-4",
+ "ResourceId": "xxxx-xxxx",
+ "PrivateIp": "127.0.0.0",
+ "PublicIp": "100.0.0.0",
+ "CurrentData": "1.06%",
+ "AlarmTime": "2024/08/0514:45:16GMT+08:00",
+ "AlarmRecordID": "ah1722xxxxxx",
+ "AlarmRuleName": "test-xxx",
+ "IsOriginalValue": true,
+ "Filter": "原始值",
+ "ComparisonOperator": "u003e=",
+ "Value": "0%",
+ "Unit": "%",
+ "Count": 1,
+ "EpName": "default"
+ }
+}
+```
+
+### 步骤二: 在云监控服务控制台设置通知对象
+1. 登录华为云监控服务控制台
+2. 选择 **告警** > **告警通知** > **通知对象** > **创建通知对象**
+3. 选择渠道:HTTP 或者 HTTPS
+4. 添加 HertzBeat 作为告警接收端配置
+- 请求地址: http://{hertzbeat_host}:1157/api/alerts/report/huaweicloud-ces
+
+### 步骤三: 在云监控服务控制台设置通知组
+1. 登录华为云监控服务控制台
+2. 选择 **告警** > **告警通知** > **通知组** > **创建通知组**
+3. 选择通知对象:步骤二设置的 **通知对象**
+- 您也可以在已有的通知组添加 **通知对象**
+
+### 步骤四: 在云监控服务控制台设置通知策略
+1. 登录华为云监控服务控制台
+2. 选择 **告警** > **告警通知** > **通知策略** > **创建通知策略**
+3. 选择 **通知范围** > **接收对象** > **通知组** > 选择步骤三设置的 **通知组**
+4. 选择 **通知内容模板** > **指标模板** 跟 **事件模板** -> 选择步骤一设置的 **通知模板**
+5. 其他的请按需选择配置
+
+### 常见问题
+
+#### 告警未触发
+- 确保 Webhook URL 可以被 华为云监控服务(CES) 通知访问
+- 确保 **通知策略**、**通知组**、**通知对象**、**通知内容模板** 设置的正确性
+- 确保 **告警** > **告警规则** 设置的正确性/是否已启用,可查阅 **告警记录** 是否有触发告警
+- 注意:已创建的 **通知对象** 加入到 **通知组** 后,**消息通知服务(SMN)** 会向订阅终端发送订阅确认信息,需确认后方可收到告警通知。
+ - 创建完通知组以后,会在 **消息通知服务(SMN)** > **主题管理** > **主题** 中同步创建主题,并在 **消息通知服务(SMN)**
> **主题管理** > **订阅** 中创建订阅信息。HertzBeat 添加了自动订阅的功能,如果状态不是(已确认),请手动请求订阅
+- 注意:若多个 **通知对象** 创建名称不一致,但通知渠道的对象一致,则只会收到一次订阅确认信息。
+
+#### 其他
+- HertzBeat 添加了加入到 **通知组** 后自动订阅的功能。
+- 为了确保安全,HertzBeat 支持了 **消息签名认证**,通过签名串验证消息的合法性。
+
+#### 更多信息请参考
+- [告警](https://support.huaweicloud.com/usermanual-ces/ces_01_0067.html)
+- [校验消息签名](https://support.huaweicloud.com/usermanual-smn/smn_ug_a9003.html)
+- [请求订阅](https://support.huaweicloud.com/usermanual-smn/smn_ug_0046.html)
+-
[HTTP(S)消息格式](https://support.huaweicloud.com/usermanual-smn/smn_ug_a9002.html)
+
+
+
diff --git a/web-app/src/assets/doc/alert-integration/huaweicloud-ces.zh-TW.md
b/web-app/src/assets/doc/alert-integration/huaweicloud-ces.zh-TW.md
new file mode 100644
index 0000000000..882d24a5ba
--- /dev/null
+++ b/web-app/src/assets/doc/alert-integration/huaweicloud-ces.zh-TW.md
@@ -0,0 +1,77 @@
+>將華為雲監控服務(CES)的告警通過 Webhook 方式發送到 HertzBeat 告警平臺。
+
+### 步驟一: 在雲監控服務控制檯設置通知模板
+1. 登錄華為雲監控服務控制檯
+2. 選擇 **告警** > **告警通知** > **通知內容模板** > **創建通知內容模板**
+3. 渠道類型:設置為 HTTP/HTTPS 、通知類型:按需選擇、數據格式: JSON
+4. 確保如下 JSON 正確預覽
+```
+{
+ "version": "v1",
+ "data": {
+ "AccountName": "RDS_test",
+ "Namespace": "彈性雲服務器",
+ "DimensionName": "雲服務器",
+ "ResourceName": "ecs-test",
+ "MetricName": "CPU使用率",
+ "IsAlarm": true,
+ "AlarmLevel": "重要",
+ "Region": "華北-烏蘭察布-二零三",
+ "RegionId": "cn-north-4",
+ "ResourceId": "xxxx-xxxx",
+ "PrivateIp": "127.0.0.0",
+ "PublicIp": "100.0.0.0",
+ "CurrentData": "1.06%",
+ "AlarmTime": "2024/08/0514:45:16GMT+08:00",
+ "AlarmRecordID": "ah1722xxxxxx",
+ "AlarmRuleName": "test-xxx",
+ "IsOriginalValue": true,
+ "Filter": "原始值",
+ "ComparisonOperator": "u003e=",
+ "Value": "0%",
+ "Unit": "%",
+ "Count": 1,
+ "EpName": "default"
+ }
+}
+```
+
+### 步驟二: 在雲監控服務控制檯設置通知對象
+1. 登錄華為雲監控服務控制檯
+2. 選擇 **告警** > **告警通知** > **通知對象** > **創建通知對象**
+3. 選擇渠道:HTTP 或者 HTTPS
+4. 添加 HertzBeat 作為告警接收端配置
+- 請求地址: http://{hertzbeat_host}:1157/api/alerts/report/huaweicloud-ces
+
+### 步驟三: 在雲監控服務控制檯設置通知組
+1. 登錄華為雲監控服務控制檯
+2. 選擇 **告警** > **告警通知** > **通知組** > **創建通知組**
+3. 選擇通知對象:步驟二設置的 **通知對象**
+- 您也可以在已有的通知組添加 **通知對象**
+
+### 步驟四: 在雲監控服務控制檯設置通知策略
+1. 登錄華為雲監控服務控制檯
+2. 選擇 **告警** > **告警通知** > **通知策略** > **創建通知策略**
+3. 選擇 **通知範圍** > **接收對象** > **通知組** > 選擇步驟三設置的 **通知組**
+4. 選擇 **通知內容模板** > **指標模板** 跟 **事件模板** -> 選擇步驟一設置的 **通知模板**
+5. 其他的請按需選擇配置
+
+### 常見問題
+
+#### 告警未觸發
+- 確保 Webhook URL 可以被 華為雲監控服務(CES) 通知訪問
+- 確保 **通知策略**、**通知組**、**通知對象**、**通知內容模板** 設置的正確性
+- 確保 **告警** > **告警規則** 設置的正確性/是否已啟用,可查閱 **告警記錄** 是否有觸發告警
+- 注意:已創建的 **通知對象** 加入到 **通知組** 後,**消息通知服務(SMN)** 會向訂閱終端發送訂閱確認信息,需確認後方可收到告警通知。
+ - 創建完通知組以後,會在 **消息通知服務(SMN)** > **主題管理** > **主題** 中同步創建主題,並在 **消息通知服務(SMN)**
> **主題管理** > **訂閱** 中創建訂閱信息。HertzBeat 添加了自動訂閱的功能,如果狀態不是(已確認),請手動請求訂閱
+- 注意:若多個 **通知對象** 創建名稱不一致,但通知渠道的對象一致,則只會收到一次訂閱確認信息。
+
+#### 其他
+- HertzBeat 添加了加入到 **通知組** 後自動訂閱的功能。
+- 為了確保安全,HertzBeat 支持了 **消息簽名認證**,通過簽名串驗證消息的合法性。
+
+#### 更多信息請參考
+- [告警](https://support.huaweicloud.com/usermanual-ces/ces_01_0067.html)
+- [校驗消息簽名](https://support.huaweicloud.com/usermanual-smn/smn_ug_a9003.html)
+- [請求訂閱](https://support.huaweicloud.com/usermanual-smn/smn_ug_0046.html)
+-
[HTTP(S)消息格式](https://support.huaweicloud.com/usermanual-smn/smn_ug_a9002.html)
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index ce74374127..a8d0f44931 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -101,6 +101,7 @@
"alert.integration.source.uptime-kuma": "Uptime Kuma",
"alert.integration.source.zabbix": "Zabbix",
"alert.integration.source.alibabacloud-sls": "AlibabaCloud-SLS",
+ "alert.integration.source.huaweicloud-ces": "Huawei Cloud Eye",
"alert.integration.token.desc": "Token you generated that can be used to
access the HertzBeat API.",
"alert.integration.token.new": "Click to Generate Token",
"alert.integration.token.notice": "Token only be displayed once. Please keep
your token secure. Do not share it with others.",
diff --git a/web-app/src/assets/i18n/ja-JP.json
b/web-app/src/assets/i18n/ja-JP.json
index 6781ebbf39..6a0b1c28c3 100644
--- a/web-app/src/assets/i18n/ja-JP.json
+++ b/web-app/src/assets/i18n/ja-JP.json
@@ -101,6 +101,7 @@
"alert.integration.source.uptime-kuma": "Uptime Kuma",
"alert.integration.source.zabbix": "Zabbix",
"alert.integration.source.alibabacloud-sls": "AlibabaCloud-SLS",
+ "alert.integration.source.huaweicloud-ces": "Huawei Cloud Eye",
"alert.integration.token.desc": "HertzBeat APIにアクセスするために生成したトークン。",
"alert.integration.token.new": "トークンを生成するにはクリック",
"alert.integration.token.notice":
"トークンは一度だけ表示されます。トークンを安全に保管し、他人と共有しないでください。",
diff --git a/web-app/src/assets/i18n/pt-BR.json
b/web-app/src/assets/i18n/pt-BR.json
index bd25d96d6b..0d0455959a 100644
--- a/web-app/src/assets/i18n/pt-BR.json
+++ b/web-app/src/assets/i18n/pt-BR.json
@@ -246,6 +246,7 @@
"alert.integration.source.tencent": "Monitoramento de nuvem Tencent",
"alert.integration.source.webhook": "PadrãoWebhook",
"alert.integration.source.alibabacloud-sls": "AlibabaCloud-SLS",
+ "alert.integration.source.huaweicloud-ces": "Huawei Cloud Eye",
"alert.integration.token.desc": "O token gerado pode ser usado para
acessar a API HertzBeat",
"alert.integration.token.new": "Clique para gerar token",
"alert.integration.token.notice": "Este conteúdo será exibido apenas uma
vez, por favor guarde seu token adequadamente e não o divulgue a terceiros.",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index 4fa296ae88..aa60387170 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -101,6 +101,7 @@
"alert.integration.source.uptime-kuma": "Uptime Kuma",
"alert.integration.source.zabbix": "Zabbix",
"alert.integration.source.alibabacloud-sls": "阿里云日志服务 SLS",
+ "alert.integration.source.huaweicloud-ces": "华为云监控服务",
"alert.integration.token.desc": "生成的 Token 可用于访问 HertzBeat API",
"alert.integration.token.new": "点击生成 Token",
"alert.integration.token.notice": "此内容只会展示一次,请妥善保管您的 Token,不要泄露给他人",
diff --git a/web-app/src/assets/i18n/zh-TW.json
b/web-app/src/assets/i18n/zh-TW.json
index fc8b7e116c..3c75766e11 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -101,6 +101,7 @@
"alert.integration.source.uptime-kuma": "Uptime Kuma",
"alert.integration.source.zabbix": "Zabbix",
"alert.integration.source.alibabacloud-sls": "阿里雲端日誌服務 SLS",
+ "alert.integration.source.huaweicloud-ces": "華為雲監控服務",
"alert.integration.token.desc": "生成的 Token 可用于访问 HertzBeat API",
"alert.integration.token.new": "点击生成 Token",
"alert.integration.token.notice": "此内容只会展示一次,请妥善保管您的 Token,不要泄露给他人",
diff --git a/web-app/src/assets/img/integration/huaweicloud.svg
b/web-app/src/assets/img/integration/huaweicloud.svg
new file mode 100644
index 0000000000..0807b90573
--- /dev/null
+++ b/web-app/src/assets/img/integration/huaweicloud.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 396.53
299.12"><title>Huawei logo</title><path
d="M539.79,595.81a1.46,1.46,0,0,0,1.61.11c17.44-8.72,90.1-45.64,120.13-75,0,0,19.05-15.29,20-39.94,2.15-34.12-32.19-65.77-32.19-65.77s-63.51,77-110.12,178.69a2,2,0,0,0,.54,1.94m167-79.44S603.19,572,549.59,608.19a1.59,1.59,0,0,0-.65,1.72s.54,1,1.29,1c19.16,0,91.71.11,93.65-.22a70.13,70.13,0,0,0,21-4.84s25.83-8.18,39.18-37.57c0,0,11.95-23.9,2.69-51.88M552.28,620a2.07,2.07,0,0,0-1.4,1,1.72,1.
[...]
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]