diff --git a/pom.xml b/pom.xml index b350345..afc7af4 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,11 @@ pom import + + com.sun.mail + javax.mail + 1.6.2 + diff --git a/web-support-lib/pom.xml b/web-support-lib/pom.xml index c996065..186c551 100644 --- a/web-support-lib/pom.xml +++ b/web-support-lib/pom.xml @@ -31,7 +31,6 @@ spring-boot-starter-aop - com.squareup.okhttp3 okhttp @@ -41,6 +40,22 @@ com.alibaba transmittable-thread-local + + com.sun.mail + javax.mail + provided + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + \ No newline at end of file diff --git a/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/AlertClient.java b/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/AlertClient.java new file mode 100644 index 0000000..61db730 --- /dev/null +++ b/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/AlertClient.java @@ -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; + } + +} diff --git a/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/AlertClientImpl.java b/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/AlertClientImpl.java new file mode 100644 index 0000000..f08fbed --- /dev/null +++ b/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/AlertClientImpl.java @@ -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>> consumer; + private final ScheduledExecutorService executor; + + private final RateLimiter rateLimiter; + + ListMultimap alertsMap = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); + + @Builder + public AlertClientImpl(Consumer>> 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> map = alertsMap.asMap(); + try { + consumer.accept(map); + } catch (Exception ex) { + log.error("consume alerts failed", ex); + //ignore + } + map.clear(); + } +} diff --git a/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/EmailAlertConsumer.java b/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/EmailAlertConsumer.java new file mode 100644 index 0000000..09a9c87 --- /dev/null +++ b/web-support-lib/src/main/java/cn/axzo/foundation/web/support/alert/EmailAlertConsumer.java @@ -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>> { + + 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> 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> alerts) { + StringBuilder sb = new StringBuilder(); + sb.append(""); + + alerts.entrySet().forEach(e -> { + AlertClient.AlertKey alertKey = e.getKey(); + Collection messages = e.getValue(); + sb.append(""); + + messages.forEach(m -> sb.append("")); + + sb.append(""); + }); + sb.append("
").append(alertKey.getKey()).append("
") + .append(DATE_TIME_FORMATTER.format(m.getTime())) + .append("") + .append(m.getMsg()) + .append("
").append(alertKey.getStack()).append("
"); + return sb.toString(); + } +} diff --git a/web-support-lib/src/test/java/cn/axzo/foundation/web/support/alert/AlertClientImplTest.java b/web-support-lib/src/test/java/cn/axzo/foundation/web/support/alert/AlertClientImplTest.java new file mode 100644 index 0000000..fc2ca7d --- /dev/null +++ b/web-support-lib/src/test/java/cn/axzo/foundation/web/support/alert/AlertClientImplTest.java @@ -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()); + } + } + +} \ No newline at end of file diff --git a/web-support-lib/src/test/java/cn/axzo/foundation/web/support/alert/EmailAlertConsumerTest.java b/web-support-lib/src/test/java/cn/axzo/foundation/web/support/alert/EmailAlertConsumerTest.java new file mode 100644 index 0000000..9c5dc83 --- /dev/null +++ b/web-support-lib/src/test/java/cn/axzo/foundation/web/support/alert/EmailAlertConsumerTest.java @@ -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); + + } + +} \ No newline at end of file