日志的作用

        日志可以用来定位问题、性能分析、运营监控、业务分析等。接下来,我将介绍如何在开发过程中有效地输出日志以及要注意哪些问题。

日志的级别

        java的日志级别我一般使用以下几个级别,分别为debug、info、warn、error。debug用于开发环境或者测试环境,在生产环境下必须关闭debug级别输出和控制台日志输出,因为在大并发情况下会影响程序性能,当你在大并发的环境下无法排查出程序慢的原因,有可能是日志输出导致的。

        info用于输出程序的关键运行步骤,warn输出潜在的警告日志,error输出错误日志。

异常日志的处理

        不要使用Exception.printStackTrace(),printStackTrace只会输出日志到控制台,使用log.error把堆栈信息输出error文本日志,方便记录并定位问题。

        不要在catch代码块中throw new Exception,除非catch输出了当前的error日志,不然抛出新的异常会难以定位问题代码。

日志的格式

        标准化的日志输出有助于统一团队规范,规范化可以提高我们的开发、运维效率。以下是一般情况下的日志输出格式:时间、线程名、类和方法名、行号、请求参数、响应参数等,具体请依据业务进行裁剪。

追踪日志

        如何定位某一次线程的处理全过程?因为java输出的线程id会在日志文本中重复,为了追踪某次的线程处理过程,可以在日志中增加追踪id,使用org.slf4j.MDC增加追踪id,方便查看日志流程。代码和xml配置如下:

org.slf4j.MDC.put("trace.id",org.apache.commons.lang3.RandomStringUtils.randomNumeric(4));
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
 <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{trace.id}] %-5level %logger{50} - %msg%n</pattern>
</encoder>

日志的安全

        日志内容,如密码、个人隐私数据等,对输出的日志进行过滤或脱敏

        日志文件大小,日志文件需要进行切割和归档,避免撑爆硬盘或单个文件太大导致无法打开阅读。

日志框架

        java的日志框架众多有log4j、log4j2、logback、logback-spring,使用SLF4J统一日志代码,方便我们切换不同的日志框架。

        在Spring Boot中日志加载优先级:logback-spring>logback>Log4j2>Log4j

        在启动参数中使用 -Dlogging.config=classpath:my-custom-logback.xml 指定自定义的日志配置文件。也可以在配置文件中指定配置logging.config=classpath:custom-logback.xml

        优先选择logback-spring,因为它利用 Spring 提供的一些特性,比如占位符(placeholders)和环境变量的支持,可以实现日志配置化参数。同时Logback-Spring有无重启重新加载配置机制:<configuration scan="true" scanPeriod="30 seconds">

关于sql日志

        对于sql日志,使用alibaba druid数据源的监控功能,可以查看sql的统计信息与日志执行情况。

spring boot 下开启日志输出配置:

package com.moreair;

import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

@Configuration
public class DruidWebConfig {

    @Value("${spring.datasource.druid.statViewServlet.urlPattern}")
    private String servletUrlPattern;

    @Value("${spring.datasource.druid.statViewServlet.loginUsername}")
    private String loginUsername;

    @Value("${spring.datasource.druid.statViewServlet.loginPassword}")
    private String loginPassword;

    @Value("${spring.datasource.druid.webStatFilter.urlPattern}")
    private String webStatFilterUrlPattern;

    @Value("${spring.datasource.druid.webStatFilter.exclusions}")
    private String webStatFilterExclusions;

    @Bean
    public ServletRegistrationBean druidServletRegistrationBean() {
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
        servletRegistrationBean.setServlet(new StatViewServlet());
        servletRegistrationBean.addUrlMappings(servletUrlPattern);
        servletRegistrationBean.addInitParameter("allow", "");
        servletRegistrationBean.addInitParameter("deny", "");
        servletRegistrationBean.addInitParameter("loginUsername", loginUsername);
        servletRegistrationBean.addInitParameter("loginPassword", loginPassword);
        return servletRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean duridFilterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        Map<String, String> initParams = new HashMap<String, String>();
        //设置忽略请求
        initParams.put("exclusions", webStatFilterExclusions);
        filterRegistrationBean.setInitParameters(initParams);
        filterRegistrationBean.addUrlPatterns(webStatFilterUrlPattern);
        return filterRegistrationBean;
    }
}

 yml配置,注意loginUsername和loginPassword是登录用户与密码,urlPattern为url地址:

