一、背景簡介
項目中日志的管理是基礎功能之一,不同的用戶和場景下對日志都有特定的需求,從而需要用不同的策略進行日志采集和管理,如果是在分布式的項目中,日志的體系設計更加復雜。
- 日志類型:業務操作、信息打印、請求鏈路;
- 角色需求:研發端、用戶端、服務級、系統級;
用戶與需求
- 用戶端:核心數據的增刪改,業務操作日志;
- 研發端:日志采集與管理策略,異常日志監控;
- 服務級:關鍵日志打印,問題發現與排查;
- 系統級:分布式項目中鏈路生成,監控體系;
不同的場景中,需要選用不同的技術手段去實現日志采集管理,例如日志打印、操作記錄、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