feat: 增加邮件告警

This commit is contained in:
zengxiaobo 2024-05-28 18:32:21 +08:00
parent d2e515ecec
commit b9a2345e19
7 changed files with 332 additions and 1 deletions

View File

@ -61,6 +61,11 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@ -31,7 +31,6 @@
<artifactId>spring-boot-starter-aop</artifactId> <artifactId>spring-boot-starter-aop</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.squareup.okhttp3</groupId> <groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId> <artifactId>okhttp</artifactId>
@ -41,6 +40,22 @@
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId> <artifactId>transmittable-thread-local</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -0,0 +1,65 @@
package cn.axzo.foundation.web.support.alert;
import cn.axzo.foundation.util.VarParamFormatter;
import com.google.common.base.Throwables;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import java.time.LocalDateTime;
public interface AlertClient {
void post(Alert alert);
@Data
class Alert {
private static final int MAX_MESSAGE_LENGTH = 8_000;
private static final int MAX_STACK_LENGTH = 16_000;
private static final int MAX_STACK_HEAD_LENGTH = 12_000;
private static final int MAX_STACK_TAIL_LENGTH = MAX_STACK_LENGTH - MAX_STACK_HEAD_LENGTH;
String key;
String message;
String stack;
@Builder
public Alert(String key, Throwable ex, String message, Object... objects) {
this.key = key;
this.message = message;
this.message = StringUtils.left(VarParamFormatter.format(message, objects), MAX_MESSAGE_LENGTH);
if (ex != null) {
String exClassName = ex.getClass().getCanonicalName();
String errorStack = Throwables.getStackTraceAsString(ex);
if (StringUtils.length(errorStack) > MAX_STACK_LENGTH) {
errorStack = StringUtils.left(errorStack, MAX_STACK_HEAD_LENGTH)
+ "\\n ... \\n"
+ StringUtils.right(errorStack, MAX_STACK_TAIL_LENGTH);
}
this.stack = String.format("%s {%s}", exClassName, errorStack);
}
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class AlertKey {
String key;
String stack;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class AlertMessage {
String msg;
LocalDateTime time;
}
}

View File

@ -0,0 +1,79 @@
package cn.axzo.foundation.web.support.alert;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.util.concurrent.RateLimiter;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Slf4j
public class AlertClientImpl implements AlertClient {
private final Consumer<Map<AlertKey, Collection<AlertMessage>>> consumer;
private final ScheduledExecutorService executor;
private final RateLimiter rateLimiter;
ListMultimap<AlertKey, AlertMessage> alertsMap = Multimaps.synchronizedListMultimap(ArrayListMultimap.create());
@Builder
public AlertClientImpl(Consumer<Map<AlertKey, Collection<AlertMessage>>> consumer,
ScheduledThreadPoolExecutor executor,
Integer period,
Integer consumeImmediatelyPreSecond) {
this.consumer = consumer;
this.executor = executor;
//如果每秒出现了10次, 则马上触发
this.rateLimiter = RateLimiter.create(Optional.ofNullable(consumeImmediatelyPreSecond).orElse(10));
executor.scheduleAtFixedRate(this::consume,
5, Optional.ofNullable(period).orElse(10), TimeUnit.MINUTES);
}
@Override
public void post(Alert alert) {
AlertKey.builder()
.key(alert.getKey())
.stack(alert.getStack())
.build();
AlertMessage.builder()
.msg(alert.getMessage())
.time(LocalDateTime.now())
.build();
alertsMap.put(AlertKey.builder()
.key(alert.getKey())
.stack(alert.getStack())
.build(),
AlertMessage.builder()
.msg(alert.getMessage())
.time(LocalDateTime.now())
.build());
//获取令牌, 如果成功则不处理, 否则表示需要立即触发消费
if (!rateLimiter.tryAcquire(1)) {
consume();
}
}
private void consume() {
Map<AlertKey, Collection<AlertMessage>> map = alertsMap.asMap();
try {
consumer.accept(map);
} catch (Exception ex) {
log.error("consume alerts failed", ex);
//ignore
}
map.clear();
}
}

View File

@ -0,0 +1,100 @@
package cn.axzo.foundation.web.support.alert;
import cn.axzo.foundation.web.support.AppRuntime;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.security.Security;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
@Slf4j
public class EmailAlertConsumer implements Consumer<Map<AlertClient.AlertKey, Collection<AlertClient.AlertMessage>>> {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
AppRuntime appRuntime;
Session session;
String from;
String to;
@Builder
public EmailAlertConsumer(AppRuntime appRuntime,
String host,
Integer port,
String username,
String password,
String from,
String to) {
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
String portStr = Optional.ofNullable(port).map(Objects::toString).orElse("465");
Properties props = new Properties();
props.setProperty("mail.smtp.host", Optional.ofNullable(host).orElse("smtp.qiye.aliyun.com"));
props.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
props.setProperty("mail.smtp.socketFactory.fallback", "false");
//设置端口
props.setProperty("mail.smtp.port", portStr);
//启用调试
props.setProperty("mail.debug", "true");
props.setProperty("mail.smtp.socketFactory.port", portStr);
props.setProperty("mail.smtp.auth", "true");
this.session = Session.getInstance(props, new Authenticator() {
public PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
this.from = from;
this.to = to;
this.appRuntime = appRuntime;
}
@Override
public void accept(Map<AlertClient.AlertKey, Collection<AlertClient.AlertMessage>> alerts) {
Message message = new MimeMessage(session);
try {
message.setFrom(new InternetAddress(from));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
message.setSubject(String.format("[%s]服务[%s]告警[%s]", appRuntime.getEnv().name(), appRuntime.getAppName(),
DATE_TIME_FORMATTER.format(LocalDateTime.now())));
MimeBodyPart content = new MimeBodyPart();
content.setContent(build(alerts), "text/html;charset=utf-8");
MimeMultipart mimeMultipart = new MimeMultipart();
mimeMultipart.addBodyPart(content);
message.setContent(mimeMultipart);
Transport.send(message);
} catch (Exception ex) {
log.error("send email failed", ex);
}
}
protected String build(Map<AlertClient.AlertKey, Collection<AlertClient.AlertMessage>> alerts) {
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html><html><body><table border=\"1\">");
alerts.entrySet().forEach(e -> {
AlertClient.AlertKey alertKey = e.getKey();
Collection<AlertClient.AlertMessage> messages = e.getValue();
sb.append("<tr><td colspan=\"2\" align=\"center\">").append(alertKey.getKey()).append("</td></tr>");
messages.forEach(m -> sb.append("<tr><td>")
.append(DATE_TIME_FORMATTER.format(m.getTime()))
.append("</td><td>")
.append(m.getMsg())
.append("</td></tr>"));
sb.append("<tr><td colspan=\"2\">").append(alertKey.getStack()).append("</td></tr>");
});
sb.append("</table></body></html>");
return sb.toString();
}
}

View File

@ -0,0 +1,32 @@
package cn.axzo.foundation.web.support.alert;
import org.junit.jupiter.api.Test;
import java.util.concurrent.ScheduledThreadPoolExecutor;
class AlertClientImplTest {
@Test
void testMail() {
AlertClient alertClient = AlertClientImpl.builder()
.consumer(EmailAlertConsumer.builder()
.host("smtp.qiye.aliyun.com")
.port(465)
.username("zengxiaobo@axzo.cn")
.password("Zxb19861109")
.from("zengxiaobo@axzo.cn")
.to("wangsiqian@axzo.cn")
.build())
.executor(new ScheduledThreadPoolExecutor(1))
.build();
for (int i = 0; i < 15; i++) {
alertClient.post(AlertClient.Alert.builder()
.ex(new RuntimeException())
.key("keykeykey")
.message("messagemessagemessagemessage")
.build());
}
}
}

View File

@ -0,0 +1,35 @@
package cn.axzo.foundation.web.support.alert;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
class EmailAlertConsumerTest {
@Test
void buildContent() {
EmailAlertConsumer consumer = EmailAlertConsumer.builder()
.host("1")
.port(44)
.username("aaa")
.password("aaaa")
.build();
String build = consumer.build(ImmutableMap.of(
AlertClient.AlertKey.builder()
.key("message")
.stack(Throwables.getStackTraceAsString(new RuntimeException()))
.build(),
ImmutableList.of(AlertClient.AlertMessage.builder()
.msg("msg1")
.time(LocalDateTime.now())
.build())
));
System.out.printf(build);
}
}