日誌管理系統,多種方式總結

知了一笑發表於2022-02-28

一、背景簡介

專案中日誌的管理是基礎功能之一,不同的使用者和場景下對日誌都有特定的需求,從而需要用不同的策略進行日誌採集和管理,如果是在分散式的專案中,日誌的體系設計更加複雜。

  • 日誌型別:業務操作、資訊列印、請求鏈路;
  • 角色需求:研發端、使用者端、服務級、系統級;

使用者與需求

  • 使用者端:核心資料的增刪改,業務操作日誌;
  • 研發端:日誌採集與管理策略,異常日誌監控;
  • 服務級:關鍵日誌列印,問題發現與排查;
  • 系統級:分散式專案中鏈路生成,監控體系;

不同的場景中,需要選用不同的技術手段去實現日誌採集管理,例如日誌列印、操作記錄、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

相關文章