目录
什么是日志?
日志(Log)是一种记录系统或者应用程序在运行期间发生的时间、操作、错误、状态变化等信息的文件或数据集合。广泛应用于计算机系统、软件开发、服务器等。
为什么要学习日志?
其实日志在我们前面就有应用过,如我们使用的 System.out.println() 就是用来打印日志的。通过日志,我们可以发现和定位问题所在,或者根据日志来分析程序的运行过程。
但随着项目复杂度的提升,我们对日志的打印也有了更高的要求,而不仅仅是定位排查问题。比如,需要记录一些用户的操作记录,有可能需要用日志来记录用户的一些喜好,或者需要把日志持久化,便于后序进行数据分析,而 System.out.println() 并不能很好的满足我们的要求,所以我们需要使用一些专业的日志框架。
日志的用途
日志主要是为了发现问题、分析问题、定位问题,此外,日志还有其他用途:
- 系统监控:监控几乎是一个成熟系统的标配,我们可以通过日志记录这个系统的运行状态,每一个方法响应的时间、响应状态等,对数据进行分析,设置不同的规则,超过阈值时进行报警。比如统计日志中关键字的数量,并在关键字达到一定条件时报警。
- 数据采集:
数据采集是⼀个⽐较⼤的范围,采集的数据可以作⽤在很多⽅⾯,⽐如数据统计,推荐排序等。
-
数据统计:统计⻚⾯的浏览量(PV),访客量(UV),点击量等,根据这些数据进⾏数据分析,优化公司运营策略
-
推荐排序:⽬前推荐排序应⽤在各个领域,我们经常接触的各⾏各业很多也都涉及推荐排序,⽐如购物,⼴告,新闻等领域。数据采集是推荐排序⼯作中必须做的⼀环,系统通过⽇志记录用户的浏览历史,停留时⻓,算法⼈员通过分析这些数据,训练模型,给用户做推荐。
-
-
安全监控与审计:随着互联网的发展,许多企业的关键业务越来越多运行于网络之上。⽹络安全越来越受到⼤家的关注,系统安全也成为了项⽬中的⼀个重要环节,安全审计也是系统中⾮常重要的部分。国家的政策法规、⾏业标准等都明确对⽇志审计提出了要求。通过系统⽇志分析,可以判断⼀些⾮法攻击,⾮法调⽤,以及系统处理过程中的安全隐患。
日志的使用
在启动Spring Boot项目的时候,其实就有日志输出了。
可以看到日志中包含了许多信息:包括时间、以及哪个类打印的日志等等。
SpringBoot中内置了日志框架 Slfj4,我们可以在程序中直接调用 Slfj4 来输出日志。
日志框架
SpringBoot内置了日志框架,那么Spring中的日志框架是怎么样的?
SLF4J不同于其他日志框架,它不是一个真正的日志实现,而是一个抽象层,对日志框架制定的一种规范、标准、接口。所有SLF4J并不能独立使用,需要和具体的日志框架配置使用。
在上图中,我们可以看到,Spring中日志框架分为两层,但和我们直接交接的是日志门面,日志的打印是由日志实现完成的,而不是日志门面,日志门面相当于一种中间层,这种模式就门面模式。
门面模式(外观模式)
门面模式(Facade Pattern),也叫外观模式,是一种软件设计模式,它为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用。
门面模式主要包含两种角色:
- 外观角色(Facade):也称为门面角色,是系统对外的统一接口;
- 子系统角色(SubSystem):可以同时有一个或多个SubSystem,每个SubSystem都不是一个单独的类,而是一个类的集合。SubSystem并不知道Facade的存在,对于SubSystem而言,Facade只是另外一个客户端而已(即Facade对SubSystem透明)。
为什么日志框架要门面模式?
其实主要是为了解耦,如果我们直接使用日志实现中的接口来打印日志的话,那么假如我们的项目中使用的是LogBack中的接口来打印日志,那么假如LogBack出现了漏洞,导致我们的日志打印出现问题,那么就需要更换成其他的日志(如JUL等),那么我们就需要将每一处使用LogBack的地方进行修改,这样的成本太高了。所以就需要使用门面模式,相当于一个中介,我们只需要告诉中介
我们的要求,那么中介就会帮我们安排好,这样,就算底层日志实现的接口发生变化,日志门面会帮我们安排好,我们也不需要发生任何改变。
我们可以用代码模拟一下:
//开发人员调用log打印日志
slf4j.log("xxxxxx");
public void log(String msg){
logBack.info(msg);
}
假设默认是使用logBack中的接口来打印的,那么假如logBack出现Bug等问题需要更换底层日志实现接口,那么我们调用的方法不用更改,只需要在 slf4j 中把logBack改成其他日志实现(如log4j)即可。
public void log(String msg){
log4j.info(msg);
}
门面模式优点
- 减少了系统的相互依赖.实现了客⼾端与子系统的耦合关系,这使得⼦系统的变化不会影响到调⽤它 的客⼾端;
- 提⾼了灵活性,简化了客⼾端对子系统的使⽤难度,客户端⽆需关心子系统的具体实现⽅式,而只需 要和门面对象交互即可.
- 提⾼了安全性.可以灵活设定访问权限,不在⻔⾯对象中开通⽅法,就⽆法访问
打印日志
打印日志的步骤:
- 在程序中得到日志对象;
- 利用日志对象输出要打印的内容。
获取日志对象
在程序中获取日志对象需要使用日志工厂 LoggerFactory 来获取。
需要注意的是:我们这里使用的Logger类是在 org.slf4j 包中的。
private static final Logger logger= LoggerFactory.getLogger(LogController.class);
通过日志对象打印日志
当我们获取到 Logger 对象之后,我们就可以借助 Logger 类中的方法来打印出对应的日志。
可以看到,在Logger类中,有很多方法,而我们主要打印日志的方法是:trace()、deBug()、info()、warn()、error()方法。这几种方法有什么区别?我们后面讲。
package com.example.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/log")
public class LogController {
private static final Logger logger= LoggerFactory.getLogger(LogController.class);
@RequestMapping("getLog")
public String getLog(){
logger.info("我是一个info");
logger.error("我是一个error");
logger.debug("我是一个debug");
logger.warn("我是一个warn");
logger.trace("我是一个trace");
return "Ok";
}
}
可以看到,当我们运行并访问之后,虽然控制台有输出我们添加的日志,但只有Info、warn、error这几个出现。
这就涉及到了日志的级别。
日志的级别
日志的级别代表着日志信息对应问题的严重性,为了更快的筛选出符合目标的日志信息。
日志的级别从高到低依次是:FATAL、ERROR、WARN、INFO、DEBUG、TRACE。
- FATAL:致命信息,表示需要立即被处理的系统级错误。
- ERROR:错误信息,级别较高的错误日志信息,但仍然不影响系统的正常运行。
- WARN:警告信息,不影响使用,但要注意的问题。
- INFO:普通信息,用于记录程序正常运行时的一些信息,例如系统启动完成,请求处理完成等。
- DEBUG:调试信息,需要调试时候的关键信息打印。
- TRACE:追踪信息,比DEBUG更细粒度的信息事件。(除非有特殊用意,否则使用DEBUG级别代替即可)
如果我们不手动设置日志的级别,默认打印级别是info,也就是比info等级高的才能打印。
日志级别是开发人员设置的,用来给开发人员看的。日志级别的正确设置,也和开发人员的经验有关。开发人员把error级别的日志设置成info,就很有可能会影响开发人员对项目运行状况的判断。出现error级别错误比较多时,不代表程序一定有问题。测试的bug级别更多是根据现象和影响范围来判断的。
日志级别的使用
SpringBoot中默认使用的门面日志是 SLF4J,默认日志实现是 logBack 。 LogBack没有 FATAL 级别,它被映射到 ERROR 。
其实如果出现FATAL日志,就表示服务已经出现了某个程度的不可用,需要系统管理员紧急介入处理,通常情况下,一个进程生命周期中应该最多只有一次FATAL记录。
SpringBoot默认的日志级别是 info 级别,如果我们想要打印更低级别的日志,那么应该如何修改日志的默认级别?
日志级别配置
既然说配置,那么想要配置日志级别,就需要在配置文件中进行配置。
yml配置文件配置:
logging:
level:
root: debug
properties文件配置:logging.lever.root=debug
运行程序:
可以看到,出现了debug级别以及debug级别之上的日志。
root表示设置当前项目所有文件的默认日志级别,那么如果我们想要在不同文件具有不同的默认日志级别,应该怎么做?
在配置文件中为指定文件配置默认日志级别就可以。
#将controller的日志级别设置为trace
logging:
level:
root: debug
com:
example:
demo:
controller: trace
这样设置就表示:除了controller包下的默认日志级别为trace,其他文件的默认日志为debug。
日志持久化
在上面讲解中,可以看出日志都是输出在控制台上的,但是如果我们把编译器一关,再重新打开,就会发现之前的日志全部消失了,但是在线上环境中,我们需要把日志保存下来,以便出现问题之后能够追溯问题,而把日志保存下来就叫持久化。
日志持久化有两种方式:
- 配置日志文件名
- 配置日志的存储目录
配置日志文件名
yml配置:
logging:
file:
name: log/logger.log
当运行后访问程序,这里会在我们的项目下生成一个log目录,在目录中生成一个文件名为logger的日志文件。
运行查看:
配置文件的存储目录
当我们使用配置日志的存储目录来实现日志的持久化存储,spring会使用默认的文件名来命名文件。如果指定文件目录不存在,也会自动创建。
yml配置:
logging:
file:
path: log
那么如果我们同时指定日志名称,有又指定日志文件的存储目录,那么会执行哪个?
可以看到,如果同时指定文件名和存储目录,会以指定日志文件名为主。也就是说:
指定日志文件名的优先级要高于指定日志文件的存储目录。
日志文件分割配置
如果我们的日志都放在一个文件中,随着项目的运行,日志文件会越来越大,所以我们需要对日志文件进行分割。
其实日志框架也有考虑到这一点,所以如果不进行配置的话,就会自动配置,默认日志文件超过10M就进行分割。
第一个配置是日志文件分割的名称定义规则,第二个则是日志文件的最大容量,当日志文件的大小大于这个最大容量的话,那么就会根据这个日志文件分割的名称定义规则创建出新的文件存储后面的日志信息。
logging.logback.rollingpolicy.file-name-pattern
配置的默认值是 ${LOG_FILE}.%d{yyyy-MM-dd}.%i
即:
这里我们来配置一下日志文件,最大容量为1kb:
logging:
file:
name: D://logs/logger.log
logback:
rolling policy:
file-name-pattern: ${LOG_FILE}.%d{yyyy-MM-dd}.%i
max-file-size: 1KB
level:
root: debug
更简便的日志输出
在上面中,如果我们想要打印日志,需要用日志工厂每次获取日志对象,且每个类都需要添加一遍,这样太繁琐了,所以,出现了一种更简便的方式。
利用Lombok中的 @slf4j 注解输出日志。
添加Lombok依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<optional>true</optional>
</dependency>
package com.example.demo.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequestMapping("/log")
public class LogController {
@RequestMapping("getLog")
public String getLog(){
log.info("我是一个info");
log.error("我是一个error");
log.debug("我是一个debug");
log.warn("我是一个warn");
log.trace("我是一个trace");
return "Ok";
}
}
这里没有log对象啊,为什么添加 @Slf4j 后就可以获取到日志对象?
Lombok 的核心原理是通过注解处理器在编译阶段动态插入代码,并利用字节码操作库确保生成的代码与手动编写的代码在运行时行为一致。这种方式不仅减少了样板代码的编写,还避免了运行时的额外开销。
Lombook利用了java编译器的注解处理机制,在编译阶段扫描代码中的注解,并根据注解生成相应的代码。