Fdd鹰眼系统日志链路追踪原理

Posted by CHuiL on July 15, 2021

鹰眼系统日志信息能够将分布式调用链上产生的日志信息串联起来,最主要归功于打印出了traceId
一个普通的请求,在分布式微服务环境下,可能又调用了很多微服务来完成该请求。不同的服务散落在不同的机器上,日志数据也可能保持在各自的机器上,就算将它们汇总在一个文件上,由于日志数据庞大,一条日志的前后可能都是毫不相干的日志信息。根本无法做到跟着请求的调用日志来排查问题。

但是如果有一个唯一标识来标记同一个请求的调用链上产生的日志,那么我们就可以根据该标识查询出该调用链上产生的所有日志信息。

那么此时的问题就为,如何产生traceId以及如何在打印日志时带上traceID

鹰眼系统日志是利用skywalking+logback自带的功能实现打印日志带上traceId的。因为利用了skywalking,所以traceId自然就是使用skywalking产生的traceId。

打印日志带上TraceId

<configuration scan="true" scanPeriod="30 seconds">
    <include optional="true" resource="logback-include.xml"/>           
                                                                             
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">                                         
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %X{fddTraceId} %msg%n</Pattern>
        </encoder>
    </appender>                                                                                                                       
    //自定义"tid"格式转换器
    <conversionRule conversionWord="tid" converterClass="org.apache.skywalking.apm.toolkit.log.logback.v1.x.LogbackPatternConverter"/>
                                                     
    <appender name="SERVER_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGS_DIR:-logs}/server-ELK.log</file>                                         
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${LOGS_DIR:-logs}/server.%d{yyyy-MM-dd}-ELK.log</FileNamePattern>
            <MaxHistory>5</MaxHistory>                                                 
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>      
                <pattern>
                    <pattern>                                  
                        {             
                        "ts": "%date{yyyy-MM-dd HH:mm:ss.SSS}",
                        "tid": "%tid",      
                        "level": "%level",        
                        "thread": "%thread",
                        "class": "%logger{60}:%L",
                        "method": "%method",          
                        "message": "%message",
                        "exception": "%exception{10}",
                        "appId":"${DomainID}"
                        } 
                    </pattern>
                </pattern>      
                <version/>
                <logLevelValue/>
            </providers>
        </encoder>
    </appender>
                                             
    <root>                           
        <level value="${LOGS_LEVEL:-INFO}" />
        <appender-ref ref="STDOUT" />
        <appender-ref ref="SERVER_LOG"/>
    </root>     
</configuration>

先从logback.xml配置文件入手,可以观察到这里定义了tid这个字段来输出traceId,而tid的输出由org.apache.skywalking.apm.toolkit.log.logback.v1.x.LogbackPatternConverter这个类来转换。

这里每个%后面带着的信息,都会有一个对应的converter来生成数据。例如levelLevelConverter来生成,还有date等等。 可以在package ch.qos.logback.classic下的PatternLayout中看到。

public class PatternLayout extends PatternLayoutBase<ILoggingEvent> {

    public static final Map<String, String> defaultConverterMap = new HashMap<String, String>();
    public static final String HEADER_PREFIX = "#logback.classic pattern: ";

    static {
        defaultConverterMap.putAll(Parser.DEFAULT_COMPOSITE_CONVERTER_MAP);

        defaultConverterMap.put("d", DateConverter.class.getName());
        defaultConverterMap.put("date", DateConverter.class.getName());

        defaultConverterMap.put("r", RelativeTimeConverter.class.getName());
        defaultConverterMap.put("relative", RelativeTimeConverter.class.getName());
        
        defaultConverterMap.put("level", LevelConverter.class.getName());
        defaultConverterMap.put("le", LevelConverter.class.getName());
        defaultConverterMap.put("p", LevelConverter.class.getName());
        ....
    }
public class LevelConverter extends ClassicConverter {
    public String convert(ILoggingEvent le) {
        return le.getLevel().toString();
    }
}

这里主要的就是convert这个方法,接受一个event,返回一个字符串。

public abstract String convert(E event);

这里我们看一下tid指定的这个LogbackPatternConverter是如何返回traceId的。

public class LogbackPatternConverter extends ClassicConverter {
    public LogbackPatternConverter() {
    }

    public String convert(ILoggingEvent iLoggingEvent) {
        return "TID: N/A";
    }
}

发现这里根本就没有输出traceId,而是输出一个默认的TID:N/A。真正输出traceId的其实是在skywalking-agent的apm-tooklt-logback-1.x-activation插件上实现的。由该插件对该conver方法进行增强。 可以查看skywalking中关于apm-sniffer/apm-tookit-activation/apm-tookit-logback-1.x-activation的源码。

public class LogbackPatternConverterActivation extends ClassInstanceMethodsEnhancePluginDefine {

    public static final String INTERCEPT_CLASS = "org.apache.skywalking.apm.toolkit.activation.log.logback.v1.x.PrintTraceIdInterceptor";
    public static final String ENHANCE_CLASS = "org.apache.skywalking.apm.toolkit.log.logback.v1.x.LogbackPatternConverter";
    public static final String ENHANCE_METHOD = "convert";

    /**
     * @return the target class, which needs active.
     */
    @Override
    protected ClassMatch enhanceClass() {
        //这里指定增强`LogbackPatternConverter`这个类
        return byName(ENHANCE_CLASS);
    }

    /**
     * @return null, no need to intercept constructor of enhance class.
     */
    @Override
    public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
        return null;
    }

    /**
     * @return the collection of {@link StaticMethodsInterceptPoint}, represent the intercepted methods and their
     * interceptors.
     */
    @Override
    public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodsInterceptPoint[] {
            new InstanceMethodsInterceptPoint() {
                @Override
                //进一步指定了`LogbackPatternConverter`这个类的`convert`方法
                public ElementMatcher<MethodDescription> getMethodsMatcher() {
                    return named(ENHANCE_METHOD).and(takesArgumentWithType(0, "ch.qos.logback.classic.spi.ILoggingEvent"));
                }

                @Override
                public String getMethodsInterceptor() {
                    //交由`PrintTraceIdInterceptor`这个类进行增强的逻辑
                    return INTERCEPT_CLASS;
                }

                @Override
                public boolean isOverrideArgs() {
                    return false;
                }
            }
        };
    }
}

看看org.apache.skywalking.apm.toolkit.activation.log.logback.v1.x.PrintTraceIdInterceptor这个类

public class PrintTraceIdInterceptor implements InstanceMethodsAroundInterceptor {

    @Override
    public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        MethodInterceptResult result) throws Throwable {

    }

    @Override
    public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        Object ret) throws Throwable {
        if (!ContextManager.isActive()) {
            if (allArguments[0] instanceof EnhancedInstance) {
                SkyWalkingContext skyWalkingContext = (SkyWalkingContext) ((EnhancedInstance) allArguments[0]).getSkyWalkingDynamicField();
                if (skyWalkingContext != null) {
                    return "TID:" + skyWalkingContext.getTraceId();
                }
            }
        }
        return "TID:" + ContextManager.getGlobalTraceId();
    }

    @Override
    public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments,
        Class<?>[] argumentsTypes, Throwable t) {

    }
}

在这里就能明白TID是如何被打印出来的了

在logback中配置tid这个字段的conver为skywalking的log-tookit中的LogbackPatternConverter 。使其在输出tid字段数据的时候,调用LogbackPatternConverter的cover方法,但是这个方法并不是真正输出traceId的地方,只是起到一个触发的作用,毕竟traceId的传播都在skywalking-agent的相关增强逻辑中,所以输出tarceId也由插件进行实现,直接增强LogbackPatternConverter的cover方法,在调用该方法后返回真正的traceId。