一、背景簡介
專案中日誌的管理是基礎功能之一,不同的使用者和場景下對日誌都有特定的需求,從而需要用不同的策略進行日誌採集和管理,如果是在分散式的專案中,日誌的體系設計更加複雜。
- 日誌型別:業務操作、資訊列印、請求鏈路;
- 角色需求:研發端、使用者端、服務級、系統級;
使用者與需求
- 使用者端:核心資料的增刪改,業務操作日誌;
- 研發端:日誌採集與管理策略,異常日誌監控;
- 服務級:關鍵日誌列印,問題發現與排查;
- 系統級:分散式專案中鏈路生成,監控體系;
不同的場景中,需要選用不同的技術手段去實現日誌採集管理,例如日誌列印、操作記錄、ELK體系等,注意要避免日誌管理導致程式異常中斷的情況。
越是複雜的系統設計和業務場景,就越依賴日誌的輸出資訊,在大規模的架構中,通常還會搭建獨立的日誌平臺,提供日誌資料的採集、儲存、分析等整套解決方案。
二、Slf4j元件
1、外觀模式
日誌的元件遵守外觀設計模式,Slf4j作為日誌體系的外觀物件,定義規範日誌的標準,日誌能力的具體實現交由各個子模組去實現;Slf4j明確日誌物件的載入方法和功能介面,與客戶端互動提供日誌管理功能;
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Impl.class) ;
通常禁止直接使用Logback、Log4j等具體實現元件的API,避免元件替換帶來不必要的麻煩,可以做到日誌的統一維護。
2、SPI介面
從Slf4j和Logback元件互動來看,在日誌的使用過程中,基本的切入點即使用Slf4j的介面,識別並載入Logback中的具體實現;SPI定義的介面規範,通常作為第三方(外部)元件的實現。
上述SPI作為兩套元件的連線點,通過原始碼大致看下載入過程,追溯LoggerFactory的原始碼即可:
public final class org.slf4j.LoggerFactory {
private final static void performInitialization() {
bind();
}
private final static void bind() {
try {
StaticLoggerBinder.getSingleton();
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
}
}
}
}
此處只貼出了幾行示意性質的原始碼,在LoggerFactory中執行初始化繫結關聯的時候,如果沒有找到具體的日誌實現元件,是會報告出相應的異常資訊,並且採用的是System.err輸出錯誤提示。
三、自定義元件
1、功能封裝
對於日誌(或其他)常用功能,通常會在程式碼工程中封裝獨立的程式碼包,作為公共依賴,統一管理和維護,對於日誌的自定義封裝可以參考之前的文件,這裡通常涉及幾個核心點:
- starter載入:封裝包配置成starter元件,可以被框架掃描和載入;
- aop切面程式設計:通常在相關方法上新增日誌註解,即可自動記錄動作;
- annotation註解:定義日誌記錄需要標記的核心引數和處理邏輯;
至於如何組裝日誌內容,適配業務語義,以及後續的管理流程,則根據具體場景設計相應的策略即可,比如日誌怎麼儲存、是否實時分析、是否非同步執行等。
2、物件解析
在自定義註解中,會涉及到物件解析的問題,即在註解中放入要從物件中解析的屬性,並且把值拼接到日誌內容中,可以增強業務日誌的語義可讀性。
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class Test {
public static void main(String[] args) {
// Map集合
HashMap<String,Object> infoMap = new HashMap<>() ;
infoMap.put("info","Map的描述") ;
// List集合
ArrayList<Object> arrayList = new ArrayList<>() ;
arrayList.add("List-00");
arrayList.add("List-01");
// User物件
People oldUser = new People("Wang",infoMap,arrayList) ;
People newUser = new People("LiSi",infoMap,arrayList) ;
// 包裝物件
WrapObj wrapObj = new WrapObj("WrapObject",oldUser,newUser) ;
// 物件屬性解析
SpelExpressionParser parser = new SpelExpressionParser();
// objName
Expression objNameExp = parser.parseExpression("#root.objName");
System.out.println(objNameExp.getValue(wrapObj));
// oldUser
Expression oldUserExp = parser.parseExpression("#root.oldUser");
System.out.println(oldUserExp.getValue(wrapObj));
// newUser.userName
Expression userNameExp = parser.parseExpression("#root.newUser.userName");
System.out.println(userNameExp.getValue(wrapObj));
// newUser.hashMap[info]
Expression ageMapExp = parser.parseExpression("#root.newUser.hashMap[info]");
System.out.println(ageMapExp.getValue(wrapObj));
// oldUser.arrayList[1]
Expression arr02Exp = parser.parseExpression("#root.oldUser.arrayList[1]");
System.out.println(arr02Exp.getValue(wrapObj));
}
}
@Data
@AllArgsConstructor
class WrapObj {
private String objName ;
private People oldUser ;
private People newUser ;
}
@Data
@AllArgsConstructor
class People {
private String userName ;
private HashMap<String,Object> hashMap ;
private ArrayList<Object> arrayList ;
}
注意上面使用的SpelExpressionParser
解析器,即Spring框架的原生API;業務中遇到的很多問題,建議都優先從核心依賴(Spring+JDK)中尋找解決方式,多花時間熟悉系統中核心元件的全貌,對開發視野和思路會有極大的幫助。
3、模式設計
這裡看一個比較複雜的自定義日誌解決思路,通過AOP模式識別日誌註解,並解析註解中要記錄的物件屬性,構建相應的日誌主體,最後根據註解標記的場景去適配不同的業務策略:
對於功能的通用性要求越高,在封裝時內建的適配策略就要越抽象,在處理複雜的邏輯流程時,要善於將不同的元件搭配使用,可以分擔業務支撐的壓力,形成穩定可靠的解決方案。
四、分散式鏈路
1、鏈路識別
基於微服務實現的分散式系統,處理一個請求會經過多個子服務,如果過程中某個服務發生異常,需要定位這個異常歸屬的請求動作,從而更好的去判斷異常原因並復現解決。
定位的動作則依賴一個核心的標識:TraceId-軌跡ID,即請求在各個服務流轉時,會攜帶該請求繫結的TraceId,這樣可以識別不同服務的哪些動作為同一個請求產生的。
通過TraceId和SpanId即可還原出請求的鏈路檢視,再結合相關日誌列印記錄等動作,則可以快速解決異常問題。在微服務體系中Sleuth元件提供了該能力的支撐。
鏈路檢視的核心引數可以整合Slf4j元件中,這裡可以參考org.slf4j.MDC
語法,MDC提供日誌前後的引數傳遞對映能力,內部包裝Map容器管理引數;在Logback元件中,StaticMDCBinder
提供該能力的繫結,這樣日誌列印也可以攜帶鏈路檢視的標識,做到該能力的完整整合。
2、ELK體系
鏈路檢視產生的日誌是非常龐大的,那這些文件類的日誌如何管理和快速查詢使用同樣是個關鍵問題,很常見的一個解決方案即ELK體系,現在已更新換代為ElasticStack產品。
- Kibana:可以在Elasticsearch中使用圖形和圖表對資料進行視覺化;
- Elasticsearch:提供資料的儲存,搜尋和分析引擎的能力;
- Logstash:資料處理管道,能夠同時從多個來源採集、轉換、推送資料;
Logstash提供日誌採集和傳輸能力,Elasticsearch儲存大量JSON格式的日誌記錄,Kibana則可以檢視化展現資料。
3、服務與配置
配置依賴:需要在服務中配置Logstash地址和埠,即日誌傳輸地址,以及服務名稱;
spring:
application:
name: app_serve
logstash:
destination:
uri: Logstash-地址
port: Logstash-埠
配置讀取:Logback元件配置中載入上述核心引數,這樣在配置上下文中可以通過name的值使用該引數;
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="butte_app" />
<springProperty scope="context" name="DES_URI" source="logstash.destination.uri" />
<springProperty scope="context" name="DES_PORT" source="logstash.destination.port" />
日誌傳輸:對傳輸內容做相應的配置,指定LogStash服務配置,編碼,核心引數等;
<appender name="LogStash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<!-- 日誌傳輸地址 -->
<destination>${DES_URI:- }:${DES_PORT:- }</destination>
<!-- 日誌傳輸編碼 -->
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<!-- 日誌傳輸引數 -->
<pattern>
<pattern>
{
"severity": "%level",
"service": "${APP_NAME:-}",
"trace": "%X{X-B3-TraceId:-}",
"span": "%X{X-B3-SpanId:-}",
"exportable": "%X{X-Span-Export:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"rest": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
輸出格式:還可以通過日誌的格式設定,管理日誌檔案或者控制檯的輸出內容;
<pattern>%d{yyyy-MM-dd HH:mm:ss} %contextName [%thread] %-5level %logger{100} - %msg %n</pattern>
關於Logback元件日誌的其他配置,例如輸出位置,級別,資料傳輸方式等,可以多參考官方文件,不斷優化。
4、資料通道
再看看資料傳輸到Logstash服務後,如何再傳輸到ES的,這裡也需要相應的傳輸配置,注意logstash和ES推薦使用相同的版本,本案例中是6.8.6
版本。
配置檔案:logstash-butte.conf
input {
tcp {
host => "192.168.37.139"
port => "5044"
codec => "json"
}
}
output {
elasticsearch {
hosts => ["http://localhost:9200"]
index => "log-%{+YYYY.MM.dd}"
}
}
- 輸入配置:指定logstash連線的host和埠,並且指定資料格式為json型別;
- 輸出配置:指定日誌資料輸出的ES地址,並指定index索引按天的建立方式;
啟動logstash服務
/opt/logstash-6.8.6/bin/logstash -f /opt/logstash-6.8.6/config/logstash-butte.conf
這樣完整的ELK日誌管理鏈路就實現了,通過使用Kibana工具就可以檢視日誌記錄,根據TraceId就可以找到檢視鏈路。
五、參考原始碼
應用倉庫:
https://gitee.com/cicadasmile/butte-flyer-parent
元件封裝:
https://gitee.com/cicadasmile/butte-frame-parent