<?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/monitoring/05-platform-adaptation/</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/monitoring/05-platform-adaptation/index.xml" rel="self" type="application/rss+xml"/><item><title>JS/TS 平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/js-ts-platform/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/js-ts-platform/</guid><description>&lt;p>瀏覽器環境中的監控 SDK 面臨三個平台特有的限制：跨域請求被 CORS 攔截、Service Worker 可以攔截和修改請求、SPA 的路由變換不觸發頁面載入事件。每個限制需要 SDK 在設計層面做適配。&lt;/p>
&lt;h2 id="cors-限制">CORS 限制&lt;/h2>
&lt;p>瀏覽器的同源政策限制網頁向不同 origin 發送請求。SDK 的 HTTP POST 送到 collector endpoint 時，如果 collector 和網頁不在同一個 origin（protocol + domain + port 都相同），瀏覽器會先發送 preflight OPTIONS 請求確認 server 允許跨域存取。&lt;/p>
&lt;p>SDK 端的適配：&lt;/p>
&lt;p>使用 &lt;code>navigator.sendBeacon(url, data)&lt;/code> 代替 &lt;code>fetch&lt;/code> / &lt;code>XMLHttpRequest&lt;/code>。sendBeacon 不受 CORS 限制（瀏覽器對 beacon 請求不做 preflight），且在頁面 unload 時仍能可靠送出 — 適合 close flush 場景。&lt;/p>
&lt;p>sendBeacon 的限制：payload 大小有上限（通常 64KB），不能自訂 Content-Type header（固定為 &lt;code>text/plain&lt;/code> 或 &lt;code>application/x-www-form-urlencoded&lt;/code>），沒有回應 — 送出後無法知道 server 是否收到。&lt;/p>
&lt;p>如果需要 fetch（例如需要讀取回應或送出大 payload），collector 端需要設定 CORS header：&lt;code>Access-Control-Allow-Origin&lt;/code>、&lt;code>Access-Control-Allow-Methods: POST&lt;/code>、&lt;code>Access-Control-Allow-Headers: Content-Type&lt;/code>。&lt;/p>
&lt;h2 id="service-worker-攔截">Service Worker 攔截&lt;/h2>
&lt;p>Service Worker 可以攔截頁面發出的所有 HTTP 請求（包括 SDK 的 POST 請求到 collector）。如果應用程式的 Service Worker 有 cache 策略（cache-first、network-first），SDK 的監控請求可能被快取而非送到 collector。&lt;/p>
&lt;p>SDK 端的適配：&lt;/p>
&lt;p>在 fetch 請求中加 &lt;code>cache: 'no-store'&lt;/code> 防止 Service Worker 快取監控請求。或在請求 URL 加唯一的 query parameter（&lt;code>?_t=timestamp&lt;/code>）讓每次請求的 URL 都不同，繞過 cache 比對。&lt;/p>
&lt;p>如果 SDK 本身提供 Service Worker 模組（在 Service Worker 內攔截 error），需要注意 Service Worker 的生命週期和頁面不同 — Service Worker 可能在頁面關閉後仍在執行，也可能在空閒時被瀏覽器終止。&lt;/p>
&lt;h2 id="spa-路由變換偵測">SPA 路由變換偵測&lt;/h2>
&lt;p>Single Page Application 的路由變換（React Router、Vue Router、Angular Router）不觸發頁面重新載入。從監控角度看，使用者在不同「頁面」之間切換，但 &lt;code>window.onload&lt;/code> 只在首次載入時觸發一次。&lt;/p>
&lt;p>SDK 需要偵測 SPA 路由變換來記錄 &lt;code>lifecycle.view.change&lt;/code> 事件。偵測方式：&lt;/p>
&lt;p>&lt;code>History API&lt;/code> 攔截：monkey-patch &lt;code>history.pushState&lt;/code> 和 &lt;code>history.replaceState&lt;/code>，在呼叫前後記錄路由變換。同時監聽 &lt;code>popstate&lt;/code> 事件處理瀏覽器的上一頁/下一頁。&lt;/p>
&lt;p>&lt;code>MutationObserver&lt;/code>：監聽 DOM 變化偵測頁面內容更新。但 MutationObserver 觸發頻率高，需要 debounce 並搭配 URL 變化檢查，避免把 DOM 微調誤判為路由變換。&lt;/p>
&lt;p>框架特定的 hook：如果 SDK 提供框架整合套件（React / Vue / Angular plugin），可以用框架的 router 事件（&lt;code>useNavigate&lt;/code> hook、&lt;code>router.afterEach&lt;/code> guard）直接取得路由變換資訊，比 monkey-patch History API 更可靠。&lt;/p>
&lt;p>JS/TS 的平台限制理解後，其他平台各有各的挑戰 — &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/flutter-platform/" data-link-title="Flutter 平台適配" data-link-desc="Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量">Flutter 平台適配&lt;/a>處理 isolate 和 platform channel 的問題。所有平台共同面對的 &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">timestamp 一致性&lt;/a>問題（時區、精度、clock drift）在獨立章節中展開。SDK 的跨平台公開 API 設計見&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">模組三 SDK 公開 API&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>瀏覽器環境中的監控 SDK 面臨三個平台特有的限制：跨域請求被 CORS 攔截、Service Worker 可以攔截和修改請求、SPA 的路由變換不觸發頁面載入事件。每個限制需要 SDK 在設計層面做適配。</p>
<h2 id="cors-限制">CORS 限制</h2>
<p>瀏覽器的同源政策限制網頁向不同 origin 發送請求。SDK 的 HTTP POST 送到 collector endpoint 時，如果 collector 和網頁不在同一個 origin（protocol + domain + port 都相同），瀏覽器會先發送 preflight OPTIONS 請求確認 server 允許跨域存取。</p>
<p>SDK 端的適配：</p>
<p>使用 <code>navigator.sendBeacon(url, data)</code> 代替 <code>fetch</code> / <code>XMLHttpRequest</code>。sendBeacon 不受 CORS 限制（瀏覽器對 beacon 請求不做 preflight），且在頁面 unload 時仍能可靠送出 — 適合 close flush 場景。</p>
<p>sendBeacon 的限制：payload 大小有上限（通常 64KB），不能自訂 Content-Type header（固定為 <code>text/plain</code> 或 <code>application/x-www-form-urlencoded</code>），沒有回應 — 送出後無法知道 server 是否收到。</p>
<p>如果需要 fetch（例如需要讀取回應或送出大 payload），collector 端需要設定 CORS header：<code>Access-Control-Allow-Origin</code>、<code>Access-Control-Allow-Methods: POST</code>、<code>Access-Control-Allow-Headers: Content-Type</code>。</p>
<h2 id="service-worker-攔截">Service Worker 攔截</h2>
<p>Service Worker 可以攔截頁面發出的所有 HTTP 請求（包括 SDK 的 POST 請求到 collector）。如果應用程式的 Service Worker 有 cache 策略（cache-first、network-first），SDK 的監控請求可能被快取而非送到 collector。</p>
<p>SDK 端的適配：</p>
<p>在 fetch 請求中加 <code>cache: 'no-store'</code> 防止 Service Worker 快取監控請求。或在請求 URL 加唯一的 query parameter（<code>?_t=timestamp</code>）讓每次請求的 URL 都不同，繞過 cache 比對。</p>
<p>如果 SDK 本身提供 Service Worker 模組（在 Service Worker 內攔截 error），需要注意 Service Worker 的生命週期和頁面不同 — Service Worker 可能在頁面關閉後仍在執行，也可能在空閒時被瀏覽器終止。</p>
<h2 id="spa-路由變換偵測">SPA 路由變換偵測</h2>
<p>Single Page Application 的路由變換（React Router、Vue Router、Angular Router）不觸發頁面重新載入。從監控角度看，使用者在不同「頁面」之間切換，但 <code>window.onload</code> 只在首次載入時觸發一次。</p>
<p>SDK 需要偵測 SPA 路由變換來記錄 <code>lifecycle.view.change</code> 事件。偵測方式：</p>
<p><code>History API</code> 攔截：monkey-patch <code>history.pushState</code> 和 <code>history.replaceState</code>，在呼叫前後記錄路由變換。同時監聽 <code>popstate</code> 事件處理瀏覽器的上一頁/下一頁。</p>
<p><code>MutationObserver</code>：監聽 DOM 變化偵測頁面內容更新。但 MutationObserver 觸發頻率高，需要 debounce 並搭配 URL 變化檢查，避免把 DOM 微調誤判為路由變換。</p>
<p>框架特定的 hook：如果 SDK 提供框架整合套件（React / Vue / Angular plugin），可以用框架的 router 事件（<code>useNavigate</code> hook、<code>router.afterEach</code> guard）直接取得路由變換資訊，比 monkey-patch History API 更可靠。</p>
<p>JS/TS 的平台限制理解後，其他平台各有各的挑戰 — <a href="/blog/monitoring/05-platform-adaptation/flutter-platform/" data-link-title="Flutter 平台適配" data-link-desc="Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量">Flutter 平台適配</a>處理 isolate 和 platform channel 的問題。所有平台共同面對的 <a href="/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">timestamp 一致性</a>問題（時區、精度、clock drift）在獨立章節中展開。SDK 的跨平台公開 API 設計見<a href="/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">模組三 SDK 公開 API</a>。</p>
]]></content:encoded></item><item><title>Flutter 平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/flutter-platform/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/flutter-platform/</guid><description>&lt;p>Flutter 應用程式在 Dart VM 中執行，有自己的執行緒模型（Isolate）、原生平台橋接（Platform channel）和 app 生命週期管理。監控 SDK 在 Flutter 中需要處理的平台特殊問題集中在這三個面向。&lt;/p>
&lt;h2 id="isolate-安全">Isolate 安全&lt;/h2>
&lt;p>Dart 的 Isolate 是獨立的記憶體空間，Isolate 之間不共享記憶體，只能透過 message passing 溝通。SDK 的記憶體 buffer 存在於 main isolate 中，其他 isolate 產生的事件需要透過 port 傳送到 main isolate 才能進入 buffer。&lt;/p>
&lt;p>SDK 端的適配：&lt;/p>
&lt;p>提供 &lt;code>Monitor.eventFromIsolate(SendPort port)&lt;/code> 方法，在子 isolate 中透過 port 把事件送回 main isolate。或者提供 isolate-aware 的 &lt;code>Monitor.init()&lt;/code> 變體，在子 isolate 中初始化一個輕量的 event forwarder。&lt;/p>
&lt;p>如果 SDK 使用 compute 或 Isolate.spawn 做背景任務（例如壓縮 buffer），需要透過 port 把結果送回 main isolate — 背景 isolate 無法直接存取 main isolate 的 HTTP client 或 buffer。&lt;/p>
&lt;h2 id="platform-channel-攔截">Platform channel 攔截&lt;/h2>
&lt;p>Flutter 透過 Platform channel 呼叫原生平台功能（iOS 的 Swift/ObjC、Android 的 Kotlin/Java）。Platform channel 的呼叫可能失敗（原生端未實作、參數格式錯誤、原生端拋出例外），這些錯誤在 Dart 端表現為 &lt;code>PlatformException&lt;/code>。&lt;/p>
&lt;p>SDK 可以攔截 Platform channel 的呼叫記錄每次呼叫的方法名稱、參數、結果和耗時。攔截方式是替換 &lt;code>ServicesBinding.defaultBinaryMessenger&lt;/code> 的處理器，在轉發前後記錄事件。&lt;/p>
&lt;p>攔截的價值是：Platform channel 的錯誤通常難以 debug（stack trace 跨越 Dart 和原生兩層），監控記錄提供「呼叫了哪個 channel method、傳了什麼參數、在哪一層失敗」的完整 context。&lt;/p>
&lt;p>注意：攔截 Platform channel 會增加每次呼叫的延遲（記錄事件的開銷）。對高頻的 Platform channel 呼叫（例如每幀都呼叫的渲染相關 channel），攔截可能影響效能。SDK 應該提供 channel 過濾機制 — 只攔截特定 channel 或只在 debug mode 攔截。&lt;/p>
&lt;h2 id="app-lifecycle-事件">App lifecycle 事件&lt;/h2>
&lt;p>Flutter 的 &lt;code>WidgetsBindingObserver&lt;/code> 提供 app 生命週期回呼：&lt;/p>
&lt;ul>
&lt;li>&lt;code>didChangeAppLifecycleState(AppLifecycleState state)&lt;/code> — app 在 resumed（前景）、inactive（部分可見）、paused（背景）、detached（即將關閉）之間切換。&lt;/li>
&lt;/ul>
&lt;p>SDK 在 init 時註冊 observer，記錄每次狀態轉換為 lifecycle 事件。&lt;/p>
&lt;p>lifecycle 事件在 flush 策略中有特殊意義：&lt;/p>
&lt;p>&lt;strong>paused（進入背景）&lt;/strong>：觸發 flush — 把 buffer 中的事件送出，因為 app 在背景可能被系統殺掉，buffer 中的事件會遺失。iOS 在 app 進入背景後約 5 秒 suspend，flush 必須在這個時間窗口內完成。&lt;/p>
&lt;p>&lt;strong>resumed（回到前景）&lt;/strong>：檢查上次 flush 是否成功。如果 paused 時的 flush 失敗（網路超時），在 resumed 時重試。&lt;/p>
&lt;p>&lt;strong>detached（即將關閉）&lt;/strong>：呼叫 &lt;code>Monitor.close()&lt;/code> 做最後一次 flush 和資源釋放。detached 的時間窗口更短，close flush 可能被截斷。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Python 平台的適配 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配&lt;/a>&lt;/li>
&lt;li>跨平台 timestamp 一致性 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">跨平台 timestamp 一致性&lt;/a>&lt;/li>
&lt;li>自動攔截機制 → &lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">模組三 自動攔截&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Flutter 應用程式在 Dart VM 中執行，有自己的執行緒模型（Isolate）、原生平台橋接（Platform channel）和 app 生命週期管理。監控 SDK 在 Flutter 中需要處理的平台特殊問題集中在這三個面向。</p>
<h2 id="isolate-安全">Isolate 安全</h2>
<p>Dart 的 Isolate 是獨立的記憶體空間，Isolate 之間不共享記憶體，只能透過 message passing 溝通。SDK 的記憶體 buffer 存在於 main isolate 中，其他 isolate 產生的事件需要透過 port 傳送到 main isolate 才能進入 buffer。</p>
<p>SDK 端的適配：</p>
<p>提供 <code>Monitor.eventFromIsolate(SendPort port)</code> 方法，在子 isolate 中透過 port 把事件送回 main isolate。或者提供 isolate-aware 的 <code>Monitor.init()</code> 變體，在子 isolate 中初始化一個輕量的 event forwarder。</p>
<p>如果 SDK 使用 compute 或 Isolate.spawn 做背景任務（例如壓縮 buffer），需要透過 port 把結果送回 main isolate — 背景 isolate 無法直接存取 main isolate 的 HTTP client 或 buffer。</p>
<h2 id="platform-channel-攔截">Platform channel 攔截</h2>
<p>Flutter 透過 Platform channel 呼叫原生平台功能（iOS 的 Swift/ObjC、Android 的 Kotlin/Java）。Platform channel 的呼叫可能失敗（原生端未實作、參數格式錯誤、原生端拋出例外），這些錯誤在 Dart 端表現為 <code>PlatformException</code>。</p>
<p>SDK 可以攔截 Platform channel 的呼叫記錄每次呼叫的方法名稱、參數、結果和耗時。攔截方式是替換 <code>ServicesBinding.defaultBinaryMessenger</code> 的處理器，在轉發前後記錄事件。</p>
<p>攔截的價值是：Platform channel 的錯誤通常難以 debug（stack trace 跨越 Dart 和原生兩層），監控記錄提供「呼叫了哪個 channel method、傳了什麼參數、在哪一層失敗」的完整 context。</p>
<p>注意：攔截 Platform channel 會增加每次呼叫的延遲（記錄事件的開銷）。對高頻的 Platform channel 呼叫（例如每幀都呼叫的渲染相關 channel），攔截可能影響效能。SDK 應該提供 channel 過濾機制 — 只攔截特定 channel 或只在 debug mode 攔截。</p>
<h2 id="app-lifecycle-事件">App lifecycle 事件</h2>
<p>Flutter 的 <code>WidgetsBindingObserver</code> 提供 app 生命週期回呼：</p>
<ul>
<li><code>didChangeAppLifecycleState(AppLifecycleState state)</code> — app 在 resumed（前景）、inactive（部分可見）、paused（背景）、detached（即將關閉）之間切換。</li>
</ul>
<p>SDK 在 init 時註冊 observer，記錄每次狀態轉換為 lifecycle 事件。</p>
<p>lifecycle 事件在 flush 策略中有特殊意義：</p>
<p><strong>paused（進入背景）</strong>：觸發 flush — 把 buffer 中的事件送出，因為 app 在背景可能被系統殺掉，buffer 中的事件會遺失。iOS 在 app 進入背景後約 5 秒 suspend，flush 必須在這個時間窗口內完成。</p>
<p><strong>resumed（回到前景）</strong>：檢查上次 flush 是否成功。如果 paused 時的 flush 失敗（網路超時），在 resumed 時重試。</p>
<p><strong>detached（即將關閉）</strong>：呼叫 <code>Monitor.close()</code> 做最後一次 flush 和資源釋放。detached 的時間窗口更短，close flush 可能被截斷。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Python 平台的適配 → <a href="/blog/monitoring/05-platform-adaptation/python-platform/" data-link-title="Python 平台適配" data-link-desc="GIL 與 threading、atexit 可靠性、subprocess 監控 — Python SDK 的平台特殊考量">Python 平台適配</a></li>
<li>跨平台 timestamp 一致性 → <a href="/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">跨平台 timestamp 一致性</a></li>
<li>自動攔截機制 → <a href="/blog/monitoring/03-sdk-design/auto-intercept/" data-link-title="自動攔截機制" data-link-desc="JS window.onerror / Flutter FlutterError.onError / Python sys.excepthook — 各平台攔截未捕獲例外的機制和限制">模組三 自動攔截</a></li>
</ul>
]]></content:encoded></item><item><title>Python 平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/python-platform/</guid><description>&lt;p>Python 的執行模型（GIL 限制並行、atexit 不保證執行、subprocess 獨立 process）讓監控 SDK 在 Python 環境中需要特別處理 flush 的執行方式、程序退出時的事件保存和子程序的監控。&lt;/p>
&lt;h2 id="gil-與-threading">GIL 與 threading&lt;/h2>
&lt;p>Python 的 Global Interpreter Lock（GIL）讓同一時間只有一個 thread 執行 Python bytecode。SDK 的 flush 操作（HTTP POST 到 collector）如果在主 thread 執行，會阻塞主程式的其他工作。&lt;/p>
&lt;p>SDK 端的適配：&lt;/p>
&lt;p>在 daemon thread 中執行 flush。Daemon thread 在主 thread 結束時自動終止，不需要手動 join。SDK 的 flush 計時器在 daemon thread 中運行，buffer 的存取用 threading.Lock 保護。&lt;/p>
&lt;p>GIL 對 SDK 的影響比想像的小：HTTP 請求是 I/O bound 操作，CPython 在等待 I/O 時釋放 GIL。所以 flush 的 HTTP POST 在 daemon thread 中執行時，主 thread 可以繼續工作。GIL 只在 CPU-bound 的操作上造成瓶頸 — SDK 的 buffer 操作和事件序列化是 CPU-bound 但耗時極短（微秒級），影響可忽略。&lt;/p>
&lt;h3 id="asyncio-環境">asyncio 環境&lt;/h3>
&lt;p>Python 的 asyncio 程式（FastAPI、aiohttp）使用事件迴圈而非 threading。SDK 在 asyncio 環境中應該用 &lt;code>asyncio.create_task&lt;/code> 而非 threading 執行 flush，避免在事件迴圈中阻塞。&lt;/p>
&lt;p>SDK 可以在 init 時自動偵測是否在 asyncio 環境中（檢查 &lt;code>asyncio.get_running_loop()&lt;/code> 是否存在），自動切換 flush 的執行方式。&lt;/p>
&lt;h2 id="atexit-可靠性">atexit 可靠性&lt;/h2>
&lt;p>&lt;code>atexit.register&lt;/code> 在 Python 程序正常退出時執行註冊的清理函式。SDK 在 init 時註冊 atexit handler 做最後一次 flush。&lt;/p>
&lt;p>atexit 不執行的場景：&lt;/p>
&lt;ul>
&lt;li>&lt;code>os._exit()&lt;/code> 直接終止 process，跳過所有清理&lt;/li>
&lt;li>SIGKILL（&lt;code>kill -9&lt;/code>）強制終止，作業系統直接回收 process&lt;/li>
&lt;li>未處理的 fatal signal（SIGSEGV、SIGABRT）導致 crash&lt;/li>
&lt;/ul>
&lt;p>對於 SIGTERM 和 SIGINT，Python 預設會執行 atexit handler（前提是 signal handler 沒有被覆蓋）。SDK 可以額外註冊 &lt;code>signal.signal(signal.SIGTERM, handler)&lt;/code> 確保在收到 SIGTERM 時觸發 flush。&lt;/p>
&lt;p>實務影響：&lt;code>os._exit()&lt;/code> 和 SIGKILL 導致的事件遺失無法避免。使用本地 persistence（&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer&lt;/a>）可以降低影響 — 事件在寫入本地檔案後，即使 process 被強制終止，下次啟動時仍可補發。&lt;/p>
&lt;h2 id="短生命週期腳本">短生命週期腳本&lt;/h2>
&lt;p>SDK 的預設設計假設長期運行的 app — flush interval 定期觸發、daemon thread 持續運行、atexit 是最後防線。但 Python SDK 的一個重要場景是短命腳本（CI/CD hook、pre-commit hook、CLI 工具的子命令），生命週期可能 &amp;lt; 1 秒。這個場景下 SDK 的行為和長期 app 完全不同。&lt;/p>
&lt;h3 id="什麼會壞">什麼會壞&lt;/h3>
&lt;p>&lt;strong>flush interval 來不及觸發&lt;/strong>。預設 30 秒的 flush interval，但腳本在 200ms 內結束。計時器還沒觸發，buffer 中的事件從未送出。&lt;/p></description><content:encoded><![CDATA[<p>Python 的執行模型（GIL 限制並行、atexit 不保證執行、subprocess 獨立 process）讓監控 SDK 在 Python 環境中需要特別處理 flush 的執行方式、程序退出時的事件保存和子程序的監控。</p>
<h2 id="gil-與-threading">GIL 與 threading</h2>
<p>Python 的 Global Interpreter Lock（GIL）讓同一時間只有一個 thread 執行 Python bytecode。SDK 的 flush 操作（HTTP POST 到 collector）如果在主 thread 執行，會阻塞主程式的其他工作。</p>
<p>SDK 端的適配：</p>
<p>在 daemon thread 中執行 flush。Daemon thread 在主 thread 結束時自動終止，不需要手動 join。SDK 的 flush 計時器在 daemon thread 中運行，buffer 的存取用 threading.Lock 保護。</p>
<p>GIL 對 SDK 的影響比想像的小：HTTP 請求是 I/O bound 操作，CPython 在等待 I/O 時釋放 GIL。所以 flush 的 HTTP POST 在 daemon thread 中執行時，主 thread 可以繼續工作。GIL 只在 CPU-bound 的操作上造成瓶頸 — SDK 的 buffer 操作和事件序列化是 CPU-bound 但耗時極短（微秒級），影響可忽略。</p>
<h3 id="asyncio-環境">asyncio 環境</h3>
<p>Python 的 asyncio 程式（FastAPI、aiohttp）使用事件迴圈而非 threading。SDK 在 asyncio 環境中應該用 <code>asyncio.create_task</code> 而非 threading 執行 flush，避免在事件迴圈中阻塞。</p>
<p>SDK 可以在 init 時自動偵測是否在 asyncio 環境中（檢查 <code>asyncio.get_running_loop()</code> 是否存在），自動切換 flush 的執行方式。</p>
<h2 id="atexit-可靠性">atexit 可靠性</h2>
<p><code>atexit.register</code> 在 Python 程序正常退出時執行註冊的清理函式。SDK 在 init 時註冊 atexit handler 做最後一次 flush。</p>
<p>atexit 不執行的場景：</p>
<ul>
<li><code>os._exit()</code> 直接終止 process，跳過所有清理</li>
<li>SIGKILL（<code>kill -9</code>）強制終止，作業系統直接回收 process</li>
<li>未處理的 fatal signal（SIGSEGV、SIGABRT）導致 crash</li>
</ul>
<p>對於 SIGTERM 和 SIGINT，Python 預設會執行 atexit handler（前提是 signal handler 沒有被覆蓋）。SDK 可以額外註冊 <code>signal.signal(signal.SIGTERM, handler)</code> 確保在收到 SIGTERM 時觸發 flush。</p>
<p>實務影響：<code>os._exit()</code> 和 SIGKILL 導致的事件遺失無法避免。使用本地 persistence（<a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">離線 buffer</a>）可以降低影響 — 事件在寫入本地檔案後，即使 process 被強制終止，下次啟動時仍可補發。</p>
<h2 id="短生命週期腳本">短生命週期腳本</h2>
<p>SDK 的預設設計假設長期運行的 app — flush interval 定期觸發、daemon thread 持續運行、atexit 是最後防線。但 Python SDK 的一個重要場景是短命腳本（CI/CD hook、pre-commit hook、CLI 工具的子命令），生命週期可能 &lt; 1 秒。這個場景下 SDK 的行為和長期 app 完全不同。</p>
<h3 id="什麼會壞">什麼會壞</h3>
<p><strong>flush interval 來不及觸發</strong>。預設 30 秒的 flush interval，但腳本在 200ms 內結束。計時器還沒觸發，buffer 中的事件從未送出。</p>
<p><strong>daemon thread 隨主 thread 結束</strong>。SDK 用 daemon thread 執行 flush 計時器。Python 的 daemon thread 在最後一個非 daemon thread 結束時被殺 — 不會等待 daemon thread 完成當前工作。如果 flush 正在進行中（HTTP POST 送到一半），daemon thread 被殺，HTTP 請求中斷，事件丟失。</p>
<p><strong>atexit 的執行順序不確定</strong>。atexit handler 在 daemon thread 被殺之後執行。如果 SDK 的 atexit handler 嘗試在 daemon thread 中 flush，會失敗（thread 已死）。atexit handler 必須在主 thread 中同步 flush。</p>
<h3 id="正確的短命腳本模式">正確的短命腳本模式</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">from</span> <span class="nn">monitor</span> <span class="kn">import</span> <span class="n">Monitor</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">Monitor</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="n">endpoint</span><span class="o">=</span><span class="s2">&#34;http://localhost:9090/v1/events&#34;</span><span class="p">,</span> <span class="n">app</span><span class="o">=</span><span class="s2">&#34;my-hook&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 做事...</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">Monitor</span><span class="o">.</span><span class="n">event</span><span class="p">(</span><span class="s2">&#34;hook.run&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;hook&#34;</span><span class="p">:</span> <span class="s2">&#34;branch-check&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># 結束前必須呼叫 close</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="n">Monitor</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>  <span class="c1"># close 內同步 flush，不依賴 daemon thread</span></span></span></code></pre></div><p><code>close()</code> 是唯一可靠的 flush 時機。<code>close()</code> 的實作在短命腳本場景下必須：</p>
<ol>
<li><strong>同步執行 HTTP POST</strong>，不委託給 daemon thread — 主 thread 呼叫 <code>close()</code> 時直接在當前 thread 送出</li>
<li><strong>設 HTTP timeout</strong> — 短命腳本不能等太久，3 秒的 timeout 是合理的</li>
<li><strong>flush 失敗時靜默放棄</strong> — 短命腳本的主要職責不是監控，SDK 失敗不應影響腳本的 exit code</li>
</ol>
<p><code>atexit</code> 仍然註冊，作為開發者忘記呼叫 <code>close()</code> 的備份。但 atexit 是 best-effort — 在 <code>os._exit()</code> 和 SIGKILL 下不執行。</p>
<h3 id="flush-interval-在短命腳本中的角色">flush interval 在短命腳本中的角色</h3>
<p>flush interval 對短命腳本無意義 — 腳本在第一次 interval 觸發前就結束了。SDK 可以偵測「init 到 close 的間隔 &lt; flush interval」的模式，在 debug log 中提示開發者考慮降低 interval 或直接依賴 <code>close()</code> flush。</p>
<p>但不建議把 flush interval 設為 0（停用）— 同一個 SDK 設定可能同時用於長期 app 和短命腳本，interval 對長期 app 仍然有用。</p>
<h2 id="subprocess-監控">Subprocess 監控</h2>
<p>Python 程式中的 <code>subprocess.Popen</code> 啟動的子程序是獨立的 process，不共享 SDK 的 buffer 和網路連線。子程序的錯誤和事件需要獨立的監控機制。</p>
<p>兩種方式：</p>
<p><strong>子程序獨立初始化 SDK</strong>：子程序的 Python 腳本自己呼叫 <code>Monitor.init()</code>，獨立送事件到 collector。適合子程序是長時間運行的 Python 程式。</p>
<p><strong>父程序代理</strong>：父程序讀取子程序的 stdout/stderr，從輸出中解析事件（子程序用約定格式印出事件），父程序的 SDK 代理送出。適合子程序是短命的腳本或非 Python 程式。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Go 平台的適配 → <a href="/blog/monitoring/05-platform-adaptation/go-platform/" data-link-title="Go 平台適配" data-link-desc="Graceful shutdown、signal handling、HTTP server 自身監控 — Go SDK 和 collector 端共同面對的平台問題">Go 平台適配</a></li>
<li>跨平台 timestamp 一致性 → <a href="/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">跨平台 timestamp 一致性</a></li>
<li>離線 buffer 策略 → <a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">模組三 離線 buffer 與重試</a></li>
</ul>
]]></content:encoded></item><item><title>Go 平台適配</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/go-platform/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/go-platform/</guid><description>&lt;p>Go 的 monitoring SDK 和其他平台 SDK 的定位不同。JS / Flutter / Python SDK 是 client-side 的事件上報工具，Go SDK 更常用在 server-side — 包括 collector 本身的自身監控。Go 的 goroutine 並行模型、signal handling 機制和 HTTP server 的 graceful shutdown 是 Go 環境中的三個核心適配問題。&lt;/p>
&lt;h2 id="graceful-shutdown">Graceful shutdown&lt;/h2>
&lt;p>Go 程式收到 SIGTERM 或 SIGINT 時需要在退出前完成清理：flush 剩餘的 buffer、關閉網路連線、寫入最後的 lifecycle 事件。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">stop&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">signal&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NotifyContext&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="nx">syscall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SIGTERM&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">syscall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SIGINT&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">defer&lt;/span> &lt;span class="nf">stop&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Done&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1">// signal received, start graceful shutdown&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nx">monitor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Close&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WithTimeout&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="mi">5&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Second&lt;/span>&lt;span class="p">))&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>graceful shutdown 的時間窗口由部署環境決定。Kubernetes 的預設 terminationGracePeriodSeconds 是 30 秒，Docker 的 stop timeout 是 10 秒。SDK 的 Close 方法接受 context 讓呼叫端控制超時。&lt;/p>
&lt;h3 id="http-server-的-shutdown-順序">HTTP server 的 shutdown 順序&lt;/h3>
&lt;p>如果 Go 程式同時是 HTTP server 和 monitoring SDK 的使用者，shutdown 順序需要正確：&lt;/p>
&lt;ol>
&lt;li>停止接受新連線（&lt;code>server.Shutdown(ctx)&lt;/code>）&lt;/li>
&lt;li>等待進行中的請求完成&lt;/li>
&lt;li>flush 監控 buffer（&lt;code>monitor.Close(ctx)&lt;/code>）&lt;/li>
&lt;li>關閉 log 和其他資源&lt;/li>
&lt;/ol>
&lt;p>如果先 close monitor 再 shutdown server，進行中的請求產生的事件會在 monitor 已關閉後嘗試送出，被靜默丟棄。&lt;/p>
&lt;h2 id="signal-handling">Signal handling&lt;/h2>
&lt;p>Go 的 &lt;code>signal.Notify&lt;/code> 和 &lt;code>signal.NotifyContext&lt;/code> 是接收 OS signal 的標準方式。SDK 在 init 時不應該自己註冊 signal handler — 這會和應用程式的 signal handling 衝突（Go 的 signal handler 是先到先得，後註冊的覆蓋先註冊的）。&lt;/p>
&lt;p>SDK 端的適配方式是提供 &lt;code>Close&lt;/code> 方法讓應用程式在自己的 signal handler 中呼叫，而非 SDK 內部攔截 signal。應用程式控制 shutdown 流程，SDK 只負責在被告知關閉時 flush 和清理。&lt;/p>
&lt;h3 id="panic-recovery">panic recovery&lt;/h3>
&lt;p>Go 的 panic 會終止當前 goroutine。如果 panic 發生在 main goroutine 且沒有 recover，程式直接退出，SDK 的 buffer 中的事件遺失。&lt;/p>
&lt;p>SDK 可以提供 &lt;code>monitor.RecoverAndReport()&lt;/code> 讓開發者在 goroutine 的入口用 &lt;code>defer monitor.RecoverAndReport()&lt;/code> 攔截 panic，記錄 error 事件後再 re-panic（保持原有的 crash 行為）。&lt;/p>
&lt;p>HTTP handler 的 panic 可以用 middleware 攔截：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">monitorMiddleware&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">next&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Handler&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Handler&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandlerFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">monitor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RecoverAndReport&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ServeHTTP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="http-server-自身監控">HTTP server 自身監控&lt;/h2>
&lt;p>Go 常用來寫 collector 本身。Collector 需要監控自己的健康狀態 — 請求處理速率、錯誤率、goroutine 數量、記憶體使用量。&lt;/p></description><content:encoded><![CDATA[<p>Go 的 monitoring SDK 和其他平台 SDK 的定位不同。JS / Flutter / Python SDK 是 client-side 的事件上報工具，Go SDK 更常用在 server-side — 包括 collector 本身的自身監控。Go 的 goroutine 並行模型、signal handling 機制和 HTTP server 的 graceful shutdown 是 Go 環境中的三個核心適配問題。</p>
<h2 id="graceful-shutdown">Graceful shutdown</h2>
<p>Go 程式收到 SIGTERM 或 SIGINT 時需要在退出前完成清理：flush 剩餘的 buffer、關閉網路連線、寫入最後的 lifecycle 事件。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">ctx</span><span class="p">,</span> <span class="nx">stop</span> <span class="o">:=</span> <span class="nx">signal</span><span class="p">.</span><span class="nf">NotifyContext</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGTERM</span><span class="p">,</span> <span class="nx">syscall</span><span class="p">.</span><span class="nx">SIGINT</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">defer</span> <span class="nf">stop</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="o">&lt;-</span><span class="nx">ctx</span><span class="p">.</span><span class="nf">Done</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1">// signal received, start graceful shutdown</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nx">monitor</span><span class="p">.</span><span class="nf">Close</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">WithTimeout</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">(),</span> <span class="mi">5</span><span class="o">*</span><span class="nx">time</span><span class="p">.</span><span class="nx">Second</span><span class="p">))</span></span></span></code></pre></div><p>graceful shutdown 的時間窗口由部署環境決定。Kubernetes 的預設 terminationGracePeriodSeconds 是 30 秒，Docker 的 stop timeout 是 10 秒。SDK 的 Close 方法接受 context 讓呼叫端控制超時。</p>
<h3 id="http-server-的-shutdown-順序">HTTP server 的 shutdown 順序</h3>
<p>如果 Go 程式同時是 HTTP server 和 monitoring SDK 的使用者，shutdown 順序需要正確：</p>
<ol>
<li>停止接受新連線（<code>server.Shutdown(ctx)</code>）</li>
<li>等待進行中的請求完成</li>
<li>flush 監控 buffer（<code>monitor.Close(ctx)</code>）</li>
<li>關閉 log 和其他資源</li>
</ol>
<p>如果先 close monitor 再 shutdown server，進行中的請求產生的事件會在 monitor 已關閉後嘗試送出，被靜默丟棄。</p>
<h2 id="signal-handling">Signal handling</h2>
<p>Go 的 <code>signal.Notify</code> 和 <code>signal.NotifyContext</code> 是接收 OS signal 的標準方式。SDK 在 init 時不應該自己註冊 signal handler — 這會和應用程式的 signal handling 衝突（Go 的 signal handler 是先到先得，後註冊的覆蓋先註冊的）。</p>
<p>SDK 端的適配方式是提供 <code>Close</code> 方法讓應用程式在自己的 signal handler 中呼叫，而非 SDK 內部攔截 signal。應用程式控制 shutdown 流程，SDK 只負責在被告知關閉時 flush 和清理。</p>
<h3 id="panic-recovery">panic recovery</h3>
<p>Go 的 panic 會終止當前 goroutine。如果 panic 發生在 main goroutine 且沒有 recover，程式直接退出，SDK 的 buffer 中的事件遺失。</p>
<p>SDK 可以提供 <code>monitor.RecoverAndReport()</code> 讓開發者在 goroutine 的入口用 <code>defer monitor.RecoverAndReport()</code> 攔截 panic，記錄 error 事件後再 re-panic（保持原有的 crash 行為）。</p>
<p>HTTP handler 的 panic 可以用 middleware 攔截：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="nf">monitorMiddleware</span><span class="p">(</span><span class="nx">next</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">http</span><span class="p">.</span><span class="nf">HandlerFunc</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="k">defer</span> <span class="nx">monitor</span><span class="p">.</span><span class="nf">RecoverAndReport</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">next</span><span class="p">.</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="http-server-自身監控">HTTP server 自身監控</h2>
<p>Go 常用來寫 collector 本身。Collector 需要監控自己的健康狀態 — 請求處理速率、錯誤率、goroutine 數量、記憶體使用量。</p>
<p>Collector 的自身監控和接收外部事件是兩個獨立的管線。自身監控的 metric 可以寫入獨立的 JSONL 檔案（和外部事件分開），或透過 Go 的 <code>expvar</code> / <code>runtime.ReadMemStats</code> 暴露為 HTTP endpoint。</p>
<p>自身監控的關鍵指標：</p>
<ul>
<li><code>collector.events.received</code>：每秒收到的事件數</li>
<li><code>collector.events.invalid</code>：schema 驗證失敗的事件數</li>
<li><code>collector.storage.write_duration_ms</code>：寫入 JSONL 的耗時</li>
<li><code>collector.goroutines</code>：goroutine 數量（洩漏偵測）</li>
<li><code>collector.memory.alloc_mb</code>：記憶體使用量</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>跨平台 timestamp 一致性 → <a href="/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/" data-link-title="跨平台 timestamp 一致性" data-link-desc="時區、精度、clock drift — 不同平台產生的 timestamp 在 collector 端需要能正確比對和排序">跨平台 timestamp 一致性</a></li>
<li>Collector 的架構設計 → <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">模組四 Collector 設計</a></li>
<li>SDK 公開 API 的 Close 方法 → <a href="/blog/monitoring/03-sdk-design/public-api/" data-link-title="SDK 公開 API 設計" data-link-desc="init / event / error / metric / flush / close 六個方法構成 SDK 的完整生命週期 — 跨平台共用相同 API 介面">模組三 SDK 公開 API</a></li>
</ul>
]]></content:encoded></item><item><title>跨平台 timestamp 一致性</title><link>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/cross-platform-timestamp/</guid><description>&lt;p>跨平台的監控系統收到來自不同平台（JS / Flutter / Python / Go）的事件，每個平台的 timestamp 格式、精度和時鐘來源不同。Collector 需要對這些 timestamp 做排序、分組和時間範圍查詢，一致性問題會導致事件順序錯亂和分析結果偏差。&lt;/p>
&lt;h2 id="統一格式iso-8601--時區偏移">統一格式：ISO 8601 + 時區偏移&lt;/h2>
&lt;p>所有平台的 SDK 統一使用 ISO 8601 格式，包含毫秒精度和時區偏移：&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">2026-06-19T14:30:00.123+08:00&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>避免使用 Unix timestamp（秒或毫秒）作為僅有的時間表示 — Unix timestamp 沒有時區資訊，如果 SDK 端和 collector 端在不同時區，需要額外的 metadata 才能正確轉換。&lt;/p>
&lt;p>避免使用「本地時間不帶時區」的格式（&lt;code>2026-06-19T14:30:00&lt;/code>）— 無法區分 UTC+8 的 14:30 和 UTC+0 的 14:30。&lt;/p>
&lt;h2 id="各平台的-timestamp-來源">各平台的 timestamp 來源&lt;/h2>
&lt;h3 id="javascript">JavaScript&lt;/h3>
&lt;p>&lt;code>Date.now()&lt;/code> 回傳毫秒精度的 Unix timestamp。&lt;code>new Date().toISOString()&lt;/code> 回傳 UTC 時間的 ISO 8601 字串。&lt;/p>
&lt;p>SDK 應該用 &lt;code>Intl.DateTimeFormat&lt;/code> 或手動計算時區偏移，產生帶本地時區的 ISO 8601 字串 — collector 端需要知道事件的本地時間，以便做使用者時區的分析。&lt;/p>
&lt;p>&lt;code>performance.now()&lt;/code> 提供微秒精度的高解析度時間，但起點是頁面載入時間，無法用來產生絕對 timestamp。用於計算 duration（兩個時間點的差值），不用於記錄事件時間。&lt;/p>
&lt;h3 id="flutter--dart">Flutter / Dart&lt;/h3>
&lt;p>&lt;code>DateTime.now()&lt;/code> 回傳本地時間的 DateTime 物件。&lt;code>DateTime.now().toUtc()&lt;/code> 轉成 UTC。&lt;code>DateTime.now().toIso8601String()&lt;/code> 產生 ISO 8601 字串，但不包含時區偏移（Dart 的 ISO 8601 格式不包含 offset）。&lt;/p>
&lt;p>SDK 需要手動附加時區偏移：&lt;code>DateTime.now().timeZoneOffset&lt;/code> 取得偏移量，手動格式化為 &lt;code>+08:00&lt;/code> 格式附加到 ISO 8601 字串後面。&lt;/p>
&lt;h3 id="python">Python&lt;/h3>
&lt;p>&lt;code>datetime.now(timezone.utc)&lt;/code> 取得 UTC 時間。&lt;code>datetime.now().astimezone()&lt;/code> 取得本地時間帶時區。&lt;code>.isoformat()&lt;/code> 產生帶時區偏移的 ISO 8601 字串。&lt;/p>
&lt;p>Python 3.2+ 的 &lt;code>datetime&lt;/code> 原生支援 timezone-aware 的 ISO 8601 輸出，是各平台中最完整的。&lt;/p>
&lt;h3 id="go">Go&lt;/h3>
&lt;p>&lt;code>time.Now()&lt;/code> 回傳帶時區的 Time 值。&lt;code>time.Now().Format(time.RFC3339Milli)&lt;/code> 產生帶毫秒和時區偏移的字串。&lt;/p>
&lt;p>Go 的 &lt;code>time.RFC3339Nano&lt;/code> 提供奈秒精度，但監控事件不需要這個精度 — 毫秒足夠。&lt;/p>
&lt;h2 id="clock-drift">Clock drift&lt;/h2>
&lt;p>不同裝置的系統時鐘可能有偏差（clock drift）。使用者手機的時鐘比 collector server 快 5 分鐘，SDK 產生的 timestamp 會比 collector 收到時間早 5 分鐘。&lt;/p>
&lt;p>Clock drift 的影響：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>排序錯亂&lt;/strong>：裝置 A（時鐘快）和裝置 B（時鐘慢）的事件混合排序時，時間順序可能和真實發生順序不一致&lt;/li>
&lt;li>&lt;strong>告警延遲計算錯誤&lt;/strong>：collector 用「事件 timestamp 到收到時間的差值」計算延遲，clock drift 讓延遲值不準確&lt;/li>
&lt;/ul>
&lt;p>處理策略：&lt;/p>
&lt;p>&lt;strong>Collector 記錄 receive_timestamp&lt;/strong>：每筆事件除了 SDK 端的 timestamp，collector 在收到時附加 &lt;code>receive_timestamp&lt;/code>。兩者的差值用於估算 clock drift 和網路延遲。&lt;/p>
&lt;p>&lt;strong>容忍而非修正&lt;/strong>：在數秒到數分鐘級的 drift 範圍內，容忍 drift 帶來的排序不精確。跨裝置的事件排序本身就不需要毫秒精度 — 分析的粒度通常是秒或分鐘。&lt;/p>
&lt;p>&lt;strong>異常值偵測&lt;/strong>：timestamp 比 receive_timestamp 早超過 1 小時，或晚超過 5 分鐘，標記為可疑的 clock drift — 可能是使用者手動調整了系統時鐘。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>JS 平台適配 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/js-ts-platform/" data-link-title="JS/TS 平台適配" data-link-desc="CORS 限制、Service Worker 攔截、SPA 路由變換偵測 — 瀏覽器環境中 SDK 需要處理的平台特殊問題">JS/TS 平台適配&lt;/a>&lt;/li>
&lt;li>Flutter 平台適配 → &lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/flutter-platform/" data-link-title="Flutter 平台適配" data-link-desc="Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量">Flutter 平台適配&lt;/a>&lt;/li>
&lt;li>Log schema 中的 timestamp 欄位 → &lt;a href="https://tarrragon.github.io/blog/monitoring/02-log-schema/event-schema-fields/" data-link-title="event.schema.json 完整欄位解說" data-link-desc="監控事件的 JSON Schema 定義 — 每個欄位的語意、必填/選填、資料型別和設計理由">模組二 event.schema.json 欄位解說&lt;/a>&lt;/li>
&lt;li>各平台的 error 攔截差異影響 test 設計 → &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/" data-link-title="模組五：測試設計判斷" data-link-desc="Mock 邊界判斷、assertion 設計、test data 代表性、flaky test 診斷">testing 模組五 測試設計判斷&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>跨平台的監控系統收到來自不同平台（JS / Flutter / Python / Go）的事件，每個平台的 timestamp 格式、精度和時鐘來源不同。Collector 需要對這些 timestamp 做排序、分組和時間範圍查詢，一致性問題會導致事件順序錯亂和分析結果偏差。</p>
<h2 id="統一格式iso-8601--時區偏移">統一格式：ISO 8601 + 時區偏移</h2>
<p>所有平台的 SDK 統一使用 ISO 8601 格式，包含毫秒精度和時區偏移：</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">2026-06-19T14:30:00.123+08:00</span></span></code></pre></div><p>避免使用 Unix timestamp（秒或毫秒）作為僅有的時間表示 — Unix timestamp 沒有時區資訊，如果 SDK 端和 collector 端在不同時區，需要額外的 metadata 才能正確轉換。</p>
<p>避免使用「本地時間不帶時區」的格式（<code>2026-06-19T14:30:00</code>）— 無法區分 UTC+8 的 14:30 和 UTC+0 的 14:30。</p>
<h2 id="各平台的-timestamp-來源">各平台的 timestamp 來源</h2>
<h3 id="javascript">JavaScript</h3>
<p><code>Date.now()</code> 回傳毫秒精度的 Unix timestamp。<code>new Date().toISOString()</code> 回傳 UTC 時間的 ISO 8601 字串。</p>
<p>SDK 應該用 <code>Intl.DateTimeFormat</code> 或手動計算時區偏移，產生帶本地時區的 ISO 8601 字串 — collector 端需要知道事件的本地時間，以便做使用者時區的分析。</p>
<p><code>performance.now()</code> 提供微秒精度的高解析度時間，但起點是頁面載入時間，無法用來產生絕對 timestamp。用於計算 duration（兩個時間點的差值），不用於記錄事件時間。</p>
<h3 id="flutter--dart">Flutter / Dart</h3>
<p><code>DateTime.now()</code> 回傳本地時間的 DateTime 物件。<code>DateTime.now().toUtc()</code> 轉成 UTC。<code>DateTime.now().toIso8601String()</code> 產生 ISO 8601 字串，但不包含時區偏移（Dart 的 ISO 8601 格式不包含 offset）。</p>
<p>SDK 需要手動附加時區偏移：<code>DateTime.now().timeZoneOffset</code> 取得偏移量，手動格式化為 <code>+08:00</code> 格式附加到 ISO 8601 字串後面。</p>
<h3 id="python">Python</h3>
<p><code>datetime.now(timezone.utc)</code> 取得 UTC 時間。<code>datetime.now().astimezone()</code> 取得本地時間帶時區。<code>.isoformat()</code> 產生帶時區偏移的 ISO 8601 字串。</p>
<p>Python 3.2+ 的 <code>datetime</code> 原生支援 timezone-aware 的 ISO 8601 輸出，是各平台中最完整的。</p>
<h3 id="go">Go</h3>
<p><code>time.Now()</code> 回傳帶時區的 Time 值。<code>time.Now().Format(time.RFC3339Milli)</code> 產生帶毫秒和時區偏移的字串。</p>
<p>Go 的 <code>time.RFC3339Nano</code> 提供奈秒精度，但監控事件不需要這個精度 — 毫秒足夠。</p>
<h2 id="clock-drift">Clock drift</h2>
<p>不同裝置的系統時鐘可能有偏差（clock drift）。使用者手機的時鐘比 collector server 快 5 分鐘，SDK 產生的 timestamp 會比 collector 收到時間早 5 分鐘。</p>
<p>Clock drift 的影響：</p>
<ul>
<li><strong>排序錯亂</strong>：裝置 A（時鐘快）和裝置 B（時鐘慢）的事件混合排序時，時間順序可能和真實發生順序不一致</li>
<li><strong>告警延遲計算錯誤</strong>：collector 用「事件 timestamp 到收到時間的差值」計算延遲，clock drift 讓延遲值不準確</li>
</ul>
<p>處理策略：</p>
<p><strong>Collector 記錄 receive_timestamp</strong>：每筆事件除了 SDK 端的 timestamp，collector 在收到時附加 <code>receive_timestamp</code>。兩者的差值用於估算 clock drift 和網路延遲。</p>
<p><strong>容忍而非修正</strong>：在數秒到數分鐘級的 drift 範圍內，容忍 drift 帶來的排序不精確。跨裝置的事件排序本身就不需要毫秒精度 — 分析的粒度通常是秒或分鐘。</p>
<p><strong>異常值偵測</strong>：timestamp 比 receive_timestamp 早超過 1 小時，或晚超過 5 分鐘，標記為可疑的 clock drift — 可能是使用者手動調整了系統時鐘。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>JS 平台適配 → <a href="/blog/monitoring/05-platform-adaptation/js-ts-platform/" data-link-title="JS/TS 平台適配" data-link-desc="CORS 限制、Service Worker 攔截、SPA 路由變換偵測 — 瀏覽器環境中 SDK 需要處理的平台特殊問題">JS/TS 平台適配</a></li>
<li>Flutter 平台適配 → <a href="/blog/monitoring/05-platform-adaptation/flutter-platform/" data-link-title="Flutter 平台適配" data-link-desc="Isolate 安全、Platform channel 攔截、app lifecycle 事件 — Flutter SDK 的平台特殊考量">Flutter 平台適配</a></li>
<li>Log schema 中的 timestamp 欄位 → <a href="/blog/monitoring/02-log-schema/event-schema-fields/" data-link-title="event.schema.json 完整欄位解說" data-link-desc="監控事件的 JSON Schema 定義 — 每個欄位的語意、必填/選填、資料型別和設計理由">模組二 event.schema.json 欄位解說</a></li>
<li>各平台的 error 攔截差異影響 test 設計 → <a href="/blog/testing/05-test-design-judgment/" data-link-title="模組五：測試設計判斷" data-link-desc="Mock 邊界判斷、assertion 設計、test data 代表性、flaky test 診斷">testing 模組五 測試設計判斷</a></li>
</ul>
]]></content:encoded></item></channel></rss>