spring:
  datasource:  
    config:
      username: 
      password: 
      driver: 
      url: 
    druid:
      initialSize: 5
      maxActive: 100
      maxPoolPreparedStatementPerConnectionSize: 20
      #配置获取连接等待超时的时间,毫秒,配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁
      maxWait: 60000
      #连接保持空闲而不被驱逐的最小时间,单位是毫秒
      minEvictableIdleTimeMillis: 30000
      #      最小连接数
      minIdle: 5
      #是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。默认false
      poolPreparedStatements: true
      #  单位:秒,检测连接是否有效的超时时间。
      #底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法,执行validationQuery语句的超时时间
      validationQueryTimeout: 30
      testOnBorrow: false
      testOnReturn: false
      testWhileIdle: true
      timeBetweenEvictionRunsMillis: 60000
      useGlobalDataSourceStat: true
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
      filters: stat
      webStatFilter:
        enabled: true
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
        profileEnable: true
        sessionStatEnable: true
        urlPattern: /*
      statViewServlet:
        enabled: true
        loginPassword: admin_aa
        loginUsername: admin321
        urlPattern: /druid/*
    type: com.alibaba.druid.pool.DruidDataSource

springmvc项目需要在web.xml中配置:

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">
    <!-- 添加druid监控-->
    <servlet>
        <servlet-name>DruidStatView</servlet-name>
        <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
        <init-param>
            <!-- 用户名 -->
            <param-name>loginUsername</param-name>
            <param-value>user_druid</param-value>
        </init-param>
        <init-param>
            <!-- 密码 -->
            <param-name>loginPassword</param-name>
            <param-value>user_kk_in</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>DruidStatView</servlet-name>
        <url-pattern>/druid/*</url-pattern>
    </servlet-mapping>
    <!-- 添加Web应用等监控-->
    <filter>
        <filter-name>DruidWebStatFilter</filter-name>
        <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
        <init-param>
            <param-name>exclusions</param-name>
            <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
        </init-param>
        <init-param>
            <param-name>profileEnable</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>principalCookieName</param-name>
            <param-value>USER_COOKIE</param-value>
        </init-param>
        <init-param>
            <param-name>principalSessionName</param-name>
            <param-value>USER_SESSION</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>DruidWebStatFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

关于日志入库

        使用EFK或者ELK进行日志收集、查看。如果想要对日志进行二次处理,可以使用kafka将日志采集到消息队列中,es可以接收日志,同时也可以编写消费端代码对日志进行二次处理。

最后附上一些常用的日志配置:

 logback-spring,其中log.level通过配置文件传入从而实现日志级别配置化。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <springProperty scope="context" name="LOG_LEVEL" source="log.level"/>
    <springProperty scope="context" name="NAME" source="spring.application.name"/>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoder 默认配置为PatternLayoutEncoder -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <pattern>%red(%d{yyyy-MM-dd HH:mm:ss}) %green(%5p) %highlight([%t]) [%X{trace.id}]
                %boldYellow(%replace(%caller{1}){'\t|Caller.{1}0|\r\n', ''}) - %cyan(%m%n)
            </pattern>
        </encoder>
    </appender>
    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <File>logs/${NAME}/info/info.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/${NAME}/info/info-%d{yyyyMMdd}.log.%i</fileNamePattern>
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>1</maxHistory>
            <totalSizeCap>1024MB</totalSizeCap>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{trace.id}] %-5level %logger{50} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>
    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <File>logs/${NAME}/error/error.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/${NAME}/error/error-%d{yyyyMMdd}.log.%i
            </fileNamePattern>
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>1</maxHistory>
            <totalSizeCap>500MB</totalSizeCap>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{trace.id}] %-5level %logger{50} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>
    <appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter>
        <File>logs/${NAME}/debug/debug.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/${NAME}/debug/debug-%d{yyyyMMdd}.log.%i
            </fileNamePattern>
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>1</maxHistory>
            <totalSizeCap>500MB</totalSizeCap>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{trace.id}] %-5level %logger{50} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
    </appender>
    <root level="${LOG_LEVEL}}">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="DEBUG"/>
        <appender-ref ref="ERROR"/>
        <appender-ref ref="INFO"/>
    </root>
</configuration>

log4j:

log4j.rootCategory=,info,error
#控制台输出级别
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Threshold=INFO
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%d{ISO8601}] [traceId-%X{trace.id}] [%t:%l]-[%p] %m%n
#DEBUG输出级别
log4j.appender.debug=org.apache.log4j.RollingFileAppender
log4j.appender.debug.File=..\\logs\\app-name\\debug\\debug.log
log4j.appender.debug.MaxFileSize=20MB
log4j.appender.debug.MaxBackupIndex=10
log4j.appender.debug.Append=true
log4j.appender.debug.Threshold=DEBUG
log4j.appender.debug.layout=org.apache.log4j.PatternLayout
log4j.appender.debug.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} [%t:%l] [traceId-%X{trace.id}] - [%p] %m%n
#INFO输出级别
log4j.appender.info=org.apache.log4j.RollingFileAppender
log4j.appender.info.File=..\\logs\\app-name\\info\\info.log
log4j.appender.info.MaxFileSize=100MB
log4j.appender.info.MaxBackupIndex=10
log4j.appender.info.Append=true
log4j.appender.info.Threshold=INFO
log4j.appender.info.layout=org.apache.log4j.PatternLayout
log4j.appender.info.layout.ConversionPattern=%-d{yyyy-MM-dd HH\:mm\:ss} [%c]-[%p] [traceId-%X{trace.id}] [%t] (%F\:%L) %m %n
#ERROR输出级别
log4j.appender.error=org.apache.log4j.RollingFileAppender
log4j.appender.error.MaxFileSize=20MB
log4j.appender.error.MaxBackupIndex=10
log4j.appender.error.Threshold=ERROR
log4j.appender.error.Append=true
log4j.appender.error.File=..\\logs\\app-name\\error\\error.log
log4j.appender.error.layout=org.apache.log4j.PatternLayout
log4j.appender.error.layout.ConversionPattern=%-d{yyyy-MM-dd HH\:mm\:ss} [%c]-[%p] [traceId-%X{trace.id}] [%t] (%F\:%L) %m %n

 喜欢的可以关注本人公众号:moreair,定期分享文章!

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