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]