This is an automated email from the ASF dual-hosted git repository.

Duansg 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 76d28add2e maintenance: bound SMS client failure details (#4282)
76d28add2e is described below

commit 76d28add2ee49f5162693fbc31f34bc9297a4284
Author: Logic <[email protected]>
AuthorDate: Wed Aug 5 07:11:46 2026 +0800

    maintenance: bound SMS client failure details (#4282)
---
 .../alert/service/impl/AlibabaSmsClientImpl.java   |  30 ++-
 .../alert/service/impl/AwsSmsClientImpl.java       |  27 +-
 .../alert/service/impl/SmsFailureMessages.java     |  55 ++++
 .../alert/service/impl/SmsLocalSmsClientImpl.java  |  34 ++-
 .../alert/service/impl/TencentSmsClientImpl.java   |  47 ++--
 .../alert/service/impl/TwilioSmsClientImpl.java    |  37 ++-
 .../alert/service/impl/UniSmsClientImpl.java       |  25 +-
 .../alert/service/impl/SmsClientLoggingTest.java   | 300 +++++++++++++++++++++
 8 files changed, 468 insertions(+), 87 deletions(-)

diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaSmsClientImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaSmsClientImpl.java
index 34b6ebe1d0..1876d53940 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaSmsClientImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlibabaSmsClientImpl.java
@@ -26,18 +26,14 @@ import 
org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
 import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
 import org.apache.hertzbeat.common.support.exception.SendMessageException;
 import org.apache.hertzbeat.common.util.JsonUtil;
-import org.apache.hertzbeat.common.util.LogUtil;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
-import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
@@ -65,7 +61,6 @@ public class AlibabaSmsClientImpl implements SmsClient {
     private final String accessKeySecret;
     private final String signName;
     private final String templateCode;
-    private static final Logger logger = 
LoggerFactory.getLogger(AlibabaSmsClientImpl.class);
 
     public AlibabaSmsClientImpl(AlibabaSmsProperties config) {
         if (config != null) {
@@ -154,31 +149,37 @@ public class AlibabaSmsClientImpl implements SmsClient {
             httpPost.setHeader("x-acs-content-sha256",
                     CryptoUtils.sha256Hex(""));
 
-            log.info("Sending Alibaba SMS request to {}", url + ", params: " + 
templateParam + "headers: " + Arrays.toString(httpPost.getAllHeaders()));
+            log.debug("Sending SMS request via Alibaba Cloud");
 
             // Send request and handle response
             try (CloseableHttpResponse response = 
httpClient.execute(httpPost)) {
                 int statusCode = response.getStatusLine().getStatusCode();
                 String responseBody = 
EntityUtils.toString(response.getEntity());
 
-                log.info("SMS response status: {}, body: {}", statusCode, 
responseBody);
+                log.debug("Alibaba Cloud SMS response status: {}", statusCode);
 
                 if (statusCode != 200) {
-                    throw new SendMessageException("HTTP request failed with 
status code: " + statusCode + ", response: " + responseBody);
+                    throw SmsFailureMessages.httpStatus("Alibaba Cloud SMS", 
statusCode);
                 }
 
                 JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
+                if (jsonResponse == null || jsonResponse.get("Code") == null) {
+                    throw SmsFailureMessages.invalidResponse("Alibaba Cloud 
SMS");
+                }
                 String code = jsonResponse.get("Code").asText();
                 if (!"OK".equals(code)) {
-                    String message = jsonResponse.get("Message").asText();
-                    throw new SendMessageException(code + ":" + message);
+                    throw SmsFailureMessages.providerCode("Alibaba Cloud SMS", 
code);
                 }
 
-                log.info("Successfully sent SMS to phone: {}", phoneNumber);
+                log.info("Successfully sent SMS via Alibaba Cloud");
             }
+        } catch (SendMessageException e) {
+            log.warn("Failed to send SMS via Alibaba Cloud");
+            throw e;
         } catch (Exception e) {
-            LogUtil.warn(logger, "Failed to send SMS: {0}", e.getMessage());
-            throw new SendMessageException(e.getMessage());
+            log.warn("Failed to send SMS via Alibaba Cloud, failure type: {}",
+                    e.getClass().getSimpleName());
+            throw SmsFailureMessages.requestFailed("Alibaba Cloud SMS");
         }
     }
 
@@ -196,7 +197,8 @@ public class AlibabaSmsClientImpl implements SmsClient {
             // Step 4: Build authorization header
             return ALGORITHM + " Credential=" + accessKeyId + 
",SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;" + 
"x-acs-signature-nonce;x-acs-version,Signature=" + signature;
         } catch (Exception e) {
-            LogUtil.warn(logger, "Failed to calculate authorization {0}", 
e.getMessage());
+            log.warn("Failed to calculate Alibaba Cloud authorization, failure 
type: {}",
+                    e.getClass().getSimpleName());
             throw new RuntimeException("Failed to calculate authorization", e);
         }
     }
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AwsSmsClientImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AwsSmsClientImpl.java
index 6e9dd601c6..e019d8ff8c 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AwsSmsClientImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AwsSmsClientImpl.java
@@ -40,7 +40,6 @@ import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -116,11 +115,14 @@ public class AwsSmsClientImpl implements SmsClient {
             URI requestUri = new URI(endpoint);
 
             HttpPost httpPost = createHttpPost(requestUri, amzDate, 
payloadInString);
-            log.info("Sending AWS SMS request to {}", requestUri + "," + 
"headers: " + Arrays.toString(httpPost.getAllHeaders()));
-            executeRequest(httpClient, httpPost, phoneNumber);
+            log.debug("Sending SMS request via AWS");
+            executeRequest(httpClient, httpPost);
+        } catch (SendMessageException e) {
+            log.warn("Failed to send SMS via AWS");
+            throw e;
         } catch (Exception e) {
-            log.warn("Failed to send SMS: {}", e.getMessage());
-            throw new SendMessageException(e.getMessage());
+            log.warn("Failed to send SMS via AWS, failure type: {}", 
e.getClass().getSimpleName());
+            throw SmsFailureMessages.requestFailed("AWS SMS");
         }
     }
 
@@ -149,28 +151,27 @@ public class AwsSmsClientImpl implements SmsClient {
         return httpPost;
     }
 
-    private void executeRequest(CloseableHttpClient httpClient, HttpPost 
httpPost, String phoneNumber) throws Exception {
+    private void executeRequest(CloseableHttpClient httpClient, HttpPost 
httpPost) 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);
+            log.debug("AWS SMS response status: {}", statusCode);
 
             if (statusCode != 200) {
-                throw new SendMessageException("HTTP request failed with 
status code: " + statusCode + ", response: " + responseBody);
+                throw SmsFailureMessages.httpStatus("AWS SMS", statusCode);
             }
 
             JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
             if (jsonResponse == null) {
-                throw new SendMessageException(statusCode + ":" + 
responseBody);
+                throw SmsFailureMessages.invalidResponse("AWS SMS");
             }
 
             JsonNode responseNode = jsonResponse.get("MessageId");
             if (responseNode == null) {
-                throw new SendMessageException(statusCode + ":" + 
responseBody);
+                throw SmsFailureMessages.invalidResponse("AWS SMS");
             }
 
-            String messageId = responseNode.asText();
-            log.info("Successfully sent SMS to phone: {}, messageId: {}", 
phoneNumber, messageId);
+            log.info("Successfully sent SMS via AWS");
         }
     }
 
@@ -285,5 +286,3 @@ public class AwsSmsClientImpl implements SmsClient {
 
     }
 }
-
-
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsFailureMessages.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsFailureMessages.java
new file mode 100644
index 0000000000..3d09dddb83
--- /dev/null
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsFailureMessages.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.alert.service.impl;
+
+import java.util.regex.Pattern;
+import org.apache.hertzbeat.common.support.exception.SendMessageException;
+
+/**
+ * Builds bounded SMS failures without copying provider-controlled response
+ * bodies, request URLs, or transport exception messages.
+ */
+final class SmsFailureMessages {
+
+    private static final Pattern SAFE_PROVIDER_CODE = 
Pattern.compile("[-A-Za-z0-9_.]{1,64}");
+    private static final String UNKNOWN_PROVIDER_CODE = 
"UNKNOWN_PROVIDER_ERROR";
+
+    private SmsFailureMessages() {
+    }
+
+    static SendMessageException requestFailed(String providerLabel) {
+        return new SendMessageException(providerLabel + " request failed");
+    }
+
+    static SendMessageException httpStatus(String providerLabel, int 
statusCode) {
+        return new SendMessageException(
+                providerLabel + " request failed with HTTP status " + 
statusCode);
+    }
+
+    static SendMessageException providerCode(String providerLabel, String 
code) {
+        String safeCode = code != null && 
SAFE_PROVIDER_CODE.matcher(code).matches()
+                ? code
+                : UNKNOWN_PROVIDER_CODE;
+        return new SendMessageException(
+                providerLabel + " request failed (code: " + safeCode + ")");
+    }
+
+    static SendMessageException invalidResponse(String providerLabel) {
+        return new SendMessageException(providerLabel + " provider returned an 
invalid response");
+    }
+}
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java
index bcb16f6abb..030b65dbc7 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/SmsLocalSmsClientImpl.java
@@ -61,7 +61,7 @@ public class SmsLocalSmsClientImpl implements SmsClient {
     @Override
     public void sendMessage(NoticeReceiver receiver, NoticeTemplate 
noticeTemplate, GroupAlert alert) {
         if (Objects.isNull(receiver) || Objects.isNull(alert)) {
-            log.warn("receiver and alert can not be null! receiver: {}, 
alert:{}", receiver, alert);
+            log.warn("SMSLocal receiver and alert cannot be null");
             return;
         }
 
@@ -79,36 +79,42 @@ public class SmsLocalSmsClientImpl implements SmsClient {
             httpPost.setHeader("Token", config.getApiKey());
             httpPost.setEntity(new StringEntity(payload, 
StandardCharsets.UTF_8));
 
-            log.debug("Sending SMS request to {}, payload: {}", 
httpPost.getURI(), payload);
+            log.debug("Sending SMS request via SMSLocal");
 
             // send http request and handle response
             try (CloseableHttpResponse response = 
httpClient.execute(httpPost)) {
                 int statusCode = response.getStatusLine().getStatusCode();
                 String responseBody = 
EntityUtils.toString(response.getEntity());
 
-                log.debug("SMS response status: {}, body: {}", statusCode, 
responseBody);
+                log.debug("SMSLocal response status: {}", statusCode);
 
                 if (statusCode != 200) {
-                    throw new SendMessageException("HTTP request failed with 
status code: " + statusCode);
+                    throw SmsFailureMessages.httpStatus("SMSLocal", 
statusCode);
                 }
 
                 JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
+                if (jsonResponse == null || !jsonResponse.isArray() || 
jsonResponse.isEmpty()) {
+                    throw SmsFailureMessages.invalidResponse("SMSLocal");
+                }
                 JsonNode jsonNode = jsonResponse.get(0);
-                if (Objects.isNull(jsonNode)) {
-                    log.warn("jsonResponse parse errorCode failed: {}", 
jsonResponse);
-                    return;
+                JsonNode errorCodeNode = jsonNode.get("errorCode");
+                if (errorCodeNode == null) {
+                    throw SmsFailureMessages.invalidResponse("SMSLocal");
                 }
-                String errorCode = jsonNode.get("errorCode").asText();
+                String errorCode = errorCodeNode.asText();
                 if (!SUCCESS_CODE.equals(errorCode)) {
-                    String msgid = jsonNode.get("id").asText();
-                    throw new SendMessageException(errorCode + ":" + msgid);
+                    throw SmsFailureMessages.providerCode("SMSLocal", 
errorCode);
                 }
 
-                log.info("Successfully sent SMS to phone: {}", 
receiver.getPhone());
+                log.info("Successfully sent SMS via SMSLocal");
             }
+        } catch (SendMessageException e) {
+            log.warn("Failed to send SMS via SMSLocal");
+            throw e;
         } catch (Exception e) {
-            log.error("Failed to send SMS: {}", e.getMessage());
-            throw new SendMessageException(e.getMessage());
+            log.warn("Failed to send SMS via SMSLocal, failure type: {}",
+                    e.getClass().getSimpleName());
+            throw SmsFailureMessages.requestFailed("SMSLocal");
         }
 
     }
@@ -121,7 +127,7 @@ public class SmsLocalSmsClientImpl implements SmsClient {
     @Override
     public boolean checkConfig() {
         if (Objects.isNull(config) || Objects.isNull(config.getApiKey()) || 
config.getApiKey().isBlank()) {
-            log.warn("smslocal properties can not be null: {}", config);
+            log.warn("SMSLocal properties cannot be null or blank");
             return false;
         }
         return true;
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentSmsClientImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentSmsClientImpl.java
index 90ceefe5f8..270b6dddf8 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentSmsClientImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TencentSmsClientImpl.java
@@ -134,41 +134,56 @@ public class TencentSmsClientImpl implements SmsClient {
             httpPost.setHeader("Authorization", authorization);
             httpPost.setEntity(new StringEntity(payload, 
StandardCharsets.UTF_8));
 
-            log.debug("Sending SMS request to {}, payload: {}", 
httpPost.getURI(), payload);
+            log.debug("Sending SMS request via Tencent Cloud");
 
             // send http request and handle response
             try (CloseableHttpResponse response = 
httpClient.execute(httpPost)) {
                 int statusCode = response.getStatusLine().getStatusCode();
                 String responseBody = 
EntityUtils.toString(response.getEntity());
 
-                log.debug("SMS response status: {}, body: {}", statusCode, 
responseBody);
+                log.debug("Tencent Cloud SMS response status: {}", statusCode);
 
                 if (statusCode != 200) {
-                    throw new SendMessageException("HTTP request failed with 
status code: " + statusCode);
+                    throw SmsFailureMessages.httpStatus("Tencent Cloud SMS", 
statusCode);
                 }
 
                 JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
+                if (jsonResponse == null) {
+                    throw SmsFailureMessages.invalidResponse("Tencent Cloud 
SMS");
+                }
                 JsonNode responseNode = jsonResponse.get("Response");
+                if (responseNode == null) {
+                    throw SmsFailureMessages.invalidResponse("Tencent Cloud 
SMS");
+                }
                 JsonNode error = responseNode.get("Error");
                 if (error != null) {
-                    String code = error.get("Code").asText();
-                    String message = error.get("Message").asText();
-                    throw new SendMessageException(code + ":" + message);
+                    JsonNode codeNode = error.get("Code");
+                    if (codeNode == null) {
+                        throw SmsFailureMessages.invalidResponse("Tencent 
Cloud SMS");
+                    }
+                    throw SmsFailureMessages.providerCode("Tencent Cloud SMS", 
codeNode.asText());
                 }
                 JsonNode sendStatusSet = responseNode.get("SendStatusSet");
-                if (sendStatusSet != null && sendStatusSet.isArray() && 
sendStatusSet.size() > 0) {
-                    JsonNode firstStatus = sendStatusSet.get(0);
-                    String code = firstStatus.get("Code").asText();
-                    String message = firstStatus.get("Message").asText();
-                    if (!RESPONSE_OK.equals(code)) {
-                        throw new SendMessageException(code + ":" + message);
-                    }
+                if (sendStatusSet == null || !sendStatusSet.isArray() || 
sendStatusSet.isEmpty()) {
+                    throw SmsFailureMessages.invalidResponse("Tencent Cloud 
SMS");
+                }
+                JsonNode codeNode = sendStatusSet.get(0).get("Code");
+                if (codeNode == null) {
+                    throw SmsFailureMessages.invalidResponse("Tencent Cloud 
SMS");
+                }
+                String code = codeNode.asText();
+                if (!RESPONSE_OK.equals(code)) {
+                    throw SmsFailureMessages.providerCode("Tencent Cloud SMS", 
code);
                 }
-                log.info("Successfully sent SMS to phones: {}", 
String.join(",", phones));
+                log.info("Successfully sent SMS via Tencent Cloud");
             }
+        } catch (SendMessageException e) {
+            log.warn("Failed to send SMS via Tencent Cloud");
+            throw e;
         } catch (Exception e) {
-            log.warn("Failed to send SMS: {}", e.getMessage());
-            throw new SendMessageException(e.getMessage());
+            log.warn("Failed to send SMS via Tencent Cloud, failure type: {}",
+                    e.getClass().getSimpleName());
+            throw SmsFailureMessages.requestFailed("Tencent Cloud SMS");
         }
     }
 
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TwilioSmsClientImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TwilioSmsClientImpl.java
index 55e1fd355f..5d305e3834 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TwilioSmsClientImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/TwilioSmsClientImpl.java
@@ -97,11 +97,15 @@ public class TwilioSmsClientImpl implements SmsClient {
             URI requestUri = new URI(endpoint);
 
             HttpPost httpPost = createHttpPost(requestUri, phoneNumber, 
message);
-            log.info("Sending Twilio SMS request to {}", requestUri);
-            executeRequest(httpClient, httpPost, phoneNumber);
+            log.debug("Sending SMS request via Twilio");
+            executeRequest(httpClient, httpPost);
+        } catch (SendMessageException e) {
+            log.warn("Failed to send SMS via Twilio");
+            throw e;
         } catch (Exception e) {
-            log.warn("Failed to send SMS: {}", e.getMessage());
-            throw new SendMessageException(e.getMessage());
+            log.warn("Failed to send SMS via Twilio, failure type: {}",
+                    e.getClass().getSimpleName());
+            throw SmsFailureMessages.requestFailed("Twilio SMS");
         }
     }
 
@@ -121,41 +125,36 @@ public class TwilioSmsClientImpl implements SmsClient {
             httpPost.setEntity(new UrlEncodedFormEntity(parameters));
             return httpPost;
         } catch (Exception e) {
-            log.error("Failed to create HTTP request: {}", e.getMessage());
-            throw new SendMessageException(e.getMessage());
+            log.warn("Failed to create Twilio SMS request, failure type: {}",
+                    e.getClass().getSimpleName());
+            throw SmsFailureMessages.requestFailed("Twilio SMS");
         }
     }
 
-    private void executeRequest(CloseableHttpClient httpClient, HttpPost 
httpPost, String phoneNumber)
-            throws Exception {
+    private void executeRequest(CloseableHttpClient httpClient, HttpPost 
httpPost) 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);
+            log.debug("Twilio SMS response status: {}", statusCode);
 
             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);
+                    throw SmsFailureMessages.providerCode("Twilio SMS", 
"21608");
                 }
+                throw SmsFailureMessages.httpStatus("Twilio SMS", statusCode);
             }
 
             JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
             if (jsonResponse == null) {
-                throw new SendMessageException(statusCode + ":" + 
responseBody);
+                throw SmsFailureMessages.invalidResponse("Twilio SMS");
             }
 
             JsonNode sidNode = jsonResponse.get("sid");
             if (sidNode == null) {
-                throw new SendMessageException(statusCode + ":" + 
responseBody);
+                throw SmsFailureMessages.invalidResponse("Twilio SMS");
             }
 
-            String sid = sidNode.asText();
-            log.info("Successfully sent SMS to phone: {}, sid: {}", 
phoneNumber, sid);
+            log.info("Successfully sent SMS via Twilio");
         }
     }
 
diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImpl.java
index d4a414e12c..241229ea28 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/UniSmsClientImpl.java
@@ -102,14 +102,17 @@ public class UniSmsClientImpl implements SmsClient {
             String payload = JsonUtil.toJson(params);
             httpPost.setEntity(new StringEntity(payload, 
StandardCharsets.UTF_8));
 
-            log.info("Sending SMS request to UniSMS, payload: {}, url: {}", 
payload, url);
+            log.debug("Sending SMS request via UniSMS");
 
             try (CloseableHttpResponse response = 
httpClient.execute(httpPost)) {
-                handleResponse(response, receiver.getPhone());
+                handleResponse(response);
             }
+        } catch (SendMessageException e) {
+            log.warn("Failed to send SMS via UniSMS");
+            throw e;
         } catch (Exception e) {
-            log.error("Failed to send SMS via UniSMS: {}", e.getMessage());
-            throw new SendMessageException(e.getMessage());
+            log.warn("Failed to send SMS via UniSMS, failure type: {}", 
e.getClass().getSimpleName());
+            throw SmsFailureMessages.requestFailed("UniSMS");
         }
     }
 
@@ -145,24 +148,26 @@ public class UniSmsClientImpl implements SmsClient {
         return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
     }
 
-    private void handleResponse(CloseableHttpResponse response, String phone) 
throws IOException {
+    private void handleResponse(CloseableHttpResponse response) throws 
IOException {
         int statusCode = response.getStatusLine().getStatusCode();
         String responseBody = EntityUtils.toString(response.getEntity());
 
-        log.info("UniSMS response status: {}, body: {}", statusCode, 
responseBody);
+        log.debug("UniSMS response status: {}", statusCode);
 
         if (statusCode != 200) {
-            throw new SendMessageException("HTTP request failed with status 
code: " + statusCode + ", response: " + responseBody);
+            throw SmsFailureMessages.httpStatus("UniSMS", statusCode);
         }
 
         JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
+        if (jsonResponse == null || jsonResponse.get("code") == null) {
+            throw SmsFailureMessages.invalidResponse("UniSMS");
+        }
         String code = jsonResponse.get("code").asText();
         if (!SUCCESS_CODE.equals(code)) {
-            String message = jsonResponse.get("message").asText();
-            throw new SendMessageException(code + ":" + message);
+            throw SmsFailureMessages.providerCode("UniSMS", code);
         }
 
-        log.info("Successfully sent SMS to phone: {}", phone);
+        log.info("Successfully sent SMS via UniSMS");
     }
 
     @Override
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/SmsClientLoggingTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/SmsClientLoggingTest.java
new file mode 100644
index 0000000000..fc365ff305
--- /dev/null
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/impl/SmsClientLoggingTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Map;
+import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
+import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
+import org.apache.hertzbeat.common.entity.dto.sms.AlibabaSmsProperties;
+import org.apache.hertzbeat.common.entity.dto.sms.AwsSmsProperties;
+import org.apache.hertzbeat.common.entity.dto.sms.SmslocalSmsProperties;
+import org.apache.hertzbeat.common.entity.dto.sms.TencentSmsProperties;
+import org.apache.hertzbeat.common.entity.dto.sms.TwilioSmsProperties;
+import org.apache.hertzbeat.common.entity.dto.sms.UniSmsProperties;
+import org.apache.hertzbeat.common.support.exception.SendMessageException;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.springframework.boot.test.system.CapturedOutput;
+import org.springframework.boot.test.system.OutputCaptureExtension;
+
+/**
+ * Verifies that SMS clients do not write request credentials or message data 
to logs.
+ */
+@ExtendWith(OutputCaptureExtension.class)
+class SmsClientLoggingTest {
+
+    private static final String ACCESS_KEY = "access-key-log-sentinel";
+    private static final String PHONE = "15555550123";
+    private static final String ALERT_CONTENT = "alert-content-log-sentinel";
+    private static final String PROVIDER_BODY = "provider-body-log-sentinel";
+    private static final String SIGNED_URL = 
"https://provider.invalid/send?Signature=signed-url-log-sentinel";;
+
+    @Test
+    void requestCredentialsAndMessageDataShouldNotBeLogged(CapturedOutput 
output) throws Exception {
+        NoticeReceiver receiver = receiver();
+        GroupAlert alert = alert();
+
+        AwsSmsProperties awsProperties = new AwsSmsProperties();
+        awsProperties.setAccessKeyId(ACCESS_KEY);
+        awsProperties.setAccessKeySecret("aws-secret");
+        awsProperties.setRegion("us-east-1");
+        withSuccessfulResponse("{\"MessageId\":\"message-id\"}",
+            () -> new AwsSmsClientImpl(awsProperties).sendMessage(receiver, 
null, alert));
+
+        AlibabaSmsProperties alibabaProperties =
+            new AlibabaSmsProperties(ACCESS_KEY, "alibaba-secret", "sign", 
"template");
+        withSuccessfulResponse("{\"Code\":\"OK\"}",
+            () -> new 
AlibabaSmsClientImpl(alibabaProperties).sendMessage(receiver, null, alert));
+
+        UniSmsProperties uniProperties =
+            new UniSmsProperties(ACCESS_KEY, "unisms-secret", "sign", 
"template", "hmac");
+        withSuccessfulResponse("{\"code\":\"0\"}",
+            () -> new UniSmsClientImpl(uniProperties).sendMessage(receiver, 
null, alert));
+
+        withSuccessfulResponse("{\"sid\":\"message-id\"}",
+            () -> new 
TwilioSmsClientImpl(twilioProperties()).sendMessage(receiver, null, alert));
+
+        
withSuccessfulResponse("{\"Response\":{\"SendStatusSet\":[{\"Code\":\"Ok\"}]}}",
+            () -> new 
TencentSmsClientImpl(tencentProperties()).sendMessage(receiver, null, alert));
+
+        
withSuccessfulResponse("[{\"errorCode\":\"200\",\"id\":\"message-id\"}]",
+            () -> new 
SmsLocalSmsClientImpl(smslocalProperties()).sendMessage(receiver, null, alert));
+
+        String logs = output.getAll();
+        assertFalse(logs.contains(ACCESS_KEY));
+        assertFalse(logs.contains(PHONE));
+        assertFalse(logs.contains(ALERT_CONTENT));
+        assertFalse(logs.contains("Authorization"));
+        assertFalse(logs.contains("Signature="));
+    }
+
+    @Test
+    void failedResponsesExposeOnlyProviderAndHttpStatus(CapturedOutput output) 
throws Exception {
+        String body = "{\"message\":\"" + PROVIDER_BODY + "\",\"phone\":\"" + 
PHONE + "\"}";
+
+        SendMessageException awsFailure = withResponse(503, body,
+                () -> new 
AwsSmsClientImpl(awsProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException alibabaFailure = withResponse(502, body,
+                () -> new 
AlibabaSmsClientImpl(alibabaProperties()).sendMessage(receiver(), null, 
alert()));
+        SendMessageException uniFailure = withResponse(429, body,
+                () -> new 
UniSmsClientImpl(uniProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException twilioFailure = withResponse(429, body,
+                () -> new 
TwilioSmsClientImpl(twilioProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException tencentFailure = withResponse(429, body,
+                () -> new 
TencentSmsClientImpl(tencentProperties()).sendMessage(receiver(), null, 
alert()));
+        SendMessageException smslocalFailure = withResponse(429, body,
+                () -> new 
SmsLocalSmsClientImpl(smslocalProperties()).sendMessage(receiver(), null, 
alert()));
+
+        assertEquals("AWS SMS request failed with HTTP status 503", 
awsFailure.getMessage());
+        assertEquals("Alibaba Cloud SMS request failed with HTTP status 502", 
alibabaFailure.getMessage());
+        assertEquals("UniSMS request failed with HTTP status 429", 
uniFailure.getMessage());
+        assertEquals("Twilio SMS request failed with HTTP status 429", 
twilioFailure.getMessage());
+        assertEquals("Tencent Cloud SMS request failed with HTTP status 429", 
tencentFailure.getMessage());
+        assertEquals("SMSLocal request failed with HTTP status 429", 
smslocalFailure.getMessage());
+        assertNoSensitiveSentinels(output.getAll()
+                + awsFailure.getMessage()
+                + alibabaFailure.getMessage()
+                + uniFailure.getMessage()
+                + twilioFailure.getMessage()
+                + tencentFailure.getMessage()
+                + smslocalFailure.getMessage());
+    }
+
+    @Test
+    void providerErrorsDoNotExposeProviderMessages(CapturedOutput output) 
throws Exception {
+        SendMessageException alibabaFailure = withResponse(
+                200,
+                "{\"Code\":\"THROTTLED\",\"Message\":\"" + PROVIDER_BODY + 
"\"}",
+                () -> new 
AlibabaSmsClientImpl(alibabaProperties()).sendMessage(receiver(), null, 
alert()));
+        SendMessageException uniFailure = withResponse(
+                200,
+                "{\"code\":\"RATE_LIMITED\",\"message\":\"" + PROVIDER_BODY + 
"\"}",
+                () -> new 
UniSmsClientImpl(uniProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException awsFailure = withResponse(
+                200,
+                "{\"message\":\"" + PROVIDER_BODY + "\"}",
+                () -> new 
AwsSmsClientImpl(awsProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException twilioFailure = withResponse(
+                400,
+                "{\"code\":21608,\"message\":\"" + PROVIDER_BODY + "\"}",
+                () -> new 
TwilioSmsClientImpl(twilioProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException tencentFailure = withResponse(
+                200,
+                
"{\"Response\":{\"Error\":{\"Code\":\"THROTTLED\",\"Message\":\""
+                        + PROVIDER_BODY + "\"}}}",
+                () -> new 
TencentSmsClientImpl(tencentProperties()).sendMessage(receiver(), null, 
alert()));
+        SendMessageException smslocalFailure = withResponse(
+                200,
+                "[{\"errorCode\":\"RATE_LIMITED\",\"id\":\"" + PROVIDER_BODY + 
"\"}]",
+                () -> new 
SmsLocalSmsClientImpl(smslocalProperties()).sendMessage(receiver(), null, 
alert()));
+
+        assertEquals("Alibaba Cloud SMS request failed (code: THROTTLED)", 
alibabaFailure.getMessage());
+        assertEquals("UniSMS request failed (code: RATE_LIMITED)", 
uniFailure.getMessage());
+        assertEquals("AWS SMS provider returned an invalid response", 
awsFailure.getMessage());
+        assertEquals("Twilio SMS request failed (code: 21608)", 
twilioFailure.getMessage());
+        assertEquals("Tencent Cloud SMS request failed (code: THROTTLED)", 
tencentFailure.getMessage());
+        assertEquals("SMSLocal request failed (code: RATE_LIMITED)", 
smslocalFailure.getMessage());
+        assertNoSensitiveSentinels(output.getAll()
+                + alibabaFailure.getMessage()
+                + uniFailure.getMessage()
+                + awsFailure.getMessage()
+                + twilioFailure.getMessage()
+                + tencentFailure.getMessage()
+                + smslocalFailure.getMessage());
+    }
+
+    @Test
+    void networkExceptionsDoNotExposeSignedUrls(CapturedOutput output) throws 
Exception {
+        SendMessageException awsFailure = withNetworkFailure(
+                () -> new 
AwsSmsClientImpl(awsProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException alibabaFailure = withNetworkFailure(
+                () -> new 
AlibabaSmsClientImpl(alibabaProperties()).sendMessage(receiver(), null, 
alert()));
+        SendMessageException uniFailure = withNetworkFailure(
+                () -> new 
UniSmsClientImpl(uniProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException twilioFailure = withNetworkFailure(
+                () -> new 
TwilioSmsClientImpl(twilioProperties()).sendMessage(receiver(), null, alert()));
+        SendMessageException tencentFailure = withNetworkFailure(
+                () -> new 
TencentSmsClientImpl(tencentProperties()).sendMessage(receiver(), null, 
alert()));
+        SendMessageException smslocalFailure = withNetworkFailure(
+                () -> new 
SmsLocalSmsClientImpl(smslocalProperties()).sendMessage(receiver(), null, 
alert()));
+
+        assertEquals("AWS SMS request failed", awsFailure.getMessage());
+        assertEquals("Alibaba Cloud SMS request failed", 
alibabaFailure.getMessage());
+        assertEquals("UniSMS request failed", uniFailure.getMessage());
+        assertEquals("Twilio SMS request failed", twilioFailure.getMessage());
+        assertEquals("Tencent Cloud SMS request failed", 
tencentFailure.getMessage());
+        assertEquals("SMSLocal request failed", smslocalFailure.getMessage());
+        assertNoSensitiveSentinels(output.getAll()
+                + awsFailure.getMessage()
+                + alibabaFailure.getMessage()
+                + uniFailure.getMessage()
+                + twilioFailure.getMessage()
+                + tencentFailure.getMessage()
+                + smslocalFailure.getMessage());
+    }
+
+    private void withSuccessfulResponse(String responseBody, Runnable 
operation) throws Exception {
+        withResponse(200, responseBody, operation, false);
+    }
+
+    private SendMessageException withResponse(int statusCode, String 
responseBody, Runnable operation)
+            throws Exception {
+        return withResponse(statusCode, responseBody, operation, true);
+    }
+
+    private SendMessageException withResponse(
+            int statusCode,
+            String responseBody,
+            Runnable operation,
+            boolean expectsFailure) throws Exception {
+        CloseableHttpClient httpClient = mock(CloseableHttpClient.class);
+        CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+        StatusLine statusLine = mock(StatusLine.class);
+        when(statusLine.getStatusCode()).thenReturn(statusCode);
+        when(response.getStatusLine()).thenReturn(statusLine);
+        when(response.getEntity()).thenReturn(new StringEntity(responseBody, 
ContentType.APPLICATION_JSON));
+        when(httpClient.execute(any(HttpPost.class))).thenReturn(response);
+
+        try (MockedStatic<HttpClients> httpClients = 
mockStatic(HttpClients.class)) {
+            
httpClients.when(HttpClients::createDefault).thenReturn(httpClient);
+            if (expectsFailure) {
+                return assertThrows(SendMessageException.class, 
operation::run);
+            }
+            operation.run();
+            return null;
+        }
+    }
+
+    private SendMessageException withNetworkFailure(Runnable operation) throws 
Exception {
+        CloseableHttpClient httpClient = mock(CloseableHttpClient.class);
+        when(httpClient.execute(any(HttpPost.class)))
+                .thenThrow(new IOException(SIGNED_URL + "&phone=" + PHONE + 
"&body=" + PROVIDER_BODY));
+        try (MockedStatic<HttpClients> httpClients = 
mockStatic(HttpClients.class)) {
+            
httpClients.when(HttpClients::createDefault).thenReturn(httpClient);
+            return assertThrows(SendMessageException.class, operation::run);
+        }
+    }
+
+    private NoticeReceiver receiver() {
+        NoticeReceiver receiver = new NoticeReceiver();
+        receiver.setPhone(PHONE);
+        return receiver;
+    }
+
+    private GroupAlert alert() {
+        GroupAlert alert = new GroupAlert();
+        alert.setGroupKey("instance");
+        alert.setCommonLabels(Map.of());
+        alert.setCommonAnnotations(Map.of("summary", ALERT_CONTENT, 
"description", ALERT_CONTENT));
+        return alert;
+    }
+
+    private AwsSmsProperties awsProperties() {
+        AwsSmsProperties properties = new AwsSmsProperties();
+        properties.setAccessKeyId(ACCESS_KEY);
+        properties.setAccessKeySecret("aws-secret");
+        properties.setRegion("us-east-1");
+        return properties;
+    }
+
+    private AlibabaSmsProperties alibabaProperties() {
+        return new AlibabaSmsProperties(ACCESS_KEY, "alibaba-secret", "sign", 
"template");
+    }
+
+    private UniSmsProperties uniProperties() {
+        return new UniSmsProperties(ACCESS_KEY, "unisms-secret", "sign", 
"template", "hmac");
+    }
+
+    private TwilioSmsProperties twilioProperties() {
+        return new TwilioSmsProperties(ACCESS_KEY, "twilio-secret", 
"twilio-phone");
+    }
+
+    private TencentSmsProperties tencentProperties() {
+        return new TencentSmsProperties(ACCESS_KEY, "tencent-secret", 
"app-id", "sign", "template");
+    }
+
+    private SmslocalSmsProperties smslocalProperties() {
+        return new SmslocalSmsProperties(ACCESS_KEY);
+    }
+
+    private void assertNoSensitiveSentinels(String text) {
+        assertFalse(text.contains(ACCESS_KEY));
+        assertFalse(text.contains(PHONE));
+        assertFalse(text.contains(ALERT_CONTENT));
+        assertFalse(text.contains(PROVIDER_BODY));
+        assertFalse(text.contains(SIGNED_URL));
+        assertFalse(text.contains("signed-url-log-sentinel"));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to