Java实战 | 抓取方法调用栈中的内存日志(干货)

1、前言

如果你想在运行时动态抓取到某个类的某个方法的其内部打印的所有log.info日志信息到内存中该如何获取? 这篇文章告诉你答案

2、Logback打印日志原理

整体流程

org.slf4j.Logger.info
- ch.qos.logback.classic.Logger.info
 - ch.qos.logback.classic.Logger. 	
  -  ch.qos.logback.classic.Logger.callAppenders
   - ch.qos.logback.classic.Logger.appendLoopOnAppenders
    - ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders
     - ch.qos.logback.core.Appender.doAppend
      - ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend
       - ch.qos.logback.core.OutputStreamAppender.subAppend
        - ch.qos.logback.core.encoder.LayoutWrappingEncoder.encode
         - ch.qos.logback.core.OutputStreamAppender.writeBytes

首先Logback核心对象是Logger, 并且全局唯一的。 Logger间具有树级的层次关系,即每个Logger内部都会指向一个父Logger以及维护其子Looger列表, 最顶层的根Logger名字为 ROOT 。 一般Logger的层级关系与指定的Logger名字有关。 比如我在某个类里随意创建了一个名为A的Logger
Logger log = LoggerFactory.getLogger("A"); 最终它直接挂在根Logger下面, 如下图

Java实战 | 抓取方法调用栈中的内存日志(干货)_第1张图片

而如果我指定Logger名字为 org.util, 即 Logger log = LoggerFactory.getLogger("org.util"); 最终层次关系如下图: 会先创建名为org的Logger,再创建名为 org.util的Logger。 也就是以逗号为区分逐级往下构建树级关系。

Java实战 | 抓取方法调用栈中的内存日志(干货)_第2张图片

那一般Logger打印一条日志无论是通过info、error、warn都会调用到 ch.qos.logback.classic.Logger#buildLoggingEventAndAppend 方法, 主要是先将消息包装成一个日志事件LoggingEvent, 每个Logger内部都会维护系统自定义的Appender列表,比如控制台Appender、文件Appender等等。 所以发布一个日志事件后Logger就会遍历持有的所有Appender进行日志的输出。 并且当Logger的additive属性为true时, 会将该日志事件传递给该Logger的父级Logger

3、基于Logback动态获取运行日志

3.1、LogCollector

日志收集器, 每收集到一条日志则执行该方法。 基于此可扩展实现, 比如每收集到一条日志就放到内存中或者发送到其他地方。


/**
 *  日志收集器
 */
public interface LogCollector {

     /** 收集日志
      * @param logInfo        日志信息
      */
     void addLogs(String logInfo);

}

/**
 *   内存日志收集器
 *              将收集到的日志添加到内存的集合中
 */
public class ListLogCollector implements LogCollector {

    private final List<String> logs = new ArrayList<>();

    public List<String> getLogs() {
        return logs;
    }

    @Override
    public void addLogs(String msg){
        logs.add(msg);
    }
}

3.2、自定义日志转换器

Logback每打印一条日志就会发送一个日志事件对象,里面包含所有日志信息。 Logback的Appender 会使用此转换器去将日志事件对象转换成一条日志字符串。 一般 Logback的 ConsoleAppender 默认使用的是 TTLLLayout 或者 PatternLayout

/**
   将logback日志事件转换成日志消息字符串
*/
public class MyLayout extends LayoutBase<ILoggingEvent> {

    @Override
    public String doLayout(ILoggingEvent event) {
        if (!isStarted()) {
            return CoreConstants.EMPTY_STRING;
        }
        StringBuilder sb = new StringBuilder();

        long timestamp = event.getTimeStamp();

        Instant instant = Instant.ofEpochMilli(timestamp);
        LocalDateTime dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());

