<?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>Log-Schema on Tarragon</title><link>https://tarrragon.github.io/blog/tags/log-schema/</link><description>Recent content in Log-Schema 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/tags/log-schema/index.xml" rel="self" type="application/rss+xml"/><item><title>event.schema.json 完整欄位解說</title><link>https://tarrragon.github.io/blog/monitoring/02-log-schema/event-schema-fields/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/02-log-schema/event-schema-fields/</guid><description>&lt;p>事件 schema 定義了每一筆監控事件的資料結構。統一的 schema 讓 SDK、collector、查詢工具使用同一個資料契約 — SDK 知道該送什麼欄位，collector 知道該驗證什麼，查詢工具知道該讀什麼。&lt;/p>
&lt;h2 id="核心欄位">核心欄位&lt;/h2>
&lt;h3 id="type必填">type（必填）&lt;/h3>
&lt;p>事件類型。對應四類事件分類（&lt;a href="https://tarrragon.github.io/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">模組一&lt;/a>）：&lt;code>event&lt;/code>、&lt;code>error&lt;/code>、&lt;code>metric&lt;/code>、&lt;code>lifecycle&lt;/code>。&lt;/p>
&lt;p>Collector 用 type 決定事件的處理路徑 — error 類型觸發告警規則，metric 類型進入數值聚合，event 類型進入行為分析。&lt;/p>
&lt;h3 id="name必填">name（必填）&lt;/h3>
&lt;p>事件名稱。使用 namespace.action 格式（&lt;a href="https://tarrragon.github.io/blog/monitoring/01-mental-model/event-naming-convention/" data-link-title="事件命名規範" data-link-desc="namespace.action 格式的事件命名、命名一致性的工程價值、和商業方案命名慣例的對應">事件命名規範&lt;/a>）。例如 &lt;code>terminal.connect.done&lt;/code>、&lt;code>auth.biometric.failed&lt;/code>。&lt;/p>
&lt;p>name 是查詢和統計的主要索引。&lt;code>grep &amp;quot;terminal.connect&amp;quot;&lt;/code> 找到所有連線事件；按 name 分群計數得到功能使用頻率。&lt;/p>
&lt;h3 id="timestamp必填">timestamp（必填）&lt;/h3>
&lt;p>事件發生的時間。ISO 8601 格式，包含時區偏移。&lt;code>2026-06-19T14:30:00.123+08:00&lt;/code>。&lt;/p>
&lt;p>Timestamp 由 SDK 在事件發生時記錄，不是 collector 收到時記錄。兩者可能有延遲（離線 buffer、網路延遲），以 SDK 端的時間為準。&lt;/p>
&lt;h3 id="source必填">source（必填）&lt;/h3>
&lt;p>事件來源的識別資訊。包含產生事件的 SDK、app 名稱、版本、平台、OS 版本。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&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="nt">&amp;#34;source&amp;#34;&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="nt">&amp;#34;sdk&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;flutter&amp;#34;&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="nt">&amp;#34;app&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;app_tunnel&amp;#34;&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="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;1.2.0&amp;#34;&lt;/span>&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="nt">&amp;#34;platform&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ios&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;os&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;17.4&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>sdk&lt;/code> 標明產生事件的 SDK 種類（&lt;code>js&lt;/code> / &lt;code>flutter&lt;/code> / &lt;code>python&lt;/code> / &lt;code>go&lt;/code>）。同一個平台可能有不同的 SDK——iOS 上可能是 Flutter SDK 或未來的 Swift 原生 SDK——sdk 欄位讓 collector 區分事件來自哪個 SDK 實作，platform 無法替代這個識別。&lt;code>sdk&lt;/code> 和 &lt;code>platform&lt;/code> 為必填，&lt;code>app&lt;/code>、&lt;code>version&lt;/code>、&lt;code>os&lt;/code> 為選填。&lt;/p>
&lt;p>Source 讓同一個 collector 接收多個 app 的事件時可以區分來源。也用於分析「哪個版本的 error 率最高」、「哪個 OS 版本有特定問題」。&lt;/p>
&lt;h4 id="platform-合法值與自動偵測">platform 合法值與自動偵測&lt;/h4>
&lt;p>&lt;code>platform&lt;/code> 由 SDK init 時自動偵測，開發者不需手動設定。各 SDK 的偵測來源和映射規則：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>SDK&lt;/th>
 &lt;th>偵測來源&lt;/th>
 &lt;th>映射規則&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Python&lt;/td>
 &lt;td>&lt;code>sys.platform&lt;/code>&lt;/td>
 &lt;td>&lt;code>darwin&lt;/code>→&lt;code>macos&lt;/code>、&lt;code>linux&lt;/code>→&lt;code>linux&lt;/code>、&lt;code>win32&lt;/code>→&lt;code>windows&lt;/code>、其他直接傳原值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Flutter&lt;/td>
 &lt;td>&lt;code>Platform.operatingSystem&lt;/code>&lt;/td>
 &lt;td>回傳值（&lt;code>ios&lt;/code>/&lt;code>android&lt;/code>/&lt;code>macos&lt;/code>/&lt;code>linux&lt;/code>/&lt;code>windows&lt;/code>）即合法值，無需映射&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JS&lt;/td>
 &lt;td>瀏覽器環境&lt;/td>
 &lt;td>固定為 &lt;code>web&lt;/code>；OS 偵測（如需要）從 &lt;code>navigator.userAgentData&lt;/code> 解析&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Go&lt;/td>
 &lt;td>&lt;code>runtime.GOOS&lt;/code>&lt;/td>
 &lt;td>&lt;code>darwin&lt;/code>→&lt;code>macos&lt;/code>、&lt;code>linux&lt;/code>→&lt;code>linux&lt;/code>、&lt;code>windows&lt;/code>→&lt;code>windows&lt;/code>、映射邏輯同 Python&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>以上映射是 SDK init 時的預設自動偵測行為。Python 和 Go 的 runtime 回傳系統內部名稱（&lt;code>darwin&lt;/code>、&lt;code>win32&lt;/code>），SDK 負責映射到 schema 定義的標準名稱。Flutter 的 &lt;code>dart:io Platform.operatingSystem&lt;/code> 恰好回傳合法值。JS SDK 在瀏覽器環境中無法可靠偵測 OS，platform 統一為 &lt;code>web&lt;/code>。&lt;/p>
