最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

Java Spring Boot 項(xiàng)目中使用結(jié)構(gòu)化日志節(jié)省時(shí)間

2021-11-10 09:37 作者:信碼由韁  | 我要投稿

【注】本文譯自:?Saving Time with Structured Logging - Reflectoring(
https://reflectoring.io/structured-logging/)


日志記錄是調(diào)查事件和了解應(yīng)用程序中發(fā)生的事情的終極資源。每個應(yīng)用程序都有某種類型的日志。

然而,這些日志通常很混亂,分析它們需要付出很多努力。在本文中,我們將研究如何利用結(jié)構(gòu)化日志來大大增加日志的價(jià)值。

我們將通過一些非常實(shí)用的技巧來提高應(yīng)用程序日志數(shù)據(jù)的價(jià)值,并使用?Logz.io?作為日志平臺來查詢?nèi)罩尽?/p>

?代碼示例

本文附有?GitHub 上(
https://github.com/thombergs/code-examples/tree/master/logging/structured-logging)的工作代碼示例。

什么是結(jié)構(gòu)化日志?

“正?!比罩臼欠墙Y(jié)構(gòu)化的。它們通常包含一個消息字符串:

2021-08-08 18:04:14.721 INFO 12402 --- [ main] i.r.s.StructuredLoggingApplication : Started StructuredLoggingApplication in 0.395 seconds (JVM running for 0.552)

此消息包含我們在調(diào)查事件或分析問題時(shí)希望獲得的所有信息:

  • 日志事件的日期

  • 創(chuàng)建日志事件的記錄器的名稱,以及

  • 日志消息本身。

所有信息都在該日志消息中,但很難查詢這些信息!由于所有信息都在一個字符串中,如果我們想從日志中獲取特定信息,就必須解析和搜索這個字符串。

例如,如果我們只想查看特定記錄器的日志,則日志服務(wù)器必須解析所有日志消息,檢查它們是否具有識別記錄器的特定模式,然后根據(jù)所需的記錄器過濾日志消息。

結(jié)構(gòu)化日志包含相同的信息,但采用結(jié)構(gòu)化形式而不是非結(jié)構(gòu)化字符串。通常,結(jié)構(gòu)化日志以 JSON 格式呈現(xiàn):

{ ? ?"timestamp": "2021-08-08 18:04:14.721", ? ?"level": "INFO", ? ?"logger": "io.reflectoring....StructuredLoggingApplication", ? ?"thread": "main", ? ?"message": "Started StructuredLoggingApplication ..."}

這種 JSON 結(jié)構(gòu)允許日志服務(wù)器有效地存儲,更重要的是檢索日志。

例如,現(xiàn)在可以通過?timestamp?或?logger?輕松過濾日志,而且搜索比解析特定模式的字符串更有效。

但是結(jié)構(gòu)化日志的價(jià)值并不止于此:我們可以根據(jù)需要向結(jié)構(gòu)化日志事件中添加任何自定義字段!?我們可以添加上下文信息來幫助我們識別問題,或者我們可以向日志添加指標(biāo)。

憑借我們現(xiàn)在觸手可及的所有數(shù)據(jù),我們可以創(chuàng)建強(qiáng)大的日志查詢和儀表板,即使我們剛在半夜醒來調(diào)查事件,我們也能找到所需的信息。

現(xiàn)在讓我們看幾個用例,它們展示了結(jié)構(gòu)化日志記錄的強(qiáng)大功能。

為所有日志事件添加代碼路徑

我們首先要看的是代碼路徑。每個應(yīng)用程序通常有幾個不同的路徑,傳入請求可以通過應(yīng)用程序??紤]這個圖:


此示例具有(至少)三種不同的代碼路徑,傳入請求可以采用這些路徑:

  • 用戶代碼路徑:用戶正在從他們的瀏覽器使用應(yīng)用程序。瀏覽器向 Web 控制器發(fā)送請求,控制器調(diào)用領(lǐng)域代碼。

  • 第三方系統(tǒng)代碼路徑:應(yīng)用程序的 HTTP API 也從第三方系統(tǒng)調(diào)用。在這個例子中,第三方系統(tǒng)調(diào)用與用戶瀏覽器相同的 web 控制器。

  • 計(jì)時(shí)器代碼路徑:與許多應(yīng)用程序一樣,此應(yīng)用程序有一些由計(jì)時(shí)器觸發(fā)的計(jì)劃任務(wù)。

這些代碼路徑中的每一個都可以具有不同的特征。域服務(wù)涉及所有三個代碼路徑。在涉及域服務(wù)錯誤的事件期間,了解導(dǎo)致錯誤的代碼路徑將大有幫助!

