关于日志我知道哪些?

260 阅读5分钟

1.说一些理论

1.1 什么是日志

生活中的日志是记录你生活的点点滴滴,让它把你内心的世界表露出来,更好的诠释自己的内心世界,而电脑里的日志可以是有价值的信息宝库,也可以是毫无价值的数据泥潭。

非常喜欢百科的解释。用一句话来讲的话,日志是记录代码执行的过程。 而我们需要明确以及关注的是: 哪些执行过程需要记录、如何将这些过程记录下来、记录下来之后用来干什么

1.2 为什么要记录日志(哪些需要记录)

  • 代码调试:通过打印变量值/代码块的流转信息,可以帮助我们验证代码实现是否符合预期
  • 问题定位:程序出现故障或者非预期业务信息时候,可以帮助我们快速定位问题
  • 指标监控:通过打印业务身份信息以及关键业务日志可以对核心业务指标/性能指标进行监控
  • 行为分析:记录用户的行为并做离线分析,可以对用户进行画像分析

1.3 什么时候记录日志

  • 程序入口/结束: 在程序入口打印请求参数以及返回结果,这个可以帮助我们记录外部传入是什么,我们返回的结果是什么,有助于撕逼
  • 条件表达式的流转:任何一个有if/else的地方需要打印日志来记录 代码的走向
  • 异常捕获:代码执行有运行期异常; 业务逻辑不符合预期等等
  • 第三方依赖:调用下游接口,需要打印请求/返回结果/rt等信息
  • 关键的执行节点: 对业务的关键节点需要打印日志,这对于排查问题非常有帮助

1.4 如何将这些过程记录下来

日志记录本质上是一个文件流的io操作。讨论如何记录的问题,只关注采用哪个轮子以及每行的日志格式内容能不能看懂。

  • 轮子: SLS4J框架(至于是logback还是log4j都行)
  • 日志格式(不同的业务场景,不同的的日志归类可能不同,这里只关注最通用的):
[时间][日志级别][traceId/rpcId][线程][类.方法.代码行]|[业务身份]|[业务标识]|[扩展信息]

1.5 日志的种类

类别格式化
业务日志
错误日志
监控日志
其他日志

2. show-code

日志(轮子)的配置

logback配置构成

  • configuration 根节点
    • appender

    • logger

    • property

“日志内容” -> looger -> appender

appender:

  • 按照指定的<-layout->输出日志内容并且转换<-property->;
  • 不同的appender有不同的输出日志的方式(控制台/文件);
  • 不同的appender IO写入的方式不同(同步/异步)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- https://github.com/spring-projects/spring-boot/blob/v1.5.13.RELEASE/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
  

    <property name="APP_NAME" value="appName"/>
    <property name="LOG_PATH" value="${user.home}/${APP_NAME}/logs"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date %-4level [%C{0}.%M:%line] - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
    </appender>

    <!--基础日志-->
    <appender name="BASE_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/service.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/service.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
            <maxFileSize>1GB</maxFileSize>
            <totalSizeCap>10GB</totalSizeCap>
            <maxHistory>3</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date %level [%C{0}.%M:%L] [%X{EAGLEEYE_TRACE_ID}] - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    

    <root level="INFO">
        <appender-ref ref="BASE_LOG"/>
        <appender-ref ref="ERROR_LOG"/>
    </root>

</configuration>

日志的高级玩法

日志如何自动添加变量

使用MDC的方式

  1. MDC(Mapped Diagnostic Context),它是Log4J和Logback框架中提供的一个线程局部变量,可以在应用程序的任何地方,设置这些变量的值,然后就可以在日志文件中以变量的形式输出这些值。
  2. 可以在应用程序的任何地方,动态地设置变量的值,然后使用MDC的“put”方法将其存储在其内部的HashMap中,在程序打印日志时,就可以使用MDC的“get”方法将其取出,从而实现日志文件自动添加变量。
//在某个线程中设置MDC的值
MDC.put("key1","value1");
MDC.put("key2","value2");
//在日志中使用MDC的值
logger.info("MDC中的变量key1的值为:{},key2的值为:{}",MDC.get("key1"),MDC.get("key2"));
//记得移除MDC
MDC.remove("key1");
MDC.remove("key2");`

  1. 在xml配置读,代码指定写
<conversionRule conversionWord="mdc" converterClass="org.slf4j.MDC"/>

<!-- 将MDC变量放入日志中 --> 
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern> %d{"yyyy-MM-dd HH:mm:ss"} %-5level [%X{mdc}] - %msg%n </pattern> </layout> 
</appender>

怎么让日志并行打印

<appender name="applicationLogFile" class="ch.qos.logback.classic.AsyncAppender">
    <queueSize>4096</queueSize>
    <includeCallerData>false</includeCallerData>
    <neverBlock>true</neverBlock>
    <appender-ref ref="APPLICATION"/>
</appender>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern> %d{"yyyy-MM-dd HH:mm:ss"} %-5level [%X{mdc}] - %msg%n </pattern> </layout> 
</appender>

怎么实现一个切面自动打印入参和出参数

自定义注解+切面

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodLog {

    /**
     * 关键字,默认是方法名
     *
     * @return {@link String}
     */
    String monitorKey() default "";

    /**
     * 是否需要打印方法的返回结果,默认打印
     *
     * @return boolean
     */
    boolean noResult() default false;
}

public class MethodLogAnnotationAspect implements Ordered {

    public static Logger hsfAccessLog = LoggerFactory.getLogger("XXXX");

    /**
     * 类上的注解
     */
    @Around("@within(methodLog)")
    public Object processClassAnnotation(ProceedingJoinPoint pjp, MethodLog methodLog) throws Throwable {
      
        MethodSignature method = (MethodSignature)pjp.getSignature();
        methodLog = Optional.ofNullable(method.getMethod().getAnnotation(MethodLog.class)).orElse(methodLog);

        return process(pjp, methodLog);
    }

    /**
     * 方法上的注解
     */
    @Around("@annotation(methodLog)")
    public Object processMethodAnnotation(ProceedingJoinPoint pjp, MethodLog methodLog) throws Throwable {
        return process(pjp, methodLog);
    }

    private Object process(ProceedingJoinPoint pjp, MethodLog methodLog) throws Throwable {

        MonitorLog monitorLog = MonitorLog.start("METHOD_ASPECT ", hsfAccessLog,
            "key",
            "method",
            "appName",
            "clientIp",
            "args",
            "result");
        // 如果没有设置monitorKey,则默认为方法名称
        String monitorKey = methodLog.monitorKey();
        if (Strings.isNullOrEmpty(monitorKey)) {
            monitorKey = pjp.getSignature().getName();
        }
        String invokeInterfaceName = getInvokeInterfaceName(pjp);

        try {
            Object methodResult = pjp.proceed();
            monitorLog.success(
                monitorKey + " ",
                invokeInterfaceName + " ",
                XXXUtil.getClientAppName() + " ",
                XXXUtil.getClientIp() + " ",
                JSON.toJSONString(pjp.getArgs()) + " ",
                (methodLog.noResult() ? null : JSON.toJSONString(methodResult)) + " ");

            return methodResult;
        } catch (Exception e) {
           
            throw e;
        }
    }

3.日志打印规范

  1. 日志一定要规范化,封装统一的输出工具,用于监控、数据分析、问题查询; |业务身份|traceId|核心业务信息

  2. 关键的业务逻辑节点需要打印日志 if/else

  3. 尽可能避免使用JSONObject.toString,可以直接使用实体的toString (高并发场景火焰图发现json序列化耗时还是挺高的)

  4. 避免打印重复的日志,比如method1 调用 method2 ,只需要在method2打印入参和返回即可

  5. 避免打印无关紧要日志,比如log.info("method invoke this ponit ")

    👇下面是一些通用的规范

  • 日志格式:日志格式需要统一,采用 standardized logging format 。
  • 日志类型:按照日志的级别分类,分为 ERROR、WARN、INFO、DEBUG、TRACE 五类。
  • 日志输出:在生产环境中,需要设置日志级别,限制控制日志输出,降低性能损耗。
  • 日志审查:日志质量需要及时检查,定期进行日志内容审查。
  • 上下文信息:日志格式需要携带上下文信息,例如应用名称、模块名称、业务流程标识等,用于定位问题。
  • 异常信息:在发生异常的地方,需要记录详细的异常信息,例如异常类型、异常信息、异常栈等。