閒談軟體設計:日誌框架

更新 發佈閱讀 13 分鐘


在整理完【閒談軟體設計 2014 — 2024】電子書後,有好一陣子沒有寫技術部落格了,主要是開始認真找下一份工作,畢竟還是要想辦法讓自己有收入,大概到 2024 的六月底,有了幾份還不錯的 offers,但猶豫了好一陣子後,決定再闖一次,跟朋友一起從零開始一家新公司,接下來的文章,主要是這段時間系統從 0 到 1 的過程中,我覺得有趣的設計決策。


新公司初期只有我一個開發者,系統開發最初的三個月都集中在後端,加上沒有上到雲端 (也因此,省了三個月的雲端費用),全靠涵蓋率超過九成的單元測試確保功能正確性,在這情況下,自然用不到日誌,真遇到問題,有更好追蹤流程的 debugger,日誌就被放在 backlog 中的後面。

直到 app/web 的開發者加入,為了能協作,才終於把後端上到雲端作為 beta 環境 (production 環境是等到要送審 app 時才上雲端),這下就真的需要日誌了,趁系統的複雜度還在可控範圍,是該好好規劃下日誌該怎麼做了,當時在設計文件中寫下幾個要滿足的 criteria

  • 關聯式查詢。這是最重要的,可以將相關的日誌簡單地用一個條件,就全部找出來,這樣可以把完整的情境重建,更好地找出複雜問題的原因。
  • 支援雲生態。Java 有很多日誌的套件,要上雲其實很容易,只要找到合適的 appender 就行。
  • 富含情境。有時候日誌不是單單印出一句話,這樣對於查找問題並沒有幫助,而是要將當初的情境記錄下來,但不太可能把所有的資料都記錄下來,那些資料是有用的,這完全得靠開發者決定。
  • 結構化日誌。純文字日誌 (plain logs) 並不是沒有用,只是我個人沒有很愛在終端機上 command 去找日誌,反而比較常用 GCP 的介面,根據結構下條件,例如 context.merchantId = "xyz"
  • 和商業邏輯核心保持距離。不要讓商業邏輯核心為上述的 criteria 服務,最好是能做到:對商業邏輯核心來說上面的 criteria 幾乎透明。要寫日誌,取得一個 logger 物件,放入必要的情境和敘述就完事。

要滿足第一個 criteria,我立馬就想到 correlation ID,這是之前看 書摘《建構微服務》時注意到的,當時的系統已經複雜到一定的程度,加上值班時或是系統出現問題時,查找問題都要花上不少時間,還不見得能把所有相關的日誌都關聯起來,因此特別特別,雖然慢慢在既有的系統加入類似的概念,但總覺得是東補西補,沒有很漂亮。

ThreadLocal

老實說,書中對於 correlation ID 的描述很簡單,大意是用 UUID 然後往後帶。但要怎麼帶到後面給所有相關的日誌?一個簡單的作法,把 correlation ID 塞進 logger 物件,然後把 logger 往下傳,但這會有個問題,函式都會多一個與功能毫不相關的參數,弱型別的語言,把 logger參數放在最後一個,測試時不帶無所謂,但這次我選的是強型別語言,這招不管用,更別說,強型別語言也無法隨意把 correlation ID 塞進 logger 中。

那把 correlation ID 放到某個靜態變數裡?那就是危險的全域變數了,一個 web application,效能再怎麼差,同時也要處理數十個請求,correlation ID 放全域變數,那不就被改來改去,根本沒法用。

還好 Java 有 ThreadLocal 可以用,ThreadLocal 可以視作一個全域變數的儲存池,但只有放進去池子的執行緒可以取得跟修改放進去的資料,其他執行緒是看不到也無法修改,也就是說,只要一件任務不會有二或多個執行緒交替執行來完成,那就很適用放在 ThreadLocal 裡。恰巧,我這次沒有使用任何 async 或是 reactive 框架,完全是靠 virtual threads 來衝負載 (參閱 閒談軟體設計:Java virtual thread),每個請求都是建一個 virtual thread 來處理,完全不用擔心跨 thread 的問題。

富含情境

因此,首先先來個 Loggers 作為 factory,讓產品的程式碼沒有與任何日誌框架有直接的耦合。接著,在 Loggers 加入一個可以塞入任意 key-value 的 context 函式,而這個函式的實作也很單純,取得一個 ThreadLocal 物件,然後把東西塞進去。塞進去的資料可以透過 flush 清除,但只有當初放進去的執行緒才能清除對應的資料。

