天天看點

腦洞:位元組碼加強 (1) 日志收集方案

背景

需求:

java技術棧,要接入目前所有的項目到日志中心,需求看似比較簡單

但是實施的過程中各種問題,項目所屬不同部門,使用開發架構不同,人員能力水準不同,
  1. 方案選擇:
    • 方案1 各個項目接入logstash,
      各類日志架構都有,log4j logback log4j2(apache.logging.log4j),十分混亂,而且開發人員需要引入jar,有可能會與現有的jar版本沖突,有一定的開發成本.           
    • 方案2 基于現有的檔案日志,filebeat收集之後,再進行分析,發送到消息隊列,然後轉儲到elastic search.
      可是日志格式不統一,kao           
    • 方案3 基于位元組碼加強,
      重寫log4j logback log4j2中關于列印日志的方法,進行攔截.
        加入我們自己的邏輯,将日志以可控的形式進行記錄,将消息直接入mq,再由消費端進行消費(可以是logstash或者自研的處理程式),然後将此程式內建至基礎docker鏡像,JAVA_OPTS參數設定javaagent路徑即可           
  2. 實施:

位元組碼加強架構選擇:asm bytebuddy jvm-sandbox,前兩個堅決不用,原因:我不相信我自己寫的代碼,爛+懶

是以選jvm-sandbox

github位址

還有另一個原因,類隔離,對加強的項目沒有影響

項目裡面有個比較好了解的demo,攔截異常的,

可以在這裡檢視

上代碼,再解釋(log4j)

new EventWatchBuilder(moduleEventWatcher)
                .onClass("org.apache.log4j.Category")
                .onBehavior("callAppenders")
                .onWatch(new AdviceListener() {
                    @Override
                    public void afterReturning(Advice advice) {
                        try {
                            //定義一個錯誤級别(預設保持與ERROR一緻
                            int errorLevel = 40000;
                            //擷取event變量
                            Object event = advice.getParameterArray()[0];
                            // 擷取event對應的日志級别
                            int level = invokeMethod(invokeMethod(event, "getLevel"), "toInt");
                            // 擷取日志的列印時間
                            long timeStamp = invokeField(event, "timeStamp");
                            // 擷取日志格式化後的字元串
                            String msg = invokeMethod(event, "getRenderedMessage");
                            // 擷取logger name
                            String loggerName = invokeMethod(event, "getLoggerName");
                            // 擷取線程名
                            String threadName = invokeMethod(event, "getThreadName");
                            // 如果小于預設的錯誤級别
                            if (level < errorLevel) {
                                // 将日志資訊發送到本地隊列,等待(異步)發送
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, null);
                            } else {
                                // 如果是錯誤級别,定義throwable變量
                                Throwable throwable = null;
                                // 擷取ThrowableInformation資訊
                                Object throwProxy = invokeMethod(event, "getThrowableInformation");
                                if (throwProxy != null) {
                                    // 從throwable代理類中擷取真實錯誤資訊
                                    throwable = invokeMethod(throwProxy, "getThrowable");
                                }
                                // 将帶有錯誤資訊的消息發送到本地消息隊列,待發送
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, throwable);
                                // 接入點評的CAT,将錯誤資訊輸出到CAT大盤,用于報警
                                Cat.logError("[ERROR] " + msg, throwable);
                                // 設定目前的context有錯誤資訊,做後續處理
                                Cat.getManager().setHasError(true);
                            }

                        } catch (Exception ex) {
                            //黑洞
                        }
                    }
                });           

org.apache.log4j.Category.callAppenders 這個方法是log4j架構,在write message之前調用的方法,是将符合設定的level的message寫入各個配置中定義的appender.我們加強這段代碼,相當于增加了一個自定義的 appender,把資料輸入進去.

說明一下,裡面用了反射,是使用了緩存的反射,是jvm-sandbox的機制,因為classloader的類加載政策,目前隻能使用反射,經測試,并不會對性能有明顯損失,後續文章會将性能測試貼出.

接下來 logback加強,一樣的類似,基本和log4j沒有差別

new EventWatchBuilder(moduleEventWatcher)
                .onClass("ch.qos.logback.classic.Logger")
                .onBehavior("callAppenders")
                .onWatch(new AdviceListener() {
                    @Override
                    public void afterReturning(Advice advice) {
                        try {
                            int errorLevel = 40000;
                            Object event = advice.getParameterArray()[0];
                            int level = invokeMethod(invokeMethod(event, "getLevel"), "toInt");
                            long timeStamp = invokeMethod(event, "getTimeStamp");
                            String msg = invokeMethod(event, "getFormattedMessage");
                            String loggerName = invokeMethod(event, "getLoggerName");
                            String threadName = invokeMethod(event, "getThreadName");
                            if (level < errorLevel) {
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, null);
                            } else {
                                Throwable throwable = null;
                                Object throwProxy = invokeMethod(event, "getThrowableProxy");
                                if (throwProxy != null) {
                                    throwable = invokeMethod(throwProxy, "getThrowable");
                                }
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, throwable);
                                Cat.logError("[ERROR] " + msg, throwable);
                                Cat.getManager().setHasError(true);
                            }
                        } catch (Exception ex) {
                            //黑洞
                        }
                    }
                });           

不解釋 ,接下來 log4j2 ,比較類似 ,差別是,log4j2的level值,和logback log4j不同, 是反過來的,而且值也不同,是以做了一個轉換

new EventWatchBuilder(moduleEventWatcher)
                .onClass("org.apache.logging.log4j.core.config.LoggerConfig")
                .onBehavior("callAppenders")
                .onWatch(new AdviceListener() {
                    @Override
                    public void afterReturning(Advice advice) {
                        try {
                            int errorLevel = 40000;
                            Object event = advice.getParameterArray()[0];
                            int level = invokeMethod(invokeMethod(event, "getLevel"), "intLevel");
                            if (level >= 500) {
                                level = 10000;
                            } else if (level >= 400) {
                                level = 20000;
                            } else if (level >= 300) {
                                level = 30000;
                            } else if (level >= 200) {
                                level = 40000;
                            } else if (level >= 100) {
                                level = 40000;
                            } else {
                                level = 40000;
                            }
                            long timeStamp = invokeMethod(event, "getTimeMillis");
                            String msg = invokeMethod(invokeMethod(event, "getMessage"), "getFormattedMessage");
                            String loggerName = invokeMethod(event, "getLoggerName");
                            String threadName = invokeMethod(event, "getThreadName");
                            if (level < errorLevel) {
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, null);
                            } else {
                                Throwable throwable = invokeMethod(event, "getThrown");
                                offerAppLog(timeStamp, msg, level, loggerName, threadName, throwable);
                                Cat.logError("[ERROR] " + msg, throwable);
                                Cat.getManager().setHasError(true);
                            }
                        } catch (Exception ex) {
                            //黑洞
                        }
                    }
                });           
ok ,以上就是第三種日志收集方案的核心代碼,本系列文章完成之前會開放源碼供參考.

腦洞:位元組碼加強 (2) 動态日志level

腦洞:位元組碼加強 (3) APM方案埋點解析

腦洞:位元組碼加強 (4) tomcat通路日志收集

腦洞:位元組碼加強 (5) 業務問題排查方案

腦洞:位元組碼加強 (6) 性能測試