springboot项目中日志使用

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;
    }

}

需求说明 (1)使用UserDaoImp1类的方法查找用户,并用User类的getUserInfo()方法输出用户信息 (2)使用一个不存在的用户名查找用户,使用try-catch对抛出的异常进行处理 实现思路及关键代码 (1)在测试类调用UserDaoImp类的addUser(User user)方法,添加用户,然后用findUser(String uName)方法查找并输出用户信息 (2)在测试类调用UserDaoImp1类的findUser(String uName)方法,使用不存在的用户名查找用户,并试图输出用户信息 (3)对抛出的异常使用try-catch进行异常处理。 实践二:使用try-catch-finally进行异常处理 需求说明 (1)对实践1的异常使用try-catch-finally进行异常处理 (2)在finally块输出是否抛出了异常 实现思路及关键代码 (1)在任务一的代码上增加finally块 (2)为了判断在finally块输出是否抛出异常,可以设置一个变量,在catch块里修改这个变量 实践三:使用throw和throws 需求说明 修改UserDaoImpl类的updateUser(User user)方法,要求如果用户id被修改,则: (1)不执行更新 (2)抛出一个Exception异常 (3)异常消息是“用户id不能修改” 实现思路及关键代码 (1)修改UserDao类的updateUser(User user)方法,声明抛出异常 (2)修改UserDaoImpl类的updateUser(User user)方法,加入判断语句,并抛出异常 (3)在测试类调用,并进行异常处理 实践四:使用log4j 需求说明 (1)使用log4j输出日志信息 (2)查看输出日志信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值