skywalking日志输出源码浅析

1,703 阅读4分钟

日志选择

skywalking(以下简称sw)支持两种日志格式,pattern和json。

在启动流程中

public class SkyWalkingAgent {
    private static ILog LOGGER = LogManager.getLogger(SkyWalkingAgent.class);

首先默认会getLogger

然后在初始化配置时

        try {
            SnifferConfigInitializer.initializeCoreConfig(agentArgs);
        } catch (Exception e) {
            // try to resolve a new logger, and use the new logger to write the error log here
            LogManager.getLogger(SkyWalkingAgent.class)
                    .error(e, "SkyWalking agent initialized failure. Shutting down.");
            return;
        } finally {
            // refresh logger again after initialization finishes
            LOGGER = LogManager.getLogger(SkyWalkingAgent.class);
        }

因为会读取用户自定义的配置,所以在catch和finally里重新getLogger,默认的是pattern模式,如果用户配置是json,则重新get了logger,这样在后面的流程里会输出用户希望的日志格式,但是在读取用户配置之前如果抛出了异常,则还是默认的pattern模式

        initializeConfig(Config.class);
        // reconfigure logger after config initialization
        configureLogger();
        LOGGER = LogManager.getLogger(SnifferConfigInitializer.class);

        if (StringUtil.isEmpty(Config.Agent.SERVICE_NAME)) {
            throw new ExceptionInInitializerError("`agent.service_name` is missing.");
        }
        if (StringUtil.isEmpty(Config.Collector.BACKEND_SERVICE)) {
            throw new ExceptionInInitializerError("`collector.backend_service` is missing.");
        }
        if (Config.Plugin.PEER_MAX_LENGTH <= 3) {
            LOGGER.warn(
                "PEER_MAX_LENGTH configuration:{} error, the default value of 200 will be used.",
                Config.Plugin.PEER_MAX_LENGTH
            );
            Config.Plugin.PEER_MAX_LENGTH = 200;
        }

configureLogger()根据配置决定日志格式,Config.Logging.RESOLVER是用户可定义的配置项,sw将配置映射到config类中

    static void configureLogger() {
        switch (Config.Logging.RESOLVER) {
            case JSON:
                LogManager.setLogResolver(new JsonLogResolver());
                break;
            case PATTERN:
            default:
                LogManager.setLogResolver(new PatternLogResolver());
        }
    }

日志级别

首先是skywalking定义的日志级别

public enum LogLevel {
    TRACE, DEBUG, INFO, WARN, ERROR, OFF
}

利用枚举的compareTo()方法

    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

org.apache.skywalking.apm.agent.core.logging.core.AbstractLogger

    @Override
    public boolean isWarnEnable() {
        return LogLevel.WARN.compareTo(Config.Logging.LEVEL) >= 0;
    }

只要配置日志级别大于当前使用的日志级别输出就可以打印

    @Override
    public void warn(String message, Object... objects) {
        if (this.isWarnEnable()) {
            this.logger(LogLevel.WARN, replaceParam(message, objects), null);
        }
    }

replaceParam()方法匹配{}占位符

protected String replaceParam(String message, Object... parameters) {
    if (message == null) {
        return message;
    }
    int startSize = 0;
    int parametersIndex = 0;
    int index;
    String tmpMessage = message;
    while ((index = message.indexOf("{}", startSize)) != -1) {
        if (parametersIndex >= parameters.length) {
            break;
        }
        /**
* @Fix the Illegal group reference issue
*/
        tmpMessage = tmpMessage.replaceFirst("\{\}", Matcher.quoteReplacement(String.valueOf(parameters[parametersIndex++])));
        startSize = index + 2;
    }
    return tmpMessage;
}

设计

logManger

获取日志静态对象使用manager

 public class LogManager {
    private static LogResolver RESOLVER = new PatternLogResolver();

    public static void setLogResolver(LogResolver resolver) {
        LogManager.RESOLVER = resolver;
    }

    public static ILog getLogger(Class<?> clazz) {
        if (RESOLVER == null) {
            return NoopLogger.INSTANCE;
        }
        return LogManager.RESOLVER.getLogger(clazz);
    }

    public static ILog getLogger(String clazz) {
        if (RESOLVER == null) {
            return NoopLogger.INSTANCE;
        }
        return LogManager.RESOLVER.getLogger(clazz);
    }
}

resolver里来获取logger对象

下面以parttern为例,json的

PatternLogger

public class PatternLogResolver implements LogResolver {

    @Override
    public ILog getLogger(Class<?> clazz) {
        return new PatternLogger(clazz, Config.Logging.PATTERN);
    }

    @Override
    public ILog getLogger(String clazz) {
        return new PatternLogger(clazz, Config.Logging.PATTERN);
    }
}

默认的pattern格式

public static String PATTERN = "%level %timestamp %thread %class : %msg %throwable";

PatternLogger继承AbstractLogger,其中AbstractLogger利用策略模式创建一个存放各类日志关键字替换内容的converter map

Converter

    static {
        DEFAULT_CONVERTER_MAP.put("thread", ThreadConverter.class);
        DEFAULT_CONVERTER_MAP.put("level", LevelConverter.class);
        DEFAULT_CONVERTER_MAP.put("agent_name", AgentNameConverter.class);
        DEFAULT_CONVERTER_MAP.put("timestamp", DateConverter.class);
        DEFAULT_CONVERTER_MAP.put("msg", MessageConverter.class);
        DEFAULT_CONVERTER_MAP.put("throwable", ThrowableConverter.class);
        DEFAULT_CONVERTER_MAP.put("class", ClassConverter.class);
    }

以threadConverter为例

public class ThreadConverter implements Converter {
    @Override
    public String convert(LogEvent logEvent) {
        return Thread.currentThread().getName();
    }

