簡體   English   中英

使用 Java 和一般情況下的日志記錄:最佳實踐?

[英]Logging in Java and in general: Best Practices?

有時當我看到我的日志記錄代碼時,我想知道我是否做得對。 對此可能沒有明確的答案,但我有以下擔憂:

庫類

我有幾個庫類可能會記錄一些INFO消息。 致命錯誤報告為異常。 目前,我的類中有一個靜態記錄器實例,其類名作為日志記錄名稱。 (Log4j 的: Logger.getLogger(MyClass.class)

這是正確的方法嗎? 也許這個庫類的用戶不想要我的實現中的任何消息,或者想要將它們重定向到特定於應用程序的日志。 我應該允許用戶從“外部世界”設置記錄器嗎? 您如何處理此類案件?

一般日志

在某些應用程序中,我的類可能希望將日志消息寫入未由類名標識的特定日志。 (即: HTTP Request log )做這種事情的最好方法是什么? 想到了一個查找服務......

您的約定非常標准且非常好(恕我直言)。

需要注意的一件事是過多的 unnedded 調試調用導致的內存碎片,因此,使用 Log4J(和大多數其他 Java 日志框架),您最終會得到如下結果:

if (log.isDebugEnabled()) {
  log.debug("...");
}

因為構建該日志消息(您可能沒有使用)可能會很昂貴,特別是如果完成數千或數百萬次。

您的 INFO 級別日志記錄不應該太“健談”(從您所說的來看,聽起來並非如此)。 INFO 消息通常應該是有意義和重要的,例如正在啟動和停止的應用程序。 如果遇到問題,您可能想知道的事情。 當您真正遇到要診斷的問題時,更多地使用調試/精細級別日志記錄。 調試/精細日志記錄通常僅在需要時打開。 信息通常始終處於打開狀態。

如果有人不想從您的課程中獲得特定的 INFO 消息,他們當然可以隨意更改您的 log4j 配置以不獲取它們。 Log4j 在這個部門非常簡單(與 Java 1.4 日志記錄相反)。

至於您的 HTTP 事情,我通常認為這不是 Java 日志記錄的問題,因為通常一個類負責您感興趣的內容,因此您只需將其放在一個地方。 在(在我的經驗中很少見)當您想要跨看似不相關的類的通用日志消息時,只需放入一些可以輕松獲取的標記。

以下是我在所有項目中遵循的一套指導方針,以確保良好的性能。 我已經根據互聯網上各種來源的輸入形成了這套指南。

到今天為止,我相信 Log4j 2 是迄今為止登錄 Java 的最佳選擇。

基准可在此處獲得。 我為獲得最佳性能而遵循的做法如下:

  1. 我目前避免使用 SLF4J,原因如下:
  2. 使用異步記錄器執行所有常規記錄以獲得更好的性能
  3. 使用同步記錄器將錯誤消息記錄在單獨的文件中,因為我們希望在錯誤消息發生時立即看到它
  4. 不要在常規日志記錄中使用位置信息,例如文件名、類名、方法名、行號,因為為了派生這些信息,框架會拍攝堆棧的快照並遍歷它。 這會影響性能。 因此,僅在錯誤日志中使用位置信息,而不在常規日志中使用
  5. 為了跟蹤由單獨線程處理的單個請求,請考慮使用線程上下文和隨機 UUID,如此所述
  6. 由於我們將錯誤記錄在單獨的文件中,因此將上下文信息也記錄在錯誤日志中非常重要。 例如,如果應用程序在處理文件時遇到錯誤,則在錯誤日志文件中打印文件名和正在處理的文件記錄以及堆棧跟蹤
  7. 日志文件應該是 grep-able 且易於理解的。 例如,如果應用程序處理多個文件中的客戶記錄,則每個日志消息應如下所示:
 12:01:00,127 INFO FILE_NAME=file1.txt - Processing starts 12:01:00,127 DEBUG FILE_NAME=file1.txt, CUSTOMER_ID=756 12:01:00,129 INFO FILE_NAME=file1.txt - Processing ends
  1. 使用 SQL 標記記錄所有 SQL 語句,如下所示,並使用過濾器啟用或禁用它:
 private static final Marker sqlMarker = MarkerManager.getMarker("SQL"); private void method1() { logger.debug(sqlMarker, "SELECT * FROM EMPLOYEE"); }
  1. 使用 Java 8 Lambda 記錄所有參數。 當給定的日志級別被禁用時,這將使應用程序免於格式化消息:
 int i=5, j=10; logger.info("Sample output {}, {}", ()->i, ()->j);
  1. 不要使用字符串連接。 使用如上所示的參數化消息

  2. 使用日志配置的動態重新加載,以便應用程序自動重新加載日志配置中的更改,而無需重新啟動應用程序

  3. 不要使用printStackTrace()System.out.println()

  4. 應用程序應在退出前關閉記錄器:

 LogManager.shutdown();
  1. 最后,供大家參考,我使用如下配置:
 <?xml version="1.0" encoding="UTF-8"?> <Configuration monitorinterval="300" status="info" strict="true"> <Properties> <Property name="filePath">${env:LOG_ROOT}/SAMPLE</Property> <Property name="filename">${env:LOG_ROOT}/SAMPLE/sample </Property> <property name="logSize">10 MB</property> </Properties> <Appenders> <RollingFile name="RollingFileRegular" fileName="${filename}.log" filePattern="${filePath}/sample-%d{yyyy-dd-MM}-%i.log"> <Filters> <MarkerFilter marker="SQL" onMatch="DENY" onMismatch="NEUTRAL" /> </Filters> <PatternLayout> <Pattern>%d{HH:mm:ss,SSS} %m%n </Pattern> </PatternLayout> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true" /> <SizeBasedTriggeringPolicy size="${logSize}" /> </Policies> </RollingFile> <RollingFile name="RollingFileError" fileName="${filename}_error.log" filePattern="${filePath}/sample_error-%d{yyyy-dd-MM}-%i.log" immediateFlush="true"> <PatternLayout> <Pattern>%d{HH:mm:ss,SSS} %p %c{1.}[%L] [%t] %m%n </Pattern> </PatternLayout> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true" /> <SizeBasedTriggeringPolicy size="${logSize}" /> </Policies> </RollingFile> </Appenders> <Loggers> <AsyncLogger name="com" level="trace"> <AppenderRef ref="RollingFileRegular"/> </AsyncLogger> <Root includeLocation="true" level="trace"> <AppenderRef ref="RollingFileError" level="error" /> </Root> </Loggers> </Configuration>
  1. 所需的 Maven 依賴項在這里:
 <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.8.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.1</version> </dependency> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.3.6</version> </dependency> <!-- (Optional)To be used when working with the applications using Log4j 1.x --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-1.2-api</artifactId> <version>2.8.1</version> </dependency>

@cletus 的回答中,他寫道

if (log.isDebugEnabled()) {
  log.debug("val is " + value);
}

這可以通過使用SLF4J來克服。 它提供了格式化幫助

log.debug("val is {}", value);

僅當級別為調試時才構造消息。

因此,現在出於性能和穩定性的原因,建議使用 SL4J 及其配套記錄器 Logback。

關於實例化記錄器,我使用 Eclipse Java 模板設置記錄器取得了一些成功:

private static Logger log = Logger.getLogger(${enclosing_type}.class);

這避免了 JVM 亂搞堆棧跟蹤的問題,並減少了(也許是微不足道的)從一開始就創建堆棧跟蹤的開銷。

使用這樣的模板的好處是,如果您想為記錄器設置一個統一的標准,您可以與您的團隊共享它。

看起來 IntelliJ 支持表示封閉類型名稱的模板變量的相同概念。 我看不到在 NetBeans 中輕松做到這一點的方法。

我正在查看應用程序的日志級別,目前正在檢測一種模式:

private static final Logger logger = Logger.getLogger(Things.class)

public void bla() {
  logger.debug("Starting " + ...)
  // Do stuff
  ...
  logger.debug("Situational")

  // Algorithms
  for(Thing t : things) {
    logger.trace(...)
  }

  // Breaking happy things
  if(things.isEmpty){
    logger.warn("Things shouldn't be empty!")
  }

  // Catching things
  try {
    ...
  } catch(Exception e) {
    logger.error("Something bad happened")
  }

  logger.info("Completed "+...)
}

log4j2-file 定義了一個 socket-appender,帶有一個故障轉移文件-appender。 還有一個控制台附加程序。 有時我在情況需要時使用 log4j2 標記。

認為額外的觀點可能會有所幫助。

您描述的那種 log4j 配置的首選選項是使用log4j 配置文件 這允許您實現的用戶完全按照您的要求進行操作,因為他們稍后可以使用更適合他們自己實現的內容覆蓋您的配置。 請參閱此處以獲得非常徹底的入門。

我可能從某個地方偷了這個,但它很好。

它減少了在復制和粘貼時混淆記錄器的風險^h^h^h 重構,並且輸入更少。

在您的代碼中:

private final static Logger logger = LoggerFactory.make();

...在 LoggerFactory 中:

public static Logger make() {
    Throwable t = new Throwable();
    StackTraceElement directCaller = t.getStackTrace()[1];
    return Logger.getLogger(directCaller.getClassName());
}

(請注意,堆棧轉儲是在初始化期間完成的。堆棧跟蹤可能不會被 JVM 優化掉,但實際上並不能保證)

另外,我認為簡單的 Java 日志門面 (SLF4J) ( http://www.slf4j.org/ ) 很重要。 由於在一個大項目的不同部分使用不同的日志框架的一些問題,SLF4J 是解決問題以成功管理這些部分的事實標准,不是嗎?

第二個概念:似乎一些老派的任務可以被Aspect-Oriented-Programming替代,Spring frmwrk 有它自己的實現,AOP-approach for logging here at StackOverflow 和here on Spring blog。

對於任何記錄器 API,我們至少有以下日志級別:錯誤 > 警告 > 信息 > 調試 > 跟蹤

並且我們可以使用每個日志級別來編寫不同類型的日志,以更好地了解我們收集的痕跡:

跟蹤——如果我們在每個方法的入口點用方法名稱和方法參數以及在出口點用返回值/對象編寫跟蹤會更好,

注意 – 最好遵循我們的編碼指南並模塊化編寫方法,那么在這種情況下,我們不需要在方法之間編寫多個日志行來打印數據。

調試- 我們將在方法中間添加調試日志以顯示滿足哪個 if/else/switch 條件,以及我們從 DB 中獲取的數據並在方法中使用它等等。 注意——不要在調試中添加那些作為參數發送或作為值返回的數據,因為這些數據已經被跟蹤級別打印(盡量不要多次打印相同的日志)。

信息- 假設客戶端有日志級別信息,那么如果他們看到日志,您想向他顯示什么消息和所有內容,所以將這些內容添加到信息中。 示例 – 成功創建/刪除/修改 Blabla 連接或 Blabla 鏈接鎖定/解鎖或 blabla 節點/節點觸發 blabla 同步。

警告- 這是一種罕見的情況,但是在編寫代碼時我們遇到了一些在正常情況下不可能發生的情況,它只是由於任何過時的條目或任何損壞發生,通常我們忽略這種情況,但如果我們這樣做會更好添加這樣的條件並在那里添加交戰日志。 示例 – 我從一個表中查詢,其條件不是主鍵或唯一的列,但它被告知它總是只返回一行,所以get(0) ,所以在這種情況下,我們應該寫一個條件,比如 if resultSet .size > 1 添加一些帶有更好消息的警告日志。

錯誤——錯誤日志應該出現在每個不期望的 catch 塊中,並且應該正確打印完整的堆棧跟蹤(而不僅僅是錯誤消息)。 同樣在 catch 塊中,人們在沒有記錄現有異常跟蹤的情況下拋出一個新異常,在這種情況下,我們沒有得到實際的異常點。 因此,在每個帶有完整堆棧跟蹤的 catch 塊中寫入錯誤日志是非常必要的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM