1、为什么加日志
1.1 日志是什么?
日志文件提供精确的系统记录,根据日志最终定位到错误详情和根源。日志的特点是,它描述一些离散的(不连续的)事件。例如:应用通过一个滚动的文件输出 INFO 或 ERROR 信息,并通过日志收集系统,存储到一些存储引擎(Elasticsearch)中方便查询。
1.2 日志作用
-
打印调试:即可以用日志来记录变量或者某一段逻辑。记录程序运行的流程,即程序运行了哪些代码,方便排查逻辑问题。
-
问题定位:程序出异常或者出故障时快速的定位问题,方便后期解决问题。因为线上生产环境无法 debug,在测试环境去模拟一套生产环境,费时费力。所以依靠日志记录的信息定位问题,这点非常重要。还可以记录流量,后期可以通过 ELK(包括 EFK 进行流量统计)。
-
用户行为日志:记录用户的操作行为,用于大数据分析,比如监控、风控、推荐等等。这种日志,一般是给其他团队分析使用,而且可能是多个团队,因此一般会有一定的格式要求,开发者应该按照这个格式来记录,便于其他团队的使用。当然,要记录哪些行为、操作,一般也是约定好的,因此,开发者主要是执行的角色。
-
根因分析(甩锅必备*):即在关键地方记录日志。方便在和各个终端定位问题时,别人说时你的程序问题,你可以理直气壮的拿出你的日志说,看,我这里运行了,状态也是对的。这样,对方就会乖乖去定位他的代码,而不是互相推脱。
1.3 什么时候记录日志
-
系统初始化:系统或者服务的启动参数。核心模块或者组件初始化过程中往往依赖一些关键配置,根据参数不同会提供不一样的服务。务必在这里记录 INFO 日志,打印出参数以及启动完成态服务表述。
-
编程语言提示异常:如今各类主流的编程语言都包括异常机制,业务相关的流行框架有完整的异常模块。这类捕获的异常是系统告知开发人员需要加以关注的,是质量非常高的报错。应当适当记录日志,根据实际结合业务的情况使用 WARN 或者 ERROR 级别。
-
业务流程预期不符:除开平台以及编程语言异常之外,项目代码中结果与期望不符时也是日志场景之一,简单来说所有流程分支都可以加入考虑。取决于开发人员判断能否容忍情形发生。常见的合适场景包括外部参数不正确,数据处理问题导致返回码不在合理范围内等等。
-
系统核心角色,组件关键动作:系统中核心角色触发的业务动作是需要多加关注的,是衡量系统正常运行的重要指标,建议记录 INFO 级别日志,比如电商系统用户从登录到下单的整个流程;微服务各服务节点交互;核心数据表增删改;核心组件运行等等,如果日志频度高或者打印量特别大,可以提炼关键点 INFO 记录,其余酌情考虑 DEBUG 级别。
-
第三方服务远程调用:微服务架构体系中有一个重要的点就是第三方永远不可信,对于第三方服务远程调用建议打印请求和响应的参数,方便在和各个终端定位问题,不会因为第三方服务日志的缺失变得手足无措。
2、日志框架
SpringBoot工程自带logback和slf4j的依赖
Slf4j 英文全称为 “ Simple Logging Facade for Java ”,为 Java 提供的简单日志门面。Facade 门面,更底层一点说就是接口。它允许用户以自己的喜好,在工程中通过 Slf4j 接入不同的日志系统。
Logback 是 Slf4j 的原生实现框架,同样也是出自 Log4j 一个人之手,但拥有比 Log4j 更多的优点、特性和更做强的性能,Logback 相对于 Log4j 拥有更快的执行速度。基于我们先前在 Log4j 上的工作,Logback 重写了内部的实现,在某些特定的场景上面,甚至可以比之前的速度快上 10 倍。在保证 Logback 的组件更加快速的同时,同时所需的内存更加少。
配置
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<property resource="logback.properties"/>
<!--appender通过使用该标签指定日志的收集策略-->
<!--name属性指定appender命名-->
<!-- class属性指定输出策略,通常有两种,控制台输出和文件输出-->
<appender name="CONSOLE-LOG" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
</layout>
</appender>
<!--获取比info级别高(包括info级别)但除error级别的日志-->
<appender name="INFO-LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--filter标签,通过使用该标签指定过滤策略-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--level标签指定过滤的类型-->
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<!--encoder标签,使用该标签下的标签指定日志输出格式-->
<encoder>
<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
</encoder>
<!--滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--路径-->
<fileNamePattern>${LOG_INFO_HOME}//%d.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<appender name="ERROR-LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
</encoder>
<!--rollingPolicy标签指定收集策略,比如基于时间进行收集-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--fileNamePattern标签指定生成日志保存路径-->
<fileNamePattern>${LOG_ERROR_HOME}//%d.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!--必填标签,用来指定最基础的日志输出级别-->
<root level="info">
<!--添加append-->
<appender-ref ref="CONSOLE-LOG" />
<appender-ref ref="INFO-LOG" />
<appender-ref ref="ERROR-LOG" />
</root>
</configuration>
异步日志输出
之前的日志配置方式是基于同步的,每次日志输出到文件都会进行一次磁盘IO。采用异步写日志的方式而不让此次写日志发生磁盘IO,阻塞线程从而造成不必要的性能损耗。异步输出日志的方式很简单,添加一个基于异步写日志的appender,并指向原先配置的appender即可
<!-- 异步输出 -->
<appender name="ASYNC-INFO" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="INFO-LOG"/>
</appender>
<appender name="ASYNC-ERROR" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="ERROR-LOG"/>
</appender>
多环境下的日志配置
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!--
简要描述
日志格式 => %d{HH:mm:ss.SSS}(时间) [%-5level](日志级别) %logger{36}(logger名字最长36个字符,否则按照句点分割) - %msg%n(具体日志信息并且换行)
开发环境 => ${basepackage}包下控制台打印DEBUG级别及以上、其他包控制台打印INFO级别及以上
演示(测试)环境 => ${basepackage}包下控制台打印INFO级别及以上、其他包控制台以及文件打印WARN级别及以上
生产环境 => 控制台以及文件打印ERROR级别及以上
日志文件生成规则如下:
文件生成目录 => ${logdir}
当日的log文件名称 => ${appname}.log
其他时候的log文件名称 => ${appname}.%d{yyyy-MM-dd}.log
日志文件最大 => ${maxsize}
最多保留 => ${maxdays}天
-->
<!--自定义参数 -->
<!--用来指定日志文件的上限大小,那么到了这个值,就会删除旧的日志-->
<property name="maxsize" value="30MB" />
<!--只保留最近90天的日志-->
<property name="maxdays" value="90" />
<!--application.yml 传递参数 -->
<!--log文件生成目录-->
<springProperty scope="context" name="logdir" source="resources.logdir"/>
<!--应用名称-->
<springProperty scope="context" name="appname" source="resources.appname"/>
<!--项目基础包-->
<springProperty scope="context" name="basepackage" source="resources.basepackage"/>
<!--输出到控制台 ConsoleAppender-->
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<!--展示格式 layout-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
</pattern>
</layout>
</appender>
<!--输出到文件 FileAppender-->
<appender name="fileLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--
日志名称,如果没有File 属性,那么只会使用FileNamePattern的文件路径规则
如果同时有<File>和<FileNamePattern>,那么当天日志是<File>,明天会自动把今天
的日志改名为今天的日期。即,<File> 的日志都是当天的。
-->
<File>${logdir}/${appname}.log</File>
<!--滚动策略,按照时间滚动 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路径,定义了日志的切分方式——把每一天的日志归档到一个文件中,以防止日志填满整个磁盘空间-->
<FileNamePattern>${logdir}/${appname}.%d{yyyy-MM-dd}.log</FileNamePattern>
<maxHistory>${maxdays}</maxHistory>
<totalSizeCap>${maxsize}</totalSizeCap>
</rollingPolicy>
<!--日志输出编码格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 开发环境-->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="consoleLog"/>
</root>
<!--
additivity是子Logger 是否继承 父Logger 的 输出源(appender) 的标志位
在这里additivity配置为false代表如果${basepackage}中有INFO级别日志则子looger打印 root不打印
-->
<logger name="${basepackage}" level="DEBUG" additivity="false">
<appender-ref ref="consoleLog"/>
</logger>
</springProfile>
<!-- 演示(测试)环境-->
<springProfile name="test">
<root level="WARN">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</root>
<logger name="${basepackage}" level="INFO" additivity="false">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</logger>
</springProfile>
<!-- 生产环境 -->
<springProfile name="prod">
<root level="ERROR">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</root>
</springProfile>
</configuration>
application.yml
#应用配置
resources:
# log文件写入地址
logdir: logs/
# 应用名称
appname: spring-boot-example
# 日志打印的基础扫描包
basepackage: com.spring.demo.springbootexample
使用不同环境启动测试logger配置是否生效,在开发环境下将打印DEBUG级别以上的四条logger记录,在演示环境下降打印INFO级别以上的三条记录并写入文件,在生产环境下只打印ERROR级别以上的一条记录并写入文件
@RequestMapping("/logger")
@ResponseBody
public WebResult logger() {
logger.trace("日志输出 {}", "trace");
logger.debug("日志输出 {}", "debug");
logger.info("日志输出 {}", "info");
logger.warn("日志输出 {}", "warn");
logger.error("日志输出 {}", "error");
return "00";
}
切面日志处理
- 定义注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface OutputLog {
boolean value() default true;
}
- 切面类
@Aspect
@Component
public class LoggerAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggerAspect.class);
@Pointcut("@annotation(com.link.springmvc.logAop.OutputLog)")
public void weblog(){
}
@Around("weblog()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
List<Object> logArgs = Arrays.stream(point.getArgs())
.filter(arg -> (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse)))
.collect(Collectors.toList());
try {
logger.info("请求url={}, 请求参数={}", request.getRequestURI(), JSON.toJSONString(logArgs));
} catch (Exception e) {
logger.error("请求参数获取异常", e);
}
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
try {
logger.info("请求耗时={}ms, 返回结果={}", time, JSON.toJSONString(result));
} catch (Exception e) {
logger.error("返回参数获取异常", e);
}
return result;
}
}