然後,提供 logger 函式取得一個 Logger 物件。Logger 本身沒有什麼複雜的邏輯,主要是管理 local context,和提供 debuginfowarning 和 error 等函式的常用參數 overloading 實作。這次除了用 ThreadLocal 提供全域的 context,還加入了僅限於該物件的 local context,以及寫日誌時當下的 context,最終三個 context 會被合併送給 appender。

到這邊為止,算是已經有一個富含情境的日誌框架了,但還沒有滿足最一開始說的關聯式查詢,只是幫 correlation ID 找到一個地方儲存。

關聯式查詢

接下來便是確保 correlation ID 在正確的時機都被放到入 context 中。若仔細想想整個後端系統的運作,會發現一件任務的發動大概就幾種情況 (閒談軟體設計:多種 work 類型 ):

  • API 請求
  • 定時執行 (cronjob)
  • 事件驅動

也就是說,只要在這些任務發動時,產生或沿用 correlation ID 放到 context 中,那就可以把相關聯的日誌都透過 correlation ID 關聯起來。

針對 API 請求,先幫所有 API 請求加上 before handler,這裡 context 參數是 Javalin 的請求物件,先看看請求者是否提供 request ID,如果有,就把該 request ID 作為 correlation ID,如果沒有,就產生一個 UUID。

改天再聊為什麼沒有使用 Spring boot 而選用 Javalin

接著是定時執行,在目前的設計裡,所有的 cronjob 都有 setuprun 和 clean 三個函式,setup 和 clean 負責初始化資源和清理資源,因此,只需要在 setup 執行時將 jobId 視作 correlation ID 即可。

事件驅動稍微麻煩一點,API 請求和 cronjob 執行時往往都在一個程序 (Process) 裡,但事件驅動卻是常常跨程序,例如有個處理登入的 API 請求,在完成後發出一個 authenticated 事件,然後另一個應用程序接收到事件後開始處理,這時 correlation ID 若沒有跟著傳送到處理事件的程序,那該程序寫入的日誌就關聯不起來。

當初,為了與框架保持距離寫的 EventCenter 抽象層,此時,正好可以發揮額外的功能,將 correlation ID 加在事件信封的 metadata 中

然後在收到事件時,從 metadata 取出 correlation ID 放到 context 後,才將信封中真正的事件交給訂閱者,如此,correlation ID 便跨越程序的邊界。

到這裡,滿足了關聯式查詢的 criteria,如圖,只需要 correlation ID 就能將兩個不同程序相關聯的日誌全部找出來。

raw-image

結構化日誌和支援雲生態

接著,要如何滿足結構化日誌和支援雲生態?其實剛剛的 cronjob 範例程式碼中藏有玄機,在初始化日誌時,有用到一個 GCPLogAppender,這物件很單純只做一件事,將資訊轉成 GCP 要求的格式,例如將 log level 改成 severity。最後,以 JSON 格式印到 system out。

就這樣,GCP 當發現 system out 裡有類似 JSON 的訊息,會自動解析,最後在 GCP 的搜尋介面可以看到結構化的資料。

raw-image

和商業邏輯核心保持距離

最後一個 criteria,和商業邏輯核心保持距離,實際上,已經完成了,由於商業邏輯層只使用 Loggers 和 Logger 這兩個自訂抽象層:

商業邏輯層除了提供情境,上述其他如滿足關聯式查詢和結構式日誌等實作細節,商業邏輯層完全不需要知道如何達成,使用時也不用做任何事,假設哪天要將程式改上到 AWS 環境,最多只需要提供一個 AWSLogAppender,然後在最外層,也就是應用程式的進入點換上去就搞定,商業邏輯層完全不需要更動。

前世今生

到這邊,本來以為整個設計已經很夠用了,但使用一陣子後,突然發現有個情況要處理。故事是當初在追某個 long polling 下來的事件,發現無法找到當初發出這個事件的過程。原因是發出事件的 Request A,其 correlation ID 會一路到到後面 (下圖藍色路徑),這邊都符合預期,但發起 long polling 的 Request B,自己的 correlation ID (下圖橘色路徑),在原有的設計 Request B 在 subscribe 的當下,其 correlation ID 會被 Request A 的 correlation ID 取代掉了,於是用 Request B 的 correlation ID 查詢時,其實是找不到什麼東西的。

raw-image

那怎麼辦?兩者只能選一個嗎?小孩子才做選擇,當然是兩者都留下來啊,透過檢查 context 是否已經有 correlation ID,如果有,就將其合併。

於是可以發現,當用 Request B 的 correlation ID 查詢時,會看到 context 被合併了。

