前言
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 错误级别
详情请戳官方文档:
本文主要记录 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, or TRACE. 日志级别: 错误, 警告, 信息, 调试或跟踪.
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 日志只记录到控制台, 不写日志文件. 如果希望在控制台输出之外编写日志文件, 则需要进行配置
- spring:
- 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 文件的生成
当然, 使用简单配置照样能进行按日期分类
- spring:
- 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: 这个功能可得小心使用, 因为日志会暴露很多信息
LoggingWebSocketServer
使用 WebSocket 实现实时获取, 建立 WebSocket 连接后创建一个线程任务, 每秒读取一次最新的日志文件, 第一次只取后面 200 行, 后面取相比上次新增的行, 为了在页面上更加方便的阅读日志, 对日志级别单词进行着色(PS: 如何创建 springboot 的 websocket, 请戳: SpringBoot 系列 --WebSocket)
- package cn.huanzi.ims.socket;
- import lombok.extern.slf4j.Slf4j;
- 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.HashMap;
- import java.util.Map;
- /**
- * WebSocket 获取实时日志并输出到 Web 页面
- */
- @Slf4j
- @Component
- @ServerEndpoint(value = "/websocket/logging", configurator = MyEndpointConfigure.class)
- public class LoggingWebSocketServer {
- /**
- * 连接集合
- */
- private static Map<String, Session> sessionMap = new HashMap<String, Session>();
- private static Map<String, Integer> lengthMap = new HashMap<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()) + "/ims.log";
- // 字符流
- reader = new BufferedReader(new FileReader(filePath));
- Object[] lines = reader.lines().toArray();
- // 只取从上次之后产生的日志
- Object[] copyOfRange = Arrays.copyOfRange(lines, lengthMap.get(session.getId()), lines.length);
- for (int i = 0; i <copyOfRange.length; i++) {
- String line = (String) copyOfRange[i];
- // 按等级标颜色, 更加美观
- 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>");
- 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) {
- // 从集合中删除
- for (Map.Entry<String, Session> entry : sessionMap.entrySet()) {
- if (entry.getValue() == session) {
- sessionMap.remove(entry.getKey());
- lengthMap.remove(entry.getKey());
- break;
- }
- }
- }
- /**
- * 发生错误时调用
- */
- @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="width:100%;height: 600px;background-color: ghostwhite; 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 Windows) {
- 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 (Windows.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://www.cnblogs.com/huanzi-qch/p/11041300.html