<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>模組二：客戶端可觀測性 on Tarragon</title><link>https://tarrragon.github.io/blog/testing/02-client-observability/</link><description>Recent content in 模組二：客戶端可觀測性 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/testing/02-client-observability/index.xml" rel="self" type="application/rss+xml"/><item><title>三層 log 設計</title><link>https://tarrragon.github.io/blog/testing/02-client-observability/three-layer-log-design/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/02-client-observability/three-layer-log-design/</guid><description>&lt;p>客戶端 log 分成三層，每層記錄不同粒度的資訊，服務不同的 debug 場景。三層的區別在於回答的問題不同：連線生命週期回答「整體流程走到哪一步」，protocol 訊息回答「通訊細節是什麼」，使用者行為回答「使用者做了什麼操作」。&lt;/p>
&lt;h2 id="連線生命週期-log">連線生命週期 log&lt;/h2>
&lt;p>連線生命週期 log 記錄的是「流程走到第幾步、每步成功或失敗」。這一層的 log 粒度是步驟級 — 不記錄每一個封包或每一次函式呼叫，只記錄流程中的關鍵節點。&lt;/p>
&lt;p>以 app_tunnel 的連線流程為例，連線生命週期包含五步：biometric 認證 → credential 讀取 → WebSocket 連線 → auth token 發送 → stream 訂閱。每步完成時記一條 log，失敗時記一條包含原因的 log。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[conn] Step 1/5: biometric auth completed (duration: 320ms)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">[conn] Step 2/5: credential loaded (user: admin)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">[conn] Step 3/5: WebSocket connected (url: wss://...)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">[conn] Step 4/5: auth token sent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">[conn] Step 5/5: stream subscribed, ready&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>app_tunnel 在實機測試前六個核心元件中只有兩個有 log，且全是 W2 修復時事後補上的（&lt;a href="https://tarrragon.github.io/blog/testing/cases/client-log-absent-debug-cost/" data-link-title="T.C4 Client-side log 缺失導致 debug 只能靠實機盲測" data-link-desc="Flutter app 六個核心元件中只有兩個有 log（且全是 W2 hotfix 補的），連線失敗時開發者無法從任何 log 判斷失敗發生在哪一步 — 被迫用最昂貴的 debug 方式：插拔裝置反覆測試">T.C4&lt;/a>）。W2-002 auth token 問題的 debug 過程中，開發者無法從任何 log 判斷失敗發生在五步中的哪一步。如果有連線生命週期 log，第一次連線就能看到「Step 3 完成，Step 4 未執行」— 直接定位到 auth token 缺失。&lt;/p>
&lt;p>連線生命週期 log 在所有模式（debug 和 release）都應該啟用。這層 log 量小（每次連線 5-10 條），不影響效能，但在 production 問題回報時是第一手資訊來源。&lt;/p>
&lt;h2 id="protocol-訊息-log">Protocol 訊息 log&lt;/h2>
&lt;p>Protocol 訊息 log 記錄的是通訊協議層面的細節：發送和接收的 frame type、payload 前綴、handshake 參數、逾時值。這一層的粒度比連線生命週期更細 — 每一次 send/receive 都記錄。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[proto] TX: text frame, payload: {&amp;#34;AuthToken&amp;#34;:&amp;#34;base64...&amp;#34;} (42 bytes)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">[proto] RX: text frame, payload prefix: &amp;#34;0&amp;#34; (output data, 128 bytes)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">[proto] TX: binary frame, payload: [72, 101, 108, 108, 111] (5 bytes)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Protocol log 在 debug 時幫助確認「程式碼發送了什麼、收到了什麼」。app_tunnel 的 text/binary frame 問題（&lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1&lt;/a>）如果有 protocol log，開發者會在 log 中看到 &lt;code>TX: binary frame&lt;/code> 而非預期的 &lt;code>TX: text frame&lt;/code> — 直接指向 frame type 問題。&lt;/p></description><content:encoded><![CDATA[<p>客戶端 log 分成三層，每層記錄不同粒度的資訊，服務不同的 debug 場景。三層的區別在於回答的問題不同：連線生命週期回答「整體流程走到哪一步」，protocol 訊息回答「通訊細節是什麼」，使用者行為回答「使用者做了什麼操作」。</p>
<h2 id="連線生命週期-log">連線生命週期 log</h2>
<p>連線生命週期 log 記錄的是「流程走到第幾步、每步成功或失敗」。這一層的 log 粒度是步驟級 — 不記錄每一個封包或每一次函式呼叫，只記錄流程中的關鍵節點。</p>
<p>以 app_tunnel 的連線流程為例，連線生命週期包含五步：biometric 認證 → credential 讀取 → WebSocket 連線 → auth token 發送 → stream 訂閱。每步完成時記一條 log，失敗時記一條包含原因的 log。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[conn] Step 1/5: biometric auth completed (duration: 320ms)
</span></span><span class="line"><span class="ln">2</span><span class="cl">[conn] Step 2/5: credential loaded (user: admin)
</span></span><span class="line"><span class="ln">3</span><span class="cl">[conn] Step 3/5: WebSocket connected (url: wss://...)
</span></span><span class="line"><span class="ln">4</span><span class="cl">[conn] Step 4/5: auth token sent
</span></span><span class="line"><span class="ln">5</span><span class="cl">[conn] Step 5/5: stream subscribed, ready</span></span></code></pre></div><p>app_tunnel 在實機測試前六個核心元件中只有兩個有 log，且全是 W2 修復時事後補上的（<a href="/blog/testing/cases/client-log-absent-debug-cost/" data-link-title="T.C4 Client-side log 缺失導致 debug 只能靠實機盲測" data-link-desc="Flutter app 六個核心元件中只有兩個有 log（且全是 W2 hotfix 補的），連線失敗時開發者無法從任何 log 判斷失敗發生在哪一步 — 被迫用最昂貴的 debug 方式：插拔裝置反覆測試">T.C4</a>）。W2-002 auth token 問題的 debug 過程中，開發者無法從任何 log 判斷失敗發生在五步中的哪一步。如果有連線生命週期 log，第一次連線就能看到「Step 3 完成，Step 4 未執行」— 直接定位到 auth token 缺失。</p>
<p>連線生命週期 log 在所有模式（debug 和 release）都應該啟用。這層 log 量小（每次連線 5-10 條），不影響效能，但在 production 問題回報時是第一手資訊來源。</p>
<h2 id="protocol-訊息-log">Protocol 訊息 log</h2>
<p>Protocol 訊息 log 記錄的是通訊協議層面的細節：發送和接收的 frame type、payload 前綴、handshake 參數、逾時值。這一層的粒度比連線生命週期更細 — 每一次 send/receive 都記錄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[proto] TX: text frame, payload: {&#34;AuthToken&#34;:&#34;base64...&#34;} (42 bytes)
</span></span><span class="line"><span class="ln">2</span><span class="cl">[proto] RX: text frame, payload prefix: &#34;0&#34; (output data, 128 bytes)
</span></span><span class="line"><span class="ln">3</span><span class="cl">[proto] TX: binary frame, payload: [72, 101, 108, 108, 111] (5 bytes)</span></span></code></pre></div><p>Protocol log 在 debug 時幫助確認「程式碼發送了什麼、收到了什麼」。app_tunnel 的 text/binary frame 問題（<a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1</a>）如果有 protocol log，開發者會在 log 中看到 <code>TX: binary frame</code> 而非預期的 <code>TX: text frame</code> — 直接指向 frame type 問題。</p>
<p>Protocol log 在 release mode 應該能關閉。這層 log 量大（每次鍵盤輸入一條），且 payload 可能包含敏感資訊。Debug mode 預設啟用，release mode 提供開關（例如隱藏設定頁的 toggle）讓進階使用者在回報問題時開啟。</p>
<h2 id="使用者行為-log">使用者行為 log</h2>
<p>使用者行為 log 記錄的是使用者在 UI 上的操作：按鈕點擊、畫面切換、設定變更。這層 log 的粒度是操作級 — 使用者做了一個有意義的動作記一條。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[ui] screen: HomeScreen, action: tap Connect Terminal
</span></span><span class="line"><span class="ln">2</span><span class="cl">[ui] screen: TerminalScreen, state: connecting → connected
</span></span><span class="line"><span class="ln">3</span><span class="cl">[ui] screen: TerminalScreen, action: tap back button
</span></span><span class="line"><span class="ln">4</span><span class="cl">[ui] screen: HomeScreen, state: returned from terminal</span></span></code></pre></div><p>使用者行為 log 在兩個場景有價值：第一，debug 時還原使用者操作路徑 — 「使用者做了什麼導致問題出現」；第二，結合狀態矩陣（<a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一</a>）做狀態轉換的實際覆蓋率分析 — 哪些狀態轉換在真實使用中經常發生，哪些從未發生。</p>
<p>使用者行為 log 在 release mode 啟用時需要注意隱私。記錄「使用者切換了畫面」是合理的；記錄「使用者輸入了密碼 abc123」需要 redaction 機制（<a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a>）。</p>
<h2 id="三層的關係">三層的關係</h2>
<p>三層 log 各自獨立運作，debug 時通常按照從粗到細的順序使用。</p>
<p><strong>粗篩</strong>：先看連線生命週期 log，確認流程走到哪一步。如果 Step 3 失敗，問題在 WebSocket 連線層。</p>
<p><strong>細查</strong>：切到 protocol 訊息 log，看 Step 3 的連線嘗試中發送和接收了什麼。如果看到 binary frame 發送但沒有回應，問題可能在 frame type。</p>
<p><strong>還原</strong>：如果問題和使用者操作有關（例如只在特定操作順序下觸發），看使用者行為 log，還原操作路徑。</p>
<p>三層 log 用同一個時間戳和 correlation ID（例如連線 session ID），讓跨層比對可行。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>在功能規格中定義 log 點 → <a href="/blog/testing/02-client-observability/log-point-in-spec/" data-link-title="功能規格中的 log 點定義方法" data-link-desc="把 log 點設計從 debug 階段前移到功能規格階段 — 每個功能的規格文件新增可觀測性欄位，列出啟動 / 步驟 / 錯誤 / 完成四類 log 點">功能規格中的 log 點定義方法</a></li>
<li>事後補 log 和設計產物 log 的品質差異 → <a href="/blog/testing/02-client-observability/hotfix-log-vs-designed-log/" data-link-title="「事後補 log」vs「設計產物 log」的品質差異" data-link-desc="事後補的 log 是救火工具、設計產物的 log 是可觀測性基礎設施 — 從 app_tunnel 的 W2 hotfix log 拆解兩者在格式、覆蓋率、維護成本上的差異">「事後補 log」vs「設計產物 log」的品質差異</a></li>
<li>Log 收集方案選擇 → <a href="/blog/testing/02-client-observability/log-endpoint-tradeoff/" data-link-title="自架 log endpoint vs 商業方案的取捨判斷" data-link-desc="自用工具用自架 log receiver（20 行 Go &#43; grep）、商業 app 用 Sentry/Crashlytics — 判斷依據是使用者規模和 debug 需求">自架 log endpoint vs 商業方案</a></li>
<li>事件分類與收集策略 → <a href="/blog/monitoring/01-mental-model/" data-link-title="模組一：監控心智模型" data-link-desc="四類事件（event / error / metric / lifecycle）的分類與收集策略">monitoring 模組一 監控心智模型</a></li>
</ul>
]]></content:encoded></item><item><title>功能規格中的 log 點定義方法</title><link>https://tarrragon.github.io/blog/testing/02-client-observability/log-point-in-spec/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/02-client-observability/log-point-in-spec/</guid><description>&lt;p>Log 點定義是功能規格的一部分，和 API schema 同級。功能規格描述「這個功能做什麼」，log 點規格描述「這個功能執行時留下什麼可觀察的紀錄」。把 log 點設計前移到規格階段，讓 log 成為功能的設計產物，而非事後的 debug 工具（本章合成，TF-9 Derive）。&lt;/p>
&lt;h2 id="四類-log-點">四類 log 點&lt;/h2>
&lt;p>每個功能的 log 點按執行時機分成四類。&lt;/p>
&lt;h3 id="啟動-log">啟動 log&lt;/h3>
&lt;p>功能開始執行時記錄。回答「這個功能是否被觸發了」。&lt;/p>
&lt;p>啟動 log 包含觸發來源（使用者操作、系統排程、外部事件）和初始參數（連線目標、操作類型）。如果一個功能從未被觸發，啟動 log 的缺席就是線索。&lt;/p>
&lt;h3 id="步驟-log">步驟 log&lt;/h3>
&lt;p>功能執行過程中的每個關鍵步驟完成時記錄。回答「流程走到哪裡了」。&lt;/p>
&lt;p>步驟 log 的粒度依功能複雜度而定。三步驟的功能每步記一條；十步驟的功能可以只記關鍵的三到五步。判斷標準是：如果這一步失敗，開發者是否需要知道失敗點在哪。&lt;/p>
&lt;h3 id="錯誤-log">錯誤 log&lt;/h3>
&lt;p>步驟失敗、例外捕獲、非預期狀態出現時記錄。回答「出了什麼問題」。&lt;/p>
&lt;p>錯誤 log 必須包含足夠的 context 讓開發者不需要重現問題就能判斷原因。至少包含：哪一步失敗、失敗原因（error message）、當時的關鍵狀態值。&lt;/p>
&lt;h3 id="完成-log">完成 log&lt;/h3>
&lt;p>功能正常結束時記錄。回答「功能是否成功完成、花了多久」。&lt;/p>
&lt;p>完成 log 包含執行結果和耗時。和啟動 log 配對使用 — 有啟動但沒有完成代表功能中途異常退出。&lt;/p>
&lt;h2 id="在功能規格中加可觀測性欄位">在功能規格中加可觀測性欄位&lt;/h2>
&lt;p>以 app_tunnel 的「連線到 ttyd 終端機」功能為例，傳統規格只寫：&lt;/p>
&lt;ul>
&lt;li>輸入：使用者選擇的伺服器&lt;/li>
&lt;li>處理：建立 WebSocket 連線、發送 auth token、開始接收 terminal output&lt;/li>
&lt;li>輸出：終端機畫面顯示 terminal output&lt;/li>
&lt;/ul>
&lt;p>加上可觀測性欄位後：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>log 點&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>啟動&lt;/td>
 &lt;td>connect.start&lt;/td>
 &lt;td>目標 URL、觸發來源（使用者操作 / 自動重連）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>步驟&lt;/td>
 &lt;td>connect.biometric.done&lt;/td>
 &lt;td>認證結果、耗時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>步驟&lt;/td>
 &lt;td>connect.credential.loaded&lt;/td>
 &lt;td>使用者名稱（密碼 redact）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>步驟&lt;/td>
 &lt;td>connect.ws.connected&lt;/td>
 &lt;td>連線 URL、耗時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>步驟&lt;/td>
 &lt;td>connect.auth.sent&lt;/td>
 &lt;td>token 長度（內容 redact）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>步驟&lt;/td>
 &lt;td>connect.stream.subscribed&lt;/td>
 &lt;td>stream 狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>錯誤&lt;/td>
 &lt;td>connect.{step}.failed&lt;/td>
 &lt;td>失敗步驟、error message、retry count&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>完成&lt;/td>
 &lt;td>connect.done&lt;/td>
 &lt;td>總耗時、最終狀態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表在功能規格階段就能寫出來，因為它只依賴功能的流程設計，不依賴實作細節。功能流程確定後，每一步在哪裡需要 log 點就確定了。&lt;/p>
&lt;h2 id="log-點命名規則">log 點命名規則&lt;/h2>
&lt;p>統一的命名規則讓 log 可以被 grep、過濾和統計。&lt;/p>
&lt;p>&lt;strong>階層式命名&lt;/strong>：&lt;code>{功能}.{步驟}.{事件}&lt;/code>。例如 &lt;code>connect.ws.connected&lt;/code>、&lt;code>connect.auth.failed&lt;/code>。&lt;/p>
&lt;p>&lt;strong>事件後綴統一&lt;/strong>：&lt;code>start&lt;/code>（啟動）、&lt;code>done&lt;/code>（步驟完成）、&lt;code>failed&lt;/code>（失敗）、&lt;code>complete&lt;/code>（功能完成）。&lt;/p>
&lt;p>&lt;strong>和程式碼結構對應&lt;/strong>：log 點名稱對應到程式碼中的函式或模組。&lt;code>connect.biometric.done&lt;/code> 對應 &lt;code>BiometricService.authenticate()&lt;/code> 的成功路徑。這讓開發者看到 log 名稱就知道去哪裡找程式碼。&lt;/p>
&lt;h2 id="log-點規格的-review-檢查">log 點規格的 review 檢查&lt;/h2>
&lt;p>功能規格 review 時，可觀測性欄位的檢查要點：&lt;/p>
&lt;p>&lt;strong>每步都有 log&lt;/strong>：流程中的每個步驟在成功和失敗時都有對應的 log 點。遺漏的步驟意味著該步驟出問題時無法從 log 判斷。&lt;/p>
&lt;p>&lt;strong>錯誤 log 有足夠 context&lt;/strong>：error log 只寫「連線失敗」不夠；需要寫「連線失敗」加上 error code、目標 URL、已完成的步驟。&lt;/p>
&lt;p>&lt;strong>敏感欄位有 redaction 標記&lt;/strong>：密碼、token、個人資料在 log 規格中標記為 redact，實作時用 redaction 機制處理。&lt;/p>
&lt;p>&lt;strong>啟動和完成配對&lt;/strong>：每個功能有啟動 log 就應該有完成 log，形成完整的生命週期。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>三層 log 的詳細設計 → &lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/three-layer-log-design/" data-link-title="三層 log 設計" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — 三層各自的職責、詳細程度和啟停控制">三層 log 設計&lt;/a>&lt;/li>
&lt;li>事後補 log 和設計產物 log 的差異 → &lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/hotfix-log-vs-designed-log/" data-link-title="「事後補 log」vs「設計產物 log」的品質差異" data-link-desc="事後補的 log 是救火工具、設計產物的 log 是可觀測性基礎設施 — 從 app_tunnel 的 W2 hotfix log 拆解兩者在格式、覆蓋率、維護成本上的差異">「事後補 log」vs「設計產物 log」的品質差異&lt;/a>&lt;/li>
&lt;li>Log 中的敏感資訊處理 → &lt;a href="https://tarrragon.github.io/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Log 點定義是功能規格的一部分，和 API schema 同級。功能規格描述「這個功能做什麼」，log 點規格描述「這個功能執行時留下什麼可觀察的紀錄」。把 log 點設計前移到規格階段，讓 log 成為功能的設計產物，而非事後的 debug 工具（本章合成，TF-9 Derive）。</p>
<h2 id="四類-log-點">四類 log 點</h2>
<p>每個功能的 log 點按執行時機分成四類。</p>
<h3 id="啟動-log">啟動 log</h3>
<p>功能開始執行時記錄。回答「這個功能是否被觸發了」。</p>
<p>啟動 log 包含觸發來源（使用者操作、系統排程、外部事件）和初始參數（連線目標、操作類型）。如果一個功能從未被觸發，啟動 log 的缺席就是線索。</p>
<h3 id="步驟-log">步驟 log</h3>
<p>功能執行過程中的每個關鍵步驟完成時記錄。回答「流程走到哪裡了」。</p>
<p>步驟 log 的粒度依功能複雜度而定。三步驟的功能每步記一條；十步驟的功能可以只記關鍵的三到五步。判斷標準是：如果這一步失敗，開發者是否需要知道失敗點在哪。</p>
<h3 id="錯誤-log">錯誤 log</h3>
<p>步驟失敗、例外捕獲、非預期狀態出現時記錄。回答「出了什麼問題」。</p>
<p>錯誤 log 必須包含足夠的 context 讓開發者不需要重現問題就能判斷原因。至少包含：哪一步失敗、失敗原因（error message）、當時的關鍵狀態值。</p>
<h3 id="完成-log">完成 log</h3>
<p>功能正常結束時記錄。回答「功能是否成功完成、花了多久」。</p>
<p>完成 log 包含執行結果和耗時。和啟動 log 配對使用 — 有啟動但沒有完成代表功能中途異常退出。</p>
<h2 id="在功能規格中加可觀測性欄位">在功能規格中加可觀測性欄位</h2>
<p>以 app_tunnel 的「連線到 ttyd 終端機」功能為例，傳統規格只寫：</p>
<ul>
<li>輸入：使用者選擇的伺服器</li>
<li>處理：建立 WebSocket 連線、發送 auth token、開始接收 terminal output</li>
<li>輸出：終端機畫面顯示 terminal output</li>
</ul>
<p>加上可觀測性欄位後：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>log 點</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>啟動</td>
          <td>connect.start</td>
          <td>目標 URL、觸發來源（使用者操作 / 自動重連）</td>
      </tr>
      <tr>
          <td>步驟</td>
          <td>connect.biometric.done</td>
          <td>認證結果、耗時</td>
      </tr>
      <tr>
          <td>步驟</td>
          <td>connect.credential.loaded</td>
          <td>使用者名稱（密碼 redact）</td>
      </tr>
      <tr>
          <td>步驟</td>
          <td>connect.ws.connected</td>
          <td>連線 URL、耗時</td>
      </tr>
      <tr>
          <td>步驟</td>
          <td>connect.auth.sent</td>
          <td>token 長度（內容 redact）</td>
      </tr>
      <tr>
          <td>步驟</td>
          <td>connect.stream.subscribed</td>
          <td>stream 狀態</td>
      </tr>
      <tr>
          <td>錯誤</td>
          <td>connect.{step}.failed</td>
          <td>失敗步驟、error message、retry count</td>
      </tr>
      <tr>
          <td>完成</td>
          <td>connect.done</td>
          <td>總耗時、最終狀態</td>
      </tr>
  </tbody>
</table>
<p>這張表在功能規格階段就能寫出來，因為它只依賴功能的流程設計，不依賴實作細節。功能流程確定後，每一步在哪裡需要 log 點就確定了。</p>
<h2 id="log-點命名規則">log 點命名規則</h2>
<p>統一的命名規則讓 log 可以被 grep、過濾和統計。</p>
<p><strong>階層式命名</strong>：<code>{功能}.{步驟}.{事件}</code>。例如 <code>connect.ws.connected</code>、<code>connect.auth.failed</code>。</p>
<p><strong>事件後綴統一</strong>：<code>start</code>（啟動）、<code>done</code>（步驟完成）、<code>failed</code>（失敗）、<code>complete</code>（功能完成）。</p>
<p><strong>和程式碼結構對應</strong>：log 點名稱對應到程式碼中的函式或模組。<code>connect.biometric.done</code> 對應 <code>BiometricService.authenticate()</code> 的成功路徑。這讓開發者看到 log 名稱就知道去哪裡找程式碼。</p>
<h2 id="log-點規格的-review-檢查">log 點規格的 review 檢查</h2>
<p>功能規格 review 時，可觀測性欄位的檢查要點：</p>
<p><strong>每步都有 log</strong>：流程中的每個步驟在成功和失敗時都有對應的 log 點。遺漏的步驟意味著該步驟出問題時無法從 log 判斷。</p>
<p><strong>錯誤 log 有足夠 context</strong>：error log 只寫「連線失敗」不夠；需要寫「連線失敗」加上 error code、目標 URL、已完成的步驟。</p>
<p><strong>敏感欄位有 redaction 標記</strong>：密碼、token、個人資料在 log 規格中標記為 redact，實作時用 redaction 機制處理。</p>
<p><strong>啟動和完成配對</strong>：每個功能有啟動 log 就應該有完成 log，形成完整的生命週期。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>三層 log 的詳細設計 → <a href="/blog/testing/02-client-observability/three-layer-log-design/" data-link-title="三層 log 設計" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — 三層各自的職責、詳細程度和啟停控制">三層 log 設計</a></li>
<li>事後補 log 和設計產物 log 的差異 → <a href="/blog/testing/02-client-observability/hotfix-log-vs-designed-log/" data-link-title="「事後補 log」vs「設計產物 log」的品質差異" data-link-desc="事後補的 log 是救火工具、設計產物的 log 是可觀測性基礎設施 — 從 app_tunnel 的 W2 hotfix log 拆解兩者在格式、覆蓋率、維護成本上的差異">「事後補 log」vs「設計產物 log」的品質差異</a></li>
<li>Log 中的敏感資訊處理 → <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a></li>
</ul>
]]></content:encoded></item><item><title>自架 log endpoint vs 商業方案的取捨判斷</title><link>https://tarrragon.github.io/blog/testing/02-client-observability/log-endpoint-tradeoff/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/02-client-observability/log-endpoint-tradeoff/</guid><description>&lt;p>Log 收集方案的選擇取決於兩個因素：使用者在哪裡（同機 / 同網段 / 外部網路），以及 log 的消費者是誰（開發者自己 / 維運團隊 / 客服團隊）。自用工具和商業產品對這兩個因素的答案不同，適合不同的方案。&lt;/p>
&lt;h2 id="自架-log-endpoint-的適用場景">自架 log endpoint 的適用場景&lt;/h2>
&lt;p>自架 log endpoint 適合的前提是：client 和 server 在同一個網路內（同機、同 LAN、同 VPN/tailnet），log 的唯一消費者是開發者本人。&lt;/p>
&lt;p>app_tunnel 就是這個場景。Server（ttyd）和 client（Flutter app）在同一台機器或同一個 Tailscale tailnet 內。開發者同時是使用者和維運者。Log 的消費方式是 grep — 不需要 dashboard、不需要告警、不需要多人共享。&lt;/p>
&lt;p>在這個場景下，自架 log endpoint 的成本遠低於商業方案。一個 Go 程式開 HTTP endpoint 接收 JSON log 寫入檔案，20 行程式碼就能完成。Client 端的 &lt;code>AppLogger&lt;/code> 在 debug mode 同時寫 console 和 POST 到 endpoint。Debug 時用 &lt;code>grep&lt;/code> + &lt;code>jq&lt;/code> 查詢，不需要額外工具。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Client (Flutter) → HTTP POST /log → Go receiver → JSON file → grep/jq&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個方案沒有外部依賴、沒有帳號管理、沒有費用、沒有資料隱私顧慮（log 不離開本機網路）。&lt;/p>
&lt;h2 id="商業方案的適用場景">商業方案的適用場景&lt;/h2>
&lt;p>商業方案（Sentry、Crashlytics、Datadog）適合的前提是：使用者分佈在外部網路，log 的消費者包含非開發者（維運、客服、產品），且需要告警和趨勢分析。&lt;/p>
&lt;p>商業方案提供的能力包括：跨網路收集（SDK 自動處理網路不穩定和批次傳輸）、多人查看 dashboard、告警規則設定、crash 報告自動分群、用戶 session 重播。這些能力在自用工具場景下不需要，在商業產品場景下是基礎需求。&lt;/p>
&lt;p>商業方案的成本包括：SDK 整合和設定、帳號和權限管理、月費（依事件量計費）、資料隱私合規（log 傳到第三方伺服器）。&lt;/p>
&lt;h2 id="判斷流程">判斷流程&lt;/h2>
&lt;h3 id="使用者在哪裡">使用者在哪裡&lt;/h3>
&lt;p>使用者和 server 在同一個網路內（自用工具、內部工具、開發期測試）→ 自架 log endpoint 是成本最低的選擇。&lt;/p>
&lt;p>使用者在外部網路（上架 app store、SaaS 產品、B2B 部署）→ 商業方案的跨網路收集能力是必要的，自架需要處理的 edge case（離線緩衝、重試、批次傳輸）太多。&lt;/p>
&lt;h3 id="log-消費者是誰">Log 消費者是誰&lt;/h3>
&lt;p>只有開發者自己 → grep/jq 足夠，不需要 dashboard。&lt;/p>
&lt;p>包含非技術人員（客服、產品經理）→ 需要視覺化 dashboard 和搜尋介面，商業方案的 UI 是這個需求的標準答案。&lt;/p>
&lt;h3 id="是否需要告警">是否需要告警&lt;/h3>
&lt;p>開發者自己用、即時看 log → 不需要告警。&lt;/p>
&lt;p>有維運值班、需要被動發現問題 → 需要告警規則，商業方案內建。&lt;/p>
&lt;h2 id="混合方案">混合方案&lt;/h2>
&lt;p>開發期用自架 log endpoint（零成本、即時可用），production 切換到商業方案 — 這個策略可行的前提是 log 層的 API 設計足夠抽象。&lt;/p>
&lt;p>&lt;code>AppLogger&lt;/code> 提供統一的 log 介面（&lt;code>log(level, name, data)&lt;/code>），底層實作在 debug mode 寫 console + POST 到本機 endpoint，在 release mode 寫 console + 呼叫 Sentry/Crashlytics SDK。切換只改 &lt;code>AppLogger&lt;/code> 的底層實作，不改呼叫端。&lt;/p>
&lt;p>這個抽象的投資在自用工具階段就值得做 — 即使目前不需要商業方案，統一的 log 介面也讓 log 點的管理更一致。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>三層 log 的詳細設計 → &lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/three-layer-log-design/" data-link-title="三層 log 設計" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — 三層各自的職責、詳細程度和啟停控制">三層 log 設計&lt;/a>&lt;/li>
&lt;li>在功能規格中定義 log 點 → &lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/log-point-in-spec/" data-link-title="功能規格中的 log 點定義方法" data-link-desc="把 log 點設計從 debug 階段前移到功能規格階段 — 每個功能的規格文件新增可觀測性欄位，列出啟動 / 步驟 / 錯誤 / 完成四類 log 點">功能規格中的 log 點定義方法&lt;/a>&lt;/li>
&lt;li>Log 收集後的 schema 設計 → &lt;a href="https://tarrragon.github.io/blog/monitoring/02-log-schema/" data-link-title="模組二：Log Schema 設計" data-link-desc="跨平台統一事件格式、欄位設計、版本演進策略">monitoring 模組二 Log Schema&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Log 收集方案的選擇取決於兩個因素：使用者在哪裡（同機 / 同網段 / 外部網路），以及 log 的消費者是誰（開發者自己 / 維運團隊 / 客服團隊）。自用工具和商業產品對這兩個因素的答案不同，適合不同的方案。</p>
<h2 id="自架-log-endpoint-的適用場景">自架 log endpoint 的適用場景</h2>
<p>自架 log endpoint 適合的前提是：client 和 server 在同一個網路內（同機、同 LAN、同 VPN/tailnet），log 的唯一消費者是開發者本人。</p>
<p>app_tunnel 就是這個場景。Server（ttyd）和 client（Flutter app）在同一台機器或同一個 Tailscale tailnet 內。開發者同時是使用者和維運者。Log 的消費方式是 grep — 不需要 dashboard、不需要告警、不需要多人共享。</p>
<p>在這個場景下，自架 log endpoint 的成本遠低於商業方案。一個 Go 程式開 HTTP endpoint 接收 JSON log 寫入檔案，20 行程式碼就能完成。Client 端的 <code>AppLogger</code> 在 debug mode 同時寫 console 和 POST 到 endpoint。Debug 時用 <code>grep</code> + <code>jq</code> 查詢，不需要額外工具。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Client (Flutter) → HTTP POST /log → Go receiver → JSON file → grep/jq</span></span></code></pre></div><p>這個方案沒有外部依賴、沒有帳號管理、沒有費用、沒有資料隱私顧慮（log 不離開本機網路）。</p>
<h2 id="商業方案的適用場景">商業方案的適用場景</h2>
<p>商業方案（Sentry、Crashlytics、Datadog）適合的前提是：使用者分佈在外部網路，log 的消費者包含非開發者（維運、客服、產品），且需要告警和趨勢分析。</p>
<p>商業方案提供的能力包括：跨網路收集（SDK 自動處理網路不穩定和批次傳輸）、多人查看 dashboard、告警規則設定、crash 報告自動分群、用戶 session 重播。這些能力在自用工具場景下不需要，在商業產品場景下是基礎需求。</p>
<p>商業方案的成本包括：SDK 整合和設定、帳號和權限管理、月費（依事件量計費）、資料隱私合規（log 傳到第三方伺服器）。</p>
<h2 id="判斷流程">判斷流程</h2>
<h3 id="使用者在哪裡">使用者在哪裡</h3>
<p>使用者和 server 在同一個網路內（自用工具、內部工具、開發期測試）→ 自架 log endpoint 是成本最低的選擇。</p>
<p>使用者在外部網路（上架 app store、SaaS 產品、B2B 部署）→ 商業方案的跨網路收集能力是必要的，自架需要處理的 edge case（離線緩衝、重試、批次傳輸）太多。</p>
<h3 id="log-消費者是誰">Log 消費者是誰</h3>
<p>只有開發者自己 → grep/jq 足夠，不需要 dashboard。</p>
<p>包含非技術人員（客服、產品經理）→ 需要視覺化 dashboard 和搜尋介面，商業方案的 UI 是這個需求的標準答案。</p>
<h3 id="是否需要告警">是否需要告警</h3>
<p>開發者自己用、即時看 log → 不需要告警。</p>
<p>有維運值班、需要被動發現問題 → 需要告警規則，商業方案內建。</p>
<h2 id="混合方案">混合方案</h2>
<p>開發期用自架 log endpoint（零成本、即時可用），production 切換到商業方案 — 這個策略可行的前提是 log 層的 API 設計足夠抽象。</p>
<p><code>AppLogger</code> 提供統一的 log 介面（<code>log(level, name, data)</code>），底層實作在 debug mode 寫 console + POST 到本機 endpoint，在 release mode 寫 console + 呼叫 Sentry/Crashlytics SDK。切換只改 <code>AppLogger</code> 的底層實作，不改呼叫端。</p>
<p>這個抽象的投資在自用工具階段就值得做 — 即使目前不需要商業方案，統一的 log 介面也讓 log 點的管理更一致。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>三層 log 的詳細設計 → <a href="/blog/testing/02-client-observability/three-layer-log-design/" data-link-title="三層 log 設計" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — 三層各自的職責、詳細程度和啟停控制">三層 log 設計</a></li>
<li>在功能規格中定義 log 點 → <a href="/blog/testing/02-client-observability/log-point-in-spec/" data-link-title="功能規格中的 log 點定義方法" data-link-desc="把 log 點設計從 debug 階段前移到功能規格階段 — 每個功能的規格文件新增可觀測性欄位，列出啟動 / 步驟 / 錯誤 / 完成四類 log 點">功能規格中的 log 點定義方法</a></li>
<li>Log 收集後的 schema 設計 → <a href="/blog/monitoring/02-log-schema/" data-link-title="模組二：Log Schema 設計" data-link-desc="跨平台統一事件格式、欄位設計、版本演進策略">monitoring 模組二 Log Schema</a></li>
</ul>
]]></content:encoded></item><item><title>「事後補 log」vs「設計產物 log」的品質差異</title><link>https://tarrragon.github.io/blog/testing/02-client-observability/hotfix-log-vs-designed-log/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/02-client-observability/hotfix-log-vs-designed-log/</guid><description>&lt;p>事後補 log 和設計產物 log 的差別在於產出時機和品質標準。事後補的 log 在 debug 壓力下產出，目的是「讓這次的問題能被定位」；設計產物的 log 在功能規格階段產出，目的是「讓未來任何問題都能被定位」。兩者的品質差異在格式統一性、覆蓋完整性和長期維護成本三個面向上表現明顯。&lt;/p>
&lt;h2 id="格式統一性">格式統一性&lt;/h2>
&lt;p>app_tunnel 在 W2 修復時補的 &lt;code>developer.log&lt;/code> 格式不統一（&lt;a href="https://tarrragon.github.io/blog/testing/cases/client-log-absent-debug-cost/" data-link-title="T.C4 Client-side log 缺失導致 debug 只能靠實機盲測" data-link-desc="Flutter app 六個核心元件中只有兩個有 log（且全是 W2 hotfix 補的），連線失敗時開發者無法從任何 log 判斷失敗發生在哪一步 — 被迫用最昂貴的 debug 方式：插拔裝置反覆測試">T.C4&lt;/a>）。不同元件由不同時間點、不同 debug 需求補上的 log，各自有各自的風格：&lt;/p>
&lt;p>有的帶 &lt;code>name:&lt;/code> 參數讓 log 可以按元件過濾：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">developer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;WS connected&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">name:&lt;/span> &lt;span class="s1">&amp;#39;ConnectionManager&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>有的不帶，混在全域 log 裡無法過濾：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">developer&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;auth token sent&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>有的帶 &lt;code>// i18n-exempt&lt;/code> 標記（因為 linter 會對 hardcoded string 報警），有的忘了加。有的把錯誤訊息放在 &lt;code>error:&lt;/code> 參數，有的用字串串接。&lt;/p>
&lt;p>這些不一致來自事後補 log 的結構性原因：每條 log 是在解決當下問題時加的，沒有統一規範，也沒有 review。加完能定位問題就提交，下次遇到新問題再加新的 log — 格式隨機。&lt;/p>
&lt;p>設計產物 log 在產出前就有命名規則和格式規範（見 &lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/log-point-in-spec/" data-link-title="功能規格中的 log 點定義方法" data-link-desc="把 log 點設計從 debug 階段前移到功能規格階段 — 每個功能的規格文件新增可觀測性欄位，列出啟動 / 步驟 / 錯誤 / 完成四類 log 點">功能規格中的 log 點定義方法&lt;/a>）。所有 log 點走同一個 &lt;code>AppLogger&lt;/code> 介面，name、level、結構化欄位在規格階段就定義好，實作時照規格寫。&lt;/p>
&lt;h2 id="覆蓋完整性">覆蓋完整性&lt;/h2>
&lt;p>事後補 log 的覆蓋範圍由「哪些問題已經發生過」決定。W2-002 auth token 問題觸發了 &lt;code>ConnectionManager&lt;/code> 和 &lt;code>TerminalScreen&lt;/code> 的 log 補充，但 &lt;code>TtydProtocol&lt;/code>、&lt;code>BiometricService&lt;/code>、&lt;code>CredentialRepository&lt;/code>、&lt;code>EnrollmentScreen&lt;/code> 四個元件仍然零 log — 因為這四個元件在 W2 的 debug 過程中不是瓶頸。&lt;/p>
&lt;p>六個核心元件中四個零 log 的狀態意味著：下次如果問題出在 &lt;code>BiometricService&lt;/code>（例如特定 iOS 版本的 biometric API 行為改變），debug 又會回到「手動加 log → 重新編譯 → 插拔裝置」的循環。事後補 log 只覆蓋已知問題的路徑，對未知問題沒有防護。&lt;/p>
&lt;p>設計產物 log 的覆蓋範圍由功能流程的步驟數決定。每個功能規格列出所有步驟的 log 點，不管這些步驟是否曾經出過問題。&lt;code>BiometricService.authenticate()&lt;/code> 在規格中就有 start/done/failed 三個 log 點，無論是否遇過 biometric 問題。&lt;/p>
&lt;h2 id="維護成本">維護成本&lt;/h2>
&lt;p>事後補 log 隨 debug 過程累積，沒有統一管理。隨時間推移：&lt;/p>
&lt;ul>
&lt;li>某些 log 的觸發條件已經不存在了（被修復的 bug 對應的 log），但沒人清理&lt;/li>
&lt;li>某些 log 的格式和新加的 log 不一致，但沒人統一&lt;/li>
&lt;li>某些 log 的 context 資訊不足（當時能定位問題是因為開發者記得 context，半年後換人接手就不夠了）&lt;/li>
&lt;li>某些 log 在 release build 中不該出現但忘了加條件&lt;/li>
&lt;/ul>
&lt;p>設計產物 log 有規格文件作為 source of truth。功能變更時更新規格中的 log 點列表，刪除的步驟對應的 log 點一起刪除，新增的步驟對應的 log 點一起新增。Log 的生命週期和功能的生命週期綁定。&lt;/p>
&lt;h2 id="從事後補過渡到設計產物">從事後補過渡到設計產物&lt;/h2>
&lt;p>已有的事後補 log 不需要全部重寫。過渡策略是：&lt;/p>
&lt;p>&lt;strong>統一入口&lt;/strong>：建立 &lt;code>AppLogger&lt;/code> 封裝，把現有的 &lt;code>developer.log&lt;/code> 呼叫改為走 &lt;code>AppLogger&lt;/code>。這一步不改 log 內容，只改呼叫方式，讓後續的格式統一和功能切換有統一入口。&lt;/p></description><content:encoded><![CDATA[<p>事後補 log 和設計產物 log 的差別在於產出時機和品質標準。事後補的 log 在 debug 壓力下產出，目的是「讓這次的問題能被定位」；設計產物的 log 在功能規格階段產出，目的是「讓未來任何問題都能被定位」。兩者的品質差異在格式統一性、覆蓋完整性和長期維護成本三個面向上表現明顯。</p>
<h2 id="格式統一性">格式統一性</h2>
<p>app_tunnel 在 W2 修復時補的 <code>developer.log</code> 格式不統一（<a href="/blog/testing/cases/client-log-absent-debug-cost/" data-link-title="T.C4 Client-side log 缺失導致 debug 只能靠實機盲測" data-link-desc="Flutter app 六個核心元件中只有兩個有 log（且全是 W2 hotfix 補的），連線失敗時開發者無法從任何 log 判斷失敗發生在哪一步 — 被迫用最昂貴的 debug 方式：插拔裝置反覆測試">T.C4</a>）。不同元件由不同時間點、不同 debug 需求補上的 log，各自有各自的風格：</p>
<p>有的帶 <code>name:</code> 參數讓 log 可以按元件過濾：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">developer</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;WS connected&#39;</span><span class="p">,</span> <span class="nl">name:</span> <span class="s1">&#39;ConnectionManager&#39;</span><span class="p">);</span></span></span></code></pre></div><p>有的不帶，混在全域 log 裡無法過濾：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">developer</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="s1">&#39;auth token sent&#39;</span><span class="p">);</span></span></span></code></pre></div><p>有的帶 <code>// i18n-exempt</code> 標記（因為 linter 會對 hardcoded string 報警），有的忘了加。有的把錯誤訊息放在 <code>error:</code> 參數，有的用字串串接。</p>
<p>這些不一致來自事後補 log 的結構性原因：每條 log 是在解決當下問題時加的，沒有統一規範，也沒有 review。加完能定位問題就提交，下次遇到新問題再加新的 log — 格式隨機。</p>
<p>設計產物 log 在產出前就有命名規則和格式規範（見 <a href="/blog/testing/02-client-observability/log-point-in-spec/" data-link-title="功能規格中的 log 點定義方法" data-link-desc="把 log 點設計從 debug 階段前移到功能規格階段 — 每個功能的規格文件新增可觀測性欄位，列出啟動 / 步驟 / 錯誤 / 完成四類 log 點">功能規格中的 log 點定義方法</a>）。所有 log 點走同一個 <code>AppLogger</code> 介面，name、level、結構化欄位在規格階段就定義好，實作時照規格寫。</p>
<h2 id="覆蓋完整性">覆蓋完整性</h2>
<p>事後補 log 的覆蓋範圍由「哪些問題已經發生過」決定。W2-002 auth token 問題觸發了 <code>ConnectionManager</code> 和 <code>TerminalScreen</code> 的 log 補充，但 <code>TtydProtocol</code>、<code>BiometricService</code>、<code>CredentialRepository</code>、<code>EnrollmentScreen</code> 四個元件仍然零 log — 因為這四個元件在 W2 的 debug 過程中不是瓶頸。</p>
<p>六個核心元件中四個零 log 的狀態意味著：下次如果問題出在 <code>BiometricService</code>（例如特定 iOS 版本的 biometric API 行為改變），debug 又會回到「手動加 log → 重新編譯 → 插拔裝置」的循環。事後補 log 只覆蓋已知問題的路徑，對未知問題沒有防護。</p>
<p>設計產物 log 的覆蓋範圍由功能流程的步驟數決定。每個功能規格列出所有步驟的 log 點，不管這些步驟是否曾經出過問題。<code>BiometricService.authenticate()</code> 在規格中就有 start/done/failed 三個 log 點，無論是否遇過 biometric 問題。</p>
<h2 id="維護成本">維護成本</h2>
<p>事後補 log 隨 debug 過程累積，沒有統一管理。隨時間推移：</p>
<ul>
<li>某些 log 的觸發條件已經不存在了（被修復的 bug 對應的 log），但沒人清理</li>
<li>某些 log 的格式和新加的 log 不一致，但沒人統一</li>
<li>某些 log 的 context 資訊不足（當時能定位問題是因為開發者記得 context，半年後換人接手就不夠了）</li>
<li>某些 log 在 release build 中不該出現但忘了加條件</li>
</ul>
<p>設計產物 log 有規格文件作為 source of truth。功能變更時更新規格中的 log 點列表，刪除的步驟對應的 log 點一起刪除，新增的步驟對應的 log 點一起新增。Log 的生命週期和功能的生命週期綁定。</p>
<h2 id="從事後補過渡到設計產物">從事後補過渡到設計產物</h2>
<p>已有的事後補 log 不需要全部重寫。過渡策略是：</p>
<p><strong>統一入口</strong>：建立 <code>AppLogger</code> 封裝，把現有的 <code>developer.log</code> 呼叫改為走 <code>AppLogger</code>。這一步不改 log 內容，只改呼叫方式，讓後續的格式統一和功能切換有統一入口。</p>
<p><strong>補規格</strong>：對每個功能寫出 log 點規格表（四類 log 點），比對現有 log 和規格的差距。規格中有但程式碼中沒有的 log 點 = 覆蓋缺口，補上。程式碼中有但規格中沒有的 log 點 = 可能是過時的 debug log，評估是否刪除。</p>
<p><strong>新功能走設計產物流程</strong>：從下一個新功能開始，功能規格中包含可觀測性欄位。新功能的 log 從一開始就是設計產物品質。</p>
<p>過渡的第一步是建立統一入口，具體的 log 點規格格式見<a href="/blog/testing/02-client-observability/log-point-in-spec/" data-link-title="功能規格中的 log 點定義方法" data-link-desc="把 log 點設計從 debug 階段前移到功能規格階段 — 每個功能的規格文件新增可觀測性欄位，列出啟動 / 步驟 / 錯誤 / 完成四類 log 點">功能規格中的 log 點定義方法</a>。規格中的每個 log 點屬於哪一層（連線生命週期 / protocol / 使用者行為），在<a href="/blog/testing/02-client-observability/three-layer-log-design/" data-link-title="三層 log 設計" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — 三層各自的職責、詳細程度和啟停控制">三層 log 設計</a>中定義。收集到 log 之後用自架還是商業方案處理，見<a href="/blog/testing/02-client-observability/log-endpoint-tradeoff/" data-link-title="自架 log endpoint vs 商業方案的取捨判斷" data-link-desc="自用工具用自架 log receiver（20 行 Go &#43; grep）、商業 app 用 Sentry/Crashlytics — 判斷依據是使用者規模和 debug 需求">自架 log endpoint vs 商業方案</a>的判斷流程。</p>
]]></content:encoded></item></channel></rss>