Skip to content

Commit 9fd58ce

Browse files
sarthakeashyunfan24tomsun28
authored
[feature] add twilio sms client support (apache#3159)
Co-authored-by: yunfan24 <[email protected]> Co-authored-by: tomsun28 <[email protected]>
1 parent a331c04 commit 9fd58ce

File tree

22 files changed

+466
-4
lines changed

22 files changed

+466
-4
lines changed

hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/config/SmsConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ public class SmsConfig {
6464
* Aws configuration
6565
*/
6666
private AwsSmsProperties aws;
67+
68+
/**
69+
* Twilio SMS configuration
70+
*/
71+
private TwilioSmsProperties twilio;
72+
6773
/**
6874
* Smslocal SMS configuration
6975
*/
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hertzbeat.alert.config;
19+
20+
import jakarta.validation.constraints.NotBlank;
21+
import lombok.AllArgsConstructor;
22+
import lombok.Data;
23+
import lombok.NoArgsConstructor;
24+
25+
/**
26+
* Twilio SMS configuration properties
27+
*/
28+
@Data
29+
@AllArgsConstructor
30+
@NoArgsConstructor
31+
public class TwilioSmsProperties {
32+
/**
33+
* Twilio Account SID
34+
*/
35+
@NotBlank(message = "Account SID cannot be empty")
36+
private String accountSid;
37+
38+
/**
39+
* Twilio Auth Token
40+
*/
41+
@NotBlank(message = "Auth Token cannot be empty")
42+
private String authToken;
43+
44+
/**
45+
* Twilio Issued Phone Number
46+
*/
47+
@NotBlank(message = "Twilio Phone Number cannot be empty")
48+
private String twilioPhoneNumber;
49+
}

hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/SmsClientFactory.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.hertzbeat.alert.service.impl.SmsLocalSmsClientImpl;
2424
import org.apache.hertzbeat.alert.service.impl.AwsSmsClientImpl;
2525
import org.apache.hertzbeat.alert.service.impl.TencentSmsClientImpl;
26+
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
2627
import org.apache.hertzbeat.alert.service.impl.UniSmsClientImpl;
2728
import org.apache.hertzbeat.alert.service.impl.AlibabaSmsClientImpl;
2829
import org.apache.hertzbeat.base.dao.GeneralConfigDao;
@@ -35,6 +36,7 @@
3536
import static org.apache.hertzbeat.common.constants.SmsConstants.ALIBABA;
3637
import static org.apache.hertzbeat.common.constants.SmsConstants.AWS;
3738
import static org.apache.hertzbeat.common.constants.SmsConstants.TENCENT;
39+
import static org.apache.hertzbeat.common.constants.SmsConstants.TWILIO;
3840
import static org.apache.hertzbeat.common.constants.SmsConstants.UNISMS;
3941
import static org.apache.hertzbeat.common.constants.SmsConstants.SMSLOCAL;
4042

@@ -141,9 +143,12 @@ private void createSmsClient(SmsConfig smsConfig) {
141143
case AWS:
142144
currentSmsClient = new AwsSmsClientImpl(smsConfig.getAws());
143145
break;
146+
case TWILIO:
147+
currentSmsClient = new TwilioSmsClientImpl(smsConfig.getTwilio());
148+
break;
144149
default:
145150
log.warn("[SmsClientFactory] Unsupported SMS provider type: {}", smsConfig.getType());
146151
break;
147152
}
148153
}
149-
}
154+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hertzbeat.alert.service.impl;
19+
20+
import com.fasterxml.jackson.databind.JsonNode;
21+
import lombok.extern.slf4j.Slf4j;
22+
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
23+
import org.apache.hertzbeat.alert.service.SmsClient;
24+
import org.apache.hertzbeat.common.entity.alerter.GroupAlert;
25+
import org.apache.hertzbeat.common.entity.alerter.NoticeReceiver;
26+
import org.apache.hertzbeat.common.entity.alerter.NoticeTemplate;
27+
import org.apache.hertzbeat.common.support.exception.SendMessageException;
28+
import org.apache.hertzbeat.common.util.JsonUtil;
29+
import org.apache.http.client.methods.CloseableHttpResponse;
30+
import org.apache.http.client.methods.HttpPost;
31+
import org.apache.http.impl.client.CloseableHttpClient;
32+
import org.apache.http.impl.client.HttpClients;
33+
import org.apache.http.message.BasicNameValuePair;
34+
import org.apache.http.util.EntityUtils;
35+
import org.apache.http.client.entity.UrlEncodedFormEntity;
36+
37+
import java.net.URI;
38+
import java.util.ArrayList;
39+
import java.util.Base64;
40+
import java.util.List;
41+
42+
import static org.apache.hertzbeat.common.constants.SmsConstants.TWILIO;
43+
44+
/**
45+
* Twilio SMS Client Implementation<br>
46+
* API doc: <a href=
47+
* "https://www.twilio.com/docs/sms/api">https://www.twilio.com/docs/sms/api</a>
48+
*/
49+
@Slf4j
50+
public class TwilioSmsClientImpl implements SmsClient {
51+
52+
private static final String API_URL_FORMAT = "https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json";
53+
private static final String MESSAGE_TEMPLATE = "Instance: %s, Priority: %s, Content: %s";
54+
55+
private final String accountSid;
56+
private final String authToken;
57+
private final String twilioPhoneNumber;
58+
59+
public TwilioSmsClientImpl(TwilioSmsProperties config) {
60+
if (config != null) {
61+
this.accountSid = config.getAccountSid();
62+
this.authToken = config.getAuthToken();
63+
this.twilioPhoneNumber = config.getTwilioPhoneNumber();
64+
} else {
65+
this.accountSid = "";
66+
this.authToken = "";
67+
this.twilioPhoneNumber = "";
68+
}
69+
}
70+
71+
@Override
72+
public void sendMessage(NoticeReceiver receiver, NoticeTemplate noticeTemplate, GroupAlert alert) {
73+
String instance = null;
74+
String priority = null;
75+
String content = null;
76+
if (alert.getCommonLabels() != null) {
77+
instance = alert.getCommonLabels().get("instance") == null ? alert.getGroupKey()
78+
: alert.getCommonLabels().get("instance");
79+
priority = alert.getCommonLabels().get("priority") == null ? "unknown"
80+
: alert.getCommonLabels().get("priority");
81+
content = alert.getCommonAnnotations().get("summary");
82+
content = content == null ? alert.getCommonAnnotations().get("description") : content;
83+
if (content == null) {
84+
content = alert.getCommonAnnotations().values().stream().findFirst().orElse(null);
85+
}
86+
}
87+
this.send(receiver.getPhone(), createMessage(instance, priority, content));
88+
}
89+
90+
private String createMessage(String instance, String priority, String content) {
91+
return String.format(MESSAGE_TEMPLATE, instance, priority, content);
92+
}
93+
94+
private void send(String phoneNumber, String message) {
95+
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
96+
String endpoint = String.format(API_URL_FORMAT, accountSid);
97+
URI requestUri = new URI(endpoint);
98+
99+
HttpPost httpPost = createHttpPost(requestUri, phoneNumber, message);
100+
log.info("Sending Twilio SMS request to {}", requestUri);
101+
executeRequest(httpClient, httpPost, phoneNumber);
102+
} catch (Exception e) {
103+
log.warn("Failed to send SMS: {}", e.getMessage());
104+
throw new SendMessageException(e.getMessage());
105+
}
106+
}
107+
108+
private HttpPost createHttpPost(URI requestUri, String toNumber, String message) {
109+
HttpPost httpPost = new HttpPost(requestUri);
110+
111+
String auth = accountSid + ":" + authToken;
112+
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
113+
httpPost.setHeader("Authorization", "Basic " + encodedAuth);
114+
115+
List<BasicNameValuePair> parameters = new ArrayList<>();
116+
parameters.add(new BasicNameValuePair("To", toNumber));
117+
parameters.add(new BasicNameValuePair("From", twilioPhoneNumber));
118+
parameters.add(new BasicNameValuePair("Body", message));
119+
120+
try {
121+
httpPost.setEntity(new UrlEncodedFormEntity(parameters));
122+
return httpPost;
123+
} catch (Exception e) {
124+
log.error("Failed to create HTTP request: {}", e.getMessage());
125+
throw new SendMessageException(e.getMessage());
126+
}
127+
}
128+
129+
private void executeRequest(CloseableHttpClient httpClient, HttpPost httpPost, String phoneNumber)
130+
throws Exception {
131+
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
132+
int statusCode = response.getStatusLine().getStatusCode();
133+
String responseBody = EntityUtils.toString(response.getEntity());
134+
log.info("SMS response status: {}, body: {}", statusCode, responseBody);
135+
136+
if (statusCode < 200 || statusCode >= 300) {
137+
138+
if (responseBody.contains("21608")) {
139+
throw new SendMessageException(
140+
"The Twilio trial account can only send SMS to verified phone numbers");
141+
} else {
142+
throw new SendMessageException(
143+
"HTTP request failed with status code: " + statusCode + ", response: " + responseBody);
144+
}
145+
}
146+
147+
JsonNode jsonResponse = JsonUtil.fromJson(responseBody);
148+
if (jsonResponse == null) {
149+
throw new SendMessageException(statusCode + ":" + responseBody);
150+
}
151+
152+
JsonNode sidNode = jsonResponse.get("sid");
153+
if (sidNode == null) {
154+
throw new SendMessageException(statusCode + ":" + responseBody);
155+
}
156+
157+
String sid = sidNode.asText();
158+
log.info("Successfully sent SMS to phone: {}, sid: {}", phoneNumber, sid);
159+
}
160+
}
161+
162+
@Override
163+
public String getType() {
164+
return TWILIO;
165+
}
166+
167+
@Override
168+
public boolean checkConfig() {
169+
return !(accountSid.isBlank() || authToken.isBlank() || twilioPhoneNumber.isBlank());
170+
}
171+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hertzbeat.alert.service;
19+
20+
import org.apache.hertzbeat.alert.config.TwilioSmsProperties;
21+
import org.apache.hertzbeat.alert.service.impl.TwilioSmsClientImpl;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.CsvSource;
25+
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
28+
/**
29+
* Test case for {@link TwilioSmsClientImpl}
30+
*/
31+
class TwilioSmsClientImplTest {
32+
33+
@Test
34+
void getType() {
35+
TwilioSmsProperties twilioSmsProperties = new TwilioSmsProperties();
36+
twilioSmsProperties.setAccountSid("accountSid");
37+
twilioSmsProperties.setAuthToken("authToken");
38+
twilioSmsProperties.setTwilioPhoneNumber("twilioPhoneNumber");
39+
TwilioSmsClientImpl twilioSmsClient = new TwilioSmsClientImpl(twilioSmsProperties);
40+
41+
assertEquals("twilio", twilioSmsClient.getType());
42+
}
43+
44+
@ParameterizedTest
45+
@CsvSource({
46+
"accountSid, authToken, twilioPhoneNumber, true",
47+
"accountSid, authToken, '', false",
48+
"accountSid, '', twilioPhoneNumber, false",
49+
"accountSid, '', '', false",
50+
"'', authToken, twilioPhoneNumber, false",
51+
"'', authToken, '', false",
52+
"'', '', twilioPhoneNumber, false",
53+
"'', '', '', false",
54+
})
55+
void checkConfig(String accountSid, String authToken, String twilioPhoneNumber, boolean expected) {
56+
TwilioSmsProperties twilioSmsProperties = new TwilioSmsProperties();
57+
twilioSmsProperties.setAccountSid(accountSid);
58+
twilioSmsProperties.setAuthToken(authToken);
59+
twilioSmsProperties.setTwilioPhoneNumber(twilioPhoneNumber);
60+
TwilioSmsClientImpl twilioSmsClient = new TwilioSmsClientImpl(twilioSmsProperties);
61+
62+
assertEquals(expected, twilioSmsClient.checkConfig());
63+
}
64+
}

hertzbeat-common/src/main/java/org/apache/hertzbeat/common/constants/SmsConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ public interface SmsConstants {
3535

3636
// Aws cloud SMS
3737
String AWS = "aws";
38+
39+
// Twilio SMS
40+
String TWILIO = "twilio";
3841
}

hertzbeat-manager/src/main/resources/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ alerter:
231231
access-key-id: YOUR_ACCESS_KEY_ID
232232
access-key-secret: YOUR_ACCESS_KEY_SECRET
233233
region: AWS_REGION_FOR_END_USER_MESSAGING
234+
twilio:
235+
account-sid: YOUR_ACCOUNT_SID
236+
auth-token: YOUR_AUTH_TOKEN
237+
twilio-phone-number: YOUR_TWILIO_PHONE_NUMBER
234238
scheduler:
235239
server:
236240
enabled: true

0 commit comments

Comments
 (0)