        sb.append(dateTime);
        sb.append(" [");
        sb.append(event.getThreadName());
        sb.append("] ");
        sb.append(event.getLevel().toString());
        sb.append(" ");
        sb.append(event.getLoggerName());
        sb.append(" - ");
        sb.append(event.getFormattedMessage());
        sb.append(CoreConstants.LINE_SEPARATOR);
        IThrowableProxy tp = event.getThrowableProxy();
        if (tp != null) {
            Throwable throwable = ((ThrowableProxy) tp).getThrowable();
            sb.append(getStackTrace(throwable));
        }
        return sb.toString();
    }

    public static String getStackTrace(Throwable e) {
        StringWriter sw = null;
        PrintWriter pw = null;
        try {
            sw = new StringWriter();
            pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
        } finally {
            if (sw != null) {
                try {
                    sw.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (pw != null) {
                pw.close();
            }
        }
        return sw.toString();
    }
}

3.3、LogbackAppenderContext

用于存放 Logback 核心对象的全局上下文。

@Slf4j
public class LogbackAppenderContext {

    public  static final Layout<ILoggingEvent> layout;
    public  static final LoggerContext loggerContext;
    public  static final Logger rootLogger;

    static {
        // 获取程序中构建好的根Logger对象
        loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        rootLogger = loggerContext.getLogger("ROOT");
        layout = new MyLayout();
        layout.start();
     }

    public static void addAppender(MemoryAppender memoryAppender){
        rootLogger.addAppender(memoryAppender);
        log.info("添加appender: {}", memoryAppender.getAppenderName());
    }

    public static void removeAppender(String name){
        boolean flag = rootLogger.detachAppender(name);
        log.info("删除appender: {}",name);
    }


    private static List<Appender<ILoggingEvent>> listAppender() {
        List<Appender<ILoggingEvent>> appenderList = new ArrayList<>();
        Iterator<Appender<ILoggingEvent>> appenderIterator = rootLogger.iteratorForAppenders();
        while (appenderIterator.hasNext()){
            Appender<ILoggingEvent> appender = appenderIterator.next();
            appenderList.add(appender);
        }
        return appenderList;
    }
}

3.4、自定义Appender

每打印一条日志就会回调该append方法, 相当于监听器

@Slf4j
public class MemoryAppender extends AppenderBase<ILoggingEvent> {

    /**
     *  appender 名字
     */
    private final String appenderName;

    /**
     * 事件转换器
     *          负责将日志事件转换成日志字符串
     *          每打印一条日志就会包装成一条日志事件
     */
    private final Layout<ILoggingEvent> layout;

    /**
     *  当前线程是否开启收集内存日志
     */
    private final ThreadLocal<Boolean> mark = new ThreadLocal<>();

    /**
     *  每个线程内部维护一个内存日志收集器
     */
    private final ThreadLocal<LogCollector> logCollectorTH = new ThreadLocal<>();

    public MemoryAppender(String suffix,Layout<ILoggingEvent> layout) {
        this.appenderName = "memoryAppender-" + suffix;
        this.setName(appenderName);
        this.layout = layout;
    }

    @Override
    public void start() {
        super.start();
    }

    @Override
    public void stop() {
        super.stop();
    }

    @Override
    protected void append(ILoggingEvent eventObject) {
        if (isMark()){
            String logStr = layout.doLayout(eventObject);
            logCollectorTH.get().addLogs(logStr);
        }
    }

    public void mark(){
        log.info("thread mark name:{}, id: {}", Thread.currentThread().getName(), Thread.currentThread().getId());
        mark.set(true);
    }

    public  void unMark(){
        mark.remove();
        log.info("thread unMark name:{}, id: {}", Thread.currentThread().getName(), Thread.currentThread().getId());
    }

    public boolean isMark(){
        return mark.get() != null;
    }

    public String getAppenderName() {
        return appenderName;
    }

    public void setLogCollector(LogCollector logCollector){
        logCollectorTH.set(logCollector);
    }

    public void release(){
        logCollectorTH.remove();
    }
}

3.5、LogRunResult

/**
 * 日志结果对象
 * @param 
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LogRunResult<T> {

    private T result;

    private List<String> logs;

    private Exception exception;

    public LogRunResult(T result, List<String> logs) {
        this.result = result;
        this.logs = logs;
    }

    public LogRunResult(T result) {
        this.result = result;
    }
}

3.6、MemoryLogRunner(入口)

内存日志执行器, 运行某个方法,并获取方法内打印的日志信息。

@Slf4j
public class MemoryLogRunner {

    private MemoryLogRunner(){}

    private volatile static MemoryAppender memoryAppender;

    public static LogRunResult<Void> run(Runnable runnable){
        ListLogCollector listLogCollector = new ListLogCollector();
        LogRunResult<Void> logRunResult = run(runnable, listLogCollector);
        logRunResult.setLogs(listLogCollector.getLogs());
        return logRunResult;
    }

    public static LogRunResult<Void> run(Runnable runnable, LogCollector logCollector){
        // init appender
        lazyInit();

        try {
            // 启用
            memoryAppender.mark();
            memoryAppender.setLogCollector(logCollector);

            Exception errException = null;
            try {
                runnable.run();
            } catch (Exception e) {
                String errMsg = getStackTrace(e);
                logCollector.addLogs(errMsg);
                errException = e;
            }
            LogRunResult<Void> logRunResult = new LogRunResult<>();
            if (errException != null){
                logRunResult.setException(errException);
            }
            return logRunResult;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            // 释放资源
            memoryAppender.unMark();
            memoryAppender.release();
        }
    }

    public static <R> LogRunResult<R> run(Supplier<R> function, LogCollector logCollector){
        // init appender
        lazyInit();

        try {
            // 启用
            memoryAppender.mark();
            memoryAppender.setLogCollector(logCollector);

            R result = null;
            Exception errException = null;
            try {
                result = function.get();
            } catch (Exception e) {
                String errMsg = getStackTrace(e);
                logCollector.addLogs(errMsg);
                errException = e;
            }

            LogRunResult<R> logRunResult = new LogRunResult<>(result);
            if (errException != null){
                logRunResult.setException(errException);
            }

            return logRunResult;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            // 释放资源
            memoryAppender.unMark();
            memoryAppender.release();
        }
    }

    public static <R> LogRunResult<R> run(Supplier<R> function){
        ListLogCollector listLogCollector = new ListLogCollector();
        LogRunResult<R> logRunResult = run(function, listLogCollector);
        logRunResult.setLogs(listLogCollector.getLogs());
        return logRunResult;
    }

    private static void lazyInit() {
        if (memoryAppender == null){
            synchronized (MemoryLogRunner.class){
                if (memoryAppender == null){
                    MemoryAppender appender = new MemoryAppender("",LogbackAppenderContext.layout);
                    appender.setContext(LogbackAppenderContext.loggerContext);
                    appender.start();
                    LogbackAppenderContext.addAppender(appender);

                    memoryAppender = appender;
                }
            }
        }
    }

    public static String getStackTrace(Exception e) {
        StringWriter sw = null;
        PrintWriter pw = null;
        try {
            sw = new StringWriter();
            pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
        } finally {
            if (sw != null) {
                try {
                    sw.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (pw != null) {
                pw.close();
            }
        }
        return sw.toString();
    }
}

4、 快速开始

要获取哪个方法调用的日志直接执行 MemoryLogRunnerrun方法即可

4.1、 案例1

    private static Logger log = LoggerFactory.getLogger("1");

    public static void main(String[] args) {
        Supplier<Object> supplier = () -> printLog("SB");
        LogRunResult<Object> sb = MemoryLogRunner.run(supplier);
        System.out.println();
    }


    public static Object printLog(String name){
       log.info("name:{}", name);
       log.info("age:{}", 3);
       log.info("参数错误: name: {}",name,new IllegalArgumentException("参数不对"));
       return 3;
    }

结果如下,可以看到结果里包含了内部的运行日志信息。
Java实战 | 抓取方法调用栈中的内存日志(干货)_第3张图片

4.2、案例2

通过自定义的日志收集器ListLogCollector去执行, 最终可以发现收集到的日志都存放在里面, 基于此可以自定扩展其他日志收集器,比如mysql、redis、kafka等等将收集到的日志发送到其中。

   private static Logger log = LoggerFactory.getLogger("1");

    public static void main(String[] args) {
        Runnable runnable = () -> execute("haha");
        ListLogCollector logCollector = new ListLogCollector();
        MemoryLogRunner.run(runnable, logCollector);
        // 获取收集到的日志
        List<String> logs = logCollector.getLogs();
        System.out.println();
    }

    public static void execute(String name){
        log.info("name:{}", name);
        log.info("age:{}", 3);
        log.info("参数错误: name: {}",name,new IllegalArgumentException("参数不对"));
    }

基于此工具可以对系统某些业务功能进行更加细粒度的日志监控,而无需观测整个APP系统的日志中慢慢搜索那些零散的日志信息。比如Web在线日志

你可能感兴趣的:(JavaSE,java,内存日志,logback,日志监控)