&lt;p>自動偵測之外，SDK 也接受手動覆蓋 platform 值。短生命週期的命令列腳本（如 CI pipeline step、pre-commit hook）可手動將 platform 設為 &lt;code>script&lt;/code>，表示非互動式 OS session——這類場景中 OS 名稱不是有意義的區分維度，&lt;code>script&lt;/code> 讓查詢時能篩選出所有腳本來源的事件。&lt;/p>
&lt;p>SDK 不做映射的話，collector 會收到不一致的 platform 值——同是 macOS 的事件有些標 &lt;code>darwin&lt;/code> 有些標 &lt;code>macos&lt;/code>，查詢篩選會漏事件。各平台 SDK 的執行環境適配細節見&lt;a href="https://tarrragon.github.io/blog/monitoring/05-platform-adaptation/" data-link-title="模組五：平台適配" data-link-desc="JS CORS / Flutter isolate / Python GIL / Go graceful shutdown — 各平台的特殊考量">模組五：平台適配&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>事件 schema 定義了每一筆監控事件的資料結構。統一的 schema 讓 SDK、collector、查詢工具使用同一個資料契約 — SDK 知道該送什麼欄位，collector 知道該驗證什麼，查詢工具知道該讀什麼。</p>
<h2 id="核心欄位">核心欄位</h2>
<h3 id="type必填">type（必填）</h3>
<p>事件類型。對應四類事件分類（<a href="/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">模組一</a>）：<code>event</code>、<code>error</code>、<code>metric</code>、<code>lifecycle</code>。</p>
<p>Collector 用 type 決定事件的處理路徑 — error 類型觸發告警規則，metric 類型進入數值聚合，event 類型進入行為分析。</p>
<h3 id="name必填">name（必填）</h3>
<p>事件名稱。使用 namespace.action 格式（<a href="/blog/monitoring/01-mental-model/event-naming-convention/" data-link-title="事件命名規範" data-link-desc="namespace.action 格式的事件命名、命名一致性的工程價值、和商業方案命名慣例的對應">事件命名規範</a>）。例如 <code>terminal.connect.done</code>、<code>auth.biometric.failed</code>。</p>
<p>name 是查詢和統計的主要索引。<code>grep &quot;terminal.connect&quot;</code> 找到所有連線事件；按 name 分群計數得到功能使用頻率。</p>
<h3 id="timestamp必填">timestamp（必填）</h3>
<p>事件發生的時間。ISO 8601 格式，包含時區偏移。<code>2026-06-19T14:30:00.123+08:00</code>。</p>
<p>Timestamp 由 SDK 在事件發生時記錄，不是 collector 收到時記錄。兩者可能有延遲（離線 buffer、網路延遲），以 SDK 端的時間為準。</p>
<h3 id="source必填">source（必填）</h3>
<p>事件來源的識別資訊。包含產生事件的 SDK、app 名稱、版本、平台、OS 版本。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nt">&#34;sdk&#34;</span><span class="p">:</span> <span class="s2">&#34;flutter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;app&#34;</span><span class="p">:</span> <span class="s2">&#34;app_tunnel&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;1.2.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="nt">&#34;platform&#34;</span><span class="p">:</span> <span class="s2">&#34;ios&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="nt">&#34;os&#34;</span><span class="p">:</span> <span class="s2">&#34;17.4&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>sdk</code> 標明產生事件的 SDK 種類（<code>js</code> / <code>flutter</code> / <code>python</code> / <code>go</code>）。同一個平台可能有不同的 SDK——iOS 上可能是 Flutter SDK 或未來的 Swift 原生 SDK——sdk 欄位讓 collector 區分事件來自哪個 SDK 實作，platform 無法替代這個識別。<code>sdk</code> 和 <code>platform</code> 為必填，<code>app</code>、<code>version</code>、<code>os</code> 為選填。</p>
<p>Source 讓同一個 collector 接收多個 app 的事件時可以區分來源。也用於分析「哪個版本的 error 率最高」、「哪個 OS 版本有特定問題」。</p>
<h4 id="platform-合法值與自動偵測">platform 合法值與自動偵測</h4>
<p><code>platform</code> 由 SDK init 時自動偵測，開發者不需手動設定。各 SDK 的偵測來源和映射規則：</p>
<table>
  <thead>
      <tr>
          <th>SDK</th>
          <th>偵測來源</th>
          <th>映射規則</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Python</td>
          <td><code>sys.platform</code></td>
          <td><code>darwin</code>→<code>macos</code>、<code>linux</code>→<code>linux</code>、<code>win32</code>→<code>windows</code>、其他直接傳原值</td>
      </tr>
      <tr>
          <td>Flutter</td>
          <td><code>Platform.operatingSystem</code></td>
          <td>回傳值（<code>ios</code>/<code>android</code>/<code>macos</code>/<code>linux</code>/<code>windows</code>）即合法值，無需映射</td>
      </tr>
      <tr>
          <td>JS</td>
          <td>瀏覽器環境</td>
          <td>固定為 <code>web</code>；OS 偵測（如需要）從 <code>navigator.userAgentData</code> 解析</td>
      </tr>
      <tr>
          <td>Go</td>
          <td><code>runtime.GOOS</code></td>
          <td><code>darwin</code>→<code>macos</code>、<code>linux</code>→<code>linux</code>、<code>windows</code>→<code>windows</code>、映射邏輯同 Python</td>
      </tr>
  </tbody>
