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


一、背景簡介

項目中日志的管理是基礎功能之一,不同的用戶和場景下對日志都有特定的需求,從而需要用不同的策略進行日志采集和管理,如果是在分布式的項目中,日志的體系設計更加復雜。

  • 日志類型:業務操作、信息打印、請求鏈路;
  • 角色需求:研發端、用戶端、服務級、系統級;

用戶與需求

  • 用戶端:核心數據的增刪改,業務操作日志;
  • 研發端:日志采集與管理策略,異常日志監控;
  • 服務級:關鍵日志打印,問題發現與排查;
  • 系統級:分布式項目中鏈路生成,監控體系;

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM