Logging v2 - #25
Merged
Merged
Conversation
yorukot
marked this pull request as ready for review
May 27, 2026 13:38
yukicoder0509
requested changes
Jul 25, 2026
yukicoder0509
left a comment
Member
There was a problem hiding this comment.
I think we need some descriptions of this PR. Also, please document the new APIs and the deprecated APIs in the README.
YukinaMochizuki
previously approved these changes
Aug 29, 2026
Replace the `Deprecated:` doc markers on the database wrap helpers and logutil.WithContext with plain comments, so linters and IDEs stop flagging existing call sites while the docs still point new code elsewhere. Restore the structured logging the wrappers lost: an Error on entry and a Warn with operation/unknown_error (plus table/key/value) after classification.
YukinaMochizuki
force-pushed
the
logging-v2
branch
from
August 29, 2026 07:59
10dc3aa to
b86b0e9
Compare
yukicoder0509
previously approved these changes
Aug 29, 2026
YukinaMochizuki
dismissed stale reviews from yukicoder0509 and themself
via
August 29, 2026 08:02
245bb7d
The nil-context cases are intentional: they cover SetupFlow's fallback when a caller has no context. Passing a nil context.Context variable keeps that coverage while staticcheck's SA1012 only flags a literal nil.
yukicoder0509
approved these changes
Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type of changes
這個 PR 想解決什麼
現在的 log 有兩個問題。
第一個是同一件事會被寫很多次,而且每一次都只看得到一半。以「建立使用者時 email 重複」為例,
WrapDBError在 repository 層先寫一筆 Error 再寫一筆 Warn,往上傳到problem.WriteError又寫第三筆。三筆講的是同一個失敗,可是最底層那兩筆只知道 SQL 出了什麼錯,不知道是哪個 user 發的哪個 request;最上層那筆知道 request,但 driver 的細節在中途被%v壓成字串,已經撈不回來了。查問題的時候要把三筆湊起來看,而且湊不完整。第二個是 log 的內容是寫給人讀的句子,不是可以查詢的資料。
"Failed to create user"這種 message 沒辦法拿來做聚合,想知道「這週有多少次因為 email 重複而建立失敗」只能 grep,欄位名稱也是各寫各的。會變成這樣的原因是
WrapDBError這類 helper 一次做了三件事:寫 log、分類 error、回傳 error。既然它在最底層就把 log 寫掉了,那個時間點能拿到的 context 自然就只有那麼多。所以這個 PR 做的事是把這三件事拆開,讓 error 負責把資訊揹到邊界,context 負責帶 request scope 的欄位,log 只在邊界寫一次。
使用前後的差異
以前在 repository 層這樣寫,log 就順便產生了:
同一個請求會產生三筆 log(以下為示意):
改完之後,handler 進來的地方先把這個 flow 的身分交代清楚:
repository 層只回傳分類過的 error,不寫 log:
到邊界時
problem.WriteError寫出一筆,該有的資訊都在同一筆裡面:三筆變一筆,而且底層的 driver 錯誤訊息跟上層的 request 身分同時保留下來。
換過去之後可以做到什麼
查詢從 grep 變成用欄位篩。想看某個使用者這週所有失敗的操作,條件是
enduser.id加event.outcome=failure;想看特定失敗類型的趨勢,直接對error.type做聚合。這些欄位名稱照 OpenTelemetry semantic conventions 走,collector 或 log aggregation 那邊不用另外寫 mapping。一個請求的所有 log 可以串起來。
request.id跟 trace 欄位由 context 自動帶上去,不需要每個 call site 手動掛。錯誤分類有固定的字彙表。
ErrorType跟EventOutcome都是常數,不會這裡寫failed那裡寫failure,也不會同一種失敗在不同 package 有不同講法。錯誤細節可以從發生的地方帶到寫 log 的地方。
InfoError讓底層把 operation、欄位名稱、能不能 retry 這些資訊掛在 error 上,邊界寫 log 時自動展開成error.info.*,中間層不用為了保留這些資訊而多包一層自己的 struct。檔案行號會指到真正的呼叫點。以前靠
zap.AddCallerSkip(1)手動數層數,多包一層就要改數字,很容易對不準;現在在 level helper 裡用runtime.Callers直接抓。技術細節
新增
pkg/log的 context-first APISetupFlow(ctx, logger, eventName, fields...)開場,把 request scope 的欄位放進 context,同時幫 logger 掛上event.name,回傳這兩個值。ctx或logger傳 nil 會分別退回context.Background()跟zap.NewNop()。level helper
Debug/Info/Warn/Error/DPanic/Panic/Fatal的簽章是(ctx, logger, msg, ...)。它們會呼叫Constructs把 context 上的欄位合併進來,並且補上code.file.path、code.file.name、code.line.number、code.function.name、code.namespace。收 error 的那幾個還會把 error 展開成結構化欄位。Constructs(ctx, logger)取代舊的WithContext,合併WithFields存的欄位、OTel 的trace_id/span_id/trace_flags/trace_sampled/trace_state,以及 user 和 request 的欄位。with.go提供 context 端的 helper:WithFields、WithUserID、WithUsername、WithDisplayName、WithRequestID、WithReason、WithErrorType。欄位存在以 key 為索引的 map 裡,同一個 key 重設會覆蓋,所以一筆 log 不會出現重複的 key,空 key 跟空值會直接略過。logger 端的 helper 有
WithEventName、WithEventOutcome、WithOutcome、WithTraceContext、WithUserContext,適合 logger 本身已經代表某個特定 event 的情況。constant.go定義EventOutcome常數:success、failure、cancelled、timeout、unknown。需要自訂字串時用WithOutcome。doc.go說明 context-first 跟 logger-first 兩條路徑各自的適用時機。新增
pkg/error(errutil)ErrorType常數涵蓋常見的分類(INVALID_ARGUMENT、NOT_FOUND、ALREADY_EXISTS、PERMISSION_DENIED、INTERNAL等),讓不相干的 Go error type 可以按照維運上的意義歸成同一類。InfoError[K, V]包住原本的 error,額外帶一個ErrorType跟一份 metadata map。它實作了Unwrap,所以errors.Is和errors.As照樣穿得過去。建構子是
NewInfoError和NewTypedInfoError,另外有WrapInfoError和WrapTypedInfoError兩個同義的名字。InfoCarrier跟ErrorTypeCarrier這兩個 interface 用errors.As尋找,所以掛在 error chain 深處的 metadata 到邊界一樣撈得到。ErrorFields(err)產生zap.Error、error.message、error.type(有的話)跟error.info.*;ErrorFieldsWithStacktrace(err)再多加exception.stacktrace。ErrorInfoKey提供常用的 key:operation、reason、field、retryable、user.id、request.id。調整
pkg/problemwriteProblemResponse改成收ctx,並且讓 logger 先經過logutil.Constructs,所以 problem response 會繼承整個 flow 的 context 欄位。錯誤回應會帶上
http.status_code、problem.type、problem.title、problem.detail、problem.instance跟error.kind。5xx 用 Error level 並附 stacktrace,4xx 用 Warn level 不附。新增
problemErrorKind(status)把 HTTP status 對應到分類字串。調整
pkg/databaseWrapDBError、WrapDBErrorWithKeyValue、WrapMSSQLError、WrapMSSQLErrorWithKeyValue標記為Deprecated,並且拿掉裡面的 log,只留分類。包裝方式從
%v改成%w,InternalServerError補上Unwrap(),底層的 driver error 因此可以透過errors.Is和errors.As取得。logger參數保留但不再使用,維持簽章相容。相容性
WithContext搬到deprecated.go,行為跟舊欄位名(trace_id、span_id、user_id、username、display-name)完全沒動,既有的呼叫端(包含pkg/trace/middleware.go)不受影響,只是標記為Deprecated,建議改用Constructs、WithTraceContext、WithUserContext。pkg/database那幾個 helper 的簽章沒變,但它們不再寫 log。這一點編譯器不會提醒,測試也不會失敗,原本靠這些 helper 產生 log 的服務升上來之後會悄悄少掉那些 log entry,需要改在邊界寫。這是升級時最容易忽略的地方。測試與範例
pkg/log/flow_test.go用zaptest/observer驗證SetupFlow搭配WithReason、WithErrorType、WithEventOutcome之後產生的欄位是否符合預期,另外驗證ctx、logger傳 nil 以及 event name 為空字串的情況。examples/simple-log/log.go示範完整流程:SetupFlow到 context 補欄位到WrapInfoError到logutil.Error。example/改名為examples/。Merge 前要處理的事
這條 branch 落後
main26 個 commit 且目前有衝突(main上的 #34 也動過pkg/log/logger.go),需要 rebase。README 的
pkg/log段落目前只寫WithContext,還沒涵蓋SetupFlow、level helper 跟pkg/error,review 提到的文件需求尚未處理。pkg/problem送出的是error.kind,pkg/error定義的是error.type,兩者用同一組字彙卻是不同的 key,建議收斂成一個再 merge。