背景
需求:
java技術棧,要接入目前所有的項目到日誌中心,需求看似比較簡單
但是實施的過程中各種問題,項目所屬不同部門,使用開發框架不同,人員能力水平不同,
-
方案選擇:
-
方案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路徑即可
-
- 實施:
字節碼加強框架選擇: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) 性能測試