</table>
<p>以上映射是 SDK init 時的預設自動偵測行為。Python 和 Go 的 runtime 回傳系統內部名稱（<code>darwin</code>、<code>win32</code>），SDK 負責映射到 schema 定義的標準名稱。Flutter 的 <code>dart:io Platform.operatingSystem</code> 恰好回傳合法值。JS SDK 在瀏覽器環境中無法可靠偵測 OS，platform 統一為 <code>web</code>。</p>
<p>自動偵測之外，SDK 也接受手動覆蓋 platform 值。短生命週期的命令列腳本（如 CI pipeline step、pre-commit hook）可手動將 platform 設為 <code>script</code>，表示非互動式 OS session——這類場景中 OS 名稱不是有意義的區分維度，<code>script</code> 讓查詢時能篩選出所有腳本來源的事件。</p>
<p>SDK 不做映射的話，collector 會收到不一致的 platform 值——同是 macOS 的事件有些標 <code>darwin</code> 有些標 <code>macos</code>，查詢篩選會漏事件。各平台 SDK 的執行環境適配細節見<a href="/blog/monitoring/05-platform-adaptation/" data-link-title="模組五：平台適配" data-link-desc="JS CORS / Flutter isolate / Python GIL / Go graceful shutdown — 各平台的特殊考量">模組五：平台適配</a>。</p>
<h3 id="session選填">session（選填）</h3>
<p>使用者 session 的識別資訊。Session ID（UUID）和 session 開始時間。</p>
<p>Session 用於關聯同一次使用中的多個事件。「使用者在這次 session 中做了什麼操作、遇到了什麼 error」的分析依賴 session ID。</p>
<p>去識別化要求：session ID 用 UUID 而非使用者帳號，不包含個人識別資訊（<a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">模組七</a>）。</p>
<h3 id="data選填">data（選填）</h3>
<p>事件的附加資料。自由結構的 JSON object，內容依事件類型和名稱而定。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;wss://192.168.1.100:7681/ws&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;duration_ms&#34;</span><span class="p">:</span> <span class="mi">320</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nt">&#34;step&#34;</span><span class="p">:</span> <span class="s2">&#34;3/5&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Data 欄位是 schema 中唯一的自由結構區域。核心欄位（type、name、timestamp、source）有固定格式，data 的內容由事件定義者決定。</p>
<h3 id="v必填">v（必填）</h3>
<p>Schema 版本號。整數，從 1 開始遞增。</p>
<p>版本號讓 collector 知道用哪個版本的 schema 驗證這筆事件。Schema 演進時，舊版本的事件仍可被正確處理。</p>
<h2 id="collector-附加欄位底線前綴">Collector 附加欄位（底線前綴）</h2>
<p>Collector 在事件寫入 storage 時可以附加系統層的 metadata。這些欄位使用底線前綴（<code>_flags</code>、<code>_fingerprint</code>），和 SDK 端產生的業務欄位區隔。SDK 送出的事件中不包含這些欄位 — 它們由 collector pipeline 在處理過程中計算並附加。</p>
<h3 id="_flags選填collector-附加">_flags（選填，collector 附加）</h3>
<p>Collector 端的行為分析或規則引擎偵測到異常時，在事件中附加標記。Dashboard 查詢可用 <code>_flags</code> 過濾可疑事件。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;_flags&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nt">&#34;suspicious&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;reason&#34;</span><span class="p">:</span> <span class="s2">&#34;rate_anomaly&#34;</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><p><code>suspicious</code> 標記的事件不被刪除 — 直接丟棄有誤殺正常流量的風險（行銷活動的真實流量暴增可能觸發異常偵測）。Dashboard 預設排除 <code>_flags.suspicious = true</code> 的事件，需要調查時可包含。</p>
<p>標記來源和 reason 值的定義見 <a href="/blog/monitoring/07-security-privacy/client-sdk-authentication/" data-link-title="Client-side SDK 認證的根本限制" data-link-desc="嵌在 client 端的 credential 必然可被提取 — 認清 architecture 天花板後的多層緩解策略，從 origin 驗證到 device attestation">Client-side SDK 認證</a> 的事後標記策略段。</p>
<h3 id="_fingerprint選填collector-附加">_fingerprint（選填，collector 附加）</h3>
<p>Error 事件的去重識別碼。Collector 從 error 的 type、normalized message、stack trace 計算 hash，用於把相同根因的 error 歸組。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;_fingerprint&#34;</span><span class="p">:</span> <span class="s2">&#34;a3f8c2e1b7d94f06&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Fingerprint 的計算邏輯和 error grouping 機制見 <a href="/blog/monitoring/04-collector/error-fingerprint/" data-link-title="Error Fingerprint 與去重分群" data-link-desc="把大量 error 事件歸組成可管理的 issue 列表 — fingerprint 演算法、message normalization、error_groups 表設計、自架方案的務實邊界">Error Fingerprint 與去重分群</a>。</p>
<h3 id="sdk-自監控指標">SDK 自監控指標</h3>
<p>監控系統自身的資料完整性需要獨立的指標追蹤 — SDK 用 metric 類事件回報自己的送出量和丟棄量，collector 用 endpoint 暴露處理量和拒絕量。SDK 端的指標每次 flush 成功後作為標準 schema 事件一起送出，name 以 <code>sdk.</code> 前綴標識。</p>
<table>
  <thead>
      <tr>
          <th>name</th>
          <th>含義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sdk.events.produced</code></td>
          <td>事件產生總數（取樣前）</td>
      </tr>
      <tr>
          <td><code>sdk.events.sampled</code></td>
          <td>取樣後保留的事件數</td>
      </tr>
      <tr>
          <td><code>sdk.events.sent</code></td>
          <td>成功送出的事件數（收到 200/207 的 accepted）</td>
      </tr>
      <tr>
          <td><code>sdk.events.dropped</code></td>
          <td>被 FIFO 丟棄或重試耗盡的事件數</td>
      </tr>
      <tr>
          <td><code>sdk.flush.failures</code></td>
          <td>flush 失敗次數（429 / 5xx / timeout）</td>
      </tr>
      <tr>
          <td><code>sdk.sampling.rate</code></td>
          <td>當前動態取樣率</td>
      </tr>
  </tbody>
