Java 中的結構化日誌記錄
一、簡介
應用程式日誌是用於故障排除、測量效能或簡單檢查軟體應用程式行為的重要資源。
在本教程中,我們將學習如何在 Java 中實現結構化日誌記錄以及該技術相對於非結構化日誌記錄的優勢。
2. 結構化日誌與非結構化日誌
在進入程式碼之前,讓我們先了解非結構化日誌和結構化日誌之間的主要差異。
非結構化日誌是一種以一致的格式列印但沒有結構的資訊。它只是一個文字區塊,其中有一些串聯並格式化的變數。
讓我們來看看取自演示 Spring 應用程式的非結構化日誌的一個範例:
22:25:48.111 [restartedMain] INFO osdrcRepositoryConfigurationDelegate - Finished Spring Data repository scanning in 42 ms. Found 1 JPA repository interfaces.
上面的日誌顯示了時間戳記、日誌等級、完全限定類別名稱以及 Spring 當時正在執行的操作的描述。當我們觀察應用程式行為時,這是一個有用的信息。
然而,從非結構化日誌中提取資訊比較困難。例如,識別和提取產生該日誌的類別名稱並不簡單,因為我們可能需要使用String
操作邏輯來尋找它。
相反,結構化日誌以類似字典的方式單獨顯示每個資訊。我們可以將它們視為資訊對象而不是Strings
。讓我們來看看應用於非結構化日誌範例的可能的結構化日誌解決方案:
{
"timestamp": "22:25:48.111",
"logger": "restartedMain",
"log_level": "INFO",
"class": "osdrcRepositoryConfigurationDelegate",
"message": "Finished Spring Data repository scanning in 42 ms. Found 1 JPA repository interfaces."
}
在結構化日誌中,提取特定欄位值更容易,因為我們可以使用其名稱來存取它。因此,我們不需要處理文字並找到其中的特定模式來提取資訊。例如,在我們的程式碼中,我們可以簡單地使用class
欄位來存取產生日誌的類別名稱。
3. 配置結構化日誌
在本節中,我們將深入探討使用logback
和slf4j
函式庫在 Java 應用程式中實作結構化日誌記錄的詳細資訊。
3.1.依賴關係
為了讓事情正常運作,我們需要在pom.xml
檔案中設定一些依賴項:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.4.14</version>
</dependency>
[slf4j-api](https://mvnrepository.com/artifact/org.slf4j/slf4j-api)
依賴項是logback-classic
和logback-core
依賴項的外觀。它們一起工作,在 Java 應用程式中輕鬆實現日誌記錄機制。請注意,如果我們使用 Spring Boot,則不需要新增這三個依賴項,因為它們是[spring-boot-starter-logging](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-logging) .
讓我們新增另一個依賴項, logstash-logback-encoder
,它有助於實現結構化日誌格式和佈局:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
請記住始終使用所提到的依賴項的最新版本。
3.2.配置結構化日誌的logback
基礎知識
為了以結構化的方式記錄訊息,我們需要配置logback
。為此,我們建立一個包含一些初始內容的logback.xml
檔案:
<configuration>
<appender name="jsonConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="jsonConsoleAppender"/>
</root>
</configuration>
在上面的文件中,我們配置了一個名為jsonConsoleAppender
的appender
,它使用logback-core
中現有的ConsoleAppender
類別作為其附加程序。
我們也設定了一個指向logback-encoder
庫中的LogstashEncoder
類別的encoder
。此編碼器負責將日誌事件轉換為 JSON 格式並輸出資訊。
完成所有設定後,讓我們看一個範例日誌條目:
{"@timestamp":"2023-12-20T22:16:25.2831944-03:00","@version":"1","message":"Example log message","logger_name":"info_logger","thread_name":"main","level":"INFO","level_value":20000,"custom_message":"my_message","password":"123456"}
上面的日誌行採用 JSON 格式構建,其中包含元資料資訊和自訂欄位(例如message
和password
。
3.3.改進結構化日誌
為了讓我們的記錄器更具可讀性和安全性,讓我們修改logback.xml
:
<configuration>
<appender name="jsonConsoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeCallerData>true</includeCallerData>
<jsonGeneratorDecorator class="net.logstash.logback.decorate.CompositeJsonGeneratorDecorator">
<decorator class="net.logstash.logback.decorate.PrettyPrintingJsonGeneratorDecorator"/>
<decorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
<defaultMask>XXXX</defaultMask>
<path>password</path>
</decorator>
</jsonGeneratorDecorator>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="jsonConsoleAppender"/>
</root>
</configuration>
在這裡,我們添加了一些標籤來提高輸出的可讀性,並添加了更多元數據,並混淆了一些欄位。讓我們分別來看每一項:
-
configuration
:包含日誌配置的根標籤 -
appender name
:我們定義在其他標籤中重複使用的附加器名稱 -
appender class
:實作日誌記錄附加器的類別的完全限定名稱。我們使用了logback-core
中的ConsoleAppender
類別。 -
encoder class
:日誌編碼器實現,在我們的例子中是來自logstash-logback-encoder
的LogstashEncoder
-
includeCallerData
:新增有關發起該日誌行的呼叫者程式碼的更多信息 -
jsonGeneratorDecorator
:為了以漂亮的格式列印 JSON,我們添加了帶有引用CompositeJsonGeneratorDecorator
類別的嵌套decorator
標記的標記。 -
decorator class
:我們使用PrettyPrintingJsonGeneratorDecorator
類別以更漂亮的方式列印 JSON 輸出,在不同的行中顯示每個欄位。 -
decorator class
:這裡,MaskingJsonGeneratorDecorator
類別會混淆任何欄位資料。 -
defaultMask
:取代path
標記中定義的欄位的遮罩。這對於屏蔽敏感資料並使我們的應用程式在使用結構化日誌時成為 PII 投訴人非常有用。 -
path
:應用defaultMask
標記中定義的掩碼的欄位名稱
使用新配置,與 3.2 節相同的日誌。應該類似:
{
"@timestamp" : "2023-12-20T22:44:58.0961616-03:00",
"@version" : "1",
"message" : "Example log message",
"logger_name" : "info_logger",
"thread_name" : "main",
"level" : "INFO",
"level_value" : 20000,
"custom_message" : "my_message",
"password" : "XXXX",
"caller_class_name" : "StructuredLog4jExampleUnitTest",
"caller_method_name" : "givenStructuredLog_whenUseLog4j_thenExtractCorrectInformation",
"caller_file_name" : "StructuredLog4jExampleUnitTest.java",
"caller_line_number" : 16
}
4. 實作結構化日誌
為了說明結構化日誌記錄,我們將使用帶有User
類別的演示應用程式。
4.1.建立演示User
類
我們先建立一個名為User
的 Java POJO :
public class User {
private String id;
private String name;
private String password;
// getters, setters, and all-args constructor
}
4.2.練習結構化記錄器的用例
讓我們建立一個測試類別來說明結構化日誌記錄的用法:
public class StructuredLog4jExampleUnitTest {
Logger logger = LoggerFactory.getLogger("logger_name_example");
//...
}
在這裡,我們建立了一個變數來儲存Logger
介面的實例。我們使用LoggerFactory.getLogger()
方法並以任意名稱作為參數來取得Logger
的有效實作。
現在,讓我們定義一個測試案例來在info
層級列印訊息:
@Test
void whenInfoLoggingData_thenFormatItCorrectly() {
User user = new User("1", "John Doe", "123456");
logger.atInfo().addKeyValue("user_info", user)
.log();
}
在上面的程式碼中,我們定義了一個帶有一些資料的User
。然後,我們使用LoggingEventBuilder
的addKeyValue()
方法將user_info
資訊附加到先前建立的logger
變數中。
讓我們來看看logger
如何輸出帶有新加入的資訊user_info
日誌:
{
"@timestamp" : "2023-12-21T23:58:03.0581889-03:00",
"@version" : "1",
"message" : "Processed user succesfully",
"logger_name" : "logger_name_example",
"thread_name" : "main",
"level" : "INFO",
"level_value" : 20000,
"user_info" : {
"id" : "1",
"name" : "John Doe",
"password" : "XXXX"
},
"caller_class_name" : "StructuredLog4jExampleUnitTest",
"caller_method_name" : "whenInfoLoggingData_thenFormatItCorrectly",
"caller_file_name" : "StructuredLog4jExampleUnitTest.java",
"caller_line_number" : 21
}
日誌還有助於識別程式碼中的錯誤。因此,我們也可以使用LoggingEventBuilder
來說明catch
區塊中的錯誤日誌記錄:
@Test
void givenStructuredLog_whenUseLog4j_thenExtractCorrectInformation() {
User user = new User("1", "John Doe", "123456");
try {
throwExceptionMethod();
} catch (RuntimeException ex) {
logger.atError().addKeyValue("user_info", user)
.setMessage("Error processing given user")
.addKeyValue("exception_class", ex.getClass().getSimpleName())
.addKeyValue("error_message", ex.getMessage())
.log();
}
}
在上面的測試中,我們為異常訊息和類別名稱添加了更多鍵值對。我們來看看日誌輸出:
{
"@timestamp" : "2023-12-22T00:04:52.8414988-03:00",
"@version" : "1",
"message" : "Error processing given user",
"logger_name" : "logger_name_example",
"thread_name" : "main",
"level" : "ERROR",
"level_value" : 40000,
"user_info" : {
"id" : "1",
"name" : "John Doe",
"password" : "XXXX"
},
"exception_class" : "RuntimeException",
"error_message" : "Error saving user data",
"caller_class_name" : "StructuredLog4jExampleUnitTest",
"caller_method_name" : "givenStructuredLog_whenUseLog4j_thenExtractCorrectInformation",
"caller_file_name" : "StructuredLog4jExampleUnitTest.java",
"caller_line_number" : 35
}
5. 結構化日誌記錄的優點
結構化日誌記錄比非結構化日誌記錄具有一些優勢,例如可讀性和效率。
5.1.可讀性
日誌通常是排除軟體故障、測量效能和檢查應用程式是否如預期運作的最佳工具之一。因此,創建一個可以更輕鬆地讀取日誌行的系統非常重要。
結構化日誌將資料顯示為字典,這使得人腦更容易在日誌行中搜尋特定欄位。這與使用索引搜尋書中的特定章節與逐頁閱讀內容的概念相同。
5.2.效率
一般來說,Kibana、New Relic 和 Splunk 等資料視覺化工具使用查詢語言在特定時間視窗內的所有日誌行中搜尋特定值。使用結構化日誌記錄時,日誌搜尋查詢更容易編寫,因為資料採用key-value
格式。
此外,使用結構化日誌記錄可以更輕鬆地建立有關所提供資料的業務指標。在這種情況下,以一致的結構化格式搜尋業務資料比在整個日誌文字中搜尋特定單字更容易、更有效率。
最後,搜尋結構化資料的查詢使用不太複雜的演算法,這可能會降低雲端運算成本,具體取決於所使用的工具。
六,結論
在本文中,我們看到了一種使用slf4j
和logback
在 Java 中實作結構化日誌記錄的方法。
使用格式化的結構化日誌可以讓機器和人類更快地讀取它們,使我們的應用程式更容易排除故障並降低使用日誌事件的複雜性。
與往常一樣,原始碼可以在 GitHub 上取得。