    @Override
    public String getKey() {
        return "thread";
    }
}

实现converter接口,实现了转换后的内容和获取key值

值得一提的是LiteralConverter没有装在DEFAULT_CONVERTER_MAP里,作用大概是装载匹配表达式不需要转换的文本

public class LiteralConverter implements Converter {

    private final String literal;

    public LiteralConverter(String literal) {
        this.literal = literal;
    }

    @Override
    public String convert(LogEvent logEvent) {
        return literal;
    }

    @Override
    public String getKey() {
        return "";
    }
}

parser

在构建patternLogger时,会通过构造parser类传入匹配模式和converter map来决定需要用到哪些转换器

    public void setPattern(String pattern) {
        if (StringUtil.isEmpty(pattern)) {
            pattern = DEFAULT_PATTERN;
        }
        this.pattern = pattern;
        this.converters = new Parser(pattern, DEFAULT_CONVERTER_MAP).parse();
    }
    public List<Converter> parse() {
        List<Converter> patternConverters = new ArrayList<Converter>();
        StringBuilder buf = new StringBuilder();
        while (pointer < patternLength) {
            char c = pattern.charAt(pointer);
            pointer++;
            switch (state) {
                case LITERAL_STATE:
                    handleLiteralState(c, buf, patternConverters);
                    break;
                case KEYWORD_STATE:
                    handleKeywordState(c, buf, patternConverters);
                    break;
                default:
            }
        }

state是个枚举,分为LITERAL_STATE, KEYWORD_STATE

LITERAL_STATE我理解为是初始化的状态,KEYWORD_STATE是需要转换器替换掉值的文本,例如info,thread

解析过程类似log4j

首先默认的是LITERAL_STATE,进入处理方法

    private void handleLiteralState(char c, StringBuilder buf, List<Converter> patternConverters) {
        switch (c) {
            case ESCAPE_CHAR:
                escape("%", buf);
                break;
            case PERCENT_CHAR:
                addConverter(buf, patternConverters, LiteralConverter.class);
                state = State.KEYWORD_STATE;
                break;
            default:
                buf.append(c);
        }

    }

因为最开始的首字符是%s,则进入PERCENT_CHAR,添加LiteralConverter转换器,同时修改state为KEYWORD_STATE

在下一次指针右移进入handleKeywordState()方法

    private void handleKeywordState(char c, StringBuilder buf, List<Converter> patternConverters) {
        if (Character.isJavaIdentifierPart(c)) {
            buf.append(c);
        } else if (c == PERCENT_CHAR) {
            addConverterWithKeyword(buf, patternConverters);
        } else {
            addConverterWithKeyword(buf, patternConverters);
            if (c == ESCAPE_CHAR) {
                escape("%", buf);
            } else {
                buf.append(c);
            }
            state = State.LITERAL_STATE;
        }
    }

Character.isJavaIdentifierPart(c)判断这个字符是否能作为标识符,可以则添加到buffer里,否则进行判断,如果是%s,则认为是前面遍历的需要对应转换的字符结束了,addConverterWithKeyword()添加到转换器集合中

    private void addConverterWithKeyword(StringBuilder buf, List<Converter> patternConverters) {
        String keyword = buf.toString();
        if (convertMaps.containsKey(keyword)) {
            addConverter(buf, patternConverters, convertMaps.get(keyword));
        } else {
            buf.insert(0, "%");
            addConverter(buf, patternConverters, LiteralConverter.class);
        }
    }

如果在map里取不到则视为用户自定义的文本,注意会在buf首位添加%s,因为这时会认为这一段是需要输出的文本,所以之前的%s也是用户设置需要输出的

c == ESCAPE_CHAR判断后面的字符是否需要转义

判断逻辑如下:

    private void escape(String escapeChars, StringBuilder buf, char next) {
        if (escapeChars.indexOf(next) >= 0) {
            buf.append(next);
        } else {
            switch (next) {
                case '_':
                    // the _ sequence is swallowed
                    break;
                case '\':
                    buf.append(next);
                    break;
                case 't':
                    buf.append('\t');
                    break;
                case 'r':
                    buf.append('\r');
                    break;
                case 'n':
                    buf.append('\n');
                    break;
                default:
                    throw new IllegalArgumentException("Illegal char " + next + ". It not allowed as escape characters.");
            }
        }
    }

输出

输出方式

sw根据配置项配置输出到控制台还是文件,默认是文件输出

public static LogOutput OUTPUT = LogOutput.FILE

如果是文件输出则会读取配置读取地址

public static String DIR = "";

如果是空则输出到agent所在路径下/logs

打印顺序

文件输出使用ArrayBlockingQueue来保证日志输出的顺序

控制台输出本身就是加了synchronized的锁

异常打印

在异常的转换器中

public class ThrowableConverter implements Converter {
    @Override
    public String convert(LogEvent logEvent) {
        Throwable t = logEvent.getThrowable();
        return t == null ? "" : format(t);
    }

    public static String format(Throwable t) {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        t.printStackTrace(new java.io.PrintWriter(buf, true));
        String expMessage = buf.toString();
        try {
            buf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Constants.LINE_SEPARATOR + expMessage;
    }

    @Override
    public String getKey() {
        return "throwable";
    }
}

t.printStackTrace(new java.io.PrintWriter(buf, true));

这行代码的作用是将异常堆栈信息转存到buf里