</table>
<p>Collector 端對應暴露 <code>collector.events.received</code>、<code>collector.events.rejected</code>、<code>collector.events.stored</code>、<code>collector.events.backpressure</code> 等指標，透過 <code>/metrics</code> endpoint 或 health endpoint 的擴展欄位提供。</p>
<p>完整的指標定義、端到端比對方法和損失率閾值見 <a href="/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a> 的監控損失段。</p>
<h2 id="完整-schema-範例">完整 schema 範例</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="nt">&#34;v&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;error&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;terminal.connect.failed&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nt">&#34;timestamp&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-06-19T14:30:00.123+08:00&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nt">&#34;source&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nt">&#34;sdk&#34;</span><span class="p">:</span> <span class="s2">&#34;flutter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nt">&#34;app&#34;</span><span class="p">:</span> <span class="s2">&#34;app_tunnel&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nt">&#34;version&#34;</span><span class="p">:</span> <span class="s2">&#34;1.2.0&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nt">&#34;platform&#34;</span><span class="p">:</span> <span class="s2">&#34;ios&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nt">&#34;os&#34;</span><span class="p">:</span> <span class="s2">&#34;17.4&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nt">&#34;session&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;a1b2c3d4-e5f6-7890-abcd-ef1234567890&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nt">&#34;started&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-06-19T14:25:00.000+08:00&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">  <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nt">&#34;step&#34;</span><span class="p">:</span> <span class="s2">&#34;ws_connect&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nt">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;Connection refused&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;wss://192.168.1.100:7681/ws&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="下一步路由">下一步路由</h2>
<ul>
<li>欄位設計的原則 → <a href="/blog/monitoring/02-log-schema/field-design-principles/" data-link-title="欄位設計原則" data-link-desc="source 標明來源、data 自由欄位、v 版本演進 — 三個設計原則讓 schema 在不同階段都能使用">欄位設計原則</a></li>
<li>Schema 版本演進 → <a href="/blog/monitoring/02-log-schema/schema-versioning/" data-link-title="Schema 版本演進策略" data-link-desc="Backward compatible 的增量變更 — 新增欄位不改版、改名或改型別才改版、collector 同時支援多版本">Schema 版本演進策略</a></li>
<li>和 OpenTelemetry 的差異 → <a href="/blog/monitoring/02-log-schema/otel-comparison/" data-link-title="跟 OpenTelemetry 的 schema 差異對照" data-link-desc="自架 event schema 和 OTLP 的設計差異 — 為什麼 client-side 監控用簡化 schema、什麼時候切換到 OTLP">跟 OpenTelemetry 的 schema 差異對照</a></li>
<li>Log 點的設計方法 → <a href="/blog/testing/02-client-observability/" data-link-title="模組二：客戶端可觀測性" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — log 設計是功能規格的一部分">testing 模組二 客戶端可觀測性</a></li>
</ul>
]]></content:encoded></item><item><title>欄位設計原則</title><link>https://tarrragon.github.io/blog/monitoring/02-log-schema/field-design-principles/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/02-log-schema/field-design-principles/</guid><description>&lt;p>事件 schema 的欄位設計遵循三個原則：來源可追溯、擴展不破壞、版本可辨識。這三個原則讓 schema 從自用工具的 grep 查詢一直到商業方案的資料管線都能正常運作。&lt;/p>
&lt;h2 id="原則一source-標明來源">原則一：source 標明來源&lt;/h2>
&lt;p>每筆事件的 source 欄位記錄「這筆事件從哪裡來」。App 名稱、版本、平台、OS 版本 — 這些資訊在事件產生時由 SDK 自動填入，不依賴使用者或開發者手動標記。&lt;/p>
&lt;p>source 的設計要點是「足夠區分但不過度」。&lt;code>sdk&lt;/code> 和 &lt;code>platform&lt;/code> 是必填——sdk 標明事件由哪個 SDK 實作產生（&lt;code>js&lt;/code> / &lt;code>flutter&lt;/code> / &lt;code>python&lt;/code> / &lt;code>go&lt;/code>），platform 標明運行平台（&lt;code>ios&lt;/code> / &lt;code>android&lt;/code> / &lt;code>web&lt;/code> / &lt;code>macos&lt;/code>）。兩者不能互相推導：同一個 platform（iOS）上可能有不同的 SDK（Flutter SDK 或 Swift 原生 SDK），同一個 SDK（Flutter）可能跑在不同 platform（iOS / Android / Web）。App 名稱和版本能區分「這是哪個 app 的哪個版本送來的事件」。OS 版本用於分析平台特定的問題（「這個 error 只出現在 iOS 17.4」）。&lt;/p>
&lt;p>不需要在 source 放裝置 ID 或使用者 ID — 這些屬於個人識別資訊，放在 source 會讓每一筆事件都攜帶 PII，增加去識別化的複雜度。Session ID 用於關聯同次使用的事件，已足夠取代裝置/使用者級別的追蹤。&lt;/p>
&lt;h2 id="原則二data-自由欄位">原則二：data 自由欄位&lt;/h2>
&lt;p>data 欄位是事件的附加資料區域，接受任意 JSON object。核心欄位（type、name、timestamp、source）有固定的 schema 驗證，data 的內容不做 schema 驗證（或做寬鬆驗證）。&lt;/p>
&lt;p>自由欄位的設計理由是「不同事件需要不同的附加資料」。&lt;code>terminal.connect.done&lt;/code> 需要 URL 和 duration；&lt;code>auth.biometric.failed&lt;/code> 需要 error code 和 fallback 方式。為每種事件定義固定的 data schema 會讓 schema 膨脹且頻繁變動。&lt;/p>
&lt;p>自由的代價是查詢時無法保證 data 內某個欄位一定存在。處理策略：查詢時用 optional access（&lt;code>data?.duration_ms&lt;/code>），統計時跳過缺少目標欄位的事件。&lt;/p>
&lt;h2 id="原則三v-版本演進">原則三：v 版本演進&lt;/h2>
&lt;p>v 欄位是整數版本號，標明「這筆事件是用哪個版本的 schema 產生的」。&lt;/p>
&lt;p>版本號解決的問題是 schema 變更時的向後相容。新版本的 SDK 產生 v=2 的事件，舊版本的 SDK 仍在產生 v=1 的事件。Collector 收到事件時根據 v 決定用哪個版本的驗證和處理邏輯。&lt;/p>
&lt;p>版本號的遞增規則：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>新增選填欄位&lt;/strong>：不需要遞增版本號。舊版事件缺少新欄位，collector 用預設值處理。&lt;/li>
&lt;li>&lt;strong>新增必填欄位&lt;/strong>：遞增版本號。舊版事件沒有這個欄位，collector 需要區分版本處理。&lt;/li>
&lt;li>&lt;strong>刪除或改名欄位&lt;/strong>：遞增版本號。collector 需要同時支援新舊版本的事件格式。&lt;/li>
&lt;li>&lt;strong>改變欄位型別&lt;/strong>：遞增版本號。string 改成 integer 等型別變更需要不同的解析邏輯。&lt;/li>
&lt;/ul>
&lt;h2 id="欄位命名慣例">欄位命名慣例&lt;/h2>
&lt;p>欄位名稱使用 snake_case（&lt;code>duration_ms&lt;/code>、&lt;code>error_code&lt;/code>），和 JSON 的慣例一致。避免在欄位名稱中編碼單位（&lt;code>duration&lt;/code> 不夠明確 — 是秒還是毫秒？），在名稱中加上單位後綴（&lt;code>duration_ms&lt;/code>、&lt;code>size_bytes&lt;/code>）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>完整欄位定義 → &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>Schema 版本演進的具體策略 → &lt;a href="https://tarrragon.github.io/blog/monitoring/02-log-schema/schema-versioning/" data-link-title="Schema 版本演進策略" data-link-desc="Backward compatible 的增量變更 — 新增欄位不改版、改名或改型別才改版、collector 同時支援多版本">Schema 版本演進策略&lt;/a>&lt;/li>
&lt;li>和 OpenTelemetry 的比較 → &lt;a href="https://tarrragon.github.io/blog/monitoring/02-log-schema/otel-comparison/" data-link-title="跟 OpenTelemetry 的 schema 差異對照" data-link-desc="自架 event schema 和 OTLP 的設計差異 — 為什麼 client-side 監控用簡化 schema、什麼時候切換到 OTLP">跟 OpenTelemetry 的 schema 差異對照&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>事件 schema 的欄位設計遵循三個原則：來源可追溯、擴展不破壞、版本可辨識。這三個原則讓 schema 從自用工具的 grep 查詢一直到商業方案的資料管線都能正常運作。</p>
<h2 id="原則一source-標明來源">原則一：source 標明來源</h2>
<p>每筆事件的 source 欄位記錄「這筆事件從哪裡來」。App 名稱、版本、平台、OS 版本 — 這些資訊在事件產生時由 SDK 自動填入，不依賴使用者或開發者手動標記。</p>
<p>source 的設計要點是「足夠區分但不過度」。<code>sdk</code> 和 <code>platform</code> 是必填——sdk 標明事件由哪個 SDK 實作產生（<code>js</code> / <code>flutter</code> / <code>python</code> / <code>go</code>），platform 標明運行平台（<code>ios</code> / <code>android</code> / <code>web</code> / <code>macos</code>）。兩者不能互相推導：同一個 platform（iOS）上可能有不同的 SDK（Flutter SDK 或 Swift 原生 SDK），同一個 SDK（Flutter）可能跑在不同 platform（iOS / Android / Web）。App 名稱和版本能區分「這是哪個 app 的哪個版本送來的事件」。OS 版本用於分析平台特定的問題（「這個 error 只出現在 iOS 17.4」）。</p>
<p>不需要在 source 放裝置 ID 或使用者 ID — 這些屬於個人識別資訊，放在 source 會讓每一筆事件都攜帶 PII，增加去識別化的複雜度。Session ID 用於關聯同次使用的事件，已足夠取代裝置/使用者級別的追蹤。</p>
<h2 id="原則二data-自由欄位">原則二：data 自由欄位</h2>
<p>data 欄位是事件的附加資料區域，接受任意 JSON object。核心欄位（type、name、timestamp、source）有固定的 schema 驗證，data 的內容不做 schema 驗證（或做寬鬆驗證）。</p>
<p>自由欄位的設計理由是「不同事件需要不同的附加資料」。<code>terminal.connect.done</code> 需要 URL 和 duration；<code>auth.biometric.failed</code> 需要 error code 和 fallback 方式。為每種事件定義固定的 data schema 會讓 schema 膨脹且頻繁變動。</p>
<p>自由的代價是查詢時無法保證 data 內某個欄位一定存在。處理策略：查詢時用 optional access（<code>data?.duration_ms</code>），統計時跳過缺少目標欄位的事件。</p>
<h2 id="原則三v-版本演進">原則三：v 版本演進</h2>
<p>v 欄位是整數版本號，標明「這筆事件是用哪個版本的 schema 產生的」。</p>
<p>版本號解決的問題是 schema 變更時的向後相容。新版本的 SDK 產生 v=2 的事件，舊版本的 SDK 仍在產生 v=1 的事件。Collector 收到事件時根據 v 決定用哪個版本的驗證和處理邏輯。</p>
<p>版本號的遞增規則：</p>
<ul>
<li><strong>新增選填欄位</strong>：不需要遞增版本號。舊版事件缺少新欄位，collector 用預設值處理。</li>
<li><strong>新增必填欄位</strong>：遞增版本號。舊版事件沒有這個欄位，collector 需要區分版本處理。</li>
<li><strong>刪除或改名欄位</strong>：遞增版本號。collector 需要同時支援新舊版本的事件格式。</li>
<li><strong>改變欄位型別</strong>：遞增版本號。string 改成 integer 等型別變更需要不同的解析邏輯。</li>
</ul>
<h2 id="欄位命名慣例">欄位命名慣例</h2>
<p>欄位名稱使用 snake_case（<code>duration_ms</code>、<code>error_code</code>），和 JSON 的慣例一致。避免在欄位名稱中編碼單位（<code>duration</code> 不夠明確 — 是秒還是毫秒？），在名稱中加上單位後綴（<code>duration_ms</code>、<code>size_bytes</code>）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整欄位定義 → <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>Schema 版本演進的具體策略 → <a href="/blog/monitoring/02-log-schema/schema-versioning/" data-link-title="Schema 版本演進策略" data-link-desc="Backward compatible 的增量變更 — 新增欄位不改版、改名或改型別才改版、collector 同時支援多版本">Schema 版本演進策略</a></li>
<li>和 OpenTelemetry 的比較 → <a href="/blog/monitoring/02-log-schema/otel-comparison/" data-link-title="跟 OpenTelemetry 的 schema 差異對照" data-link-desc="自架 event schema 和 OTLP 的設計差異 — 為什麼 client-side 監控用簡化 schema、什麼時候切換到 OTLP">跟 OpenTelemetry 的 schema 差異對照</a></li>
</ul>
]]></content:encoded></item><item><title>Schema 版本演進策略</title><link>https://tarrragon.github.io/blog/monitoring/02-log-schema/schema-versioning/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/02-log-schema/schema-versioning/</guid><description>&lt;p>Schema 版本演進的目標是讓新版 SDK 和舊版 SDK 產生的事件能被同一個 collector 正確處理。核心策略是 backward compatible 的增量變更 — 儘量用「新增選填欄位」代替「修改現有欄位」。&lt;/p>
&lt;h2 id="不需要改版的變更">不需要改版的變更&lt;/h2>
&lt;h3 id="新增選填欄位">新增選填欄位&lt;/h3>
&lt;p>在 data 區域新增欄位。舊版 SDK 送來的事件不包含這個欄位，collector 和查詢工具用「欄位不存在則忽略」的邏輯處理。&lt;/p>
&lt;p>例：v=1 的事件沒有 &lt;code>data.duration_ms&lt;/code>，v=1 的 SDK 升級後開始送 &lt;code>data.duration_ms&lt;/code>。Collector 不需要改 — 新欄位出現在 data 自由區域，不影響 schema 驗證。查詢時用 optional access。&lt;/p>
&lt;h3 id="新增事件名稱">新增事件名稱&lt;/h3>
&lt;p>新功能加入新的事件名稱（&lt;code>enrollment.qr.scan&lt;/code>）。事件名稱不受 schema 版本控制 — schema 定義的是事件的結構，不是事件名稱的清單。&lt;/p>
&lt;h2 id="需要改版的變更">需要改版的變更&lt;/h2>
&lt;h3 id="新增核心必填欄位">新增核心必填欄位&lt;/h3>
&lt;p>在核心區域（type、name、timestamp、source 同層）新增必填欄位。舊版 SDK 不會送這個欄位，collector 需要根據版本號決定是否要求這個欄位。&lt;/p>
&lt;p>例：v=2 新增必填的 &lt;code>environment&lt;/code> 欄位（production / staging / development）。v=1 的事件沒有這個欄位，collector 對 v=1 不要求 environment，對 v=2 要求 environment。&lt;/p>
&lt;h3 id="改變欄位型別">改變欄位型別&lt;/h3>
&lt;p>把 &lt;code>duration&lt;/code> 從 string（&lt;code>&amp;quot;320ms&amp;quot;&lt;/code>）改成 integer（&lt;code>320&lt;/code>）。同一個欄位的兩種型別需要不同的解析邏輯，collector 用版本號區分。&lt;/p>
&lt;h3 id="刪除或重新命名欄位">刪除或重新命名欄位&lt;/h3>
&lt;p>刪除欄位或改名（&lt;code>error_msg&lt;/code> → &lt;code>error_message&lt;/code>）需要改版。Collector 對舊版本讀舊欄位名，對新版本讀新欄位名。&lt;/p>
&lt;h2 id="collector-的多版本支援">Collector 的多版本支援&lt;/h2>
&lt;p>Collector 同時接收不同版本的事件。處理策略：&lt;/p>
&lt;h3 id="版本分派">版本分派&lt;/h3>
&lt;p>收到事件後先讀 v 欄位，分派到對應版本的處理器。每個版本的處理器知道該版本的欄位結構和驗證規則。&lt;/p>
&lt;h3 id="正規化">正規化&lt;/h3>
&lt;p>不同版本的事件正規化成統一的內部格式後儲存。正規化層處理欄位名稱對應（&lt;code>error_msg&lt;/code> → &lt;code>error_message&lt;/code>）和型別轉換（string → integer）。查詢時只面對正規化後的格式。&lt;/p>
&lt;h3 id="版本淘汰">版本淘汰&lt;/h3>
&lt;p>當所有 SDK 都升級到 v=2 後（從事件記錄中確認不再收到 v=1），可以移除 v=1 的處理器。淘汰前確認沒有離線 buffer 中的 v=1 事件尚未送達。&lt;/p>
&lt;h2 id="實務建議">實務建議&lt;/h2>
&lt;p>&lt;strong>遲改版優於早改版&lt;/strong>。每次改版增加 collector 的複雜度（多一個版本的處理器）。如果變更可以用「新增選填欄位」解決，優先選擇不改版。&lt;/p>
&lt;p>&lt;strong>一次改版包含多個變更&lt;/strong>。如果確定要改版，把多個計畫中的 breaking change 合併到同一次版本升級。v=1 → v=2 包含三個 breaking change，比 v=1 → v=2 → v=3 → v=4 各包含一個 breaking change 的維護成本低。&lt;/p>
&lt;p>&lt;strong>Schema 文件和版本號同步&lt;/strong>。每個版本的 schema 有對應的文件，記錄該版本和前一版本的差異。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>完整欄位定義 → &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>欄位設計原則 → &lt;a href="https://tarrragon.github.io/blog/monitoring/02-log-schema/field-design-principles/" data-link-title="欄位設計原則" data-link-desc="source 標明來源、data 自由欄位、v 版本演進 — 三個設計原則讓 schema 在不同階段都能使用">欄位設計原則&lt;/a>&lt;/li>
&lt;li>和 OpenTelemetry 的比較 → &lt;a href="https://tarrragon.github.io/blog/monitoring/02-log-schema/otel-comparison/" data-link-title="跟 OpenTelemetry 的 schema 差異對照" data-link-desc="自架 event schema 和 OTLP 的設計差異 — 為什麼 client-side 監控用簡化 schema、什麼時候切換到 OTLP">跟 OpenTelemetry 的 schema 差異對照&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Schema 版本演進的目標是讓新版 SDK 和舊版 SDK 產生的事件能被同一個 collector 正確處理。核心策略是 backward compatible 的增量變更 — 儘量用「新增選填欄位」代替「修改現有欄位」。</p>
<h2 id="不需要改版的變更">不需要改版的變更</h2>
<h3 id="新增選填欄位">新增選填欄位</h3>
<p>在 data 區域新增欄位。舊版 SDK 送來的事件不包含這個欄位，collector 和查詢工具用「欄位不存在則忽略」的邏輯處理。</p>
<p>例：v=1 的事件沒有 <code>data.duration_ms</code>，v=1 的 SDK 升級後開始送 <code>data.duration_ms</code>。Collector 不需要改 — 新欄位出現在 data 自由區域，不影響 schema 驗證。查詢時用 optional access。</p>
<h3 id="新增事件名稱">新增事件名稱</h3>
<p>新功能加入新的事件名稱（<code>enrollment.qr.scan</code>）。事件名稱不受 schema 版本控制 — schema 定義的是事件的結構，不是事件名稱的清單。</p>
<h2 id="需要改版的變更">需要改版的變更</h2>
<h3 id="新增核心必填欄位">新增核心必填欄位</h3>
<p>在核心區域（type、name、timestamp、source 同層）新增必填欄位。舊版 SDK 不會送這個欄位，collector 需要根據版本號決定是否要求這個欄位。</p>
<p>例：v=2 新增必填的 <code>environment</code> 欄位（production / staging / development）。v=1 的事件沒有這個欄位，collector 對 v=1 不要求 environment，對 v=2 要求 environment。</p>
<h3 id="改變欄位型別">改變欄位型別</h3>
<p>把 <code>duration</code> 從 string（<code>&quot;320ms&quot;</code>）改成 integer（<code>320</code>）。同一個欄位的兩種型別需要不同的解析邏輯，collector 用版本號區分。</p>
<h3 id="刪除或重新命名欄位">刪除或重新命名欄位</h3>
<p>刪除欄位或改名（<code>error_msg</code> → <code>error_message</code>）需要改版。Collector 對舊版本讀舊欄位名，對新版本讀新欄位名。</p>
<h2 id="collector-的多版本支援">Collector 的多版本支援</h2>
<p>Collector 同時接收不同版本的事件。處理策略：</p>
<h3 id="版本分派">版本分派</h3>
<p>收到事件後先讀 v 欄位，分派到對應版本的處理器。每個版本的處理器知道該版本的欄位結構和驗證規則。</p>
<h3 id="正規化">正規化</h3>
<p>不同版本的事件正規化成統一的內部格式後儲存。正規化層處理欄位名稱對應（<code>error_msg</code> → <code>error_message</code>）和型別轉換（string → integer）。查詢時只面對正規化後的格式。</p>
<h3 id="版本淘汰">版本淘汰</h3>
<p>當所有 SDK 都升級到 v=2 後（從事件記錄中確認不再收到 v=1），可以移除 v=1 的處理器。淘汰前確認沒有離線 buffer 中的 v=1 事件尚未送達。</p>
<h2 id="實務建議">實務建議</h2>
<p><strong>遲改版優於早改版</strong>。每次改版增加 collector 的複雜度（多一個版本的處理器）。如果變更可以用「新增選填欄位」解決，優先選擇不改版。</p>
<p><strong>一次改版包含多個變更</strong>。如果確定要改版，把多個計畫中的 breaking change 合併到同一次版本升級。v=1 → v=2 包含三個 breaking change，比 v=1 → v=2 → v=3 → v=4 各包含一個 breaking change 的維護成本低。</p>
<p><strong>Schema 文件和版本號同步</strong>。每個版本的 schema 有對應的文件，記錄該版本和前一版本的差異。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>完整欄位定義 → <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>欄位設計原則 → <a href="/blog/monitoring/02-log-schema/field-design-principles/" data-link-title="欄位設計原則" data-link-desc="source 標明來源、data 自由欄位、v 版本演進 — 三個設計原則讓 schema 在不同階段都能使用">欄位設計原則</a></li>
<li>和 OpenTelemetry 的比較 → <a href="/blog/monitoring/02-log-schema/otel-comparison/" data-link-title="跟 OpenTelemetry 的 schema 差異對照" data-link-desc="自架 event schema 和 OTLP 的設計差異 — 為什麼 client-side 監控用簡化 schema、什麼時候切換到 OTLP">跟 OpenTelemetry 的 schema 差異對照</a></li>
</ul>
]]></content:encoded></item><item><title>跟 OpenTelemetry 的 schema 差異對照</title><link>https://tarrragon.github.io/blog/monitoring/02-log-schema/otel-comparison/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/02-log-schema/otel-comparison/</guid><description>&lt;p>OpenTelemetry（OTLP）是 server-side 可觀測性的業界標準，定義了 traces、metrics、logs 三種 signal 的資料格式和傳輸協定。自架的 event schema 和 OTLP 在設計目標、複雜度和適用場景上有明確差異。&lt;/p>
&lt;h2 id="設計目標差異">設計目標差異&lt;/h2>
&lt;h3 id="otlp">OTLP&lt;/h3>
&lt;p>OTLP 的設計目標是「跨語言、跨框架、跨 vendor 的統一可觀測性標準」。它支援分散式追蹤（trace context propagation）、多維度 metric（histogram、summary、exponential histogram）、結構化 log。&lt;/p>
&lt;p>OTLP 的資料模型假設 server-side 的基礎設施：collector（如 OTel Collector）做資料路由和轉換，backend（如 Jaeger、Prometheus、Grafana）做儲存和視覺化。&lt;/p>
&lt;h3 id="自架-event-schema">自架 event schema&lt;/h3>
&lt;p>自架 schema 的設計目標是「client-side 監控的最小可用結構」。它假設的基礎設施是一個 HTTP endpoint + JSONL 檔案 + grep。不需要分散式追蹤（client 端通常是單一服務），不需要多維度 metric（counter 和 gauge 用 event 的 data 欄位表示即可）。&lt;/p>
&lt;h2 id="具體差異">具體差異&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>OTLP&lt;/th>
 &lt;th>自架 event schema&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Signal 類型&lt;/td>
 &lt;td>Trace / Metric / Log 三種獨立 signal&lt;/td>
 &lt;td>統一的 event 格式 + type 欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>傳輸格式&lt;/td>
 &lt;td>Protobuf（HTTP/gRPC）&lt;/td>
 &lt;td>JSON（HTTP POST）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Trace context&lt;/td>
 &lt;td>SpanID / TraceID / ParentSpanID&lt;/td>
 &lt;td>Session ID（無分散式追蹤）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Metric 模型&lt;/td>
 &lt;td>Sum / Gauge / Histogram / Summary&lt;/td>
 &lt;td>data 欄位中的數值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Resource&lt;/td>
 &lt;td>結構化的 resource attributes&lt;/td>
 &lt;td>source 欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema 複雜度&lt;/td>
 &lt;td>高（完整的 Protobuf 定義）&lt;/td>
 &lt;td>低（JSON Schema，核心 6 欄位）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="自架-schema-簡化了什麼">自架 schema 簡化了什麼&lt;/h2>