如果我們不知道代碼路徑,我們很容易在事件調(diào)查期間做出毫無結(jié)果的猜測。

所以,我們應(yīng)該將代碼路徑添加到日志中!以下是我們?nèi)绾问褂?Spring Boot 做到這一點(diǎn)。

為傳入的 Web 請求添加代碼路徑

在 Java 中,SLF4J 日志庫提供了?MDC?類(消息診斷上下文)。這個類允許我們向在同一線程中發(fā)出的所有日志事件添加自定義字段。

要為每個傳入的 Web 請求添加自定義字段,我們需要構(gòu)建一個攔截器,在每個請求的開頭添加?codePath?字段,甚至在我們的 Web 控制器代碼執(zhí)行之前。

我們可以通過實(shí)現(xiàn)?HandlerInterceptor?接口來做到這一點(diǎn):

public class LoggingInterceptor implements HandlerInterceptor { ? ?@Override ? ?public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) ? ? ? ? ? ?throws Exception { ? ? ? ?if (request.getHeader("X-CUSTOM-HEADER") != null) { ? ? ? ? ? ?MDC.put("codePath", "3rdParty"); ? ? ? ?} else { ? ? ? ? ? ?MDC.put("codePath", "user"); ? ? ? ?} ? ? ? ?return true; ? ?} ? ?@Override ? ?public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ? ? ? ? ? ?ModelAndView modelAndView) { ? ? ? ?MDC.remove("codePath"); ? ?} }

在?preHandle()?方法中,我們調(diào)用?MDC.put()?將?codePath?字段添加到所有日志事件中。如果請求包含標(biāo)識請求來自第三方方系統(tǒng)的標(biāo)頭,我們將代碼路徑設(shè)置為?3rdParty,否則,我們假設(shè)請求來自用戶的瀏覽器。

根據(jù)應(yīng)用的不同,這里的邏輯可能會有很大的不同,當(dāng)然,這只是一個例子。

在?postHandle()?方法中,我們不應(yīng)該忘記調(diào)用?MDC.remove()?再次刪除所有先前設(shè)置的字段,否則線程仍會保留這些字段,即使它返回到線程池,以及下一個請求 由該線程提供服務(wù)的那些字段可能仍然設(shè)置為錯誤的值。

要激活攔截器,我們需要將其添加到?InterceptorRegistry?中:

@Componentpublicclass WebConfigurer implements WebMvcConfigurer { ? ?@Override ? ?public void addInterceptors(InterceptorRegistry registry) { ? ? ? ?registry.addInterceptor(new LoggingInterceptor()); ? ?} }

就是這樣。在傳入日志事件的線程中發(fā)出的所有日志事件現(xiàn)在都具有?codePath?字段。

如果任何請求創(chuàng)建并啟動子線程,請確保在新線程生命周期開始時(shí)調(diào)用?MDC.put()。

在計(jì)劃作業(yè)中添加代碼路徑

在 Spring Boot 中,我們可以通過使用?@Scheduled?和?@EnableScheduling?注解輕松創(chuàng)建計(jì)劃作業(yè)。

要將代碼路徑添加到日志中,我們需要確保調(diào)用?MDC.put()?作為調(diào)度方法中的第一件事:

@Componentpublicclass Timer { ? ?private final DomainService domainService; ? ?private static final Logger logger = LoggerFactory.getLogger(Timer.class); ? ?public Timer(DomainService domainService) { ? ? ? ?this.domainService = domainService; ? ?} ? ?@Scheduled(fixedDelay = 5000) ? ?void scheduledHello() { ? ? ? ?MDC.put("codePath", "timer"); ? ? ? ?logger.info("log event from timer"); ? ? ? ?// do some actual work ? ? ? ?MDC.remove("codePath"); ? ?} }

這樣,從執(zhí)行調(diào)度方法的線程發(fā)出的所有日志事件都將包含字段?codePath。我們也可以創(chuàng)建我們自己的?@Job?注解或類似的注解來為我們完成這項(xiàng)工作,但這超出了本文的范圍。

為了使預(yù)定作業(yè)的日志更有價(jià)值,我們可以添加其他字段:

  • job_status:指示作業(yè)是否成功的狀態(tài)。

  • job_id:已執(zhí)行作業(yè)的 ID。

  • job_records_processed:如果作業(yè)進(jìn)行一些批處理,它可以記錄處理的記錄數(shù)。

  • ......

通過日志中的這些字段,我們可以在日志服務(wù)器獲取到很多有用的信息!

將用戶 ID 添加到用戶啟動的日志事件

典型 Web 應(yīng)用程序中的大部分工作是在來自用戶瀏覽器的 Web 請求中完成的,這些請求會觸發(fā)應(yīng)用程序中的線程,為瀏覽器創(chuàng)建響應(yīng)。

想象一下發(fā)生了一些錯誤,日志中的堆棧跟蹤顯示它與特定的用戶配置有關(guān)。但是我們不知道請求來自哪個用戶!

為了緩解這種情況,在用戶觸發(fā)的所有日志事件中包含某種用戶 ID 是非常有幫助的

由于我們知道傳入的 Web 請求大多直接來自用戶的瀏覽器,因此我們可以在創(chuàng)建的同一個?LoggingInterceptor?中添加?username?字段以添加?codePath?字段:

public class LoggingInterceptor implements HandlerInterceptor { ? ?@Override ? ?public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) ? ? ? ? ? ?throws Exception { ? ? ? ?Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); ? ? ? ?if (principal instanceof UserDetails) { ? ? ? ? ? ?String username = ((UserDetails) principal).getUsername(); ? ? ? ? ? ?MDC.put("username", username); ? ? ? ?} else { ? ? ? ? ? ?String username = principal.toString(); ? ? ? ? ? ?MDC.put("username", username); ? ? ? ?} ? ? ? ?return true; ? ?} ? ?@Override ? ?public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ? ? ? ? ? ?ModelAndView modelAndView) { ? ? ? ?MDC.remove("username"); ? ?} }

這段代碼假設(shè)我們使用 Spring Security 來管理對 Web 應(yīng)用程序的訪問。我們使用?SecurityContextHolder?來獲取?Principal?并從中提取用戶名以將其傳遞給?MDC.put()。

從服務(wù)請求的線程發(fā)出的每個日志事件現(xiàn)在都將包含用戶名字段和用戶名。

有了這個字段,我們現(xiàn)在可以過濾特定用戶請求的日志。如果用戶報(bào)告了問題,我們可以根據(jù)他們的姓名過濾日志,并極大地減少我們必須查看的日志。

根據(jù)規(guī)定,您可能希望記錄更不透明的用戶 ID 而不是用戶名。

為錯誤日志事件添加根本原因

當(dāng)我們的應(yīng)用程序出現(xiàn)錯誤時(shí),我們通常會記錄堆棧跟蹤。堆棧跟蹤幫助我們確定錯誤的根本原因。如果沒有堆棧跟蹤,我們將不知道是哪個代碼導(dǎo)致了錯誤!

但是,如果我們想在應(yīng)用程序中運(yùn)行錯誤統(tǒng)計(jì)信息,堆棧跟蹤是非常笨拙的。假設(shè)我們想知道我們的應(yīng)用程序每天總共記錄了多少錯誤,以及其中有多少是由哪個根本原因異常引起的。我們必須從日志中導(dǎo)出所有堆棧跟蹤,并對它們進(jìn)行一些手動過濾,才能得到該問題的答案!

但是,如果我們將自定義字段?rootCause?添加到每個錯誤日志事件,我們可以通過該字段過濾日志事件,然后在日志服務(wù)器的 UI 中創(chuàng)建不同根本原因的直方圖或餅圖,甚至無需導(dǎo)出數(shù)據(jù)。

在 Spring Boot 中執(zhí)行此操作的一種方法是創(chuàng)建一個?@ExceptionHandle:

@ControllerAdvicepublicclass WebExceptionHandler { ? ?private static final Logger logger = LoggerFactory.getLogger(WebExceptionHandler.class); ? ?@ExceptionHandler(Exception.class) ? ?@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) ? ?public void internalServerError(Exception e) { ? ? ? ?MDC.put("rootCause", getRootCause(e).getClass().getName()); ? ? ? ?logger.error("returning 500 (internal server error).", e); ? ? ? ?MDC.remove("rootCause"); ? ?} ? ?private Throwable getRootCause(Exception e) { ? ? ? ?Throwable rootCause = e; ? ? ? ?while (e.getCause() != null && rootCause.getCause() != rootCause) { ? ? ? ? ? ?rootCause = e.getCause(); ? ? ? ?} ? ? ? ?return rootCause; ? ?} }

我們創(chuàng)建了一個用?@ControllerAdvice?注解的類,這意味著它在我們所有的 web 控制器中都是有效的。