raw-image

打開對應的內容可以看到 correlation ID 有兩筆,其中一個是 Request B 的,另一個就是 Request A 的。

raw-image

於是就可以用 Request A 的 correlation ID,找到當初產生該事件的過程了。

raw-image

如果把藍色路徑當成是前世,橘色路徑當成是金生,透過合併 correlation ID 的方式,就把前世和金生給串起來了。

小結

上面所有的設計,程式碼其實沒有多少行,加上 GCP 自動會解析類似 JSON 的文字,也沒有用到 Java 常見的日誌框架 (SLFJ 或 Logback,只有相依的套件,像是 GCP 套件有用到)。少少的程式碼,就滿足了五個我覺得相當重要的 criteria,想想真的是蠻划算的。

留言
avatar-img
Spirit 異想世界
58會員
121內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
Spirit 異想世界的其他內容
2024/03/23
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
2024/03/23
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
2024/03/09
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
2024/03/09
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
2024/03/02
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
2024/03/02
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
看更多
你可能也想看
Thumbnail
本文深度解析賽勒布倫尼科夫的舞臺作品《傳奇:帕拉贊諾夫的十段殘篇》,如何以十段殘篇,結合帕拉贊諾夫的電影美學、象徵意象與當代政治流亡抗爭,探討藝術在儀式消失的現代社會如何承接意義,並展現不羈的自由靈魂。
Thumbnail
本文深度解析賽勒布倫尼科夫的舞臺作品《傳奇:帕拉贊諾夫的十段殘篇》,如何以十段殘篇,結合帕拉贊諾夫的電影美學、象徵意象與當代政治流亡抗爭,探討藝術在儀式消失的現代社會如何承接意義,並展現不羈的自由靈魂。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
本文分析導演巴里・柯斯基(Barrie Kosky)如何運用極簡的舞臺配置,將布萊希特(Bertolt Brecht)的「疏離效果」轉化為視覺奇觀與黑色幽默,探討《三便士歌劇》在當代劇場中的新詮釋,並藉由舞臺、燈光、服裝、音樂等多方面,分析該作如何在保留批判核心的同時,觸及觀眾的觀看位置與人性幽微。
Thumbnail
5 月將於臺北表演藝術中心映演的「2026 北藝嚴選」《海妲・蓋柏樂》,由臺灣劇團「晃晃跨幅町」製作,本文將以從舞台符號、聲音與表演調度切入,討論海妲・蓋柏樂在父權社會結構下的困境,並結合榮格心理學與馮.法蘭茲對「阿尼姆斯」與「永恆少年」原型的分析,理解女人何以走向精神性的操控、毀滅與死亡。
Thumbnail
5 月將於臺北表演藝術中心映演的「2026 北藝嚴選」《海妲・蓋柏樂》,由臺灣劇團「晃晃跨幅町」製作,本文將以從舞台符號、聲音與表演調度切入,討論海妲・蓋柏樂在父權社會結構下的困境,並結合榮格心理學與馮.法蘭茲對「阿尼姆斯」與「永恆少年」原型的分析,理解女人何以走向精神性的操控、毀滅與死亡。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
《轉轉生》(Re:INCARNATION)為奈及利亞編舞家庫德斯.奧尼奎庫與 Q 舞團創作的當代舞蹈作品,結合拉各斯街頭節奏、Afrobeat/Afrobeats、以及約魯巴宇宙觀的非線性時間,建構出關於輪迴的「誕生—死亡—重生」儀式結構。本文將從約魯巴哲學概念出發,解析其去殖民的身體政治。
Thumbnail
你好,在下最近在學習開發web,學了html css js,也得出一些心得,由於網路上已有許多教學,所以我會著重在如何開發出to do List,以及解釋我寫的程式碼。相關的教學我會直接貼網址。如果我有什麼地方出錯,或者是可以寫得更好,歡迎在下方留言,討論。 首先先介紹我的開發環境: 我用了vs
Thumbnail
你好,在下最近在學習開發web,學了html css js,也得出一些心得,由於網路上已有許多教學,所以我會著重在如何開發出to do List,以及解釋我寫的程式碼。相關的教學我會直接貼網址。如果我有什麼地方出錯,或者是可以寫得更好,歡迎在下方留言,討論。 首先先介紹我的開發環境: 我用了vs
Thumbnail
實際就業後,會發現收集與分析需求,通常都不是工程師在做,會有另一群人,以非工程的角度收集及分析需求,然後在開發過程中蹦出不同的火花,於是很好奇另一群人的想法是什麼?我不敢說這本書能完全代表另一群人的想法,但確實能夠得到很多有用的思維。推薦給所有的軟體工程師。
Thumbnail
實際就業後,會發現收集與分析需求,通常都不是工程師在做,會有另一群人,以非工程的角度收集及分析需求,然後在開發過程中蹦出不同的火花,於是很好奇另一群人的想法是什麼?我不敢說這本書能完全代表另一群人的想法,但確實能夠得到很多有用的思維。推薦給所有的軟體工程師。
Thumbnail
觀察者模式透過主題訂閱/訊息通知的機制,極度增強系統的可擴展性、靈活性以及降低組件間的耦合度。概念直觀簡單,是非常實用的設計模式。
Thumbnail
觀察者模式透過主題訂閱/訊息通知的機制,極度增強系統的可擴展性、靈活性以及降低組件間的耦合度。概念直觀簡單,是非常實用的設計模式。
Thumbnail
軟體系統的發展歷程大多相似,首重解決基本需求、提供操作介面,進而提升安全性、擴充功能、優化操作。
Thumbnail
軟體系統的發展歷程大多相似,首重解決基本需求、提供操作介面,進而提升安全性、擴充功能、優化操作。
Thumbnail
列出一套完整的程式 程式設計有許多種方法,不過通常會先列出清單的再逐一執行,這樣會加快程式設計的速度。設計通常會採取順推的辦法。所以順推的程式設計方式就是經歷觀念溝通、系統分析、資料統合、權限管理、頻率與時間、後台管理、畫面設計等等階段後,將框架設計完了以後,先列出一套完整的程式,將所有使用者都確
Thumbnail
列出一套完整的程式 程式設計有許多種方法,不過通常會先列出清單的再逐一執行,這樣會加快程式設計的速度。設計通常會採取順推的辦法。所以順推的程式設計方式就是經歷觀念溝通、系統分析、資料統合、權限管理、頻率與時間、後台管理、畫面設計等等階段後,將框架設計完了以後,先列出一套完整的程式,將所有使用者都確
Thumbnail
程式設計中不可或缺的一部分 介面是使用者與程式互動的媒介,因此介面的設計會影響使用者的體驗和感受。一個清晰明白、易懂的介面,可以讓使用者輕鬆地使用程式,並獲得良好的使用體驗。 需要與程式設計師密切溝通 設計師需要了解程式的功能和需求,並根據使用者的習慣和需求進行設計。設計師和程式設計師之間的溝
Thumbnail
程式設計中不可或缺的一部分 介面是使用者與程式互動的媒介,因此介面的設計會影響使用者的體驗和感受。一個清晰明白、易懂的介面,可以讓使用者輕鬆地使用程式,並獲得良好的使用體驗。 需要與程式設計師密切溝通 設計師需要了解程式的功能和需求,並根據使用者的習慣和需求進行設計。設計師和程式設計師之間的溝
Thumbnail
確保沒有遺漏或錯誤 程式的完整資訊資料對於程式設計至關重要。這是因為只有透過完整的資訊,我們才能確保在程式設計中沒有任何遺漏或錯誤。最終,後台管理扮演著管理系統中所有動作和行為是否符合特定標準的重要角色。 採取不符合預期的行動 這種符合性的重要性在於,當我們設計程式時,希望使用者按照預期的方式
Thumbnail
確保沒有遺漏或錯誤 程式的完整資訊資料對於程式設計至關重要。這是因為只有透過完整的資訊,我們才能確保在程式設計中沒有任何遺漏或錯誤。最終,後台管理扮演著管理系統中所有動作和行為是否符合特定標準的重要角色。 採取不符合預期的行動 這種符合性的重要性在於,當我們設計程式時,希望使用者按照預期的方式
Thumbnail
系統的分析與規劃 在談到程式設計時,首要的是進行系統的分析與規劃。程式設計的起點通常是系統分析與規劃,這涉及到如何分析和設計系統的大原則和方向。為了達到預期效果,重要的是擁有對產業的清晰邏輯認識和深入了解。 進行深入了解 若要進行系統分析,必須對企業的設計和程式設計的對象進行深入了解,以充分理
Thumbnail
系統的分析與規劃 在談到程式設計時,首要的是進行系統的分析與規劃。程式設計的起點通常是系統分析與規劃,這涉及到如何分析和設計系統的大原則和方向。為了達到預期效果,重要的是擁有對產業的清晰邏輯認識和深入了解。 進行深入了解 若要進行系統分析,必須對企業的設計和程式設計的對象進行深入了解,以充分理
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News