&lt;h3 id="不做分散式追蹤">不做分散式追蹤&lt;/h3>
&lt;p>OTLP 的 trace signal 用 TraceID 和 SpanID 把跨服務的請求關聯起來。Client-side 監控通常不需要這個能力 — app 是單一服務，不存在跨服務的請求鏈路。&lt;/p>
&lt;p>自架 schema 用 session ID 關聯同一次使用中的事件，滿足「使用者在這次操作中做了什麼」的分析需求。&lt;/p>
&lt;h3 id="不用-protobuf">不用 Protobuf&lt;/h3>
&lt;p>OTLP 用 Protobuf 編碼資料，效率高（binary 格式、schema 驗證在編譯期）。但 Protobuf 需要 schema 檔案（.proto）、程式碼生成、和 SDK 語言的 Protobuf 套件。&lt;/p>
&lt;p>自架 schema 用 JSON，人類可讀、grep 友好、不需要額外工具。JSON 的效率比 Protobuf 低（文字格式、體積較大），但在 client-side 監控的事件量下（每分鐘數十到數百筆），效率差異不構成瓶頸。&lt;/p>
&lt;h3 id="簡化-metric-模型">簡化 metric 模型&lt;/h3>
&lt;p>OTLP 的 metric signal 支援 histogram（分桶分佈）、summary（百分位）、exponential histogram（自適應分桶）。這些模型在 server-side 的高頻度 metric 收集中有意義。&lt;/p>
&lt;p>自架 schema 把 metric 記錄為 event 的 data 欄位中的數值（&lt;code>{&amp;quot;type&amp;quot;: &amp;quot;metric&amp;quot;, &amp;quot;name&amp;quot;: &amp;quot;connect.duration&amp;quot;, &amp;quot;data&amp;quot;: {&amp;quot;value_ms&amp;quot;: 320}}&lt;/code>）。統計分析在 collector 端用查詢完成，不在 schema 層做聚合。&lt;/p>
&lt;h2 id="什麼時候切換到-otlp">什麼時候切換到 OTLP&lt;/h2>
&lt;p>以下訊號出現時，自架 schema 的簡化可能成為限制：&lt;/p></description><content:encoded><![CDATA[<p>OpenTelemetry（OTLP）是 server-side 可觀測性的業界標準，定義了 traces、metrics、logs 三種 signal 的資料格式和傳輸協定。自架的 event schema 和 OTLP 在設計目標、複雜度和適用場景上有明確差異。</p>
<h2 id="設計目標差異">設計目標差異</h2>
<h3 id="otlp">OTLP</h3>
<p>OTLP 的設計目標是「跨語言、跨框架、跨 vendor 的統一可觀測性標準」。它支援分散式追蹤（trace context propagation）、多維度 metric（histogram、summary、exponential histogram）、結構化 log。</p>
<p>OTLP 的資料模型假設 server-side 的基礎設施：collector（如 OTel Collector）做資料路由和轉換，backend（如 Jaeger、Prometheus、Grafana）做儲存和視覺化。</p>
<h3 id="自架-event-schema">自架 event schema</h3>
<p>自架 schema 的設計目標是「client-side 監控的最小可用結構」。它假設的基礎設施是一個 HTTP endpoint + JSONL 檔案 + grep。不需要分散式追蹤（client 端通常是單一服務），不需要多維度 metric（counter 和 gauge 用 event 的 data 欄位表示即可）。</p>
<h2 id="具體差異">具體差異</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>OTLP</th>
          <th>自架 event schema</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Signal 類型</td>
          <td>Trace / Metric / Log 三種獨立 signal</td>
          <td>統一的 event 格式 + type 欄位</td>
      </tr>
      <tr>
          <td>傳輸格式</td>
          <td>Protobuf（HTTP/gRPC）</td>
          <td>JSON（HTTP POST）</td>
      </tr>
      <tr>
          <td>Trace context</td>
          <td>SpanID / TraceID / ParentSpanID</td>
          <td>Session ID（無分散式追蹤）</td>
      </tr>
      <tr>
          <td>Metric 模型</td>
          <td>Sum / Gauge / Histogram / Summary</td>
          <td>data 欄位中的數值</td>
      </tr>
      <tr>
          <td>Resource</td>
          <td>結構化的 resource attributes</td>
          <td>source 欄位</td>
      </tr>
      <tr>
          <td>Schema 複雜度</td>
          <td>高（完整的 Protobuf 定義）</td>
          <td>低（JSON Schema，核心 6 欄位）</td>
      </tr>
  </tbody>
</table>
<h2 id="自架-schema-簡化了什麼">自架 schema 簡化了什麼</h2>
<h3 id="不做分散式追蹤">不做分散式追蹤</h3>
<p>OTLP 的 trace signal 用 TraceID 和 SpanID 把跨服務的請求關聯起來。Client-side 監控通常不需要這個能力 — app 是單一服務，不存在跨服務的請求鏈路。</p>
<p>自架 schema 用 session ID 關聯同一次使用中的事件，滿足「使用者在這次操作中做了什麼」的分析需求。</p>
<h3 id="不用-protobuf">不用 Protobuf</h3>
<p>OTLP 用 Protobuf 編碼資料，效率高（binary 格式、schema 驗證在編譯期）。但 Protobuf 需要 schema 檔案（.proto）、程式碼生成、和 SDK 語言的 Protobuf 套件。</p>
<p>自架 schema 用 JSON，人類可讀、grep 友好、不需要額外工具。JSON 的效率比 Protobuf 低（文字格式、體積較大），但在 client-side 監控的事件量下（每分鐘數十到數百筆），效率差異不構成瓶頸。</p>
<h3 id="簡化-metric-模型">簡化 metric 模型</h3>
<p>OTLP 的 metric signal 支援 histogram（分桶分佈）、summary（百分位）、exponential histogram（自適應分桶）。這些模型在 server-side 的高頻度 metric 收集中有意義。</p>
<p>自架 schema 把 metric 記錄為 event 的 data 欄位中的數值（<code>{&quot;type&quot;: &quot;metric&quot;, &quot;name&quot;: &quot;connect.duration&quot;, &quot;data&quot;: {&quot;value_ms&quot;: 320}}</code>）。統計分析在 collector 端用查詢完成，不在 schema 層做聚合。</p>
<h2 id="什麼時候切換到-otlp">什麼時候切換到 OTLP</h2>
<p>以下訊號出現時，自架 schema 的簡化可能成為限制：</p>
<p><strong>需要和 server-side 追蹤關聯</strong>：Client 端的操作要關聯到 server 端的 trace（「使用者點擊按鈕到 database query 的完整路徑」）。需要 OTLP 的 trace context propagation。</p>
<p><strong>事件量超過 JSONL 的處理能力</strong>：每秒數千筆事件時，JSON 的解析和 JSONL 的 grep 查詢成為瓶頸。OTLP + OTel Collector + 時間序列 DB 的管線能處理更高的吞吐量。</p>
<p><strong>需要接入多個 backend</strong>：同時送資料到 Prometheus（metric）、Jaeger（trace）、Elasticsearch（log）。OTel Collector 原生支援多 backend 路由，自架方案需要自己實作。</p>
<p>切換策略：SDK 層的 API 不變（init / event / error / metric），只改底層的傳輸和編碼。從 JSON POST 改成 OTLP export，SDK 的使用者不需要改程式碼。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>自架 schema 的完整定義 → <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>Server-side 的可觀測性 → <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">backend 04 可觀測性</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>
</ul>
]]></content:encoded></item></channel></rss>