背景
需求:
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路徑即可
- 方案1 各個項目接入logstash,
- 實施:
位元組碼加強架構選擇: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) 性能測試