前言
SpringBoot对所有内部日志使用通用日志记录,但保留底层日志实现。为Java Util Logging、Log4J2和Logback提供了默认配置。在不同的情况下,日志记录器都预先配置为使用控制台输出,同时还提供可选的文件输出。默认情况下,SpringBoot使用Logback进行日志记录。
日志级别有(从高到低):FATAL(致命),ERROR
(
错误),
WARN
(
警告),
INFO
(
信息),
DEBUG
(
调试),
TRACE
(
跟踪)或者 OFF
(关闭),默认的日志配置在消息写入时将消息回显到控制台。默认情况下,将记录错误级别、警告级别和信息级别的消息。
PS:Logback does not have a FATAL
level. It is mapped to ERROR Logback没有FATAL致命级别。它被映射到ERROR错误级别
详情请戳官方文档:https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#boot-features-logging
本文主要记录Logback日志输出到文件以及实时输出到web页面
输出到文件
我们创建SpringBoot项目时,spring-boot-starter已经包含了spring-boot-starter-logging,不需要再进行引入依赖
标准日志格式
2014-03-05 10:57:51.112 INFO 45469 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.52 2014-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2014-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1358 ms 2014-03-05 10:57:51.698 INFO 45469 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/] 2014-03-05 10:57:51.702 INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
- Date and Time: Millisecond precision and easily sortable. 日期和时间:毫秒精度,易于排序。
- Log Level:
ERROR
,WARN
,INFO
,DEBUG
, orTRACE
. 日志级别:错误、警告、信息、调试或跟踪。 - Process ID. 进程ID。
- A
---
separator to distinguish the start of actual log messages. 分隔符,用于区分实际日志消息的开始。 - Thread name: Enclosed in square brackets (may be truncated for console output). 线程名称:括在方括号中(可能会被截断以用于控制台输出)。
- Logger name: This is usually the source class name (often abbreviated). 日志程序名称:这通常是源类名称(通常缩写)。
- The log message. 日志消息。
如何打印日志?
方法1
/** * 配置内部类 */ @Controller @Configuration class Config { /** * 获取日志对象,构造函数传入当前类,查找日志方便定位 */ private final Logger log = LoggerFactory.getLogger(this.getClass()); @Value("${user.home}") private String userName; /** * 端口 */ @Value("${server.port}") private String port; /** * 启动成功 */ @Bean public ApplicationRunner applicationRunner() { return applicationArguments -> { try { InetAddress ia = InetAddress.getLocalHost(); //获取本机内网IP log.info("启动成功:" + "http://" + ia.getHostAddress() + ":" + port + "/"); log.info("${user.home} :" + userName); } catch (UnknownHostException ex) { ex.printStackTrace(); } }; } }
方法2 使用lombok的@Slf4j,帮我们创建Logger对象,效果与方法1一样
/** * 配置内部类 */ @Slf4j @Controller @Configuration class Config { @Value("${user.home}") private String userName; /** * 端口 */ @Value("${server.port}") private String port;/** * 启动成功 */ @Bean public ApplicationRunner applicationRunner() { return applicationArguments -> { try { InetAddress ia = InetAddress.getLocalHost(); //获取本机内网IP log.info("启动成功:" + "http://" + ia.getHostAddress() + ":" + port + "/"); log.info("${user.home} :" + userName); } catch (UnknownHostException ex) { ex.printStackTrace(); } }; } }
简单配置
如果不需要进行复杂的日志配置,则在配置文件中进行简单的日志配置即可,默认情况下,SpringBoot日志只记录到控制台,不写日志文件。如果希望在控制台输出之外编写日志文件,则需要进行配置
logging: path: /Users/Administrator/Desktop/杂七杂八/ims #日志文件路径 file: ims.log #日志文件名称 level: root: info #日志级别 root表示所有包,也可以单独配置具体包 fatal error warn info debug trace off
重新启动项目
打开ims.log
扩展配置
Spring Boot包含许多Logback扩展,可以帮助进行高级配置。您可以在您的logback-spring.xml配置文件中使用这些扩展。如果需要比较复杂的配置,建议使用扩展配置的方式
PS:SpringBoot推荐我们使用带-spring后缀的 logback-spring.xml 扩展配置,因为默认的的logback.xml标准配置,Spring无法完全控制日志初始化。(spring扩展对springProfile节点的支持)
以下是项目常见的完整logback-spring.xml,SpringBoot默认扫描classpath下面的logback.xml、logback-spring.xml,所以不需要再指定spring.logging.config,当然,你指定也没有问题
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!--日志文件主目录:这里${user.home}为当前服务器用户主目录--> <property name="LOG_HOME" value="${user.home}/log"/> <!--日志文件名称:这里spring.application.name表示工程名称--> <springProperty scope="context" name="APP_NAME" source="spring.application.name"/> <!--默认配置--> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <!--配置控制台(Console)--> <include resource="org/springframework/boot/logging/logback/console-appender.xml"/> <!--配置日志文件(File)--> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--设置策略--> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日志文件路径:这里%d{yyyyMMdd}表示按天分类日志--> <FileNamePattern>${LOG_HOME}/%d{yyyyMMdd}/${APP_NAME}.log</FileNamePattern> <!--日志保留天数--> <MaxHistory>15</MaxHistory> </rollingPolicy> <!--设置格式--> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> <!-- 或者使用默认配置 --> <!--<pattern>${FILE_LOG_PATTERN}</pattern>--> <charset>utf8</charset> </encoder> <!--日志文件最大的大小--> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>100MB</MaxFileSize> </triggeringPolicy> </appender> <!-- 多环境配置 按照active profile选择分支 --> <springProfile name="dev"> <!--root节点 全局日志级别,用来指定最基础的日志输出级别--> <root level="INFO"> <appender-ref ref="FILE"/> <appender-ref ref="CONSOLE"/> </root> <!-- 子节点向上级传递 局部日志级别--> <logger level="WARN" name="org.springframework"/> <logger level="WARN" name="com.netflix"/> <logger level="DEBUG" name="org.hibernate.SQL"/> </springProfile> <springProfile name="prod"> </springProfile> </configuration>
启动项目,去到${user.home}当前服务器用户主目录,日志按日期进行产生,如果项目产生的日志文件比较大,还可以按照小时进行.log文件的生成
当然,使用简单配置照样能进行按日期分类
logging: path: ${user.home}/log/%d{yyyyMMdd} #日志文件路径 这里${user.home}为当前服务器用户主目录 file: ${spring.application.name}.log #日志文件名称 ${spring.application.name}为应用名 level: root: info #日志级别 root表示所有包,也可以单独配置具体包 fatal error warn info debug trace off
输出到Web页面
我们已经有日志文件.log了,为什么还要这个功能呢?(滑稽脸)为了偷懒!
当我们把项目部署到Linux服务器,当你想看日志文件,还得打开xshell连接,定位到log文件夹,麻烦;如果我们把日志输出到Web页面,当做超级管理员或者测试账号下面的一个功能,点击就开始实时获取生成的日志并输出在Web页面,是不是爽很多呢?
PS:这个功能可得小心使用,因为日志会暴露很多信息
LoggingWSServer
使用WebSocket实现实时获取,建立WebSocket连接后创建一个线程任务,每秒读取一次最新的日志文件,第一次只取后面200行,后面取相比上次新增的行,为了在页面上更加方便的阅读日志,对日志级别单词进行着色(PS:如何创建springboot的websocket,请戳:SpringBoot系列——WebSocket)
package cn.huanzi.qch.springbootlogback; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thymeleaf.util.StringUtils; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * WebSocket获取实时日志并输出到Web页面 */ @Slf4j @Component @ServerEndpoint(value = "/websocket/logging", configurator = MyEndpointConfigure.class) public class LoggingWSServer { @Value("${spring.application.name}") private String applicationName; /** * 连接集合 */ private static Map<String, Session> sessionMap = new ConcurrentHashMap<String, Session>(); private static Map<String, Integer> lengthMap = new ConcurrentHashMap<String, Integer>(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session) { //添加到集合中 sessionMap.put(session.getId(), session); lengthMap.put(session.getId(), 1);//默认从第一行开始 //获取日志信息 new Thread(() -> { log.info("LoggingWebSocketServer 任务开始"); boolean first = true; while (sessionMap.get(session.getId()) != null) { BufferedReader reader = null; try { //日志文件路径,获取最新的 String filePath = System.getProperty("user.home") + "/log/" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + "/"+applicationName+".log"; //字符流 reader = new BufferedReader(new FileReader(filePath)); Object[] lines = reader.lines().toArray(); //只取从上次之后产生的日志 Object[] copyOfRange = Arrays.copyOfRange(lines, lengthMap.get(session.getId()), lines.length); //对日志进行着色,更加美观 PS:注意,这里要根据日志生成规则来操作 for (int i = 0; i < copyOfRange.length; i++) { String line = (String) copyOfRange[i]; //先转义 line = line.replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll(""", """); //处理等级 line = line.replace("DEBUG", "<span style='color: blue;'>DEBUG</span>"); line = line.replace("INFO", "<span style='color: green;'>INFO</span>"); line = line.replace("WARN", "<span style='color: orange;'>WARN</span>"); line = line.replace("ERROR", "<span style='color: red;'>ERROR</span>"); //处理类名 String[] split = line.split("]"); if (split.length >= 2) { String[] split1 = split[1].split("-"); if (split1.length >= 2) { line = split[0] + "]" + "<span style='color: #298a8a;'>" + split1[0] + "</span>" + "-" + split1[1]; } } copyOfRange[i] = line; } //存储最新一行开始 lengthMap.put(session.getId(), lines.length); //第一次如果太大,截取最新的200行就够了,避免传输的数据太大 if(first && copyOfRange.length > 200){ copyOfRange = Arrays.copyOfRange(copyOfRange, copyOfRange.length - 200, copyOfRange.length); first = false; } String result = StringUtils.join(copyOfRange, "<br/>"); //发送 send(session, result); //休眠一秒 Thread.sleep(1000); } catch (Exception e) { //捕获但不处理 e.printStackTrace(); } finally { try { reader.close(); } catch (IOException ignored) { } } } log.info("LoggingWebSocketServer 任务结束"); }).start(); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session) { //从集合中删除 sessionMap.remove(session.getId()); lengthMap.remove(session.getId()); } /** * 发生错误时调用 */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 服务器接收到客户端消息时调用的方法 */ @OnMessage public void onMessage(String message, Session session) { } /** * 封装一个send方法,发送消息到前端 */ private void send(Session session, String message) { try { session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } }
HTML页面
页面收到数据就追加到div中,为了方便新增了几个功能:
清屏,清空div内容
滚动至底部、将div的滚动条滑到最下面
开启/关闭自动滚动,div新增内容后自动将滚动条滑到最下面,点一下开启,再点关闭,默认关闭
PS:引入公用部分,就是一些jquery等常用静态资源
<!DOCTYPE> <!--解决idea thymeleaf 表达式模板报红波浪线--> <!--suppress ALL --> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>IMS实时日志</title> <!-- 引入公用部分 --> <script th:replace="head::static"></script> </head> <body> <!-- 标题 --> <h1 style="text-align: center;">IMS实时日志</h1> <!-- 显示区 --> <div id="loggingText" contenteditable="true" style="100%;height: 600px; overflow: auto;"></div> <!-- 操作栏 --> <div style="text-align: center;"> <button onclick="$('#loggingText').text('')" style="color: green; height: 35px;">清屏</button> <button onclick="$('#loggingText').animate({scrollTop:$('#loggingText')[0].scrollHeight});" style="color: green; height: 35px;">滚动至底部 </button> <button onclick="if(window.loggingAutoBottom){$(this).text('开启自动滚动');}else{$(this).text('关闭自动滚动');};window.loggingAutoBottom = !window.loggingAutoBottom" style="color: green; height: 35px; ">开启自动滚动 </button> </div> </body> <script th:inline="javascript"> //websocket对象 let websocket = null; //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws://localhost:10086/websocket/logging"); } else { console.error("不支持WebSocket"); } //连接发生错误的回调方法 websocket.onerror = function (e) { console.error("WebSocket连接发生错误"); }; //连接成功建立的回调方法 websocket.onopen = function () { console.log("WebSocket连接成功") }; //接收到消息的回调方法 websocket.onmessage = function (event) { //追加 if (event.data) { //日志内容 let $loggingText = $("#loggingText"); $loggingText.append(event.data); //是否开启自动底部 if (window.loggingAutoBottom) { //滚动条自动到最底部 $loggingText.scrollTop($loggingText[0].scrollHeight); } } } //连接关闭的回调方法 websocket.onclose = function () { console.log("WebSocket连接关闭") }; </script> </html>
效果展示
后记
有了日志记录,我们以后写代码时就要注意了,应使用下面的正确示例
//错误示例,这样写只会输出到控制台,不会输出到日志中 System.out.println("XXX"); e.printStackTrace(); //正确示例,既输出到控制台,又输出到日志 log.info("XXX"); log.error("XXX报错",e);
SpringBoot日志暂时先记录到这里,点击官网了解更多:https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/htmlsingle/#boot-features-logging
补充
2019-07-03补充:我们之前只对日志等级关键字进行着色,还是觉得不够,因此又新增了类名着色跟HTML转义
主要修改:
效果:
2019-08-12补充:我发现有时候显示的时候,换行不太准确,我们原先是在行末追加<br/>,但有时候读取出来的一行记录是自动换行后的数据,页面显示效果很丑
因此我改成用正则([d+][d+][d+][d+]-[d+][d+]-[d+][d+] [d+][d+]:[d+][d+]:[d+][d+])去匹配日期,然后再对应的起始下标插入<br/>,从而达到与控制台输出类似的效果
匹配、插入结果
页面效果
异步输出日志
异步输出日志的方式很简单,添加一个基于异步写日志的appender
,并指向原先配置的appender
即可
<!-- 将文件输出设置成异步输出 --> <appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender"> <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 --> <discardingThreshold>0</discardingThreshold> <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 --> <queueSize>256</queueSize> <!-- 添加附加的appender,最多只能添加一个 --> <appender-ref ref="FILE"/> </appender> <!-- 将控制台输出设置成异步输出 --> <appender name="ASYNC-CONSOLE" class="ch.qos.logback.classic.AsyncAppender"> <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 --> <discardingThreshold>0</discardingThreshold> <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 --> <queueSize>256</queueSize> <!-- 添加附加的appender,最多只能添加一个 --> <appender-ref ref="CONSOLE"/> </appender>
原理很简单,主线程将日志扔到阻塞队列中,然后IO操作日志写入文件是通过新起一个线程去完成的
2020-05-26补充
e.printStackTrace();会打出详细异常,异常名称,出错位置,便于调试用,但直接调用会输出到std.err,并没有输出到日志文件中,因此需要先输出到流中再转成字符串
封装工具类
import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; /** * 捕获报错日志处理工具类 */ public class ErrorUtil { /** * Exception出错的栈信息转成字符串 * 用于打印到日志中 */ public static String errorInfoToString(Throwable e) { StringWriter sw = null; PrintWriter pw = null; try { sw = new StringWriter(); pw = new PrintWriter(sw); // 将出错的栈信息输出到printWriter中 e.printStackTrace(pw); pw.flush(); sw.flush(); } finally { if (sw != null) { try { sw.close(); } catch (IOException e1) { e1.printStackTrace(); } } if (pw != null) { pw.close(); } } return sw.toString(); } }
使用
try { //省略其他代码 } catch (Throwable e) { //之前的操作,输出控制台 e.printStackTrace(); //输出到日志文件中 log.error(ErrorUtil.errorInfoToString(e)); }