在類中,我們創(chuàng)建了一個用?@ExceptionHandler?注解的方法。對于任何 Web 控制器中出現(xiàn)的異常,都會調(diào)用此方法。它將?rootCause?MDC 字段設(shè)置為導(dǎo)致錯誤的異常類的完全限定名稱,然后記錄異常的堆棧跟蹤。

就是這樣。所有打印堆棧跟蹤的日志事件現(xiàn)在都有一個字段?rootCause,我們可以通過這個字段進(jìn)行過濾以了解我們應(yīng)用程序中的錯誤分布。

向所有日志事件添加跟蹤 ID

如果我們運(yùn)行多個服務(wù),例如在微服務(wù)環(huán)境中,分析錯誤時(shí)事情會很快變得復(fù)雜。一個服務(wù)調(diào)用另一個服務(wù),另一個服務(wù)調(diào)用再一個服務(wù),并且很難(如果可能的話)跟蹤一個服務(wù)中的錯誤到另一個服務(wù)中的錯誤。

跟蹤 ID 有助于連接一個服務(wù)中的日志事件和另一個服務(wù)中的日志事件:


在上面的示例圖中,服務(wù) 1 被調(diào)用并生成跟蹤 ID“1234”。然后它調(diào)用服務(wù) 2 和 3,將相同的跟蹤 ID 傳播給它們,以便它們可以將相同的跟蹤 ID 添加到其日志事件中,從而可以通過搜索特定的跟蹤 ID 來連接所有服務(wù)的日志事件。

對于每個傳出請求,服務(wù) 1 還會創(chuàng)建一個唯一的“跨度 ID”。雖然跟蹤跨越服務(wù) 1 的整個請求/響應(yīng)周期,但跨度僅跨越一個服務(wù)和另一個服務(wù)之間的請求/響應(yīng)周期。

我們可以自己實(shí)現(xiàn)這樣的跟蹤機(jī)制,但是有一些跟蹤標(biāo)準(zhǔn)和工具可以使用這些標(biāo)準(zhǔn)集成到跟蹤系統(tǒng)中,例如?Logz.io 的分布式跟蹤功能。

我們還是使用標(biāo)準(zhǔn)工具吧。在 Spring Boot 世界中,這就是?Spring Cloud Sleuth,我們可以通過簡單地將它添加到我們的?pom.xml,從而把該功能集成到我們的應(yīng)用程序中:

<dependencyManagement> ?<dependencies> ? ?<dependency> ? ? ?<groupId>org.springframework.cloud</groupId> ? ? ?<artifactId>spring-cloud-dependencies</artifactId> ? ? ?<version>2020.0.3</version> ? ? ?<type>pom</type> ? ? ?<scope>import</scope> ? ?</dependency> ?</dependencies></dependencyManagement><dependencies> ?<dependency> ? ?<groupId>org.springframework.cloud</groupId> ? ?<artifactId>spring-cloud-starter-sleuth</artifactId> ?</dependency></dependencies>

這會自動將跟蹤和跨度 ID 添加到我們的日志中,并在使用支持的 HTTP 客戶端時(shí)通過請求標(biāo)頭將它們從一個服務(wù)傳播到下一個服務(wù)。您可以在“使用 Spring Cloud Sleuth 在分布式系統(tǒng)中進(jìn)行跟蹤”一文中閱讀有關(guān) Spring Cloud Sleuth 的更多信息。

添加某些代碼路徑的持續(xù)時(shí)間

我們的應(yīng)用程序響應(yīng)請求所需的總持續(xù)時(shí)間是一個重要的指標(biāo)。如果速度太慢,用戶會感到沮喪。

通常,將請求持續(xù)時(shí)間作為指標(biāo)公開并創(chuàng)建顯示請求持續(xù)時(shí)間的直方圖和百分位數(shù)的儀表板是一個好主意,這樣我們就可以一目了然地了解應(yīng)用程序的健康狀況,甚至可能在違反某個閾值時(shí)收到警報(bào)。

然而,我們并不是一直在查看儀表板,我們可能不僅對總請求持續(xù)時(shí)間感興趣,而且對某些代碼路徑的持續(xù)時(shí)間感興趣。在分析日志以調(diào)查問題時(shí),了解代碼中特定路徑執(zhí)行所需的時(shí)間可能是一個重要線索。

在 Java 中,我們可能會這樣做:

