feat: 增加邮件告警
This commit is contained in:
parent
d2e515ecec
commit
b9a2345e19
5
pom.xml
5
pom.xml
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user