springboot使用log4j监控日志发送邮件
实现log4j发送邮件功能大致流程:
1、开启邮箱SMTP服务,获取SMTP登录密码
2、引入javax.mail、javax.activation依赖
3、配置log4j文件,指定邮件发送方和接收方以及发送方账号密码等
4、重写SMTPAppender(不重写也能实现邮件发送功能)
开启邮箱SMTP服务
这里以qq邮箱为例,首先进入账号管理界面,设置-账号
找到POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务,绑定手机号即可开启服务,开启服务后会给你一串字符,这个字符就是后边配置SMTP邮箱的邮箱密码
引入javax.mail、javax.activation依赖
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
配置log4j文件
### 配置根 ###(直接加上MAIl,以,分隔,不要删除原来的配置以免影响日志输出)
log4j.rootLogger = INFO,console,MAIL
注意 :以下配置是添加,不要删除原来的配置
### 配置输出到邮件 ###
# log4j的邮件发送appender,这里如果重写了SMTPAppender就写自己重写过的appender
#log4j.appender.MAIL=org.apache.log4j.net.SMTPAppender(默认的)
# 自己重写的appender
log4j.appender.MAIL=com.tian.api.ASMTPAppender.ASMTPAppender
#发送邮件的门槛,仅当等于或高于ERROR(比如FATAL)时,邮件才被发送 等级由低到高:debug<info<warn<Error<Fatal;
log4j.appender.MAIL.Threshold=ERROR
#日志发送邮件时关心的包(这里是自己添加的参数,如果不重写SMTPAppender不需要该参数,参数名可以自己命名)
log4j.appender.MAIL.includePackage=com.xxx.xxx.controller
#邮件缓冲区大小
log4j.appender.MAIL.BufferSize=16
#smtp服务地址(这里以qq为例,如果是163或者其他邮箱就换成其他邮箱的SMTP地址)
log4j.appender.MAIL.SMTPHost=smtp.qq.com
#端口(qq平台的端口一定要写587,其他的会卡住;163邮箱的尝试过写465或者587但是都会卡住)
log4j.appender.MAIL.SMTPPort=587
#邮件标题
log4j.appender.MAIL.Subject=ErrorMessage
#发件邮箱
log4j.appender.MAIL.From=123@qq.com
#收件邮箱
log4j.appender.MAIL.To=456@qq.com
#发件箱登陆用户名(与发件邮箱一致)
log4j.appender.MAIL.SMTPUsername=123@qq.com
#发件箱登陆密码(这里在邮箱开启SMTP服务后会给你一串码)
log4j.appender.MAIL.SMTPPassword=123123123
#是否打印调试信息,如果选true,则会输出和SMTP之间的握手等详细信息
log4j.appender.MAIL.SMTPDebug=true
#邮件主题
log4j.appender.MAIL.layout=org.apache.log4j.PatternLayout
log4j.appender.MAIL.layout.ConversionPattern= %d{yyyy-MM-dd HH:mm:ss.SSS} %5l [%5p] - %m%n
!!!配置到这里就可以发送邮件啦。
重写SMTPAppender
1、异步发送
默认的SMTPAppender是同步发送邮件的,就是说如果检测到日志打印了告警级别的日志,业务代码会等待邮件发送完毕再执行,执行效果如下:
所以最好改造一下进行异步发送,重写的appender如下:
public class ASMTPAppender extends SMTPAppender implements UnrecognizedElementHandler { ExecutorService fixedThreadPool = new ThreadPoolExecutor(2, 5, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy()); @Override protected void sendBuffer() { try { String s = this.formatBody(); boolean allAscii = true; for(int i = 0; i < s.length() && allAscii; ++i) { allAscii = s.charAt(i) <= 127; } MimeBodyPart part; if (allAscii) { part = new MimeBodyPart(); part.setContent(s, this.layout.getContentType()); } else { try { ByteArrayOutputStream os = new ByteArrayOutputStream(); Writer writer = new OutputStreamWriter(MimeUtility.encode(os, "quoted-printable"), "UTF-8"); writer.write(s); writer.close(); InternetHeaders headers = new InternetHeaders(); headers.setHeader("Content-Type", this.layout.getContentType() + "; charset=UTF-8"); headers.setHeader("Content-Transfer-Encoding", "quoted-printable"); part = new MimeBodyPart(headers, os.toByteArray()); } catch (Exception var7) { StringBuffer sbuf = new StringBuffer(s); for(int i = 0; i < sbuf.length(); ++i) { if (sbuf.charAt(i) >= 128) { sbuf.setCharAt(i, '?'); } } part = new MimeBodyPart(); part.setContent(sbuf.toString(), this.layout.getContentType()); } } Multipart mp = new MimeMultipart(); mp.addBodyPart(part); this.msg.setContent(mp); this.msg.setSentDate(new Date()); // 使用线程池异步发送邮件 fixedThreadPool.execute(() -> { try { Transport.send(this.msg); } catch (MessagingException var6) { LogLog.error("Error occured while sending e-mail notification.", var6); } }); } catch (MessagingException var8) { LogLog.error("Error occured while sending e-mail notification.", var8); } catch (RuntimeException var9) { LogLog.error("Error occured while sending e-mail notification.", var9); } } }
改造后的执行效果如下:
2、过滤监控文件
默认的SMTPAppender会发送所有高于log4j配置文件中log4j.appender.MAIL.Threshold属性的日志,源码见org.apache.log4j.AppenderSkeleton
public synchronized void doAppend(LoggingEvent event) { if (this.closed) { LogLog.error("Attempted to append to closed appender named [" + this.name + "]."); } else if (this.isAsSevereAsThreshold(event.getLevel())) { Filter f = this.headFilter; while(true) { if (f != null) { switch (f.decide(event)) { case -1: return; case 0: f = f.getNext(); continue; case 1: break; default: continue; } } this.append(event); return; } } }
// 这个方法会根据log4j配置文件中写入的优先级进行判断,大于等于写入的优先级才会发送邮件 public boolean isAsSevereAsThreshold(Priority priority) { return this.threshold == null || priority.isGreaterOrEqual(this.threshold); }
如果我们想监控某一个或者某几个包下的文件日志情况,可以重写append和checkEntryConditions方法对判断条件进行修改
可以在append方法调用checkEntryConditions的时候传入LoggingEvent
public void append(LoggingEvent event) { if (this.checkEntryConditions(event)) { event.getThreadName(); event.getNDC(); event.getMDCCopy(); if (this.locationInfo) { event.getLocationInformation(); } event.getRenderedMessage(); event.getThrowableStrRep(); this.cb.add(event); if (this.evaluator.isTriggeringEvent(event)) { this.sendBuffer(); } } }
对LoggingEvent中的属性进行判断,如果不符合条件不进行邮件内容的组装和发送
protected boolean checkEntryConditions(LoggingEvent event) { // 判断是否包含指定包名,这里的includePackage需要在重写的appender中定义并提供getter和setter方法,includePackage参数与log4j.appender.MAIL.includePackage属性对应 if (includePackage != null && includePackage.contains(event.getLoggerName())) { return false; } if (this.msg == null) { this.errorHandler.error("Message object not configured."); return false; } else if (this.evaluator == null) { this.errorHandler.error("No TriggeringEventEvaluator is set for appender [" + this.name + "]."); return false; } else if (this.layout == null) { this.errorHandler.error("No layout set for appender named [" + this.name + "]."); return false; } else { return true; } }
写在最后:个人认为在日志中耦合邮件的发送功能并不是一个好的选择,本文只是提供一个实现方案,有如需要可以选择EFK等监控平台进行日志的监控和邮件的发送。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构