void callThirdPartyService() throws InterruptedException { ? ?logger.info("log event from the domain service"); ? ?Instant start=Instant.now(); ? ?Thread.sleep(2000); // simulating an expensive operation ? ?Duration duration=Duration.between(start,Instant.now()); ? ?MDC.put("thirdPartyCallDuration",String.valueOf(duration.getNano())); ? ?logger.info("call to third-party service successful!"); ? ?MDC.remove("thirdPartyCallDuration"); }

假設(shè)我們正在調(diào)用第三方服務(wù)并希望將持續(xù)時(shí)間添加到日志中。使用?Instant.now()?和?Duration.between(),我們計(jì)算持續(xù)時(shí)間,將其添加到 MDC,然后創(chuàng)建日志事件。

這個日志事件現(xiàn)在將包含字段?thirdPartyCallDuration,我們可以在日志中過濾和搜索該字段。 例如,我們可能會搜索這個調(diào)用耗時(shí)過長的實(shí)例。 然后,我們可以使用用戶 ID 或跟蹤 ID,當(dāng)這需要特別長的時(shí)間時(shí),我們也可以將它們作為日志事件的字段來找出模式。

在Logz.io中查詢結(jié)構(gòu)化日志

如果我們按照關(guān)于?per-environment logging?的文章中的描述設(shè)置了日志記錄到?Logz.io,我們現(xiàn)在可以在 Logz.io 提供的 Kibana UI 中查詢?nèi)罩尽?/p>

錯誤分布

例如,我們可以查詢在?rootCause?字段中具有值的所有日志事件:

__exists__: "rootCause"

這將顯示具有根本原因的錯誤事件列表。

我們還可以在 Logz.io UI 中創(chuàng)建一個可視化來顯示給定時(shí)間范圍內(nèi)的錯誤分布:


此圖表顯示幾乎一半的錯誤是由?ThingyException?引起的,因此檢查是否可以以某種方式避免此異??赡苁莻€好主意。如果無法避免,我們應(yīng)該將其記錄在?WARN?而不是?ERROR?上,以保持錯誤日志的清潔。

跨代碼路徑的錯誤分布

例如,假設(shè)用戶抱怨預(yù)定的作業(yè)沒有正常工作。如果我們在調(diào)度方法代碼中添加了一個?job_status?字段,我們可以通過那些失敗的作業(yè)來過濾日志:

job_status: "ERROR"

為了獲得更高級的視圖,我們可以創(chuàng)建另一個餅圖可視化,顯示?job_status?和?rootCause?的分布:


我們現(xiàn)在可以看到大部分預(yù)定的作業(yè)都失敗了!我們應(yīng)該為此添加一些警報(bào)! 我們還可以查看哪些異常是大多數(shù)計(jì)劃作業(yè)的根本原因并開始調(diào)查。

檢查用戶的錯誤

或者,假設(shè)用戶名為 “user” 的用戶提出了一個支持請求,指定了它發(fā)生的大致日期和時(shí)間。我們可以使用查詢?username: user?過濾日志以僅顯示該用戶的日志,并且可以快速將用戶問題的原因歸零。

我們還可以擴(kuò)展查詢以僅顯示具有?rootCause?的該用戶的日志事件,以直接了解何時(shí)出了什么問題。

username: "user" AND _exists_: "rootCause"

結(jié)構(gòu)化您的日志

本文僅展示了幾個示例,說明我們?nèi)绾蜗蛉罩臼录砑咏Y(jié)構(gòu)并在查詢?nèi)罩緯r(shí)使用該結(jié)構(gòu)。以后可以在日志中搜索的任何內(nèi)容都應(yīng)該是日志事件中的自定義字段。添加到日志事件中的字段在很大程度上取決于我們正在構(gòu)建的應(yīng)用程序,所以在編寫代碼時(shí),一定要考慮哪些信息可以幫助您分析日志。

您可以在?GitHub 上(
https://github.com/thombergs/code-examples/tree/master/logging/structured-logging)找到本文中討論的代碼示例。


Java Spring Boot 項(xiàng)目中使用結(jié)構(gòu)化日志節(jié)省時(shí)間的評論 (共 條)

分享到微博請遵守國家法律
灌阳县| 曲阜市| 若羌县| 昌乐县| 定州市| 阿拉尔市| 墨竹工卡县| 合水县| 凭祥市| 长岛县| 石泉县| 民权县| 辰溪县| 磐石市| 聂拉木县| 卓资县| 师宗县| 青州市| 炎陵县| 蛟河市| 庆元县| 三穗县| 霍林郭勒市| 左权县| 株洲县| 宜川县| 朝阳县| 镇原县| 高雄市| 寿宁县| 康定县| 朝阳区| 莱州市| 闽清县| 益阳市| 那曲县| 景东| 乌兰察布市| 平果县| 称多县| 前郭尔|