腦洞:字節碼加強 (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) 性能測試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章