<?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>Observability on Tarragon</title><link>https://tarrragon.github.io/blog/tags/observability/</link><description>Recent content in Observability on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 01 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/observability/index.xml" rel="self" type="application/rss+xml"/><item><title>可觀測性與 log 同生命週期管理</title><link>https://tarrragon.github.io/blog/infra/06-observability-logging/log-metric-alarm-lifecycle/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/06-observability-logging/log-metric-alarm-lifecycle/</guid><description>&lt;p>可觀測性要跟它監控的資源同生命週期：log group、metric 與 alarm 寫進建立資源的同一套 IaC，資源開出來的那一刻監控就在線，而非等出事才補。這條規則的責任是讓基礎設施在出事時可被追查、在日常時可被量化，而它的建立與銷毀和被監控的資源綁在一起，則保證監控的覆蓋率不會隨時間衰退。&lt;/p>
&lt;p>沒有同生命週期管理時，新服務上線後的監控覆蓋率取決於有沒有人記得手動建立 log group 和 alarm，而這個記憶在服務數量增長後會衰退。監控缺口在平時不被注意，在事故排查時才浮現 — 需要回溯「什麼時候開始劣化」時，可能發現劣化期間根本沒有對應的 metric 資料。&lt;/p>
&lt;h2 id="同生命週期的落地方式">同生命週期的落地方式&lt;/h2>
&lt;p>可觀測性是基礎設施的一部分，它的建立、變更與銷毀要跟被監控的資源綁在同一個 apply 單位裡。一個 RDS 實例被 IaC 建立時，它的 log group、它的關鍵 metric alarm 應該在同一份 &lt;code>terraform plan&lt;/code> 裡一起出現；這個資源被 destroy 時，對應的 alarm 也一起收掉。&lt;/p>
&lt;p>落地方式是把監控宣告收進服務的 module。&lt;a href="https://tarrragon.github.io/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四（環境分離與模組化）&lt;/a>談的模組化在這裡延伸成「每個服務模組自帶它的 observability 宣告」。一個 database module 內部除了 &lt;code>aws_db_instance&lt;/code>，還包含它的 log group、CPU alarm、連線數 alarm：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># modules/database/monitoring.tf — 跟 database 資源同一個 module
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_log_group&amp;#34; &amp;#34;db_slow_query&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/rds/${var.env}/${var.db_identifier}/slowquery&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> retention_in_days&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">log_retention_days&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="n"> kms_key_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">log_kms_key_arn&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_metric_alarm&amp;#34; &amp;#34;db_cpu&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> alarm_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${var.env}-${var.db_identifier}-cpu-high&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="n"> comparison_operator&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;GreaterThanThreshold&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n"> evaluation_periods&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="n"> metric_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;CPUUtilization&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="n"> namespace&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;AWS/RDS&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="n"> period&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">300&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="n"> statistic&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Average&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="n"> threshold&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="n"> alarm_actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">oncall_sns_arn&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="n"> dimensions&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="n"> DBInstanceIdentifier&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_db_instance&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">primary&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">identifier&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這樣 &lt;code>terraform apply&lt;/code> 建資料庫的同一刻，監控就存在；&lt;code>terraform destroy&lt;/code> 砍資料庫時，孤兒 alarm 也一起清掉。新環境套用同一個 module 時，監控覆蓋率自動跟著資源走，不需要額外的人工記憶。&lt;/p>
&lt;h2 id="監控脫鉤造成的兩類漂移">監控脫鉤造成的兩類漂移&lt;/h2>
&lt;p>把監控外掛在資源之外（用另一份 IaC、另一個 repo、或手動在 console 設定）會製造兩種方向相反的漂移，兩者的共同根因都是監控跟資源不在同一個 apply 單位裡。&lt;/p>
&lt;h3 id="漂移一新資源沒有監控">漂移一：新資源沒有監控&lt;/h3>
&lt;p>service 透過 PR 加上去了，但 alarm 的建立依賴某人事後手動進 console 設定，或等另一個 repo 的 PR 跟上。於是有些 service 有 alarm、有些沒有，覆蓋率取決於「誰記得」。沒有 alarm 的 service 出事時，事故發現路徑從「告警 → 排查」退化成「客訴 → 排查」，反應時間從分鐘級退化到小時級。&lt;/p>
&lt;p>用一條查詢就能看出這個漂移有多嚴重：列出所有 RDS instance，比對各自有沒有對應的 CloudWatch alarm。沒有 alarm 的 instance 就是漂移的活證據。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 列出所有 RDS instance，比對有沒有對應的 CloudWatch alarm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws rds describe-db-instances &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;DBInstances[].DBInstanceIdentifier&amp;#39;&lt;/span> --output text &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;\t&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> &lt;span class="k">while&lt;/span> &lt;span class="nb">read&lt;/span> db&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nv">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>aws cloudwatch describe-alarms &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --alarm-name-prefix &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">db&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --query &lt;span class="s1">&amp;#39;MetricAlarms | length(@)&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">db&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">count&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> alarms&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="漂移二死資源留下殘響">漂移二：死資源留下殘響&lt;/h3>
&lt;p>資源砍了但 alarm 還在，orphan alarm 對不存在的 target 持續報 &lt;code>INSUFFICIENT_DATA&lt;/code>，跟有效 alarm 混在同一個通知頻道裡，降低告警的訊噪比。訊噪比低到一定程度後，有效的 &lt;code>INSUFFICIENT_DATA&lt;/code>（某個服務停止送 metric）也被一起略過 — 告警疲勞讓 alarm 從保護機制退化成背景噪音。&lt;/p></description><content:encoded><![CDATA[<p>可觀測性要跟它監控的資源同生命週期：log group、metric 與 alarm 寫進建立資源的同一套 IaC，資源開出來的那一刻監控就在線，而非等出事才補。這條規則的責任是讓基礎設施在出事時可被追查、在日常時可被量化，而它的建立與銷毀和被監控的資源綁在一起，則保證監控的覆蓋率不會隨時間衰退。</p>
<p>沒有同生命週期管理時，新服務上線後的監控覆蓋率取決於有沒有人記得手動建立 log group 和 alarm，而這個記憶在服務數量增長後會衰退。監控缺口在平時不被注意，在事故排查時才浮現 — 需要回溯「什麼時候開始劣化」時，可能發現劣化期間根本沒有對應的 metric 資料。</p>
<h2 id="同生命週期的落地方式">同生命週期的落地方式</h2>
<p>可觀測性是基礎設施的一部分，它的建立、變更與銷毀要跟被監控的資源綁在同一個 apply 單位裡。一個 RDS 實例被 IaC 建立時，它的 log group、它的關鍵 metric alarm 應該在同一份 <code>terraform plan</code> 裡一起出現；這個資源被 destroy 時，對應的 alarm 也一起收掉。</p>
<p>落地方式是把監控宣告收進服務的 module。<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四（環境分離與模組化）</a>談的模組化在這裡延伸成「每個服務模組自帶它的 observability 宣告」。一個 database module 內部除了 <code>aws_db_instance</code>，還包含它的 log group、CPU alarm、連線數 alarm：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># modules/database/monitoring.tf — 跟 database 資源同一個 module
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_log_group&#34; &#34;db_slow_query&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;/rds/${var.env}/${var.db_identifier}/slowquery&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  retention_in_days</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">log_retention_days</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">  kms_key_id</span>        <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">log_kms_key_arn</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">}
</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="k">resource</span> <span class="s2">&#34;aws_cloudwatch_metric_alarm&#34; &#34;db_cpu&#34;</span> {
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  alarm_name</span>          <span class="o">=</span> <span class="s2">&#34;${var.env}-${var.db_identifier}-cpu-high&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  comparison_operator</span> <span class="o">=</span> <span class="s2">&#34;GreaterThanThreshold&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  evaluation_periods</span>  <span class="o">=</span> <span class="m">3</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">  metric_name</span>         <span class="o">=</span> <span class="s2">&#34;CPUUtilization&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">  namespace</span>           <span class="o">=</span> <span class="s2">&#34;AWS/RDS&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">  period</span>              <span class="o">=</span> <span class="m">300</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">  statistic</span>           <span class="o">=</span> <span class="s2">&#34;Average&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">  threshold</span>           <span class="o">=</span> <span class="m">80</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">  alarm_actions</span>       <span class="o">=</span> <span class="p">[</span><span class="k">var</span><span class="p">.</span><span class="k">oncall_sns_arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">  dimensions</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">    DBInstanceIdentifier</span> <span class="o">=</span> <span class="k">aws_db_instance</span><span class="p">.</span><span class="k">primary</span><span class="p">.</span><span class="k">identifier</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  }
</span></span><span class="line"><span class="ln">22</span><span class="cl">}</span></span></code></pre></div><p>這樣 <code>terraform apply</code> 建資料庫的同一刻，監控就存在；<code>terraform destroy</code> 砍資料庫時，孤兒 alarm 也一起清掉。新環境套用同一個 module 時，監控覆蓋率自動跟著資源走，不需要額外的人工記憶。</p>
<h2 id="監控脫鉤造成的兩類漂移">監控脫鉤造成的兩類漂移</h2>
<p>把監控外掛在資源之外（用另一份 IaC、另一個 repo、或手動在 console 設定）會製造兩種方向相反的漂移，兩者的共同根因都是監控跟資源不在同一個 apply 單位裡。</p>
<h3 id="漂移一新資源沒有監控">漂移一：新資源沒有監控</h3>
<p>service 透過 PR 加上去了，但 alarm 的建立依賴某人事後手動進 console 設定，或等另一個 repo 的 PR 跟上。於是有些 service 有 alarm、有些沒有，覆蓋率取決於「誰記得」。沒有 alarm 的 service 出事時，事故發現路徑從「告警 → 排查」退化成「客訴 → 排查」，反應時間從分鐘級退化到小時級。</p>
<p>用一條查詢就能看出這個漂移有多嚴重：列出所有 RDS instance，比對各自有沒有對應的 CloudWatch alarm。沒有 alarm 的 instance 就是漂移的活證據。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 列出所有 RDS instance，比對有沒有對應的 CloudWatch alarm</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws rds describe-db-instances <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;DBInstances[].DBInstanceIdentifier&#39;</span> --output text <span class="p">|</span> tr <span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> db<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nv">count</span><span class="o">=</span><span class="k">$(</span>aws cloudwatch describe-alarms <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>    --alarm-name-prefix <span class="s2">&#34;</span><span class="si">${</span><span class="nv">db</span><span class="si">}</span><span class="s2">&#34;</span> --query <span class="s1">&#39;MetricAlarms | length(@)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">db</span><span class="si">}</span><span class="s2">: </span><span class="si">${</span><span class="nv">count</span><span class="si">}</span><span class="s2"> alarms&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><h3 id="漂移二死資源留下殘響">漂移二：死資源留下殘響</h3>
<p>資源砍了但 alarm 還在，orphan alarm 對不存在的 target 持續報 <code>INSUFFICIENT_DATA</code>，跟有效 alarm 混在同一個通知頻道裡，降低告警的訊噪比。訊噪比低到一定程度後，有效的 <code>INSUFFICIENT_DATA</code>（某個服務停止送 metric）也被一起略過 — 告警疲勞讓 alarm 從保護機制退化成背景噪音。</p>
<p>漂移二的成本不只是注意力。殘留的 alarm 會佔用 CloudWatch alarm 的配額（每個帳號有配額上限），大量孤兒 alarm 累積後，新服務要加 alarm 可能需要先清理舊的 — 這在事故當下是最不該花時間的事。</p>
<p>修法是把 alarm 的生命週期綁進 module：資源 destroy 時 alarm 跟著 destroy，不需要另一個流程去「記得清理」。如果因為歷史原因已經有大量孤兒 alarm，可以用 alarm 的 <code>StateValue</code> 為 <code>INSUFFICIENT_DATA</code> 且持續超過 7 天作為清理候選的篩選條件。</p>
<h2 id="log-group-設計">log group 設計</h2>
<p>Log group 是日誌的歸屬與保存單位，它要回答兩個治理問題：留多久（retention）、誰能讀（access control）。這兩個問題寫進 IaC 才能稽核，而非依賴 vendor 的隱性預設。</p>
<h3 id="retention三方取捨">Retention：三方取捨</h3>
<p>許多雲端服務在沒有明確宣告 log group 時會自動建一個、套上「永久保留」的預設值。永久保留的問題不是技術性的 — CloudWatch Logs 可以存到無限久 — 而是治理性的：日誌無限堆積、帳單緩慢長大，而沒有人做過「這條 log 該留多久」的顯式決定。</p>
<p>Retention 是成本、合規與除錯需求的三方取捨：</p>
<table>
  <thead>
      <tr>
          <th>日誌類型</th>
          <th>除錯需求</th>
          <th>合規需求</th>
          <th>建議 retention</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>應用 log（request、error）</td>
          <td>近 2-4 週</td>
          <td>通常無特殊要求</td>
          <td>14-30 天</td>
      </tr>
      <tr>
          <td>資料庫 slow query log</td>
          <td>近 1-2 週</td>
          <td>通常無特殊要求</td>
          <td>14 天</td>
      </tr>
      <tr>
          <td>存取稽核 log（CloudTrail）</td>
          <td>偶爾回溯</td>
          <td>1-7 年</td>
          <td>90-365 天 + 歸檔 S3</td>
      </tr>
      <tr>
          <td>金流 / 交易 log</td>
          <td>對帳用、偶爾</td>
          <td>依法規 3-7 年</td>
          <td>短期保留 + 長期歸檔</td>
      </tr>
  </tbody>
</table>
<p>較合理的做法是按日誌類型分層：高頻、除錯用的 application log 設短 retention，稽核相關的 access log 按合規要求設長期保留，必要時再把冷資料用 subscription filter 歸檔到更便宜的物件儲存（S3 + Glacier）。把這些值寫進 IaC，讓「為什麼這條 log 留 90 天」是一個能在 PR 上被討論的決定，而非某人半年前在 console 點的一個數字。成本參考：CloudWatch Logs 的儲存費用約 $0.03/GB/月。一個每天產生 10GB log 的服務，30 天 retention 的月費約 $9，7 天約 $2。retention 天數的選擇是合規需求（留多久才合規）與儲存成本的直接取捨，可以按 log 類型分層設定。</p>
<p>觀測平台的帳單在規模化後容易超線性成長，而缺乏 per-team cost attribution 的環境只能靠全域砍 retention 或降 sampling 來控制成本，兩者都會傷害觀測品質。把 log retention 跟 cardinality budget 的決定從全域級拆到團隊級（用 tag 歸因），才能做到「該省的省、該留的留」。這個取捨在 <a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">4.C14 觀測平台成本治理</a> 有多家企業的具體經驗。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_log_group&#34; &#34;api&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;/app/${var.env}/api&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  retention_in_days</span> <span class="o">=</span><span class="n"> var.env</span> <span class="o">==</span> <span class="s2">&#34;prod&#34;</span> <span class="err">?</span> <span class="m">30</span> <span class="err">:</span> <span class="m">7</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  kms_key_id</span>        <span class="o">=</span> <span class="k">aws_kms_key</span><span class="p">.</span><span class="k">logs</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_log_group&#34; &#34;audit&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;/app/${var.env}/audit&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  retention_in_days</span> <span class="o">=</span> <span class="m">365</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  kms_key_id</span>        <span class="o">=</span> <span class="k">aws_kms_key</span><span class="p">.</span><span class="k">logs</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">}</span></span></code></pre></div><p>Dev 環境的 retention 可以大幅縮短（7 天甚至 3 天），因為它不承擔合規責任，存取量也低，帳單節省直接對應這個差值。</p>
<h3 id="存取控制與加密">存取控制與加密</h3>
<p>「誰能讀」是 retention 之外的另一半。Log 經常夾帶 PII（使用者信箱、IP）、token 或內部結構，讀取權限要跟<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二（身分與憑證地基）</a>建立的 IAM 角色一起管。</p>
<p>常見陷阱是 log 在傳輸與儲存都加密了（<code>kms_key_id</code> 有設），卻對整個團隊開放讀取。加密保護的是靜態資料不被未授權存取，但如果整個開發團隊都有 <code>logs:GetLogEvents</code> 權限，加密形同虛設 — read 權限應該縮到值班與稽核需要的最小集合。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 只允許 oncall role 讀取 prod log
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;log_read&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    actions</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;logs:GetLogEvents&#34;, &#34;logs:FilterLogEvents&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    resources</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_cloudwatch_log_group</span><span class="p">.</span><span class="k">api</span><span class="p">.</span><span class="k">arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  }
</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></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_role_policy&#34; &#34;oncall_log_read&#34;</span> {
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  role</span>   <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">oncall_role_name</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  policy</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_iam_policy_document</span><span class="p">.</span><span class="k">log_read</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>應用層該怎麼決定哪些欄位根本不該進 log（例如在 logger 層做 PII masking），屬於資料保護的範圍，見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>。</p>
<h2 id="metric-與-alarm-設計">metric 與 alarm 設計</h2>
<p>Metric 與 alarm 寫進 IaC，目的是讓「資源被建立的同時就帶著它的健康判準」。Alarm 是一份成文約定：哪條 metric、跨多長的評估窗口、超過什麼值要通知誰。把這份約定寫進 code，它就能被 review、被版本控制、被跨環境複用。</p>
<h3 id="症狀型-vs-成因型告警">症狀型 vs 成因型告警</h3>
<p>閾值設計是訊號與雜訊的取捨。告警可以分成兩類：症狀型（symptom-based）對應的是「使用者已經受影響」的指標 — 5xx 錯誤率、p99 延遲、佇列積壓。成因型（cause-based）對應的是「某個元件在劣化但使用者可能還沒感知」的指標 — CPU 使用率、記憶體使用率、磁碟 IOPS。</p>
<p>收益最高的起點是：症狀型設 alarm 並綁通知，成因型留在 dashboard 上作為診斷線索。理由是成因和症狀之間不一定有直接關係 — CPU 在 80% 不代表使用者受影響（可能 auto-scaling 正在長新節點），而 CPU 在 30% 也不代表安全（可能是某個 goroutine 卡住了，CPU 反而閒下來）。如果每個成因指標都獨立設 alarm，告警數量會與資源數量等比增長，訊噪比下降後症狀型告警容易被成因型告警淹沒。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 症狀型 alarm：5xx 超過閾值代表使用者已受影響
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_metric_alarm&#34; &#34;api_5xx&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  alarm_name</span>          <span class="o">=</span> <span class="s2">&#34;${var.env}-api-5xx-rate&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  comparison_operator</span> <span class="o">=</span> <span class="s2">&#34;GreaterThanThreshold&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">  evaluation_periods</span>  <span class="o">=</span> <span class="m">3</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  metric_name</span>         <span class="o">=</span> <span class="s2">&#34;5XXError&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  namespace</span>           <span class="o">=</span> <span class="s2">&#34;AWS/ApiGateway&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  period</span>              <span class="o">=</span> <span class="m">60</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  statistic</span>           <span class="o">=</span> <span class="s2">&#34;Sum&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  threshold</span>           <span class="o">=</span> <span class="m">10</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  treat_missing_data</span>  <span class="o">=</span> <span class="s2">&#34;notBreaching&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">  alarm_actions</span>       <span class="o">=</span> <span class="p">[</span><span class="k">var</span><span class="p">.</span><span class="k">oncall_sns_arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">}<span class="c1">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 成因型指標：CPU 放 dashboard、不設 alarm
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="err">#</span> <span class="k">除非確認</span><span class="err">「</span><span class="k">CPU</span> <span class="k">到</span> <span class="k">X</span><span class="err">%</span> <span class="k">一定代表服務即將不可用</span><span class="err">」</span><span class="k">這個因果關係</span></span></span></code></pre></div><p>當成因和症狀之間有明確的因果閾值（例如 RDS 磁碟用量到 90% 就會開始拒絕寫入），那條成因也值得設 alarm — 關鍵是因果關係要確認過、而非假設。</p>
<h3 id="insufficient_data-的處理">INSUFFICIENT_DATA 的處理</h3>
<p><code>treat_missing_data</code> 決定了「沒收到 metric 資料點」時 alarm 怎麼判定。這個設定常被忽略，但它在兩個情境下會造成顯著差異：</p>
<p><strong>持續有資料的 metric</strong>（如 API request count）：資料突然消失通常代表服務掛了或 metric 管線斷了，應該設 <code>treat_missing_data = &quot;breaching&quot;</code> — 沒資料本身就是異常訊號。</p>
<p><strong>間歇性的 metric</strong>（如錯誤 count、某個低頻 Lambda 的 invocation）：平常就沒有資料點，沒資料代表正常運作，應該設 <code>treat_missing_data = &quot;notBreaching&quot;</code> — 避免每次低谷時段都觸發假告警。</p>
<p>判讀方式是問自己：「這條 metric 如果 10 分鐘沒有任何資料，代表好事還是壞事？」好事用 <code>notBreaching</code>，壞事用 <code>breaching</code>，不確定用 <code>ignore</code>（不改變 alarm 狀態，等下一個有資料的評估週期再判定）。</p>
<h3 id="告警必須連到動作">告警必須連到動作</h3>
<p>一條有用的 alarm 至少要綁定通知去向。<code>alarm_actions</code> 為空的 alarm 只會在 CloudWatch console 裡變色，而事故發生時沒有人會盯著 console 看 — alarm 的價值在於它主動推送到值班的人手上。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_sns_topic&#34; &#34;oncall&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;${var.env}-oncall-alerts&#34;</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></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_sns_topic_subscription&#34; &#34;pagerduty&#34;</span> {
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">  topic_arn</span> <span class="o">=</span> <span class="k">aws_sns_topic</span><span class="p">.</span><span class="k">oncall</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">  protocol</span>  <span class="o">=</span> <span class="s2">&#34;https&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">  endpoint</span>  <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">pagerduty_integration_url</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">}</span></span></code></pre></div><p>通知去向也該寫進 IaC — SNS topic、subscription、整合端點都是基礎設施的一部分。手動建的 SNS subscription 跟手動建的 alarm 有同樣的問題：沒人記得、沒人維護、出事才發現斷了。</p>
<h3 id="把基礎告警做成-module-預設">把基礎告警做成 module 預設</h3>
<p>如果每次新服務上線都要有人「記得」去加 alarm，代表 alarm 還沒進 module 模板。把基礎告警（錯誤率、延遲、健康檢查失敗）做成服務模組的預設輸出，新服務 apply 時 alarm 跟著一起生出來：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># modules/service/variables.tf
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">variable</span> <span class="s2">&#34;alarm_5xx_threshold&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  type</span>    <span class="o">=</span> <span class="k">number</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  default</span> <span class="o">=</span> <span class="m">10</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">variable</span> <span class="s2">&#34;alarm_latency_p99_ms&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  type</span>    <span class="o">=</span> <span class="k">number</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  default</span> <span class="o">=</span> <span class="m">3000</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</span></span></code></pre></div><p>開新服務時 alarm 跟著資源一起生出來，調整閾值才是該服務 owner 的選配。預設值的選擇依據是「保守但不擾民」— 初始閾值設寬一點，上線穩定後再根據實際基線收斂。</p>
<p>觀測訊號的設計有一個容易忽略的盲區：aggregated metric 會遮蔽局部惡化。Discord 在三代儲存架構的遷移過程中反覆遇到同一個問題——整體 p95 延遲正常，但少數 hot partition 或大型群組的延遲已經飆升，直到使用者回報才發現。教訓是 alarm 的維度要跟業務的 fan-out 結構對齊，而非只看全域聚合。詳見 <a href="/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">4.C13 Discord：從儲存問題回推觀測缺口</a>。規模化後叢集的動態擴縮也會改變觀測模型——擴縮事件本身要成為觀測對象，見 <a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb：K8s 規模化觀測訊號治理</a>。</p>
<h2 id="基礎設施訊號-vs-客戶端行為訊號">基礎設施訊號 vs 客戶端行為訊號</h2>
<p>本模組的可觀測性處理基礎設施訊號，<a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 監控體系</a>處理客戶端與業務行為訊號。兩者觀測的對象不同、生命週期也不同，因此分屬不同的 code 與不同的部署管道。</p>
<p>基礎設施訊號是資源層的健康狀態：log group retention、CPU、佇列深度、5xx 比例、實例存活。它們跟著資源被 IaC 建立與銷毀，回答的問題是「這個系統還活著嗎、哪裡壞了」。</p>
<p>客戶端行為訊號則是 SDK、Collector、業務埋點那一層：使用者點了什麼、轉換漏斗在哪裡流失、前端 JS 錯誤率、自訂業務事件。它們跟著產品功能演進、不跟著基礎設施資源同生共滅。</p>
<p>判讀分界的問法是：這個訊號是「資源建立時就該存在」還是「功能開發時才埋」。前者進本模組的 IaC，後者進 monitoring 那層的應用程式碼。</p>
<p>兩者在事故排查時會合流 — 基礎設施 alarm 告訴值班「RDS CPU 飆到 95%」，客戶端訊號告訴產品團隊「結帳頁面的失敗率從 0.1% 跳到 12%」。把兩條訊號交叉比對才能判斷影響範圍。但它們的擁有者、變更節奏與部署管道不同 — 基礎設施 alarm 跟著 infra PR 走，前端埋點跟著產品 sprint 走。混在同一份 code 裡會讓「誰負責這條訊號的閾值」變模糊，也讓 infra PR 的 review 範圍擴大到不相干的業務邏輯。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">monitoring 監控體系</a>：客戶端 SDK / Collector 那層的監控</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：module 化在這裡延伸成「每個模組自帶 observability 宣告」</li>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>：每個核心服務帶自己的 log 與 alarm</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：observability 變更也走 PR 與自動化護欄</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：哪些欄位不該進 log、PII 處理</li>
</ul>
]]></content:encoded></item><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>LLM Tracing</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/llm-tracing/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/llm-tracing/</guid><description>&lt;p>LLM tracing 的核心概念是「&lt;strong>把 LLM 應用的每次 LLM call / tool call / memory op / handoff 編成結構化 span、串成 trace、可在 observability 平台查詢&lt;/strong>」。對應的標準是 OpenTelemetry GenAI semantic conventions（2025 stabilizing 中）。代表平台：LangSmith、Phoenix、Braintrust、Langfuse、Datadog APM、Logfire。是 production LLM 應用 debug / cost / latency 監控的事實標準、補 traditional logging 抓不到的「為什麼 agent 跑這條路」。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>跟 traditional logging 的對比：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Traditional logging&lt;/th>
 &lt;th>LLM tracing&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>結構&lt;/td>
 &lt;td>字串 line、靠 grep&lt;/td>
 &lt;td>結構化 span、parent-child 樹&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>關聯性&lt;/td>
 &lt;td>弱（要靠 request-id 串）&lt;/td>
 &lt;td>強（trace-id + span 父子關係內建）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>屬性&lt;/td>
 &lt;td>自由 key-value&lt;/td>
 &lt;td>標準化（OTel GenAI semconv）：model / temperature / token usage / cost&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢&lt;/td>
 &lt;td>grep / log aggregator&lt;/td>
 &lt;td>Trace explorer + filter + 視覺化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>LLM 特有 attr&lt;/td>
 &lt;td>沒有&lt;/td>
 &lt;td>system prompt / tool calls / token / reasoning&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>主流 OTel GenAI span 類型：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Span 類型&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>gen_ai.client.operation&lt;/code>&lt;/td>
 &lt;td>一次完整 LLM API call&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>gen_ai.tool.execution&lt;/code>&lt;/td>
 &lt;td>一次 tool 執行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>gen_ai.agent&lt;/code>&lt;/td>
 &lt;td>Agent loop 一個 iteration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>gen_ai.embeddings&lt;/code>&lt;/td>
 &lt;td>Embedding call&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>gen_ai.memory.read/write&lt;/code>&lt;/td>
 &lt;td>Memory 操作&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個 span 標準屬性：&lt;code>gen_ai.system&lt;/code>（vendor）、&lt;code>gen_ai.request.model&lt;/code>、&lt;code>gen_ai.usage.input_tokens&lt;/code> / &lt;code>output_tokens&lt;/code>、&lt;code>gen_ai.request.temperature&lt;/code> 等。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>讀 LLM observability docs / OTel spec 看到「span」「trace」「OTel GenAI semconv」就是這 framing。寫 code 場景的判讀：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>何時值得加 tracing&lt;/strong>：超過個人 demo、有實際使用者 / production 流量、開始遇到「為什麼 agent 跑這條路」debug 問題&lt;/li>
&lt;li>&lt;strong>不該自己寫 logging&lt;/strong>：用 OTel GenAI semconv 標準化、未來可換 backend（LangSmith → Phoenix → 自架）&lt;/li>
&lt;li>&lt;strong>Trace 不只 debug、也是 eval 來源&lt;/strong>：production trace 餵回 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/llm-as-judge/" data-link-title="LLM-as-Judge" data-link-desc="用 LLM 評估另一個 LLM 的輸出品質、production eval 的主流方法、500-5000× 成本降但有 bias 要處理">LLM-as-judge&lt;/a> 做品質評估&lt;/li>
&lt;li>&lt;strong>跟 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 LLM tracing 章節&lt;/a> 的關係&lt;/strong>：本卡是定義、章節是工程實務（attribute 設計、cost monitoring、failure debug 流程）&lt;/li>
&lt;/ol></description><content:encoded><![CDATA[<p>LLM tracing 的核心概念是「<strong>把 LLM 應用的每次 LLM call / tool call / memory op / handoff 編成結構化 span、串成 trace、可在 observability 平台查詢</strong>」。對應的標準是 OpenTelemetry GenAI semantic conventions（2025 stabilizing 中）。代表平台：LangSmith、Phoenix、Braintrust、Langfuse、Datadog APM、Logfire。是 production LLM 應用 debug / cost / latency 監控的事實標準、補 traditional logging 抓不到的「為什麼 agent 跑這條路」。</p>
<h2 id="概念位置">概念位置</h2>
<p>跟 traditional logging 的對比：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Traditional logging</th>
          <th>LLM tracing</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>結構</td>
          <td>字串 line、靠 grep</td>
          <td>結構化 span、parent-child 樹</td>
      </tr>
      <tr>
          <td>關聯性</td>
          <td>弱（要靠 request-id 串）</td>
          <td>強（trace-id + span 父子關係內建）</td>
      </tr>
      <tr>
          <td>屬性</td>
          <td>自由 key-value</td>
          <td>標準化（OTel GenAI semconv）：model / temperature / token usage / cost</td>
      </tr>
      <tr>
          <td>查詢</td>
          <td>grep / log aggregator</td>
          <td>Trace explorer + filter + 視覺化</td>
      </tr>
      <tr>
          <td>LLM 特有 attr</td>
          <td>沒有</td>
          <td>system prompt / tool calls / token / reasoning</td>
      </tr>
  </tbody>
</table>
<p>主流 OTel GenAI span 類型：</p>
<table>
  <thead>
      <tr>
          <th>Span 類型</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>gen_ai.client.operation</code></td>
          <td>一次完整 LLM API call</td>
      </tr>
      <tr>
          <td><code>gen_ai.tool.execution</code></td>
          <td>一次 tool 執行</td>
      </tr>
      <tr>
          <td><code>gen_ai.agent</code></td>
          <td>Agent loop 一個 iteration</td>
      </tr>
      <tr>
          <td><code>gen_ai.embeddings</code></td>
          <td>Embedding call</td>
      </tr>
      <tr>
          <td><code>gen_ai.memory.read/write</code></td>
          <td>Memory 操作</td>
      </tr>
  </tbody>
</table>
<p>每個 span 標準屬性：<code>gen_ai.system</code>（vendor）、<code>gen_ai.request.model</code>、<code>gen_ai.usage.input_tokens</code> / <code>output_tokens</code>、<code>gen_ai.request.temperature</code> 等。</p>
<h2 id="設計責任">設計責任</h2>
<p>讀 LLM observability docs / OTel spec 看到「span」「trace」「OTel GenAI semconv」就是這 framing。寫 code 場景的判讀：</p>
<ol>
<li><strong>何時值得加 tracing</strong>：超過個人 demo、有實際使用者 / production 流量、開始遇到「為什麼 agent 跑這條路」debug 問題</li>
<li><strong>不該自己寫 logging</strong>：用 OTel GenAI semconv 標準化、未來可換 backend（LangSmith → Phoenix → 自架）</li>
<li><strong>Trace 不只 debug、也是 eval 來源</strong>：production trace 餵回 <a href="/blog/llm/knowledge-cards/llm-as-judge/" data-link-title="LLM-as-Judge" data-link-desc="用 LLM 評估另一個 LLM 的輸出品質、production eval 的主流方法、500-5000× 成本降但有 bias 要處理">LLM-as-judge</a> 做品質評估</li>
<li><strong>跟 <a href="/blog/llm/04-applications/llm-tracing-and-observability/" data-link-title="4.20 LLM tracing 與 observability" data-link-desc="OpenTelemetry GenAI semantic conventions、結構化 span 設計、cost / latency 監控、failure debug 流程、跟 LLM-as-judge eval 的串接">4.20 LLM tracing 章節</a> 的關係</strong>：本卡是定義、章節是工程實務（attribute 設計、cost monitoring、failure debug 流程）</li>
</ol>
]]></content:encoded></item><item><title>FinTech：審計證據鏈的可觀測性設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/fintech-audit-evidence-observability/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/fintech-audit-evidence-observability/</guid><description>&lt;p>本案例的核心責任是讓審計證據與運維訊號共用同一套資料邊界。FinTech 場景下，觀測資料不只是除錯用途，也是合規證據基礎。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>一家處理線上支付的金融科技公司，每日交易量約 200 萬筆，涵蓋信用卡收單、轉帳與退款。每季有外部稽核查核交易處理的完整性與存取控制，事故發生時法務需要在 48 小時內提供特定交易的完整處理鏈證據。&lt;/p>
&lt;p>初期系統把所有 log 寫到同一個 log group — application debug、request trace、交易狀態變更與使用者存取紀錄全混在一起。稽核人員要從數 TB 的 log 中撈出特定交易的完整軌跡，每次查詢耗時數小時。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="operational-log-與-audit-log-混合">Operational log 與 audit log 混合&lt;/h3>
&lt;p>Application log 記錄 debug 資訊（SQL timing、cache hit/miss、retry），audit log 記錄業務事件（交易建立、狀態變更、存取紀錄）。兩者混在同一個 pipeline 時，retention 策略互相衝突 — debug log 留 14 天夠用，但 audit log 法規要求保留 5 年。統一設成 5 年讓儲存成本暴增，統一設成 14 天則遺失合規證據。&lt;/p>
&lt;h3 id="pii-暴露在-log-中">PII 暴露在 log 中&lt;/h3>
&lt;p>早期 log 直接印出 request body，信用卡號跟身分證字號散落在各種 log entry。稽核指出 PII 在 log 系統中的暴露面超過業務需要，但 log 已經寫入後無法回溯修改。&lt;/p>
&lt;h3 id="event-correlation-斷裂">Event correlation 斷裂&lt;/h3>
&lt;p>交易從建立到完成經過多個服務（checkout-api → payment-gateway → settlement → notification），但各服務的 log 使用不同的 correlation key。Checkout 用 &lt;code>order_id&lt;/code>，payment-gateway 用 &lt;code>payment_ref&lt;/code>，settlement 用自己的 &lt;code>batch_id&lt;/code>。稽核要求「給我交易 X 的完整處理鏈」時，工程師需要手動在三個系統各自查詢再人工拼接。&lt;/p>
&lt;h2 id="解法">解法&lt;/h2>
&lt;h3 id="audit-log-分離">Audit log 分離&lt;/h3>
&lt;p>把 audit event 獨立到專屬 pipeline：交易狀態變更、使用者存取、權限變動、退款操作各自產生結構化 audit event，寫入 immutable storage（append-only、禁止刪除與修改）。Operational log 維持 14 天 retention，audit log 走 5 年 retention + cold archive。&lt;/p>
&lt;p>分離的判準是「這筆紀錄是否可能被稽核或法務要求提供」。是 → audit pipeline；否 → operational pipeline。灰色地帶（例如認證失敗 log）歸入 audit pipeline — 寧可多留不可少留。&lt;/p>
&lt;h3 id="pii-redaction-pipeline">PII redaction pipeline&lt;/h3>
&lt;p>在 log ingestion 階段加入 redaction processor：信用卡號遮罩為末四碼、身分證字號完全移除、email 保留 domain 遮罩使用者名稱。Redaction 發生在寫入儲存之前，原始資料不落地。&lt;/p>
&lt;p>需要完整 PII 的場景（如詐欺調查）走另一條授權存取管道，跟觀測 pipeline 分離。&lt;/p>
&lt;h3 id="統一-correlation-key">統一 correlation key&lt;/h3>
&lt;p>所有服務在交易入口處產生 &lt;code>trace_id&lt;/code> 和 &lt;code>transaction_id&lt;/code>，兩個 key 同時寫入每一筆 audit event 和 operational log。稽核查詢用 &lt;code>transaction_id&lt;/code> 就能撈出跨服務的完整處理鏈，不需要手動拼接。&lt;/p>
&lt;h2 id="取捨">取捨&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>混合 pipeline&lt;/th>
 &lt;th>分離 pipeline&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>建置成本&lt;/td>
 &lt;td>低（一套 pipeline）&lt;/td>
 &lt;td>中（兩套 pipeline + routing 邏輯）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>儲存成本&lt;/td>
 &lt;td>高（全部用最長 retention）&lt;/td>
 &lt;td>可控（各自 retention）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢效率&lt;/td>
 &lt;td>低（audit event 淹沒在 debug log 中）&lt;/td>
 &lt;td>高（audit 獨立查詢）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>合規風險&lt;/td>
 &lt;td>高（PII 暴露面大、retention 可能不足）&lt;/td>
 &lt;td>低（PII redacted、retention 對齊法規）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維運複雜度&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>中（需維護 routing 規則與 redaction 規則）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>分離 pipeline 的最大成本在 routing 規則的維護 — 新服務上線時要確認 audit event 走對 pipeline。解法是在 SDK 層提供 &lt;code>emit_audit_event()&lt;/code> 函式，讓 routing 在 producer 端決定，不依賴下游 pipeline 的內容判斷。&lt;/p></description><content:encoded><![CDATA[<p>本案例的核心責任是讓審計證據與運維訊號共用同一套資料邊界。FinTech 場景下，觀測資料不只是除錯用途，也是合規證據基礎。</p>
<h2 id="業務背景">業務背景</h2>
<p>一家處理線上支付的金融科技公司，每日交易量約 200 萬筆，涵蓋信用卡收單、轉帳與退款。每季有外部稽核查核交易處理的完整性與存取控制，事故發生時法務需要在 48 小時內提供特定交易的完整處理鏈證據。</p>
<p>初期系統把所有 log 寫到同一個 log group — application debug、request trace、交易狀態變更與使用者存取紀錄全混在一起。稽核人員要從數 TB 的 log 中撈出特定交易的完整軌跡，每次查詢耗時數小時。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="operational-log-與-audit-log-混合">Operational log 與 audit log 混合</h3>
<p>Application log 記錄 debug 資訊（SQL timing、cache hit/miss、retry），audit log 記錄業務事件（交易建立、狀態變更、存取紀錄）。兩者混在同一個 pipeline 時，retention 策略互相衝突 — debug log 留 14 天夠用，但 audit log 法規要求保留 5 年。統一設成 5 年讓儲存成本暴增，統一設成 14 天則遺失合規證據。</p>
<h3 id="pii-暴露在-log-中">PII 暴露在 log 中</h3>
<p>早期 log 直接印出 request body，信用卡號跟身分證字號散落在各種 log entry。稽核指出 PII 在 log 系統中的暴露面超過業務需要，但 log 已經寫入後無法回溯修改。</p>
<h3 id="event-correlation-斷裂">Event correlation 斷裂</h3>
<p>交易從建立到完成經過多個服務（checkout-api → payment-gateway → settlement → notification），但各服務的 log 使用不同的 correlation key。Checkout 用 <code>order_id</code>，payment-gateway 用 <code>payment_ref</code>，settlement 用自己的 <code>batch_id</code>。稽核要求「給我交易 X 的完整處理鏈」時，工程師需要手動在三個系統各自查詢再人工拼接。</p>
<h2 id="解法">解法</h2>
<h3 id="audit-log-分離">Audit log 分離</h3>
<p>把 audit event 獨立到專屬 pipeline：交易狀態變更、使用者存取、權限變動、退款操作各自產生結構化 audit event，寫入 immutable storage（append-only、禁止刪除與修改）。Operational log 維持 14 天 retention，audit log 走 5 年 retention + cold archive。</p>
<p>分離的判準是「這筆紀錄是否可能被稽核或法務要求提供」。是 → audit pipeline；否 → operational pipeline。灰色地帶（例如認證失敗 log）歸入 audit pipeline — 寧可多留不可少留。</p>
<h3 id="pii-redaction-pipeline">PII redaction pipeline</h3>
<p>在 log ingestion 階段加入 redaction processor：信用卡號遮罩為末四碼、身分證字號完全移除、email 保留 domain 遮罩使用者名稱。Redaction 發生在寫入儲存之前，原始資料不落地。</p>
<p>需要完整 PII 的場景（如詐欺調查）走另一條授權存取管道，跟觀測 pipeline 分離。</p>
<h3 id="統一-correlation-key">統一 correlation key</h3>
<p>所有服務在交易入口處產生 <code>trace_id</code> 和 <code>transaction_id</code>，兩個 key 同時寫入每一筆 audit event 和 operational log。稽核查詢用 <code>transaction_id</code> 就能撈出跨服務的完整處理鏈，不需要手動拼接。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>混合 pipeline</th>
          <th>分離 pipeline</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>建置成本</td>
          <td>低（一套 pipeline）</td>
          <td>中（兩套 pipeline + routing 邏輯）</td>
      </tr>
      <tr>
          <td>儲存成本</td>
          <td>高（全部用最長 retention）</td>
          <td>可控（各自 retention）</td>
      </tr>
      <tr>
          <td>查詢效率</td>
          <td>低（audit event 淹沒在 debug log 中）</td>
          <td>高（audit 獨立查詢）</td>
      </tr>
      <tr>
          <td>合規風險</td>
          <td>高（PII 暴露面大、retention 可能不足）</td>
          <td>低（PII redacted、retention 對齊法規）</td>
      </tr>
      <tr>
          <td>維運複雜度</td>
          <td>低</td>
          <td>中（需維護 routing 規則與 redaction 規則）</td>
      </tr>
  </tbody>
</table>
<p>分離 pipeline 的最大成本在 routing 規則的維護 — 新服務上線時要確認 audit event 走對 pipeline。解法是在 SDK 層提供 <code>emit_audit_event()</code> 函式，讓 routing 在 producer 端決定，不依賴下游 pipeline 的內容判斷。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a>：audit log 分離的設計原則與 PII 治理。</li>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>：把 audit trail 包成可交接的 evidence package。</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a>：audit pipeline 的 ownership 歸 platform team 還是 compliance team。</li>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 Tracing Context</a>：跨服務 correlation key 的 propagation 設計。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>稽核或法務要求提供某筆交易的完整處理鏈，工程師需要超過 1 小時才能拼出來</li>
<li>Log retention 設定跟法規要求不一致，但沒人確切知道差多少</li>
<li>PII 出現在 log search 結果中，但沒有系統性的遮罩機制</li>
<li>Application log 跟 audit log 用同一套 retention policy，儲存成本持續上升但沒人敢縮短</li>
<li>事故後法務要證據，發現關鍵時段的 log 已經因為 retention 過期而被刪除</li>
</ul>
]]></content:encoded></item><item><title>OpenTelemetry</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/</guid><description>&lt;p>OpenTelemetry（OTel）是 CNCF 開放標準、承擔三個責任：定義 traces / metrics / logs 的資料模型（spec）、提供 vendor-neutral 的 SDK 跟 auto-instrumentation、以 OTel Collector 作為 instrumentation 跟 backend 之間的抽象層。設計取捨偏向「抽象優於 vendor-specific feature」、避免 vendor lock-in 是核心動機。多數現代 observability 平台（Datadog / Honeycomb / Grafana Cloud / Cloud Operations）都接受 OTLP。&lt;/p>
&lt;p>本頁先給最短路徑、再展開日常 instrumentation 跟 Collector 部署、最後進階治理（sampling / semantic conventions / logs 成熟度）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 OTel SDK 或 auto-instrumentation 對應用程式做 instrumentation&lt;/li>
&lt;li>配置 OTLP exporter 把 telemetry 送到任一 backend&lt;/li>
&lt;li>部署 OTel Collector（agent / gateway 模式）作為 backend 切換抽象層&lt;/li>
&lt;li>區分 head-based vs tail-based sampling、選擇對應策略&lt;/li>
&lt;li>評估從 vendor SDK 遷移到 OTel SDK 的相容性風險&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-otel-跑起來">最短路徑：5 分鐘把 OTel 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 應用程式加 auto-instrumentation（範例：Python）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: opentelemetry-bootstrap -a install&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: opentelemetry-instrument --traces_exporter otlp --metrics_exporter otlp python app.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 啟動 OTel Collector&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker run -p 4317:4317 -p 4318:4318 otel/opentelemetry-collector-contrib&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. Collector 配置範例&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: otel-collector-config.yaml with otlp receiver + exporter to backend&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證 telemetry 從 app → Collector → backend 串通。實際 production 要評估 sampling、retention、cardinality。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="instrumentation-模式">Instrumentation 模式&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Auto-instrumentation：Java / Python / Node / .NET / Ruby / Go 各語言成熟度不同&lt;/li>
&lt;li>Manual instrumentation：開發者寫 trace span / metric instrument&lt;/li>
&lt;li>Library instrumentation：opentelemetry-instrumentation-&lt;lib>（HTTP client / DB / framework）&lt;/li>
&lt;/ul>
&lt;h3 id="otlp-exporter-配置">OTLP exporter 配置&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>OTLP gRPC（4317）vs HTTP（4318）&lt;/li>
&lt;li>Endpoint / headers / authentication 配置&lt;/li>
&lt;li>對應指令範例：環境變數 &lt;code>OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code>、&lt;code>OTEL_EXPORTER_OTLP_HEADERS&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="collector-部署模式">Collector 部署模式&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Agent&lt;/strong>：跟應用程式同 host / pod、做 local buffer + enrichment&lt;/li>
&lt;li>&lt;strong>Gateway&lt;/strong>：集中部署、跨多 agent 接收、做 sampling / routing&lt;/li>
&lt;li>&lt;strong>Sidecar&lt;/strong>：K8s sidecar pattern、跟 pod 同生命週期&lt;/li>
&lt;li>對應配置：receivers / processors / exporters pipeline&lt;/li>
&lt;/ul>
&lt;p>深入：&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計&lt;/a>（三種位置責任分工、pipeline 設計、collector 失效 / 記憶體壓力 / backpressure 故障演練、容量成本邊界）。&lt;/p></description><content:encoded><![CDATA[<p>OpenTelemetry（OTel）是 CNCF 開放標準、承擔三個責任：定義 traces / metrics / logs 的資料模型（spec）、提供 vendor-neutral 的 SDK 跟 auto-instrumentation、以 OTel Collector 作為 instrumentation 跟 backend 之間的抽象層。設計取捨偏向「抽象優於 vendor-specific feature」、避免 vendor lock-in 是核心動機。多數現代 observability 平台（Datadog / Honeycomb / Grafana Cloud / Cloud Operations）都接受 OTLP。</p>
<p>本頁先給最短路徑、再展開日常 instrumentation 跟 Collector 部署、最後進階治理（sampling / semantic conventions / logs 成熟度）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 OTel SDK 或 auto-instrumentation 對應用程式做 instrumentation</li>
<li>配置 OTLP exporter 把 telemetry 送到任一 backend</li>
<li>部署 OTel Collector（agent / gateway 模式）作為 backend 切換抽象層</li>
<li>區分 head-based vs tail-based sampling、選擇對應策略</li>
<li>評估從 vendor SDK 遷移到 OTel SDK 的相容性風險</li>
</ol>
<h2 id="最短路徑5-分鐘把-otel-跑起來">最短路徑：5 分鐘把 OTel 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 應用程式加 auto-instrumentation（範例：Python）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: opentelemetry-bootstrap -a install</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># TODO: opentelemetry-instrument --traces_exporter otlp --metrics_exporter otlp python app.py</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"># 2. 啟動 OTel Collector</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># TODO: docker run -p 4317:4317 -p 4318:4318 otel/opentelemetry-collector-contrib</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"># 3. Collector 配置範例</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: otel-collector-config.yaml with otlp receiver + exporter to backend</span></span></span></code></pre></div><p>最短路徑驗證 telemetry 從 app → Collector → backend 串通。實際 production 要評估 sampling、retention、cardinality。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="instrumentation-模式">Instrumentation 模式</h3>
<p>子議題：</p>
<ul>
<li>Auto-instrumentation：Java / Python / Node / .NET / Ruby / Go 各語言成熟度不同</li>
<li>Manual instrumentation：開發者寫 trace span / metric instrument</li>
<li>Library instrumentation：opentelemetry-instrumentation-<lib>（HTTP client / DB / framework）</li>
</ul>
<h3 id="otlp-exporter-配置">OTLP exporter 配置</h3>
<p>子議題：</p>
<ul>
<li>OTLP gRPC（4317）vs HTTP（4318）</li>
<li>Endpoint / headers / authentication 配置</li>
<li>對應指令範例：環境變數 <code>OTEL_EXPORTER_OTLP_ENDPOINT</code>、<code>OTEL_EXPORTER_OTLP_HEADERS</code></li>
</ul>
<h3 id="collector-部署模式">Collector 部署模式</h3>
<p>子議題：</p>
<ul>
<li><strong>Agent</strong>：跟應用程式同 host / pod、做 local buffer + enrichment</li>
<li><strong>Gateway</strong>：集中部署、跨多 agent 接收、做 sampling / routing</li>
<li><strong>Sidecar</strong>：K8s sidecar pattern、跟 pod 同生命週期</li>
<li>對應配置：receivers / processors / exporters pipeline</li>
</ul>
<p>深入：<a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計</a>（三種位置責任分工、pipeline 設計、collector 失效 / 記憶體壓力 / backpressure 故障演練、容量成本邊界）。</p>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="auto-instrumentation-跨語言成熟度">Auto-instrumentation 跨語言成熟度</h3>
<p>子議題：</p>
<ul>
<li>Java：最成熟、auto-instrumentation 廣度最大</li>
<li>Python：成熟、覆蓋主流 framework</li>
<li>Node：成熟、async context propagation 較複雜</li>
<li>Go：較弱（runtime 不支援 monkey patching）、多用 manual</li>
<li>.NET：成熟、跟 Application Insights 對齊</li>
<li>Ruby / PHP：相對較弱、覆蓋主流 framework</li>
</ul>
<h3 id="sampling-策略">Sampling 策略</h3>
<p>對應案例 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a>。子議題：</p>
<ul>
<li><strong>Head-based sampling</strong>：trace 開始時決定保留與否、低成本但 lose context</li>
<li><strong>Tail-based sampling</strong>：trace 完成後決定（依錯誤 / 延遲）、Collector 要 buffer 整個 trace</li>
<li>Sampling rate 配置（global / per-service / probabilistic）</li>
<li>對應工具：OTel Collector 的 tail_sampling processor、Refinery（Honeycomb）</li>
</ul>
<h3 id="semantic-conventions">Semantic conventions</h3>
<p>子議題：</p>
<ul>
<li>HTTP / DB / messaging / RPC 等的 attribute 命名規範</li>
<li>Resource attributes（service.name / service.version / deployment.environment）</li>
<li>Span name / status code convention</li>
<li>Migration：應用層用 OTel semantic conventions、避免 vendor-specific naming</li>
</ul>
<h3 id="logs-in-otel">Logs in OTel</h3>
<p>子議題：</p>
<ul>
<li>Logs 比 metrics / traces 較晚進 OTel spec（v1.0 較新）</li>
<li>Log signal 設計：log record 跟 span 關聯（trace_id / span_id）</li>
<li>跟 Loki / Elastic / CloudWatch 的整合</li>
<li>從現有 logging library 移轉的路徑（log-forwarding vs SDK）</li>
</ul>
<h3 id="vendor-sdk-vs-otel-sdk-遷移">Vendor SDK vs OTel SDK 遷移</h3>
<p>對應案例 <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OpenTelemetry</a> 與 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel</a>。子議題：</p>
<ul>
<li>動機：避免 vendor lock-in、多 backend 並存、開源治理</li>
<li>風險：vendor-specific feature 損失（profiling / RUM 整合）</li>
<li>遷移路徑：dual ship → cutover → cleanup</li>
<li>對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 反例：OTel migration signal drift</a></li>
</ul>
<h3 id="resource-detection">Resource detection</h3>
<p>子議題：</p>
<ul>
<li>自動偵測 cloud provider（AWS / GCP / Azure）resource attributes</li>
<li>K8s resource detector（pod / namespace / cluster）</li>
<li>Container resource detector</li>
<li>對應配置：<code>OTEL_RESOURCE_ATTRIBUTES</code></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="telemetry-沒到-backend">Telemetry 沒到 backend</h3>
<p>操作原則：先確認 SDK 配置正確、再看 Collector 是否收到、最後看 exporter 是否成功。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: 設 OTEL_LOG_LEVEL=debug 看 SDK 內部 log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: 看 Collector internal metrics（zPages / Prometheus exporter）</span></span></span></code></pre></div><p>判讀路徑：SDK → Collector → backend、三段各自獨立、要逐層 isolate。</p>
<h3 id="cardinality-explosion">Cardinality explosion</h3>
<p>操作原則：metric attribute 含 high-cardinality 值（user_id / session_id）會爆 backend 成本。判讀：看 backend 的 series 數量、找 attribute 來源。</p>
<h3 id="trace-span-gap">Trace span gap</h3>
<p>操作原則：trace 不完整、看 context propagation 是否在跨 service / 跨 thread 邊界丟失。</p>
<h3 id="auto-instrumentation-不生效">Auto-instrumentation 不生效</h3>
<p>操作原則：確認 SDK 版本跟 library version 對應、agent 啟動方式正確。對應 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a> 的踩坑經驗。</p>
<h3 id="sampling-過頭--不足">Sampling 過頭 / 不足</h3>
<p>操作原則：sampling rate 跟 backend 預算 + debug 需求對齊。判讀：debug 時找不到 trace（sampling 過頭）vs backend 成本爆（sampling 不足）。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 metrics 後端</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> / Mimir</td>
      </tr>
      <tr>
          <td>需要 SaaS APM 整合</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> / New Relic</td>
      </tr>
      <tr>
          <td>需要 logs 後端</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a> / Loki</td>
      </tr>
      <tr>
          <td>需要 high-cardinality debug</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>AWS-native</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> + X-Ray</td>
      </tr>
      <tr>
          <td>GCP-native</td>
          <td><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Operations</a></td>
      </tr>
      <tr>
          <td>Error tracking</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 SDK 完整 API</li>
<li>OTLP protocol binary format</li>
<li>各 backend 的 OTel 整合細節（見各 backend vendor 頁）</li>
<li>OTel project governance / sig 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OTel</a></td>
          <td>從 vendor SDK 遷出 OTel</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP</a></td>
          <td>GCP Cloud Trace 接受 OTLP</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS pipeline</a></td>
          <td>AWS Distro for OTel + EKS</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a></td>
          <td>OTLP ingestion / vendor SDK 移轉</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>（反例）雙軌遷移期的 signal 漂移</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 OTel 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale signals</a></td>
          <td>K8s 規模化下 OTel Collector 拓撲 / 資源訊號分層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>小型直接 SDK / 中型加 Collector / 大型 multi-backend</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：所有 04 vendor 都可作 OTel backend</li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>4.1 log schema 與搜尋規劃</title><link>https://tarrragon.github.io/blog/backend/04-observability/log-schema/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/log-schema/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>structured &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> fields&lt;/li>
&lt;li>index 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a>&lt;/li>
&lt;li>query pattern&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a> 是把事件紀錄從文字輸出變成可查詢資料的契約，責任是讓不同服務在事故時能用同一組欄位還原脈絡。&lt;/p>
&lt;p>這一頁處理的是欄位與搜尋路徑。log 的價值在於事故時能用穩定欄位找到同一個 request、同一個 tenant、同一個 dependency call 與同一段錯誤鏈，寫得多本身沒有幫助。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 log schema 時，先看 correlation fields 是否穩定，再看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 是否對齊查詢需求。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary&lt;/a> 與 service name 是否跨服務一致&lt;/li>
&lt;li>high-cardinality 欄位是否被放進可控索引，並受查詢價值與成本預算約束&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 是否依 operational debug、&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">audit&lt;/a>、compliance 分層&lt;/li>
&lt;li>query pattern 是否能支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 還原&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>log 欄位 schema 漂移、跨服務 correlation id 對不上&lt;/li>
&lt;li>事故時靠 grep 拼湊事件、無結構化查詢入口&lt;/li>
&lt;li>log 索引爆量、查詢退化但無清理流程&lt;/li>
&lt;li>log 含大量 free-form text、無一致關鍵欄位&lt;/li>
&lt;li>retention 策略全平、舊事件查不到 / 不該留的還在留&lt;/li>
&lt;/ul>
&lt;h2 id="查詢模式設計">查詢模式設計&lt;/h2>
&lt;p>Log 的寫入格式跟讀取需求是兩個不同的設計問題。寫入追求 schema 穩定與吞吐效率；讀取要在不同時間壓力下，用不同的查詢形狀取回不同精度的資料。同一份 structured log 至少被三種查詢模式讀取，每種模式對索引、延遲與結果形狀的要求不同。&lt;/p>
&lt;h3 id="即席診斷查詢">即席診斷查詢&lt;/h3>
&lt;p>事故中的查詢要在秒級內定位問題。典型操作是拿到一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 或 error code，加上 time window，撈出相關事件鏈。&lt;/p>
&lt;p>即席查詢的索引策略是把高頻過濾欄位放進結構化索引：service name、log level、error code、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary&lt;/a>。這些欄位的共同特徵是有界或半有界（error code 有限、request id 雖然無界但查詢時一定帶精確值），查詢時用等值匹配或短範圍掃描。&lt;/p>
&lt;p>即席查詢的反模式是對 free-text 欄位做全文搜尋當作主要診斷入口。全文搜尋適合探索性調查（「最近有沒有出現某個未預期的 exception message」），但事故中的時間壓力下，結構化欄位的精確查詢比全文搜尋快一到兩個數量級。&lt;/p>
&lt;h3 id="聚合趨勢查詢">聚合趨勢查詢&lt;/h3>
&lt;p>Dashboard 跟告警的查詢是定期的聚合計算：過去 5 分鐘的 error count by service、過去 1 小時的 log volume by level、某個 tenant 的 warning 趨勢。這類查詢不需要看單筆 log 的內容，而是需要 count / rate / group by 的聚合結果。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>structured <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a></li>
<li><a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a> / <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> fields</li>
<li>index 與 <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a></li>
<li>query pattern</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a> 是把事件紀錄從文字輸出變成可查詢資料的契約，責任是讓不同服務在事故時能用同一組欄位還原脈絡。</p>
<p>這一頁處理的是欄位與搜尋路徑。log 的價值在於事故時能用穩定欄位找到同一個 request、同一個 tenant、同一個 dependency call 與同一段錯誤鏈，寫得多本身沒有幫助。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 log schema 時，先看 correlation fields 是否穩定，再看 <a href="/blog/backend/knowledge-cards/search-index/" data-link-title="Search Index" data-link-desc="說明搜尋索引如何承擔全文檢索、排序與查詢體驗">search index</a> 與 <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 是否對齊查詢需求。</p>
<p>重點訊號包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary</a> 與 service name 是否跨服務一致</li>
<li>high-cardinality 欄位是否被放進可控索引，並受查詢價值與成本預算約束</li>
<li><a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 是否依 operational debug、<a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">audit</a>、compliance 分層</li>
<li>query pattern 是否能支援 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 還原</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>log 欄位 schema 漂移、跨服務 correlation id 對不上</li>
<li>事故時靠 grep 拼湊事件、無結構化查詢入口</li>
<li>log 索引爆量、查詢退化但無清理流程</li>
<li>log 含大量 free-form text、無一致關鍵欄位</li>
<li>retention 策略全平、舊事件查不到 / 不該留的還在留</li>
</ul>
<h2 id="查詢模式設計">查詢模式設計</h2>
<p>Log 的寫入格式跟讀取需求是兩個不同的設計問題。寫入追求 schema 穩定與吞吐效率；讀取要在不同時間壓力下，用不同的查詢形狀取回不同精度的資料。同一份 structured log 至少被三種查詢模式讀取，每種模式對索引、延遲與結果形狀的要求不同。</p>
<h3 id="即席診斷查詢">即席診斷查詢</h3>
<p>事故中的查詢要在秒級內定位問題。典型操作是拿到一個 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 或 error code，加上 time window，撈出相關事件鏈。</p>
<p>即席查詢的索引策略是把高頻過濾欄位放進結構化索引：service name、log level、error code、<a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant boundary</a>。這些欄位的共同特徵是有界或半有界（error code 有限、request id 雖然無界但查詢時一定帶精確值），查詢時用等值匹配或短範圍掃描。</p>
<p>即席查詢的反模式是對 free-text 欄位做全文搜尋當作主要診斷入口。全文搜尋適合探索性調查（「最近有沒有出現某個未預期的 exception message」），但事故中的時間壓力下，結構化欄位的精確查詢比全文搜尋快一到兩個數量級。</p>
<h3 id="聚合趨勢查詢">聚合趨勢查詢</h3>
<p>Dashboard 跟告警的查詢是定期的聚合計算：過去 5 分鐘的 error count by service、過去 1 小時的 log volume by level、某個 tenant 的 warning 趨勢。這類查詢不需要看單筆 log 的內容，而是需要 count / rate / group by 的聚合結果。</p>
<p>聚合查詢的負載特性跟即席查詢不同。即席查詢讀少量資料、要求低延遲；聚合查詢掃大量資料、容忍較高延遲但執行頻率高（dashboard 每 30 秒刷新一次 = 每分鐘 2 次相同的重聚合）。當 log volume 成長，重複計算聚合的成本會推高 query engine 負擔。</p>
<p>應對策略有兩種。一是在 log pipeline 把常用聚合轉成 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> — collector 端做 log-to-metric 轉換（例：把 <code>level=error</code> 的 log 計數轉成 error_log_total counter），dashboard 讀 metric 而非重掃 log。二是在查詢層設定 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a> 或快取，讓重複查詢直接取用預計算結果。</p>
<h3 id="鑑識回溯查詢">鑑識回溯查詢</h3>
<p>事後分析與合規稽核的查詢範圍大（跨天、跨週甚至跨月）、對完整性要求高、但延遲容忍也高（分鐘級回應可接受）。鑑識查詢常見的形狀是「某個 tenant 在過去 30 天內所有 authentication failure」或「某個 API 的 error 分布演變」。</p>
<p>鑑識查詢的儲存設計跟 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 直接相關。Hot tier 保留最近數天的 full-index log，warm tier 保留數週的部分索引或壓縮 log，cold tier 保留數月到數年的歸檔 log。鑑識查詢命中 cold tier 時，系統可能需要 rehydrate（把歸檔資料暫時載回可查詢狀態），這個操作本身需要時間和臨時儲存空間。</p>
<p>鑑識場景的關鍵設計決策是「哪些欄位在 cold tier 仍可查詢」。全部欄位都保留索引成本太高；只保留 timestamp + service name + tenant 的最小索引，能支援基本的範圍掃描，細節再用 rehydrate 後的全文搜尋補。</p>
<h3 id="三種模式的資源隔離">三種模式的資源隔離</h3>
<p>三種查詢模式搶同一個 query engine 時，聚合查詢的持續負載會擠壓即席查詢的回應速度。事故中團隊最需要即席查詢的低延遲，但此時 dashboard 也在高頻刷新聚合查詢，兩者競爭 query 資源。</p>
<p>可操作的隔離方式是讓即席查詢跟聚合查詢走不同的 query priority 或 query queue。Elasticsearch 的 search thread pool、Loki 的 query-frontend queue、Datadog 的 query quota 都提供某種程度的查詢隔離。設計時要把即席查詢的延遲 SLA 當作硬性約束，聚合查詢的延遲可以被彈性排程。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.7 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a> / cost：label 預算與保留階梯</li>
<li>04.8 訊號治理閉環：log-based alert 的生命週期</li>
<li>04.12 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a>：稽核訊號跟 operational log 的邊界</li>
<li>04.23 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">觀測查詢設計</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>模組二：客戶端可觀測性</title><link>https://tarrragon.github.io/blog/testing/02-client-observability/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/02-client-observability/</guid><description>&lt;p>回答「使用者的裝置上發生了什麼事」。log 設計應在功能規格階段完成，跟 API schema 同級。&lt;/p>
&lt;h2 id="對應-findings">對應 findings&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Finding&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>TF-6&lt;/td>
 &lt;td>&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>&lt;/td>
 &lt;td>6 元件中 4 個零 log，2 個全是 W2 hotfix&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TF-7&lt;/td>
 &lt;td>&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>&lt;/td>
 &lt;td>事後補的 developer.log 格式不統一&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TF-9&lt;/td>
 &lt;td>&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>&lt;/td>
 &lt;td>log 設計應在功能規格階段完成 — &lt;strong>本模組主寫&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 三層 log 設計（連線生命週期 / protocol 訊息 / 使用者行為）&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 功能規格中的 log 點定義方法&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 自架 log endpoint vs 商業方案的取捨判斷&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> 「事後補 log」vs「設計產物 log」的品質差異&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &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>：本模組教「設計 log 點」，monitoring 教「log 收集到之後怎麼處理」&lt;/li>
&lt;li>→ &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>：log 內容可能含 secret，SDK redaction 在這裡介入&lt;/li>
&lt;li>← &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一&lt;/a>：狀態矩陣可加「可觀測性」欄位&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「使用者的裝置上發生了什麼事」。log 設計應在功能規格階段完成，跟 API schema 同級。</p>
<h2 id="對應-findings">對應 findings</h2>
<table>
  <thead>
      <tr>
          <th>Finding</th>
          <th>來源</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>TF-6</td>
          <td><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></td>
          <td>6 元件中 4 個零 log，2 個全是 W2 hotfix</td>
      </tr>
      <tr>
          <td>TF-7</td>
          <td><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></td>
          <td>事後補的 developer.log 格式不統一</td>
      </tr>
      <tr>
          <td>TF-9</td>
          <td><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></td>
          <td>log 設計應在功能規格階段完成 — <strong>本模組主寫</strong></td>
      </tr>
  </tbody>
</table>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input checked="" disabled="" type="checkbox"> 三層 log 設計（連線生命週期 / protocol 訊息 / 使用者行為）</li>
<li><input checked="" disabled="" type="checkbox"> 功能規格中的 log 點定義方法</li>
<li><input checked="" disabled="" type="checkbox"> 自架 log endpoint vs 商業方案的取捨判斷</li>
<li><input checked="" disabled="" type="checkbox"> 「事後補 log」vs「設計產物 log」的品質差異</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/monitoring/02-log-schema/" data-link-title="模組二：Log Schema 設計" data-link-desc="跨平台統一事件格式、欄位設計、版本演進策略">monitoring 模組二 Log Schema</a>：本模組教「設計 log 點」，monitoring 教「log 收集到之後怎麼處理」</li>
<li>→ <a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">monitoring 模組七 資安</a>：log 內容可能含 secret，SDK redaction 在這裡介入</li>
<li>← <a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">ux-design 模組一</a>：狀態矩陣可加「可觀測性」欄位</li>
</ul>
]]></content:encoded></item><item><title>Gaming：高峰流量下的訊號新鮮度與 Cardinality</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/</guid><description>&lt;p>本案例的核心責任是避免高峰流量讓觀測系統本身失真。若訊號延遲與 cardinality 膨脹失控，值班決策會落在過期資料上。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>一個線上多人遊戲平台，日活躍使用者約 50 萬人。每逢賽季開跑或限時活動，同時在線人數在 30 分鐘內從平日基線暴增 8-10 倍，matchmaking 服務的 request rate 從 5k/s 衝到 50k/s，遊戲伺服器同時運行的 match instance 從數千增到數萬。&lt;/p>
&lt;p>觀測系統在平日運作良好 — Prometheus 單機 scrape 500 萬 active series、Grafana dashboard 查詢秒級回應、告警在 1 分鐘內觸發。但每次活動開跑時，觀測系統本身開始劣化：dashboard 查詢從秒級變成分鐘級、告警延遲 5 分鐘以上才送到、部分 metric 直接消失。值班工程師在最需要觀測的時刻失去了可信訊號。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="cardinality-爆炸">Cardinality 爆炸&lt;/h3>
&lt;p>平日的 metric label 設計包含 &lt;code>match_id&lt;/code>、&lt;code>player_id&lt;/code> 跟 &lt;code>server_instance&lt;/code>。平日 active series 約 500 萬，活動開跑後 match 跟 player 數量暴增，active series 在 30 分鐘內衝到 2000 萬。Prometheus 的 head block 記憶體從 20 GB 暴增到 80 GB，超過機器 64 GB 上限，觸發 OOM kill。&lt;/p>
&lt;p>OOM 後 Prometheus 重啟需要 replay WAL，這段時間（5-15 分鐘）完全沒有 metric。活動最需要觀測的前 30 分鐘，觀測系統反而停擺。&lt;/p>
&lt;h3 id="scrape-freshness-延遲">Scrape freshness 延遲&lt;/h3>
&lt;p>即使 Prometheus 沒 OOM，大量 target 的 scrape 時間也會拉長。平日每輪 scrape 15 秒完成，活動期間拉長到 60-90 秒。Scrape interval 設定 30 秒時，下一輪 scrape 在上一輪還沒結束時就啟動，造成 sample 丟失跟時間錯位。Dashboard 上看到的數字可能延遲 2-3 分鐘，值班人員基於過期數據做判斷。&lt;/p>
&lt;h3 id="alert-閾值失真">Alert 閾值失真&lt;/h3>
&lt;p>告警規則基於平日 baseline 設定 — 例如 &lt;code>error_rate &amp;gt; 1%&lt;/code> 觸發。活動期間的 error rate 波動更大（matchmaking 短暫排隊造成的 timeout 增加是預期行為），平日閾值在活動期間持續觸發 false positive。值班人員開始 ignore alert，真正的問題（伺服器記憶體洩漏）被淹沒在噪音中。&lt;/p>
&lt;h2 id="解法">解法&lt;/h2>
&lt;h3 id="cardinality-guardrail">Cardinality guardrail&lt;/h3>
&lt;p>把高 cardinality label 從 real-time metric 移除。&lt;code>match_id&lt;/code> 和 &lt;code>player_id&lt;/code> 不再作為 Prometheus label，改為 log 和 trace 的欄位。Real-time metric 只保留 &lt;code>region&lt;/code>、&lt;code>server_pool&lt;/code>、&lt;code>game_mode&lt;/code> 等低 cardinality 維度。&lt;/p>
&lt;p>需要 per-match 或 per-player 分析時，走 log analytics pipeline（非 real-time，延遲 5-10 分鐘可接受）。這讓 Prometheus 的 active series 在活動期間從 2000 萬降到 800 萬，留在單機可承受範圍。&lt;/p>
&lt;h3 id="pre-aggregation-recording-rules">Pre-aggregation recording rules&lt;/h3>
&lt;p>為活動期間最常查的 pattern（per-region error rate、matchmaking queue depth、server utilization）建立 recording rules。Recording rules 在 Prometheus server 端預先計算，dashboard 查詢直接讀預計算結果，避免 heavy aggregation query 在活動期間拖慢 Prometheus。&lt;/p></description><content:encoded><![CDATA[<p>本案例的核心責任是避免高峰流量讓觀測系統本身失真。若訊號延遲與 cardinality 膨脹失控，值班決策會落在過期資料上。</p>
<h2 id="業務背景">業務背景</h2>
<p>一個線上多人遊戲平台，日活躍使用者約 50 萬人。每逢賽季開跑或限時活動，同時在線人數在 30 分鐘內從平日基線暴增 8-10 倍，matchmaking 服務的 request rate 從 5k/s 衝到 50k/s，遊戲伺服器同時運行的 match instance 從數千增到數萬。</p>
<p>觀測系統在平日運作良好 — Prometheus 單機 scrape 500 萬 active series、Grafana dashboard 查詢秒級回應、告警在 1 分鐘內觸發。但每次活動開跑時，觀測系統本身開始劣化：dashboard 查詢從秒級變成分鐘級、告警延遲 5 分鐘以上才送到、部分 metric 直接消失。值班工程師在最需要觀測的時刻失去了可信訊號。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="cardinality-爆炸">Cardinality 爆炸</h3>
<p>平日的 metric label 設計包含 <code>match_id</code>、<code>player_id</code> 跟 <code>server_instance</code>。平日 active series 約 500 萬，活動開跑後 match 跟 player 數量暴增，active series 在 30 分鐘內衝到 2000 萬。Prometheus 的 head block 記憶體從 20 GB 暴增到 80 GB，超過機器 64 GB 上限，觸發 OOM kill。</p>
<p>OOM 後 Prometheus 重啟需要 replay WAL，這段時間（5-15 分鐘）完全沒有 metric。活動最需要觀測的前 30 分鐘，觀測系統反而停擺。</p>
<h3 id="scrape-freshness-延遲">Scrape freshness 延遲</h3>
<p>即使 Prometheus 沒 OOM，大量 target 的 scrape 時間也會拉長。平日每輪 scrape 15 秒完成，活動期間拉長到 60-90 秒。Scrape interval 設定 30 秒時，下一輪 scrape 在上一輪還沒結束時就啟動，造成 sample 丟失跟時間錯位。Dashboard 上看到的數字可能延遲 2-3 分鐘，值班人員基於過期數據做判斷。</p>
<h3 id="alert-閾值失真">Alert 閾值失真</h3>
<p>告警規則基於平日 baseline 設定 — 例如 <code>error_rate &gt; 1%</code> 觸發。活動期間的 error rate 波動更大（matchmaking 短暫排隊造成的 timeout 增加是預期行為），平日閾值在活動期間持續觸發 false positive。值班人員開始 ignore alert，真正的問題（伺服器記憶體洩漏）被淹沒在噪音中。</p>
<h2 id="解法">解法</h2>
<h3 id="cardinality-guardrail">Cardinality guardrail</h3>
<p>把高 cardinality label 從 real-time metric 移除。<code>match_id</code> 和 <code>player_id</code> 不再作為 Prometheus label，改為 log 和 trace 的欄位。Real-time metric 只保留 <code>region</code>、<code>server_pool</code>、<code>game_mode</code> 等低 cardinality 維度。</p>
<p>需要 per-match 或 per-player 分析時，走 log analytics pipeline（非 real-time，延遲 5-10 分鐘可接受）。這讓 Prometheus 的 active series 在活動期間從 2000 萬降到 800 萬，留在單機可承受範圍。</p>
<h3 id="pre-aggregation-recording-rules">Pre-aggregation recording rules</h3>
<p>為活動期間最常查的 pattern（per-region error rate、matchmaking queue depth、server utilization）建立 recording rules。Recording rules 在 Prometheus server 端預先計算，dashboard 查詢直接讀預計算結果，避免 heavy aggregation query 在活動期間拖慢 Prometheus。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># recording rule 示例</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">groups</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">peak_precompute</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">region:matchmaking_errors:rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">sum(rate(matchmaking_errors_total[5m])) by (region)</span></span></span></code></pre></div><h3 id="signal-tiering">Signal tiering</h3>
<p>把觀測訊號分成兩層：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>訊號類型</th>
          <th>Pipeline</th>
          <th>Freshness</th>
          <th>Cardinality 限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tier 1</td>
          <td>Golden signals（latency、error rate、throughput、saturation）</td>
          <td>Prometheus real-time</td>
          <td>&lt; 30s</td>
          <td>嚴格（低 cardinality label only）</td>
      </tr>
      <tr>
          <td>Tier 2</td>
          <td>Debug signals（per-match、per-player、per-request）</td>
          <td>Log + trace analytics</td>
          <td>5-10 min</td>
          <td>無限制</td>
      </tr>
  </tbody>
</table>
<p>Tier 1 支撐告警跟即時 dashboard，保證活動期間不劣化。Tier 2 支撐事後分析跟 root cause investigation，接受延遲。</p>
<h3 id="dynamic-alert-threshold">Dynamic alert threshold</h3>
<p>活動期間啟用「高峰模式」alert profile — 調高 error rate 閾值（1% → 5%）、加長 <code>for:</code> duration（1m → 5m）、停用已知在活動期間會 false positive 的告警。高峰模式由活動排程系統自動觸發，活動結束後自動切回平日 profile。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>高 cardinality real-time</th>
          <th>分層治理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Debug 即時性</td>
          <td>高（per-match real-time）</td>
          <td>低到中（per-match 延遲 5-10 min）</td>
      </tr>
      <tr>
          <td>Prometheus 穩定性</td>
          <td>低（活動期間 OOM 風險）</td>
          <td>高（active series 可控）</td>
      </tr>
      <tr>
          <td>Dashboard 回應速度</td>
          <td>活動期間劣化</td>
          <td>穩定（recording rules 預計算）</td>
      </tr>
      <tr>
          <td>告警可信度</td>
          <td>低（false positive 淹沒真問題）</td>
          <td>中到高（dynamic threshold 降噪）</td>
      </tr>
      <tr>
          <td>維護複雜度</td>
          <td>低（一套 pipeline）</td>
          <td>中（兩套 pipeline + 高峰模式切換）</td>
      </tr>
  </tbody>
</table>
<p>分層治理的核心取捨是犧牲 per-match real-time debug 能力，換取觀測系統在高峰期間的穩定。這個取捨在活動場景成立 — 活動期間最需要的是「整體是否健康」的判斷，per-match debug 在事後分析夠用。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality Cost Governance</a>：cardinality guardrail 的設計原則與偵測機制。</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>：scrape freshness、sampling bias 與 signal tiering。</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a>：real-time vs batch analytics pipeline 的分層設計。</li>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 Dashboard Alert</a>：dynamic alert threshold 與高峰模式切換。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>流量高峰期間 Prometheus 記憶體使用異常增長或觸發 OOM</li>
<li>Dashboard 在尖峰時段查詢變慢或 timeout，正好是最需要看的時候</li>
<li>Alert 在活動期間大量觸發但多數是 false positive，值班人員開始 ignore</li>
<li><code>prometheus_tsdb_head_series</code> 在特定時段突然暴增，結束後回落</li>
<li>Metric label 中包含高 cardinality identifier（user_id、session_id、request_id）</li>
</ul>
]]></content:encoded></item><item><title>Prometheus</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/</guid><description>&lt;p>Prometheus 是 CNCF graduated 的 metrics 系統、承擔三個責任：pull-based metrics scraping（service discovery + scrape）、PromQL 查詢與 recording rules、Alertmanager 告警與路由。設計取捨偏向「短中期 metrics + 簡單部署 + cloud-native 整合」、長期儲存交給 Mimir / Thanos / Cortex。是 Kubernetes 生態 metrics 的事實標準。&lt;/p>
&lt;p>對「K8s metrics、service metrics、需要 PromQL 表達能力、自管 metrics 棧」這條路徑、Prometheus 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 docker 跑起 Prometheus、配置 scrape target&lt;/li>
&lt;li>用 PromQL 查詢 metrics、寫 recording rules / alerting rules&lt;/li>
&lt;li>設計 service discovery（K8s / Consul / file_sd）&lt;/li>
&lt;li>看懂 cardinality 訊號、避免 label explosion&lt;/li>
&lt;li>評估長期儲存（Thanos / Mimir / Cortex）跟 remote write 的選擇&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-prometheus-跑起來">最短路徑：5 分鐘把 Prometheus 跑起來&lt;/h2>
&lt;p>先建最小 config 檔（Prometheus scrape 自己）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c"># prometheus.yml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">global&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">scrape_interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">15s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">scrape_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">job_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;prometheus&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">static_configs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">targets&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;localhost:9090&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>啟動並驗證：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 啟動 Prometheus&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name prom -p 9090:9090 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>&lt;span class="nb">pwd&lt;/span>&lt;span class="k">)&lt;/span>&lt;span class="s2">/prometheus.yml:/etc/prometheus/prometheus.yml&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> prom/prometheus
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 確認 target 正常（等 15 秒讓第一次 scrape 完成）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">curl -s http://localhost:9090/api/v1/targets &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data.activeTargets[].health&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 查詢驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">curl -s &lt;span class="s1">&amp;#39;http://localhost:9090/api/v1/query?query=up&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data.result[].value[1]&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>up&lt;/code> 回傳 &lt;code>&amp;quot;1&amp;quot;&lt;/code> 代表 Prometheus 能 scrape 自己。瀏覽器訪 &lt;code>http://localhost:9090&lt;/code> 可用 PromQL UI 互動查詢。實際 production 要配 retention、alerting rules 與 HA。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="scrape-配置與-service-discovery">Scrape 配置與 service discovery&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Static config：手動列 target、適合小規模&lt;/li>
&lt;li>File SD：動態檔案、適合外部系統推送&lt;/li>
&lt;li>Kubernetes SD：K8s API server 動態發現&lt;/li>
&lt;li>Consul SD：跟 Consul service registry 整合&lt;/li>
&lt;li>對應配置：&lt;code>scrape_configs&lt;/code> 區段&lt;/li>
&lt;/ul>
&lt;h3 id="promql-查詢">PromQL 查詢&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Instant query vs range query&lt;/li>
&lt;li>Aggregation：sum / avg / max / min / count + by / without&lt;/li>
&lt;li>Rate / increase（counter 處理）&lt;/li>
&lt;li>Histogram quantile（histogram_quantile + bucket）&lt;/li>
&lt;li>對應指令：HTTP API &lt;code>/api/v1/query&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="recording-rules--alerting-rules">Recording rules / Alerting rules&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Prometheus 是 CNCF graduated 的 metrics 系統、承擔三個責任：pull-based metrics scraping（service discovery + scrape）、PromQL 查詢與 recording rules、Alertmanager 告警與路由。設計取捨偏向「短中期 metrics + 簡單部署 + cloud-native 整合」、長期儲存交給 Mimir / Thanos / Cortex。是 Kubernetes 生態 metrics 的事實標準。</p>
<p>對「K8s metrics、service metrics、需要 PromQL 表達能力、自管 metrics 棧」這條路徑、Prometheus 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 docker 跑起 Prometheus、配置 scrape target</li>
<li>用 PromQL 查詢 metrics、寫 recording rules / alerting rules</li>
<li>設計 service discovery（K8s / Consul / file_sd）</li>
<li>看懂 cardinality 訊號、避免 label explosion</li>
<li>評估長期儲存（Thanos / Mimir / Cortex）跟 remote write 的選擇</li>
</ol>
<h2 id="最短路徑5-分鐘把-prometheus-跑起來">最短路徑：5 分鐘把 Prometheus 跑起來</h2>
<p>先建最小 config 檔（Prometheus scrape 自己）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">scrape_interval</span><span class="p">:</span><span class="w"> </span><span class="l">15s</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="nt">scrape_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span>- <span class="nt">job_name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;prometheus&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">static_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">      </span>- <span class="nt">targets</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;localhost:9090&#34;</span><span class="p">]</span></span></span></code></pre></div><p>啟動並驗證：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 啟動 Prometheus</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name prom -p 9090:9090 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v <span class="s2">&#34;</span><span class="k">$(</span><span class="nb">pwd</span><span class="k">)</span><span class="s2">/prometheus.yml:/etc/prometheus/prometheus.yml&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  prom/prometheus
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 2. 確認 target 正常（等 15 秒讓第一次 scrape 完成）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">curl -s http://localhost:9090/api/v1/targets <span class="p">|</span> jq <span class="s1">&#39;.data.activeTargets[].health&#39;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 3. 查詢驗證</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">curl -s <span class="s1">&#39;http://localhost:9090/api/v1/query?query=up&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;.data.result[].value[1]&#39;</span></span></span></code></pre></div><p><code>up</code> 回傳 <code>&quot;1&quot;</code> 代表 Prometheus 能 scrape 自己。瀏覽器訪 <code>http://localhost:9090</code> 可用 PromQL UI 互動查詢。實際 production 要配 retention、alerting rules 與 HA。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="scrape-配置與-service-discovery">Scrape 配置與 service discovery</h3>
<p>子議題：</p>
<ul>
<li>Static config：手動列 target、適合小規模</li>
<li>File SD：動態檔案、適合外部系統推送</li>
<li>Kubernetes SD：K8s API server 動態發現</li>
<li>Consul SD：跟 Consul service registry 整合</li>
<li>對應配置：<code>scrape_configs</code> 區段</li>
</ul>
<h3 id="promql-查詢">PromQL 查詢</h3>
<p>子議題：</p>
<ul>
<li>Instant query vs range query</li>
<li>Aggregation：sum / avg / max / min / count + by / without</li>
<li>Rate / increase（counter 處理）</li>
<li>Histogram quantile（histogram_quantile + bucket）</li>
<li>對應指令：HTTP API <code>/api/v1/query</code></li>
</ul>
<h3 id="recording-rules--alerting-rules">Recording rules / Alerting rules</h3>
<p>子議題：</p>
<ul>
<li>Recording rules：預先計算昂貴 query、降低 dashboard 查詢成本</li>
<li>Alerting rules：定義 alert condition + for duration + labels / annotations</li>
<li>Alertmanager：去重 / 抑制 / 分組 / routing</li>
<li>對應配置：<code>rule_files</code></li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="capacity-failure-modes/">Prometheus 容量規劃與故障模式</a>：單機容量邊界、cardinality 與 retention 的資源模型、常見故障模式與判讀</li>
<li><a href="promql-recording-rules/">PromQL 與 Recording Rules 實務</a>：常見 SLI 查詢模式、recording rules 設計慣例、效能陷阱與故障判讀</li>
<li><a href="remote-write-long-term-storage/">Remote Write 與長期儲存整合</a>：remote write 配置、Mimir / Thanos / Cortex 三家比較、故障模式與容量規劃</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="high-availability">High availability</h3>
<p>子議題：</p>
<ul>
<li>Prometheus 沒原生 HA — 跑兩個 instance scrape 同 target、靠下游去重</li>
<li>Thanos：sidecar 模式、跨 Prometheus instance 查詢統一</li>
<li>Mimir：fully replicated metric storage（多 Prometheus → Mimir）</li>
<li>對應案例 <a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale signals</a></li>
</ul>
<h3 id="cardinality-管理">Cardinality 管理</h3>
<p>對應案例 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a>。子議題：</p>
<ul>
<li>Cardinality = unique label combinations 數量</li>
<li>High-cardinality label（user_id / request_id / trace_id）會炸 Prometheus</li>
<li>偵測：<code>prometheus_tsdb_head_series</code> metric</li>
<li>修法：drop label / aggregation / 改用 traces backend（Honeycomb）</li>
</ul>
<h3 id="remote-write--read">Remote write / read</h3>
<p>子議題：</p>
<ul>
<li>Remote write：Prometheus → 長期儲存（Mimir / Cortex / Thanos / Datadog / Grafana Cloud）</li>
<li>Remote read：查詢時拉長期儲存資料</li>
<li>用 receiver / agent 模式（無 local TSDB）</li>
<li>對應配置：<code>remote_write</code> / <code>remote_read</code></li>
</ul>
<h3 id="exporters-生態">Exporters 生態</h3>
<p>子議題：</p>
<ul>
<li>Node exporter（host metrics）</li>
<li>Blackbox exporter（HTTP / TCP / ICMP probing）</li>
<li>Database exporters（postgres / mysql / redis）</li>
<li>應用層 metrics：用 client library（prometheus_client）原生暴露</li>
<li>對應 ServiceMonitor / PodMonitor（Prometheus Operator）</li>
</ul>
<h3 id="prometheus-operatork8s">Prometheus Operator（K8s）</h3>
<p>子議題：</p>
<ul>
<li>CRD：Prometheus / ServiceMonitor / PodMonitor / PrometheusRule / Alertmanager</li>
<li>自動發現 ServiceMonitor 物件、不手動改 scrape config</li>
<li>kube-prometheus-stack Helm chart</li>
<li>對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a> 對照</li>
</ul>
<h3 id="pull-vs-push-model">Pull vs Push model</h3>
<p>子議題：</p>
<ul>
<li>Pull model（Prometheus default）：service discovery、health check 自然</li>
<li>Push model（Pushgateway）：適合 short-lived job、不建議常駐 service</li>
<li>為何 Pushgateway 不推：cardinality 不易管、scrape semantics 違反</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="scrape-failure">Scrape failure</h3>
<p>操作原則：先看 target 是否健康、再看 network 跟認證。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl -s http://localhost:9090/api/v1/targets <span class="p">|</span> jq <span class="s1">&#39;.data.activeTargets[] | {job: .labels.job, health, lastError}&#39;</span></span></span></code></pre></div><h3 id="cardinality-explosion">Cardinality explosion</h3>
<p>操作原則：series 數量持續增長、可能 OOM。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl -s <span class="s1">&#39;http://localhost:9090/api/v1/query?query=prometheus_tsdb_head_series&#39;</span> <span class="p">|</span> jq <span class="s1">&#39;.data.result[].value[1]&#39;</span></span></span></code></pre></div><p>對應 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak</a> 的處理路徑。</p>
<h3 id="query-過慢">Query 過慢</h3>
<p>操作原則：query 過大範圍 / aggregation 過多 → Recording rules 預先聚合。</p>
<h3 id="alert-flapping--noise">Alert flapping / noise</h3>
<p>操作原則：alert 觸發頻繁但無實際問題、調整 <code>for:</code> duration、加 absent() check、用 Alertmanager inhibition。</p>
<h3 id="memory-pressure">Memory pressure</h3>
<p>操作原則：Prometheus retention 跟 cardinality 決定 memory。判讀：cardinality 太大 → remote write 卸載長期儲存。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>長期 retention（年級）</td>
          <td>Thanos / Mimir / Cortex / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Cloud</a></td>
      </tr>
      <tr>
          <td>需要 logs / traces</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> (Loki/Tempo) / <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a></td>
      </tr>
      <tr>
          <td>Auto-instrumentation</td>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a> + Prometheus exporter</td>
      </tr>
      <tr>
          <td>SaaS turnkey</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></td>
      </tr>
      <tr>
          <td>High-cardinality debug</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>AWS-native</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> + Managed Prometheus</td>
      </tr>
      <tr>
          <td>Pure push model</td>
          <td>StatsD / InfluxDB（不在本模組）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>PromQL 完整 syntax reference（prometheus.io/docs/prometheus/latest/querying/）</li>
<li>Exporter 內部實作</li>
<li>Alertmanager routing tree 細節</li>
<li>Operator CRD spec</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a></td>
          <td>Cardinality 管理 / freshness 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a></td>
          <td>AWS Distro + Prometheus 整合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale</a></td>
          <td>K8s metrics + Prometheus 規模化</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Prometheus 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a></td>
          <td>從 Prometheus + Datadog 雙軌走向 OTel 對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>（反例）Prometheus 指標跟新管線的語意對不齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>小型單 instance / 中型 Operator / 大型 + Mimir</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics Basics</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（Mimir）、<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>4.2 metrics 與 SLI/SLO</title><link>https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 基本型別&lt;/li>
&lt;li>latency &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a>&lt;/li>
&lt;li>error rate / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 是把服務狀態壓縮成可聚合、可比較、可告警的時間序列，責任是讓團隊看見趨勢、容量與服務健康。&lt;/p>
&lt;p>這一頁處理的是 metric 型別與計算語意。counter、gauge 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a> 各自回答不同問題；選錯型別會讓後面的 SLI、dashboard 與 alert 都建立在錯誤訊號上。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 metrics 時，先看指標型別是否對應問題，再看分母、bucket 與 label 是否穩定。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>latency 是否用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a> 補足 average 的盲點&lt;/li>
&lt;li>error rate 的分母是否能代表真實請求量&lt;/li>
&lt;li>bucket 是否覆蓋實際尾端延遲&lt;/li>
&lt;li>label 是否能切出必要維度，同時不讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality&lt;/a> 失控&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;ul>
&lt;li>用 average 而非 percentile 追 latency、p99 失真&lt;/li>
&lt;li>counter / gauge 混用、計算公式錯&lt;/li>
&lt;li>histogram bucket 沒對齊實際分佈、tail latency 被截斷&lt;/li>
&lt;li>error rate 分母不穩（流量低時誤觸發、高時稀釋）&lt;/li>
&lt;li>商業 SLI 跟 metric 對不上、靠人解釋&lt;/li>
&lt;/ul>
&lt;h2 id="聚合查詢與-recording-rule">聚合查詢與 recording rule&lt;/h2>
&lt;p>Metrics 的讀取面跟寫入面是兩個不同的效能瓶頸。寫入面的壓力來自 series 數量（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a>）；讀取面的壓力來自查詢時的聚合計算量。兩者可以獨立失控 — series 數量合理但每次 dashboard 刷新都重算複雜表達式，query engine 一樣會過載。&lt;/p>
&lt;h3 id="query-time-aggregation-的成本">Query-time aggregation 的成本&lt;/h3>
&lt;p>Dashboard panel 或 alert rule 每次觸發時，TSDB 對 raw series 執行聚合表達式（rate、sum、histogram_quantile）。當 raw series 數量大、查詢時間範圍長、dashboard 刷新頻率高，同一個計算會被反覆執行。&lt;/p>
&lt;p>一個典型的 SLO &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> panel 可能涉及：先算 rate、再除以 total、再跟 threshold 比較、最後乘以 window。每次刷新把整條運算鏈走一遍。當這類 panel 有十幾個、每 30 秒刷新一次，query engine 的 CPU 會被 dashboard 佔滿，留給事故即席查詢的餘量不夠。&lt;/p>
&lt;h3 id="recording-rule-把計算推到寫入時">Recording rule 把計算推到寫入時&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">Recording rule&lt;/a> 是 Prometheus 生態（包括 Thanos、Mimir、VictoriaMetrics）的標準應對方式：在 TSDB 內定期執行聚合表達式，把結果寫成新的 time series。Dashboard 跟 alert rule 讀 recording rule 的輸出而非重算 raw series。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 基本型別</li>
<li>latency <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a></li>
<li>error rate / <a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput</a></li>
<li><a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a> / <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a></li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 是把服務狀態壓縮成可聚合、可比較、可告警的時間序列，責任是讓團隊看見趨勢、容量與服務健康。</p>
<p>這一頁處理的是 metric 型別與計算語意。counter、gauge 與 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 各自回答不同問題；選錯型別會讓後面的 SLI、dashboard 與 alert 都建立在錯誤訊號上。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 metrics 時，先看指標型別是否對應問題，再看分母、bucket 與 label 是否穩定。</p>
<p>重點訊號包括：</p>
<ul>
<li>latency 是否用 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a> / <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 補足 average 的盲點</li>
<li>error rate 的分母是否能代表真實請求量</li>
<li>bucket 是否覆蓋實際尾端延遲</li>
<li>label 是否能切出必要維度，同時不讓 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a> 失控</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>用 average 而非 percentile 追 latency、p99 失真</li>
<li>counter / gauge 混用、計算公式錯</li>
<li>histogram bucket 沒對齊實際分佈、tail latency 被截斷</li>
<li>error rate 分母不穩（流量低時誤觸發、高時稀釋）</li>
<li>商業 SLI 跟 metric 對不上、靠人解釋</li>
</ul>
<h2 id="聚合查詢與-recording-rule">聚合查詢與 recording rule</h2>
<p>Metrics 的讀取面跟寫入面是兩個不同的效能瓶頸。寫入面的壓力來自 series 數量（<a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a>）；讀取面的壓力來自查詢時的聚合計算量。兩者可以獨立失控 — series 數量合理但每次 dashboard 刷新都重算複雜表達式，query engine 一樣會過載。</p>
<h3 id="query-time-aggregation-的成本">Query-time aggregation 的成本</h3>
<p>Dashboard panel 或 alert rule 每次觸發時，TSDB 對 raw series 執行聚合表達式（rate、sum、histogram_quantile）。當 raw series 數量大、查詢時間範圍長、dashboard 刷新頻率高，同一個計算會被反覆執行。</p>
<p>一個典型的 SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> panel 可能涉及：先算 rate、再除以 total、再跟 threshold 比較、最後乘以 window。每次刷新把整條運算鏈走一遍。當這類 panel 有十幾個、每 30 秒刷新一次，query engine 的 CPU 會被 dashboard 佔滿，留給事故即席查詢的餘量不夠。</p>
<h3 id="recording-rule-把計算推到寫入時">Recording rule 把計算推到寫入時</h3>
<p><a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">Recording rule</a> 是 Prometheus 生態（包括 Thanos、Mimir、VictoriaMetrics）的標準應對方式：在 TSDB 內定期執行聚合表達式，把結果寫成新的 time series。Dashboard 跟 alert rule 讀 recording rule 的輸出而非重算 raw series。</p>
<p>Recording rule 的設計判準是查詢頻率跟計算成本的乘積。高頻讀取（dashboard auto-refresh、每分鐘 evaluate 的 alert rule）加上高計算成本（多維度 rate + ratio + quantile）的組合最值得做 recording rule。低頻即席查詢（事故時的 ad-hoc 切片）直接查 raw series，保留完整維度。</p>
<p>Recording rule 的命名慣例用 <code>level:metric:operations</code> 格式（如 <code>job:http_requests_total:rate5m</code>），讓讀者從名稱直接判斷來源粒度跟計算方式。沒有命名慣例時，recording rule 增長到數百條後會難以維護跟除錯。</p>
<h3 id="rollup-與-downsampling">Rollup 與 downsampling</h3>
<p><a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">Rollup</a> 解決的是時間維度的讀取成本。原始資料以 15 秒間隔採集，查詢「過去 90 天的 error rate 趨勢」時需要掃描數百萬個資料點；rollup 把舊資料聚合成 5 分鐘或 1 小時粒度，查詢時只讀取聚合後的少量資料點。</p>
<p>Rollup 的聚合函數選擇影響查詢語意。Counter 用 sum 合理、gauge 用 average 合理、histogram 用 average 會失去分布資訊（p99 被壓平）。設計 rollup 時要按 metric type 指定對應的聚合函數，混用會讓長時間範圍的 dashboard 產生誤導性數值。</p>
<p>查詢路由的透明度也是設計重點。使用者把 dashboard 時間範圍從 1 小時拉到 7 天時，系統自動從 raw series 切到 rollup series，精度從 15 秒變成 5 分鐘。如果這個切換對使用者不透明，事故中觀察到的數值變化可能是精度切換的假象而非真實服務變化。</p>
<h3 id="metrics-讀取面的資源隔離">Metrics 讀取面的資源隔離</h3>
<p>Metrics 的 query engine 跟 log 一樣面臨多種查詢模式競爭資源的問題。Dashboard 定期刷新是穩定的背景負載；alert rule evaluation 是系統關鍵的定期負載；事故即席查詢是偶發的突增負載。三者搶同一個 query engine 時，dashboard 跟 alert 的穩定負載會壓縮即席查詢的可用資源。</p>
<p>Prometheus 原生的資源隔離有限，但 Thanos Query Frontend、Mimir Query Frontend、Grafana Cloud 的 query scheduler 都支援 query priority 或 query queue 分離。設計時把 alert evaluation 設為最高優先（告警不能因 query 排隊而延遲），dashboard 次之，即席查詢的延遲容忍最高但不能被完全餓死。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.6 SLI/SLO 訊號設計：把 metric 升級為 user-journey SLI</li>
<li>04.7 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a> / cost：label 治理與成本邊界</li>
<li>04.9 continuous profiling：metrics 之外的第四角觀測訊號</li>
<li>04.23 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">觀測查詢設計</a>：跨訊號類型的讀取路徑系統設計</li>
<li><a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">4.C11 Uber M3</a>：單機 Prometheus 到平台級 metrics 系統的演進</li>
</ul>
]]></content:encoded></item><item><title>4.3 tracing 與 context link</title><link>https://tarrragon.github.io/blog/backend/04-observability/tracing-context/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/tracing-context/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 模型&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> propagation&lt;/li>
&lt;li>context 斷鏈的常見邊界與修復&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> 策略的 tracing 面（SSoT 在 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7&lt;/a>）&lt;/li>
&lt;li>service graph 與依賴發現&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">Trace&lt;/a> 是把一次 request 在多個服務、queue 與背景任務中的路徑串起來的診斷訊號，責任是讓團隊從症狀追到跨服務等待點。&lt;/p>
&lt;p>Log 回答「某個服務發生了什麼」；metric 回答「某個服務的健康趨勢」；trace 回答「一次 request 跨多個服務時，時間花在哪、錯誤發生在哪一段」。三者互補，trace 的獨特價值在於它串起跨服務的因果鏈 — 沒有 trace，事故定位只能靠人工比對不同服務的 log timestamp。&lt;/p>
&lt;p>本章處理的是 context propagation — 怎麼讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 在 HTTP call、queue 投遞、背景任務啟動等邊界上正確傳遞。Context 斷掉時，trace 從「完整路徑」退化成幾段需要人工拼接的局部紀錄，跨服務診斷的時間成本會從秒級回退到分鐘甚至小時級。&lt;/p>
&lt;h2 id="trace-與-span-的結構">Trace 與 Span 的結構&lt;/h2>
&lt;h3 id="span-是-trace-的基本單位">Span 是 trace 的基本單位&lt;/h3>
&lt;p>一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 代表一段有起止時間的工作。每個 span 記錄：操作名稱（&lt;code>POST /api/orders&lt;/code>）、開始與結束時間、狀態（OK / Error）、屬性（service name、http.status_code、db.statement）與事件（exception、log message）。&lt;/p>
&lt;p>Span 之間透過 parent-child 關係組成 tree。一個 HTTP request 進入 API gateway 時建立 root span，gateway 呼叫 order service 時建立 child span，order service 查 DB 時建立另一個 child span。整棵 tree 共享同一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>，讓所有 span 可以被聚合成一次 request 的完整路徑。&lt;/p>
&lt;h3 id="trace-是-span-tree">Trace 是 span tree&lt;/h3>
&lt;p>一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 是所有共享同一個 trace id 的 span 的集合。在 waterfall view 中，trace 呈現為時間軸上的巢狀條狀圖 — root span 在最上面，child span 依序往下排列，每段的長度代表耗時。&lt;/p>
&lt;p>Waterfall view 的診斷價值是「一眼看到時間花在哪」。如果 checkout API 的 total latency 是 800ms，waterfall 會顯示 payment service 佔了 600ms — 問題定位從「整個 checkout 慢」縮小到「payment service 慢」，後續 debug 只需要看 payment service 的 log 跟 metric。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> / <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 模型</li>
<li><a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> propagation</li>
<li>context 斷鏈的常見邊界與修復</li>
<li><a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 策略的 tracing 面（SSoT 在 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>）</li>
<li>service graph 與依賴發現</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">Trace</a> 是把一次 request 在多個服務、queue 與背景任務中的路徑串起來的診斷訊號，責任是讓團隊從症狀追到跨服務等待點。</p>
<p>Log 回答「某個服務發生了什麼」；metric 回答「某個服務的健康趨勢」；trace 回答「一次 request 跨多個服務時，時間花在哪、錯誤發生在哪一段」。三者互補，trace 的獨特價值在於它串起跨服務的因果鏈 — 沒有 trace，事故定位只能靠人工比對不同服務的 log timestamp。</p>
<p>本章處理的是 context propagation — 怎麼讓 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 在 HTTP call、queue 投遞、背景任務啟動等邊界上正確傳遞。Context 斷掉時，trace 從「完整路徑」退化成幾段需要人工拼接的局部紀錄，跨服務診斷的時間成本會從秒級回退到分鐘甚至小時級。</p>
<h2 id="trace-與-span-的結構">Trace 與 Span 的結構</h2>
<h3 id="span-是-trace-的基本單位">Span 是 trace 的基本單位</h3>
<p>一個 <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 代表一段有起止時間的工作。每個 span 記錄：操作名稱（<code>POST /api/orders</code>）、開始與結束時間、狀態（OK / Error）、屬性（service name、http.status_code、db.statement）與事件（exception、log message）。</p>
<p>Span 之間透過 parent-child 關係組成 tree。一個 HTTP request 進入 API gateway 時建立 root span，gateway 呼叫 order service 時建立 child span，order service 查 DB 時建立另一個 child span。整棵 tree 共享同一個 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>，讓所有 span 可以被聚合成一次 request 的完整路徑。</p>
<h3 id="trace-是-span-tree">Trace 是 span tree</h3>
<p>一個 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 是所有共享同一個 trace id 的 span 的集合。在 waterfall view 中，trace 呈現為時間軸上的巢狀條狀圖 — root span 在最上面，child span 依序往下排列，每段的長度代表耗時。</p>
<p>Waterfall view 的診斷價值是「一眼看到時間花在哪」。如果 checkout API 的 total latency 是 800ms，waterfall 會顯示 payment service 佔了 600ms — 問題定位從「整個 checkout 慢」縮小到「payment service 慢」，後續 debug 只需要看 payment service 的 log 跟 metric。</p>
<h2 id="context-propagation">Context Propagation</h2>
<h3 id="什麼是-trace-context">什麼是 trace context</h3>
<p><a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">Trace context</a> 是跨服務傳遞 trace 身份的資料。最小的 trace context 包含 trace id（標識整條 trace）跟 parent span id（標識上游 span）。下游服務收到 trace context 後，建立新的 child span 並繼承 trace id，讓兩端的 span 歸屬同一條 trace。</p>
<p>W3C Trace Context 標準定義了 HTTP header 的傳遞格式：<code>traceparent</code> header 帶 trace id + parent span id + trace flags，<code>tracestate</code> header 帶 vendor-specific 的附加資訊。OpenTelemetry SDK 預設使用 W3C 格式；部分 vendor 有自己的 header 格式（Datadog 用 <code>x-datadog-trace-id</code>、AWS X-Ray 用 <code>X-Amzn-Trace-Id</code>），需要在 collector 或 SDK 層做格式轉換。</p>
<h3 id="propagation-的傳遞機制">Propagation 的傳遞機制</h3>
<p>HTTP call 是最常見的 propagation 路徑 — SDK 的 HTTP client middleware 自動把 trace context 注入 request header，下游 SDK 的 HTTP server middleware 自動從 header 提取 context。大部分 OpenTelemetry SDK 的 auto-instrumentation 會自動處理這一層，開發者不需要手動注入。</p>
<p>gRPC 用 metadata（等同 HTTP header）傳遞，機制類似。</p>
<p>Message queue 的 propagation 需要把 trace context 放進 message 的 header 或 metadata。Kafka 用 record header、RabbitMQ 用 message properties、NATS 用 message header。Producer 端注入、consumer 端提取。Queue 的 propagation 比 HTTP 複雜的原因是 consumer 可能在 producer 之後很久才消費 — context 的時間跨度可能從毫秒擴大到分鐘或小時。</p>
<h3 id="context-斷鏈的常見邊界">Context 斷鏈的常見邊界</h3>
<p>Context propagation 在以下邊界容易斷裂：</p>
<p><strong>Thread / goroutine / task 邊界</strong>：同步 runtime 通常用 thread-local 存放 context，新開 thread 不會自動繼承。Go 用 <code>context.Context</code> 顯式傳遞，相對不容易遺漏；Java 用 ThreadLocal，啟動新 thread 或提交到 thread pool 時 context 需要手動傳遞或用 agent auto-instrumentation。Async runtime（Node.js 的 AsyncLocalStorage、Python 的 contextvars）各有自己的 context 傳播機制。</p>
<p><strong>Queue / event 邊界</strong>：producer 把 trace context 注入 message header，consumer 提取並建立新 span。如果 producer 端的 SDK 沒有自動注入（例如用了原生 Kafka client 而非 instrumented client），context 就斷了。跨 queue 的 trace 在 waterfall view 中會出現時間斷層 — producer span 結束到 consumer span 開始之間可能有秒級到分鐘級的等待。</p>
<p><strong>Background job / cron 邊界</strong>：cron job 或 scheduled task 沒有上游 request，沒有 trace context 可繼承。這類工作需要在啟動時建立 root span，並把 job name、schedule、trigger reason 作為 span 屬性，讓 trace 至少可以追蹤 job 內部的行為。</p>
<p><strong>跨語言 / 跨 vendor 邊界</strong>：不同語言的 SDK 或不同 vendor 的 instrumentation 可能用不同的 header 格式。W3C Trace Context 標準解決了格式問題，但混用 vendor-specific SDK 時（例如一個服務用 Datadog agent、另一個用 OTel SDK），需要在 collector 層做 context format 轉換。</p>
<h3 id="斷鏈的修復策略">斷鏈的修復策略</h3>
<p>修復斷鏈的目標是讓 trace 在邊界處重新接上，不需要人工拼接。</p>
<p><strong>Queue 邊界</strong>：確保 producer 跟 consumer 都使用 instrumented client（OTel SDK 的 messaging instrumentation），而非原生 client。Instrumented client 自動處理 header 注入跟提取。Consumer 端建立的 span 用 <code>CONSUMER</code> kind 標記，waterfall view 會顯示 queue 等待時間。</p>
<p><strong>Thread pool 邊界</strong>：Java 生態用 <code>Context.wrap()</code> 包裝提交到 thread pool 的 Runnable/Callable；Go 生態用 <code>context.Context</code> 作為第一個函數參數傳遞（這是 Go 的慣例，不需要額外處理）。Auto-instrumentation agent 可以自動處理常見 thread pool（Java 的 ExecutorService、Node.js 的 worker_threads）。</p>
<p><strong>跨 vendor 邊界</strong>：在 collector 層（OTel Collector）統一轉換 header 格式。Collector 的 receiver 支援多種格式輸入，exporter 統一輸出 W3C 格式。這層轉換在 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 的 collector 中介段處理。</p>
<h2 id="trace-與-log--metric-的關聯">Trace 與 Log / Metric 的關聯</h2>
<h3 id="correlation-id-統一">Correlation id 統一</h3>
<p><a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">Trace id</a> 應該同時出現在 log 的結構化欄位中。當 log 的 <code>trace_id</code> 欄位帶著跟 trace 相同的值，debug 工作流就能從 trace waterfall 跳到某個 span 對應的 log，或從 log 跳到完整的 trace view。</p>
<p>實作方式是在 logger 初始化時，把當前 span 的 trace id 注入 log 的 context fields。OTel SDK 的 log bridge 可以自動做這件事；沒有自動橋接的框架需要手動把 <code>span.SpanContext().TraceID()</code> 寫進 log 的 <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a> 欄位。</p>
<h3 id="exemplarmetric-到-trace-的跳板">Exemplar：metric 到 trace 的跳板</h3>
<p>Metric 是聚合訊號，本身不帶單一 request 的 trace id。Exemplar 是附加在 metric 資料點上的代表性 trace id — 當某個 histogram bucket 收到一個資料點時，附帶記錄產生這個資料點的 trace id。</p>
<p>Dashboard 上看到 latency p99 升高時，可以從 exemplar 跳到一個具體的高延遲 trace，看 waterfall 定位慢在哪。Exemplar 是 metric 到 trace 的橋樑，讓聚合訊號（metric）跟個別案例（trace）連接起來。</p>
<h2 id="service-graph-與依賴發現">Service Graph 與依賴發現</h2>
<p>Trace 資料聚合後可以自動生成 service graph — 哪些服務在呼叫哪些服務、call 的頻率、延遲分布、錯誤率。這個 graph 跟手動維護的 architecture diagram 不同：它來自實際流量，反映的是「現在真的在發生什麼」而非「設計時預期會發生什麼」。</p>
<p>Service graph 的價值在於依賴發現。新服務加入後，如果有 trace instrumentation，它會自動出現在 graph 上。舊服務之間新增的依賴（例如 A 開始直接呼叫 C、繞過 B）也會被 graph 反映。手動維護的 wiki 通常落後實際狀況數週到數月。</p>
<p>Service graph 的完整性取決於 trace 的覆蓋率。如果某些服務沒有 instrumentation 或 sampling 率太低，graph 上會出現斷點或邊權不準。把 service graph 的完整性（「有多少比例的服務有 trace」）作為觀測覆蓋率的一個指標，能推動 instrumentation 的漸進覆蓋。</p>
<p>詳見 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 tracing 時，先看 propagation 是否完整，再看 sampling 是否保留可除錯樣本。</p>
<p>重點訊號包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 是否能和 log、metric 共享 <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a></li>
<li>async / queue / background job 是否能保留 parent-child 關係</li>
<li>sampling 是否能在高流量下保留錯誤與高延遲樣本（策略矩陣見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>）</li>
<li>service graph 是否能由 trace 聚合而來，並降低 wiki 手動維護成本</li>
<li>trace context 在跨語言 / 跨 vendor 邊界是否用 W3C 標準統一</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Request 跨服務後 trace 斷鏈、靠人重組</li>
<li>Async / queue 邊界 context 沒傳遞</li>
<li>採樣率太低、production debug 找不到對應 trace</li>
<li>Trace id 跟 log / metric 對不上、無共同 correlation key</li>
<li>Service graph 不存在或半年沒人看</li>
<li>多個 vendor SDK 混用、header 格式不一致</li>
<li>Background job / cron 沒有 root span、trace 無法追蹤</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只 instrument HTTP、忽略 queue</td>
          <td>Queue 消費後的 span 都是孤兒</td>
          <td>Producer / consumer 都用 instrumented client</td>
      </tr>
      <tr>
          <td>Thread pool 不傳 context</td>
          <td>平行處理的 span 不歸屬任何 trace</td>
          <td>用 Context.wrap() 或語言慣例傳遞 context</td>
      </tr>
      <tr>
          <td>Trace id 沒寫進 log</td>
          <td>從 log 找不到對應 trace、反向也找不到</td>
          <td>Logger context 注入 trace id</td>
      </tr>
      <tr>
          <td>混用 vendor header 無轉換</td>
          <td>部分服務的 span 串不進同一條 trace</td>
          <td>Collector 層統一轉換成 W3C 格式</td>
      </tr>
      <tr>
          <td>所有 span 都是 root span</td>
          <td>Trace 只有一層、沒有 parent-child 結構</td>
          <td>確認 SDK 的 context extraction 有正確從 header 繼承</td>
      </tr>
      <tr>
          <td>Background job 無 instrumentation</td>
          <td>Job 內的 DB / HTTP call 沒有 trace 可追蹤</td>
          <td>Job 啟動時建立 root span、內部操作作為 child span</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：trace 資料在 dashboard 的呈現跟 alert 設計</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：sampling 策略矩陣（Head / Tail / Adaptive / Exemplar）與保留決策</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a>：sampling 在 collector 的集中治理、跨 vendor header 轉換</li>
<li><a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a>：trace 訊號聚合成依賴圖</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：sampling bias 跟 trace 完整性的資料品質</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：trace 查詢作為即席診斷的一種模式</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>Healthcare：存取可追溯性與保留邊界</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/</guid><description>&lt;p>本案例的核心責任是讓資料主權場景下的觀測仍可追溯。Healthcare 系統常同時面臨最小存取原則、資料留存規範與跨團隊協作需求。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>一個遠距醫療平台，服務多家醫療機構（multi-tenant），處理病歷查閱、處方開立、檢驗報告與預約排程。平台受 HIPAA 跟當地個資法規範，稽核單位要求能回答「哪個使用者在什麼時間查看了哪個病患的哪份紀錄」。&lt;/p>
&lt;p>初期系統的存取紀錄散落在各服務的 application log 中 — 病歷服務記了一筆 &lt;code>GET /patient/123/records&lt;/code>，處方服務記了一筆 &lt;code>POST /prescription&lt;/code>，但兩者沒有共同的 correlation key。稽核問「護理師 A 在 3 月 15 日存取了哪些病歷」時，工程師需要在四個服務各自 grep，再用 timestamp 近似對齊，整個流程耗時半天且結果不可靠。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="存取-log-與-application-log-混合">存取 log 與 application log 混合&lt;/h3>
&lt;p>存取紀錄（誰看了什麼）跟 operational log（request timing、error、retry）寫在同一個 pipeline。Application log 的 retention 設定 30 天（除錯夠用），但法規要求存取紀錄保留 6 年。等到稽核來查詢時，超過 30 天的存取紀錄已經被刪。&lt;/p>
&lt;h3 id="跨服務存取鏈斷裂">跨服務存取鏈斷裂&lt;/h3>
&lt;p>一次病歷查閱可能經過 API gateway → auth service → patient service → record service → audit service 五個服務。每個服務各自記 log，但沒有統一的 access event correlation。Auth service 知道「誰」，patient service 知道「看了哪個病患」，record service 知道「看了哪份紀錄」— 三段資訊散落在三個服務的 log 中，無法自動關聯。&lt;/p>
&lt;h3 id="multi-tenant-retention-差異">Multi-tenant retention 差異&lt;/h3>
&lt;p>不同醫療機構受不同法規管轄 — 機構 A 在美國需要 HIPAA 6 年 retention，機構 B 在歐盟需要 GDPR 的「目的限縮」原則（保留期限隨用途而定），機構 C 在台灣需要醫療法規定的 7 年。統一 retention policy 要嘛過度保留（增加成本與 PII 暴露面），要嘛保留不足（法規風險）。&lt;/p>
&lt;h2 id="解法">解法&lt;/h2>
&lt;h3 id="data-access-audit-log-獨立-pipeline">Data access audit log 獨立 pipeline&lt;/h3>
&lt;p>把存取事件從 application log 分離出來。每當使用者查閱、修改或匯出 PHI（Protected Health Information）時，產生結構化 access event：&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;event_type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;phi_access&amp;#34;&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;actor&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;nurse-a@hospital-x.com&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;patient_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;P-2048&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;resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;medical_record/lab_result/2026-03-15&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;action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;view&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;trace_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;abc123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;access_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;acc-789&amp;#34;&lt;/span>&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="nt">&amp;#34;tenant&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;hospital-x&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;timestamp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-03-15T14:22:05Z&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Access event 寫入獨立的 immutable storage（append-only log），跟 application log 分開的 pipeline 與 retention。&lt;/p>
&lt;h3 id="cross-service-access-chain">Cross-service access chain&lt;/h3>
&lt;p>在 API gateway 入口產生 &lt;code>access_id&lt;/code>，跟 &lt;code>trace_id&lt;/code> 一起透過 context propagation 傳遞到所有下游服務。每個服務在產生 access event 時帶上這兩個 key。查詢時用 &lt;code>access_id&lt;/code> 就能撈出一次存取操作在所有服務的完整軌跡，不需要手動拼接。&lt;/p>
&lt;p>&lt;code>trace_id&lt;/code> 用於關聯 operational 訊號（latency、error），&lt;code>access_id&lt;/code> 用於關聯合規稽核。兩者可以相同也可以不同 — 關鍵是 access event 要同時帶兩個 key。&lt;/p>
&lt;h3 id="分層-retention-與-tenant-level-policy">分層 retention 與 tenant-level policy&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>儲存&lt;/th>
 &lt;th>Retention&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Hot&lt;/td>
 &lt;td>搜尋引擎（Elasticsearch / Cloud Logging）&lt;/td>
 &lt;td>90 天&lt;/td>
 &lt;td>即時查詢、事故調查&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Warm&lt;/td>
 &lt;td>Object storage（壓縮）&lt;/td>
 &lt;td>2 年&lt;/td>
 &lt;td>定期稽核、合規查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cold&lt;/td>
 &lt;td>Archive storage（冰凍）&lt;/td>
 &lt;td>6-7 年（依 tenant 法規）&lt;/td>
 &lt;td>法規保留、法務調查&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>每個 tenant 在平台建立時設定法規要求的 retention 期限。Pipeline 根據 tenant tag 自動把 access event 路由到對應的 retention tier。Tenant A 的紀錄到第 6 年自動歸檔到 cold，tenant B 在 GDPR 目的屆滿時觸發刪除審核。&lt;/p></description><content:encoded><![CDATA[<p>本案例的核心責任是讓資料主權場景下的觀測仍可追溯。Healthcare 系統常同時面臨最小存取原則、資料留存規範與跨團隊協作需求。</p>
<h2 id="業務背景">業務背景</h2>
<p>一個遠距醫療平台，服務多家醫療機構（multi-tenant），處理病歷查閱、處方開立、檢驗報告與預約排程。平台受 HIPAA 跟當地個資法規範，稽核單位要求能回答「哪個使用者在什麼時間查看了哪個病患的哪份紀錄」。</p>
<p>初期系統的存取紀錄散落在各服務的 application log 中 — 病歷服務記了一筆 <code>GET /patient/123/records</code>，處方服務記了一筆 <code>POST /prescription</code>，但兩者沒有共同的 correlation key。稽核問「護理師 A 在 3 月 15 日存取了哪些病歷」時，工程師需要在四個服務各自 grep，再用 timestamp 近似對齊，整個流程耗時半天且結果不可靠。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="存取-log-與-application-log-混合">存取 log 與 application log 混合</h3>
<p>存取紀錄（誰看了什麼）跟 operational log（request timing、error、retry）寫在同一個 pipeline。Application log 的 retention 設定 30 天（除錯夠用），但法規要求存取紀錄保留 6 年。等到稽核來查詢時，超過 30 天的存取紀錄已經被刪。</p>
<h3 id="跨服務存取鏈斷裂">跨服務存取鏈斷裂</h3>
<p>一次病歷查閱可能經過 API gateway → auth service → patient service → record service → audit service 五個服務。每個服務各自記 log，但沒有統一的 access event correlation。Auth service 知道「誰」，patient service 知道「看了哪個病患」，record service 知道「看了哪份紀錄」— 三段資訊散落在三個服務的 log 中，無法自動關聯。</p>
<h3 id="multi-tenant-retention-差異">Multi-tenant retention 差異</h3>
<p>不同醫療機構受不同法規管轄 — 機構 A 在美國需要 HIPAA 6 年 retention，機構 B 在歐盟需要 GDPR 的「目的限縮」原則（保留期限隨用途而定），機構 C 在台灣需要醫療法規定的 7 年。統一 retention policy 要嘛過度保留（增加成本與 PII 暴露面），要嘛保留不足（法規風險）。</p>
<h2 id="解法">解法</h2>
<h3 id="data-access-audit-log-獨立-pipeline">Data access audit log 獨立 pipeline</h3>
<p>把存取事件從 application log 分離出來。每當使用者查閱、修改或匯出 PHI（Protected Health Information）時，產生結構化 access event：</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;event_type&#34;</span><span class="p">:</span> <span class="s2">&#34;phi_access&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="nt">&#34;actor&#34;</span><span class="p">:</span> <span class="s2">&#34;nurse-a@hospital-x.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nt">&#34;patient_id&#34;</span><span class="p">:</span> <span class="s2">&#34;P-2048&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nt">&#34;resource&#34;</span><span class="p">:</span> <span class="s2">&#34;medical_record/lab_result/2026-03-15&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="nt">&#34;action&#34;</span><span class="p">:</span> <span class="s2">&#34;view&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="nt">&#34;trace_id&#34;</span><span class="p">:</span> <span class="s2">&#34;abc123&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="nt">&#34;access_id&#34;</span><span class="p">:</span> <span class="s2">&#34;acc-789&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nt">&#34;tenant&#34;</span><span class="p">:</span> <span class="s2">&#34;hospital-x&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="nt">&#34;timestamp&#34;</span><span class="p">:</span> <span class="s2">&#34;2026-03-15T14:22:05Z&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Access event 寫入獨立的 immutable storage（append-only log），跟 application log 分開的 pipeline 與 retention。</p>
<h3 id="cross-service-access-chain">Cross-service access chain</h3>
<p>在 API gateway 入口產生 <code>access_id</code>，跟 <code>trace_id</code> 一起透過 context propagation 傳遞到所有下游服務。每個服務在產生 access event 時帶上這兩個 key。查詢時用 <code>access_id</code> 就能撈出一次存取操作在所有服務的完整軌跡，不需要手動拼接。</p>
<p><code>trace_id</code> 用於關聯 operational 訊號（latency、error），<code>access_id</code> 用於關聯合規稽核。兩者可以相同也可以不同 — 關鍵是 access event 要同時帶兩個 key。</p>
<h3 id="分層-retention-與-tenant-level-policy">分層 retention 與 tenant-level policy</h3>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>儲存</th>
          <th>Retention</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hot</td>
          <td>搜尋引擎（Elasticsearch / Cloud Logging）</td>
          <td>90 天</td>
          <td>即時查詢、事故調查</td>
      </tr>
      <tr>
          <td>Warm</td>
          <td>Object storage（壓縮）</td>
          <td>2 年</td>
          <td>定期稽核、合規查詢</td>
      </tr>
      <tr>
          <td>Cold</td>
          <td>Archive storage（冰凍）</td>
          <td>6-7 年（依 tenant 法規）</td>
          <td>法規保留、法務調查</td>
      </tr>
  </tbody>
</table>
<p>每個 tenant 在平台建立時設定法規要求的 retention 期限。Pipeline 根據 tenant tag 自動把 access event 路由到對應的 retention tier。Tenant A 的紀錄到第 6 年自動歸檔到 cold，tenant B 在 GDPR 目的屆滿時觸發刪除審核。</p>
<h3 id="存取-log-中的-pii-處理">存取 log 中的 PII 處理</h3>
<p>Access event 本身包含 <code>patient_id</code> 跟 <code>actor</code>，這些在存取紀錄中是必要資訊（「誰看了什麼」需要這兩個欄位）。處理方式是存取控制而非遮罩 — access event storage 的讀取權限限縮到 compliance team 跟 audit 角色，engineering team 的一般查詢權限無法看到這些欄位。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>統一 retention</th>
          <th>分層 + tenant-level</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>實作複雜度</td>
          <td>低</td>
          <td>高（routing 邏輯、多層 storage）</td>
      </tr>
      <tr>
          <td>儲存成本</td>
          <td>高（全部留最長）</td>
          <td>可控（各層各自成本）</td>
      </tr>
      <tr>
          <td>合規精確度</td>
          <td>低（過度保留或保留不足）</td>
          <td>高（對齊各 tenant 法規要求）</td>
      </tr>
      <tr>
          <td>刪除能力</td>
          <td>無法按 tenant 刪</td>
          <td>可（GDPR right to erasure）</td>
      </tr>
      <tr>
          <td>查詢效率</td>
          <td>全量搜尋</td>
          <td>Hot tier 秒級、Cold tier 分鐘到小時級</td>
      </tr>
  </tbody>
</table>
<p>分層架構的最大風險是跨層查詢的延遲 — 稽核要求「給我 3 年前的存取紀錄」時，cold tier 的解凍時間可能是小時級。解法是在稽核週期前預先解凍相關 tenant 的 cold archive 到 warm tier。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a>：audit log 分離與 PII 治理。</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a>：access log pipeline 的 ownership 與 review cadence。</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>：timestamp integrity 跟跨服務時序校正。</li>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 Tracing Context</a>：access_id 跟 trace_id 的 propagation 設計。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>稽核問「使用者 X 在某段時間存取了什麼」，回答需要超過數小時的手動拼接</li>
<li>存取紀錄的 retention 跟法規要求不一致，但沒人確切量化差距</li>
<li>Multi-tenant 環境中所有 tenant 共用同一個 retention policy，無法按法規區分</li>
<li>跨服務的存取事件無法自動關聯，需要靠 timestamp 近似比對</li>
<li>PHI 相關的 log 跟一般 application log 存在同一個 storage，存取控制無法區隔</li>
</ul>
]]></content:encoded></item><item><title>Grafana Stack</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/</guid><description>&lt;p>Grafana Stack 是 Grafana Labs 提供的 OSS observability 全棧、承擔三個責任：跨 data source 統一視覺化（Grafana）、各訊號類型專屬 backend（Loki logs / Tempo traces / Mimir metrics / Pyroscope profiles）、可自管或用 Grafana Cloud（managed）。設計取捨偏向「OSS-first + signal-specific backend + 統一查詢介面」、是 Datadog 的 OSS 替代方案。&lt;/p>
&lt;p>對「需要 OSS / 自管 observability、跨 data source 統一儀表板、不想 vendor lock-in」這條路徑、Grafana Stack 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Grafana + Prometheus + Loki + Tempo 基本棧&lt;/li>
&lt;li>用 LogQL 查詢 Loki、用 TraceQL 查詢 Tempo&lt;/li>
&lt;li>設計 dashboard as code（Jsonnet / Terraform）&lt;/li>
&lt;li>評估 Mimir vs Thanos 的長期 metrics 儲存選擇&lt;/li>
&lt;li>評估 Grafana Cloud（managed）跟自管的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-grafana-stack-跑起來">最短路徑：5 分鐘把 Grafana Stack 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 用 docker-compose 跑起 Grafana + Prometheus + Loki&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker-compose.yml with grafana / prometheus / loki&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="c1"># 2. 在 Grafana 加 data source&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"># TODO: Prometheus / Loki 各自的 datasource config&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 建第一個 dashboard&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 用 explorer 試 PromQL + LogQL&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證 Grafana 起來、可訪 metrics + logs。實際 production 要評估 Mimir / Tempo + Grafana Cloud 取捨。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="grafana-視覺化">Grafana 視覺化&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Data source 配置（Prometheus / Loki / Tempo / Postgres / MySQL / Elasticsearch）&lt;/li>
&lt;li>Dashboard 設計：variable + template + panel&lt;/li>
&lt;li>Dashboard as code：Jsonnet (Grafonnet) / Terraform Grafana provider&lt;/li>
&lt;li>對應指令：HTTP API &lt;code>/api/dashboards&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="logqlloki-查詢">LogQL（Loki 查詢）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>LogQL syntax：log stream selector + filter + parser + aggregation&lt;/li>
&lt;li>跟 PromQL 對齊的設計（同樣 label-based）&lt;/li>
&lt;li>範例：&lt;code>{job=&amp;quot;app&amp;quot;} |= &amp;quot;error&amp;quot; | json | line_format &amp;quot;...&amp;quot;&lt;/code>&lt;/li>
&lt;li>對應 metrics-from-logs（unwrap + rate）&lt;/li>
&lt;/ul>
&lt;h3 id="traceqltempo-查詢">TraceQL（Tempo 查詢）&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Grafana Stack 是 Grafana Labs 提供的 OSS observability 全棧、承擔三個責任：跨 data source 統一視覺化（Grafana）、各訊號類型專屬 backend（Loki logs / Tempo traces / Mimir metrics / Pyroscope profiles）、可自管或用 Grafana Cloud（managed）。設計取捨偏向「OSS-first + signal-specific backend + 統一查詢介面」、是 Datadog 的 OSS 替代方案。</p>
<p>對「需要 OSS / 自管 observability、跨 data source 統一儀表板、不想 vendor lock-in」這條路徑、Grafana Stack 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Grafana + Prometheus + Loki + Tempo 基本棧</li>
<li>用 LogQL 查詢 Loki、用 TraceQL 查詢 Tempo</li>
<li>設計 dashboard as code（Jsonnet / Terraform）</li>
<li>評估 Mimir vs Thanos 的長期 metrics 儲存選擇</li>
<li>評估 Grafana Cloud（managed）跟自管的取捨</li>
</ol>
<h2 id="最短路徑5-分鐘把-grafana-stack-跑起來">最短路徑：5 分鐘把 Grafana Stack 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 用 docker-compose 跑起 Grafana + Prometheus + Loki</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: docker-compose.yml with grafana / prometheus / loki</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="c1"># 2. 在 Grafana 加 data source</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: Prometheus / Loki 各自的 datasource config</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 3. 建第一個 dashboard</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: 用 explorer 試 PromQL + LogQL</span></span></span></code></pre></div><p>最短路徑驗證 Grafana 起來、可訪 metrics + logs。實際 production 要評估 Mimir / Tempo + Grafana Cloud 取捨。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="grafana-視覺化">Grafana 視覺化</h3>
<p>子議題：</p>
<ul>
<li>Data source 配置（Prometheus / Loki / Tempo / Postgres / MySQL / Elasticsearch）</li>
<li>Dashboard 設計：variable + template + panel</li>
<li>Dashboard as code：Jsonnet (Grafonnet) / Terraform Grafana provider</li>
<li>對應指令：HTTP API <code>/api/dashboards</code></li>
</ul>
<h3 id="logqlloki-查詢">LogQL（Loki 查詢）</h3>
<p>子議題：</p>
<ul>
<li>LogQL syntax：log stream selector + filter + parser + aggregation</li>
<li>跟 PromQL 對齊的設計（同樣 label-based）</li>
<li>範例：<code>{job=&quot;app&quot;} |= &quot;error&quot; | json | line_format &quot;...&quot;</code></li>
<li>對應 metrics-from-logs（unwrap + rate）</li>
</ul>
<h3 id="traceqltempo-查詢">TraceQL（Tempo 查詢）</h3>
<p>子議題：</p>
<ul>
<li>TraceQL syntax：span selector + attribute + aggregation</li>
<li>範例：<code>{ span.http.status_code = 500 &amp;&amp; duration &gt; 1s }</code></li>
<li>Service graph：跨服務依賴自動分析</li>
<li>對應 trace-to-logs / trace-to-metrics 關聯查詢</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="lgtm-stack-operations/">LGTM Stack 組合運維</a>：四個元件的責任分工、部署模式、常見故障與 dashboard provisioning</li>
<li><a href="loki-design-operational-limits/">Loki 設計與操作限制</a>：label-based index 設計、LogQL 查詢模式、cardinality 治理與 Elasticsearch 差異</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="loki-設計與限制">Loki 設計與限制</h3>
<p>子議題：</p>
<ul>
<li>Storage：S3 / GCS / 本地、按 stream 切 chunks</li>
<li>Label cardinality 跟 Prometheus 一樣敏感（不是 stream content）</li>
<li>LogQL 不適合 high-cardinality content search（用 Elastic）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></li>
</ul>
<h3 id="tempo-trace-採集">Tempo trace 採集</h3>
<p>子議題：</p>
<ul>
<li>接受 OTLP / Jaeger / Zipkin protocol</li>
<li>Storage：S3 / GCS、cheap object storage</li>
<li>Trace ID lookup 為主、no full-text search（用 traces metrics 反向查）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OTel</a></li>
</ul>
<h3 id="mimir-長期-metrics-儲存">Mimir 長期 metrics 儲存</h3>
<p>子議題：</p>
<ul>
<li>Prometheus remote write 接收 metric</li>
<li>Horizontally scalable（multi-tenant）</li>
<li>跟 Thanos / Cortex 的對照（Mimir 是 Cortex fork + improvements）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale</a></li>
</ul>
<h3 id="pyroscope-continuous-profiling">Pyroscope continuous profiling</h3>
<p>子議題：</p>
<ul>
<li>CPU / memory / mutex / goroutine profiling</li>
<li>Flame graph 視覺化</li>
<li>跟 Tempo trace 關聯（trace-to-profile）</li>
<li>OSS（Grafana 收購）vs Pyroscope OG</li>
</ul>
<h3 id="grafana-cloudmanaged">Grafana Cloud（managed）</h3>
<p>子議題：</p>
<ul>
<li>Free tier 額度 + paid tier</li>
<li>含所有 stack（Metrics / Logs / Traces / Profiles）</li>
<li>Grafana Cloud vs Datadog cost 對照</li>
<li>Hybrid 模式：self-host backend + Grafana Cloud Grafana</li>
</ul>
<h3 id="unified-alerting">Unified Alerting</h3>
<p>子議題：</p>
<ul>
<li>Grafana 9+ 統一 alerting（取代 dashboard alert + Prometheus alertmanager 分裂）</li>
<li>跨 data source 寫 alert rule</li>
<li>Multi-dimensional alert（per-label）</li>
<li>對應 Alertmanager 兼容</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="dashboard-載入慢">Dashboard 載入慢</h3>
<p>操作原則：先看 query 範圍跟 panel 數、用 query inspector 看 query 時間分布。</p>
<h3 id="loki-query-過慢--失敗">Loki query 過慢 / 失敗</h3>
<p>操作原則：Loki query 需要 label filter 先縮範圍、再 content match。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: LogQL: {namespace=&#34;prod&#34;, app=&#34;api&#34;} |= &#34;error&#34;（先 label 後 filter）</span></span></span></code></pre></div><h3 id="tempo-span-gap">Tempo span gap</h3>
<p>操作原則：trace 不完整、看 sampling 設定 + Collector buffer 是否 drop。</p>
<h3 id="mimir-ingestion-失敗">Mimir ingestion 失敗</h3>
<p>操作原則：remote_write rate / size limit 撞到 Mimir quota。判讀：Mimir HTTP 429 / 413。</p>
<h3 id="grafana-跟-prometheus-disconnected">Grafana 跟 Prometheus disconnected</h3>
<p>操作原則：data source 連不上、看 Grafana log + network。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pure metrics</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 單獨用</td>
      </tr>
      <tr>
          <td>SaaS turnkey APM</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></td>
      </tr>
      <tr>
          <td>Log full-text search 為主</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a></td>
      </tr>
      <tr>
          <td>High-cardinality debug</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>AWS / GCP native</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> / <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Ops</a></td>
      </tr>
      <tr>
          <td>Error tracking</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
      <tr>
          <td>Profile only</td>
          <td>Pyroscope OSS / Polar Signals</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 Grafana plugin 細節</li>
<li>Dashboard 美術 / UX 建議</li>
<li>Grafana / Loki / Tempo / Mimir 各自完整 admin 手冊</li>
<li>Grafana 商業版 (Enterprise) 功能</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a></td>
          <td>Loki / Mimir 高峰下的 ingestion lag 與標籤治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></td>
          <td>Loki retention / compliance</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale</a></td>
          <td>Mimir scale / Prometheus 長期儲存</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Grafana Stack 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OTel</a></td>
          <td>從 X-Ray 遷出後 Tempo 是 OSS trace backend 候選</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a></td>
          <td>從 Datadog 遷出可去 Grafana Cloud</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>小型 single Grafana / 中型加 Loki+Tempo / 大型 Grafana Cloud 或 Mimir</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics Basics</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a>、<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>4.4 dashboard 與 alert 設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard&lt;/a> 設計原則：SLI 導向 vs 指標堆疊&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert&lt;/a> 設計：symptom-based vs cause-based&lt;/li>
&lt;li>Alert noise control 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook&lt;/a> linkage&lt;/li>
&lt;li>Dashboard / alert 的生命週期與 ownership&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 是把觀測訊號轉成操作入口的控制面，責任是讓團隊在正常巡檢與事故響應時看到同一組事實。&lt;/p>
&lt;p>Dashboard 讓人理解狀態，alert 讓人採取行動。兩者的設計問題不同：dashboard 的問題是「資訊太多、焦點不明」；alert 的問題是「通知太多、行動不明」。兩者都需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">ownership&lt;/a>、生命週期管理與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 連結。&lt;/p>
&lt;h2 id="dashboard-設計">Dashboard 設計&lt;/h2>
&lt;h3 id="sli-導向-vs-指標堆疊">SLI 導向 vs 指標堆疊&lt;/h3>
&lt;p>Dashboard 的常見失敗模式是「把所有能拿到的指標都放上去」。二十個 panel、五十條曲線、無法在 3 秒內回答「服務現在健康嗎」。&lt;/p>
&lt;p>SLI 導向的 dashboard 從使用者體驗出發：第一排 panel 回答「使用者感受到的健康狀態」（availability、latency percentile、error ratio），第二排回答「健康狀態的原因」（dependency latency、queue depth、resource utilization），第三排回答「趨勢與容量」（traffic growth、storage usage、capacity headroom）。&lt;/p>
&lt;p>每個 panel 都應該能回答一個具體問題。如果團隊看了某個 panel 後的反應是「所以呢？」，這個 panel 不是放錯位置就是不該存在。&lt;/p>
&lt;h3 id="dashboard-層級">Dashboard 層級&lt;/h3>
&lt;p>不同使用者看不同層級的 dashboard。把所有資訊擠在同一個 dashboard 會讓每個角色都找不到自己要的。&lt;/p>
&lt;p>&lt;strong>Service overview&lt;/strong>：on-call 工程師的第一個入口。5-8 個 panel，回答「這個服務現在有沒有問題」。SLI 指標（error rate、latency p99、availability）、最近的 alert、dependency 健康。&lt;/p>
&lt;p>&lt;strong>Debug dashboard&lt;/strong>：事故中的深入診斷入口。按 dependency 分組（database panel group、cache panel group、downstream API panel group），每組顯示延遲、錯誤率、連線數。Panel 數量多但按需展開。&lt;/p>
&lt;p>&lt;strong>Capacity dashboard&lt;/strong>：容量規劃用。週到月級的趨勢圖 — traffic growth、storage usage、connection pool saturation、cost trends。刷新頻率低（每小時或每天），panel 讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 資料。&lt;/p>
&lt;p>&lt;strong>Business dashboard&lt;/strong>：給非工程角色看。轉換率、使用者活躍度、營收指標。資料來源可能不只是觀測訊號，還包括 analytics 跟 business metrics。&lt;/p>
&lt;h3 id="dashboard-的查詢效能">Dashboard 的查詢效能&lt;/h3>
&lt;p>Dashboard 是觀測查詢設計中「聚合趨勢」模式的主要消費者（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23&lt;/a>）。每個 panel 每 30 秒刷新一次，十個團隊各自有 dashboard 就是每分鐘數百個背景查詢。&lt;/p>
&lt;p>Panel 設計時要注意查詢成本：時間範圍越長、raw series 越多、聚合越複雜，query-time cost 越高。長時間趨勢 panel 應該讀 recording rule 或 rollup series，而非每次刷新都掃描 raw data。&lt;/p>
&lt;h2 id="alert-設計">Alert 設計&lt;/h2>
&lt;h3 id="symptom-based-vs-cause-based">Symptom-based vs cause-based&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">Symptom-based alert&lt;/a> 觸發在使用者可感知的症狀上 — error rate 升高、latency p99 超過閾值、availability 下降。Cause-based alert 觸發在內部原因上 — CPU &amp;gt; 90%、disk usage &amp;gt; 85%、connection pool exhausted。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard</a> 設計原則：SLI 導向 vs 指標堆疊</li>
<li><a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert</a> 設計：symptom-based vs cause-based</li>
<li>Alert noise control 與 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a></li>
<li><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook</a> linkage</li>
<li>Dashboard / alert 的生命週期與 ownership</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard</a> 與 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 是把觀測訊號轉成操作入口的控制面，責任是讓團隊在正常巡檢與事故響應時看到同一組事實。</p>
<p>Dashboard 讓人理解狀態，alert 讓人採取行動。兩者的設計問題不同：dashboard 的問題是「資訊太多、焦點不明」；alert 的問題是「通知太多、行動不明」。兩者都需要 <a href="/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">ownership</a>、生命週期管理與 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 連結。</p>
<h2 id="dashboard-設計">Dashboard 設計</h2>
<h3 id="sli-導向-vs-指標堆疊">SLI 導向 vs 指標堆疊</h3>
<p>Dashboard 的常見失敗模式是「把所有能拿到的指標都放上去」。二十個 panel、五十條曲線、無法在 3 秒內回答「服務現在健康嗎」。</p>
<p>SLI 導向的 dashboard 從使用者體驗出發：第一排 panel 回答「使用者感受到的健康狀態」（availability、latency percentile、error ratio），第二排回答「健康狀態的原因」（dependency latency、queue depth、resource utilization），第三排回答「趨勢與容量」（traffic growth、storage usage、capacity headroom）。</p>
<p>每個 panel 都應該能回答一個具體問題。如果團隊看了某個 panel 後的反應是「所以呢？」，這個 panel 不是放錯位置就是不該存在。</p>
<h3 id="dashboard-層級">Dashboard 層級</h3>
<p>不同使用者看不同層級的 dashboard。把所有資訊擠在同一個 dashboard 會讓每個角色都找不到自己要的。</p>
<p><strong>Service overview</strong>：on-call 工程師的第一個入口。5-8 個 panel，回答「這個服務現在有沒有問題」。SLI 指標（error rate、latency p99、availability）、最近的 alert、dependency 健康。</p>
<p><strong>Debug dashboard</strong>：事故中的深入診斷入口。按 dependency 分組（database panel group、cache panel group、downstream API panel group），每組顯示延遲、錯誤率、連線數。Panel 數量多但按需展開。</p>
<p><strong>Capacity dashboard</strong>：容量規劃用。週到月級的趨勢圖 — traffic growth、storage usage、connection pool saturation、cost trends。刷新頻率低（每小時或每天），panel 讀 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 或 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 資料。</p>
<p><strong>Business dashboard</strong>：給非工程角色看。轉換率、使用者活躍度、營收指標。資料來源可能不只是觀測訊號，還包括 analytics 跟 business metrics。</p>
<h3 id="dashboard-的查詢效能">Dashboard 的查詢效能</h3>
<p>Dashboard 是觀測查詢設計中「聚合趨勢」模式的主要消費者（見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23</a>）。每個 panel 每 30 秒刷新一次，十個團隊各自有 dashboard 就是每分鐘數百個背景查詢。</p>
<p>Panel 設計時要注意查詢成本：時間範圍越長、raw series 越多、聚合越複雜，query-time cost 越高。長時間趨勢 panel 應該讀 recording rule 或 rollup series，而非每次刷新都掃描 raw data。</p>
<h2 id="alert-設計">Alert 設計</h2>
<h3 id="symptom-based-vs-cause-based">Symptom-based vs cause-based</h3>
<p><a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">Symptom-based alert</a> 觸發在使用者可感知的症狀上 — error rate 升高、latency p99 超過閾值、availability 下降。Cause-based alert 觸發在內部原因上 — CPU &gt; 90%、disk usage &gt; 85%、connection pool exhausted。</p>
<p>Symptom-based 是 alert 設計的起點。原因是：cause-based alert 容易產生大量「系統在忙但使用者沒受影響」的 false alarm。CPU 短暫衝到 95% 然後回落，如果 latency 跟 error rate 都正常，這個 alert 不需要人類介入。</p>
<p>Cause-based alert 的價值是預防性告警 — disk usage 趨勢在兩天後會滿、connection pool 使用率在高峰時逼近上限。這類 alert 不需要立即行動，但需要在工作時間排入 task。把 cause-based alert 設成 warning（不 page）、symptom-based alert 設成 critical（page on-call），能降低 noise。</p>
<h3 id="slo-based-alerting">SLO-based alerting</h3>
<p>SLO-based alerting 用 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 取代固定閾值。不是「error rate &gt; 1% 就告警」，而是「error budget 的消耗速度超過預期就告警」。</p>
<p>Burn rate alerting 的好處是自動適應基線。低流量時段的 1% error rate 可能只是幾筆錯誤、不值得 page；高流量時段的 0.5% error rate 可能代表大量使用者受影響。Burn rate 用「相對於 SLO 允許的錯誤量，目前消耗速度有多快」來判斷嚴重性，比固定閾值更能反映使用者影響。</p>
<p>SLO-based alert 的實作通常用 multi-window burn rate — 短視窗（5 分鐘）抓急性問題、長視窗（1 小時）抓慢性問題。兩個視窗都超過 burn rate 閾值時才觸發，減少單一 spike 造成的 false alarm。</p>
<p>SLI/SLO 訊號的詳細設計見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6</a>。</p>
<h3 id="alert-的必要欄位">Alert 的必要欄位</h3>
<p>每個 alert rule 應該帶以下 metadata，讓收到 page 的 on-call 工程師在 30 秒內知道下一步：</p>
<ul>
<li><strong>Severity</strong>：critical（立即行動）/ warning（工作時間處理）/ info（記錄但不通知）</li>
<li><strong>Runbook link</strong>：對應的 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> URL，描述診斷步驟跟可能的修復動作</li>
<li><strong>Owner</strong>：負責這個 alert 的團隊或服務</li>
<li><strong>Dashboard link</strong>：點進去直接看相關 panel，不用自己找 dashboard</li>
<li><strong>Summary</strong>：一句話描述發生了什麼（<code>checkout error rate &gt; 2% for 5 minutes</code>），而非只有 alert rule 名稱</li>
</ul>
<p>缺少 runbook link 的 alert 等於「通知了但不告訴你做什麼」。On-call 工程師收到不認識的 alert 時，第一反應是 ack 然後繼續觀察 — 這就是 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 的起點。</p>
<h2 id="alert-noise-control">Alert Noise Control</h2>
<h3 id="什麼是-noise">什麼是 noise</h3>
<p>Alert noise 是「觸發了但不需要人類行動」的 alert。包括：</p>
<ul>
<li><strong>False positive</strong>：條件觸發但實際沒問題（短暫 spike 觸發固定閾值、maintenance 期間的預期 error）</li>
<li><strong>Redundant alert</strong>：同一個問題觸發多個 alert（database 慢 → query timeout alert + error rate alert + latency alert 同時觸發）</li>
<li><strong>Stale alert</strong>：條件已經不適用（服務改版後舊 alert rule 沒更新、abandoned service 的 alert 還在）</li>
</ul>
<h3 id="noise-rate-量測">Noise rate 量測</h3>
<p>Noise rate = 不需要行動的 alert / 總 alert。追蹤方式是讓 on-call 工程師在 ack alert 時標記「actionable」或「noise」。月度彙整 noise rate，超過 30% 的 alert rule 進入治理流程（業界常用的基線閾值，Google SRE Workbook 建議 actionable rate 維持在 70% 以上；團隊可依自身容忍度調整）。</p>
<h3 id="降噪手段">降噪手段</h3>
<p><strong>Grouping</strong>：把同一個根因觸發的多個 alert 合併成一則通知。Alertmanager 的 <code>group_by</code> 讓同服務、同 alert name 的 alert 只發一次。</p>
<p><strong>Inhibition</strong>：高嚴重性 alert 抑制低嚴重性。Database down 觸發時，所有依賴該 database 的 query timeout alert 被抑制 — 根因已知、不需要每個症狀都通知。</p>
<p><strong>Silence / maintenance window</strong>：已知的維護活動期間暫停特定 alert。Silence 需要有過期時間，避免永久靜默掩蓋真實問題。</p>
<p><strong>Hysteresis</strong>：alert 觸發需要條件持續 N 分鐘（<code>for: 5m</code>），避免瞬間 spike 觸發。恢復也需要條件持續 N 分鐘，避免「反覆觸發 → 恢復」的 flapping。</p>
<h2 id="runbook-設計">Runbook 設計</h2>
<p><a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">Runbook</a> 是 alert 的行動指南。每個 critical alert 應該連到一份 runbook，描述「收到這個 alert 時該做什麼」。</p>
<p>Runbook 的有效結構：</p>
<ol>
<li><strong>症狀描述</strong>：這個 alert 代表什麼（「checkout error rate 超過 SLO burn rate」）</li>
<li><strong>影響評估</strong>：誰受影響、嚴重程度（「付款功能受影響、影響所有 checkout 流程」）</li>
<li><strong>診斷步驟</strong>：先看哪個 dashboard、查哪些 log、跑哪些 query</li>
<li><strong>可能的修復動作</strong>：restart service、scale up、rollback deployment、failover to backup</li>
<li><strong>升級路徑</strong>：如果 15 分鐘內無法解決，通知誰</li>
</ol>
<p>Runbook 的維護責任跟 alert 的 owner 一致。Alert rule 改了但 runbook 沒更新是常見的退化 — 把 runbook 的 last-reviewed date 作為 alert 治理的審計項目。</p>
<h2 id="dashboard-與-alert-的生命週期">Dashboard 與 Alert 的生命週期</h2>
<p>Dashboard 跟 alert 都有生命週期。建立時有用，但隨服務演進可能變得過時、冗餘或誤導。沒有生命週期管理的 dashboard / alert 系統會累積 debt — dashboard 數量膨脹但無人看、alert rule 堆疊但多數是 noise。</p>
<h3 id="ownership">Ownership</h3>
<p>每個 dashboard 跟每個 alert rule 都需要明確的 owner。Owner 負責：維護 panel / rule 的正確性、定期審視 noise rate 跟使用率、在服務變更時更新對應的 dashboard / alert。</p>
<p>沒有 owner 的 dashboard 跟 alert 應該有過期機制 — 超過 N 天沒有人訪問的 dashboard 標記為候選淘汰、超過 N 天沒有觸發的 alert rule 審視是否仍有意義。</p>
<h3 id="定期審視">定期審視</h3>
<p>Dashboard 跟 alert 的定期審視是 <a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal governance loop</a> 的一部分。每季或每次重大事故後，審視：</p>
<ul>
<li>哪些 alert 的 noise rate 過高、需要調整或刪除</li>
<li>哪些 dashboard 沒人訪問、可以合併或淘汰</li>
<li>事故中是否有缺少的 alert 或 dashboard panel</li>
</ul>
<p>Ownership 矩陣與 metadata 欄位的詳細設計見 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Dashboard 跟 alert 是否有效，最直接的訊號是 alert noise rate 跟 dashboard 訪問頻率 — noise rate 超過 30% 代表通知品質退化，dashboard 長期零訪問代表資訊跟決策脫節。</p>
<p>重點訊號包括：</p>
<ul>
<li>Alert 是否能對應到明確 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、<a href="/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">ownership</a> 與停止條件</li>
<li>Dashboard 是否有固定使用者與更新責任</li>
<li>Threshold 是否對齊 SLO、容量邊界或使用者影響</li>
<li>Noise rate 是否被追蹤並回寫治理流程</li>
<li>Dashboard panel 是否讀 recording rule 而非每次重算 raw data</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Alert 跟 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 沒連、收到 page 不知道做什麼</li>
<li>Dashboard 數量爆量、無 owner、半年無人訪問</li>
<li>同一訊號多個 alert 重複觸發、無 grouping 或 inhibition</li>
<li>Alert noise rate &gt; 30%、ack 後無實際動作，形成 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a></li>
<li>Alert threshold 用直覺數字、沒對齊 SLO / 商業承諾</li>
<li>Dashboard panel 載入慢、因為直接查 raw series 而非 recording rule</li>
<li>Maintenance window 過後 silence 沒移除、真實問題被掩蓋</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>指標堆疊 dashboard</td>
          <td>50 個 panel、看不出服務是否健康</td>
          <td>SLI 導向重構：第一排回答健康、第二排回答原因</td>
      </tr>
      <tr>
          <td>全部 cause-based alert</td>
          <td>CPU / disk / memory alert 頻繁但服務正常</td>
          <td>區分 symptom（page）跟 cause（warning）</td>
      </tr>
      <tr>
          <td>固定閾值 alert</td>
          <td>低流量時 false alarm、高流量時漏報</td>
          <td>改用 SLO burn rate alerting</td>
      </tr>
      <tr>
          <td>Alert 無 runbook</td>
          <td>On-call 收到 page 後自行摸索、MTTR 高</td>
          <td>每個 critical alert 必附 runbook link</td>
      </tr>
      <tr>
          <td>Alert 無 owner</td>
          <td>沒人維護的 alert rule 累積成 noise 來源</td>
          <td>每個 alert rule 帶 owner metadata、定期審視</td>
      </tr>
      <tr>
          <td>Dashboard 無過期機制</td>
          <td>三年累積 200 個 dashboard、多數沒人看</td>
          <td>訪問頻率追蹤 + 定期淘汰審視</td>
      </tr>
      <tr>
          <td>同一問題觸發 N 個 alert</td>
          <td>On-call 同時收到 5 則通知、不知道看哪個</td>
          <td>Alertmanager grouping + inhibition</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing</a>：trace waterfall 作為 dashboard 的診斷入口</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>：alert 的訊號源頭、burn rate alerting 的 SLI 依據</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a>：alert / dashboard 的生命週期維運</li>
<li><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 client-side / RUM</a>：補 server-side 看不到的 dashboard 維度</li>
<li><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14 anomaly detection</a>：rule-based alert 之外的統計訊號</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：dashboard / alert 的 ownership 矩陣與 metadata 欄位</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：dashboard 查詢的效能與 recording rule</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><item><title>T.C4 Client-side log 缺失導致 debug 只能靠實機盲測</title><link>https://tarrragon.github.io/blog/testing/cases/client-log-absent-debug-cost/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/cases/client-log-absent-debug-cost/</guid><description>&lt;p>這個案例的核心責任是說明「客戶端 log 設計」為什麼應該在功能企劃階段完成，而不是 debug 時才補。Log 不是 debug 工具，是可觀測性基礎設施。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的六個核心元件在實機測試前的 log 覆蓋狀態：&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>ConnectionManager&lt;/td>
 &lt;td>0 → 10&lt;/td>
 &lt;td>W2 修復後補的 &lt;code>developer.log&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TerminalScreen&lt;/td>
 &lt;td>0 → 5&lt;/td>
 &lt;td>W2 修復後補的&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TtydProtocol&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>encode/decode/buildAuth 無 log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>BiometricService&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>isAvailable/authenticate 結果無 log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CredentialRepository&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>load/save/delete 操作無 log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>EnrollmentScreen&lt;/td>
 &lt;td>0&lt;/td>
 &lt;td>QR 掃描/解析/儲存無 log&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>W2-004（P0：iOS 實機 WS stream 不觸發）的 debug 過程：無法從任何 log 判斷問題發生在 biometric → credential → WS connect → auth token → stream listen 的哪一步。開發者被迫在每個函式手動加 &lt;code>developer.log&lt;/code>，重新編譯，插拔裝置測試，反覆數次才定位到「stream 訂閱時機」問題。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>debug 成本&lt;/td>
 &lt;td>每次修改→編譯→部署→測試約 3-5 分鐘&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>定位 W2-002 (auth token) 花費&lt;/td>
 &lt;td>約 30 分鐘反覆測試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>若有連線生命週期 log&lt;/td>
 &lt;td>第一次連線就能看到「Step 3 之後無 auth token 發送」&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Log 缺失把 debug 成本從秒級升到分鐘級&lt;/strong>。如果 ConnectionManager 在企劃階段就設計了「Step 1: biometric → Step 2: credential → Step 3: WS connect → Step 4: auth token → Step 5: listen stream」五步 log，W2-002 的 auth token 問題在第一次連線就能從 log 看到「Step 3 完成，Step 4 未執行」。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>「事後補 log」的 log 品質較低&lt;/strong>。W2 修復時補的 &lt;code>developer.log&lt;/code> 格式不統一（有的帶 &lt;code>name:&lt;/code>，有的不帶；有的用 &lt;code>// i18n-exempt&lt;/code> 標記，有的忘了），沒有統一的 log 層級，沒有結構化欄位。事後補的 log 是救火工具，不是可觀測性設計。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>自用工具最適合自架 log 收集&lt;/strong>。app_tunnel 的 server 和 client 都在同一台機器上（或同一個 Tailscale tailnet），client 可以直接打 HTTP POST 到本機的 log endpoint，不需要 Sentry 或 Crashlytics。一個 Go 寫的 JSON log receiver（20 行）+ grep 就是完整的 debug 工具鏈。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Log 設計是功能規格的一部分&lt;/strong>。「連線到 ttyd 終端機」這個功能的規格不只是「建立 WS 連線」，還包含「每步有 log、失敗有 log、成功有 log」。跟 API 規格需要定義 request/response 一樣，連線功能需要定義 log 點。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>功能規格階段列出 log 點清單&lt;/strong>：每個功能的規格文件新增「可觀測性」欄位，列出啟動/步驟/錯誤/完成四類 log 點。&lt;/li>
&lt;li>&lt;strong>建立統一 log 層&lt;/strong>：封裝 &lt;code>developer.log&lt;/code> 為 &lt;code>AppLogger&lt;/code>，統一 name、level、格式。開發期用 &lt;code>developer.log&lt;/code>，後續可切換到 HTTP log endpoint。&lt;/li>
&lt;li>&lt;strong>自架 log endpoint 方案&lt;/strong>：本機 Go server 開一個 &lt;code>/log&lt;/code> POST endpoint，接收 JSON log，寫入檔案。Client 端 &lt;code>AppLogger&lt;/code> 在 debug mode 同時寫 console + POST 到 endpoint。開發期 grep 查詢，不需要 dashboard。&lt;/li>
&lt;li>&lt;strong>Protocol log 獨立一層&lt;/strong>：WebSocket frame type、payload 前綴、auth handshake 結果獨立記錄，跟 business log 分開。這層 log 在 release mode 應該能關閉。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想設計客戶端 log 方案 → &lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/" data-link-title="模組二：客戶端可觀測性" 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/three-layer-log-design/" data-link-title="三層 log 設計" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — 三層各自的職責、詳細程度和啟停控制">三層 log 設計&lt;/a>&lt;/li>
&lt;li>想建自架 log endpoint → &lt;a href="https://tarrragon.github.io/blog/testing/02-client-observability/log-endpoint-tradeoff/" data-link-title="自架 log endpoint vs 商業方案的取捨判斷" data-link-desc="自用工具用自架 log receiver（20 行 Go &amp;#43; grep）、商業 app 用 Sentry/Crashlytics — 判斷依據是使用者規模和 debug 需求">自架 log endpoint vs 商業方案&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「客戶端 log 設計」為什麼應該在功能企劃階段完成，而不是 debug 時才補。Log 不是 debug 工具，是可觀測性基礎設施。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的六個核心元件在實機測試前的 log 覆蓋狀態：</p>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>log 點數</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ConnectionManager</td>
          <td>0 → 10</td>
          <td>W2 修復後補的 <code>developer.log</code></td>
      </tr>
      <tr>
          <td>TerminalScreen</td>
          <td>0 → 5</td>
          <td>W2 修復後補的</td>
      </tr>
      <tr>
          <td>TtydProtocol</td>
          <td>0</td>
          <td>encode/decode/buildAuth 無 log</td>
      </tr>
      <tr>
          <td>BiometricService</td>
          <td>0</td>
          <td>isAvailable/authenticate 結果無 log</td>
      </tr>
      <tr>
          <td>CredentialRepository</td>
          <td>0</td>
          <td>load/save/delete 操作無 log</td>
      </tr>
      <tr>
          <td>EnrollmentScreen</td>
          <td>0</td>
          <td>QR 掃描/解析/儲存無 log</td>
      </tr>
  </tbody>
</table>
<p>W2-004（P0：iOS 實機 WS stream 不觸發）的 debug 過程：無法從任何 log 判斷問題發生在 biometric → credential → WS connect → auth token → stream listen 的哪一步。開發者被迫在每個函式手動加 <code>developer.log</code>，重新編譯，插拔裝置測試，反覆數次才定位到「stream 訂閱時機」問題。</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>debug 成本</td>
          <td>每次修改→編譯→部署→測試約 3-5 分鐘</td>
      </tr>
      <tr>
          <td>定位 W2-002 (auth token) 花費</td>
          <td>約 30 分鐘反覆測試</td>
      </tr>
      <tr>
          <td>若有連線生命週期 log</td>
          <td>第一次連線就能看到「Step 3 之後無 auth token 發送」</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>Log 缺失把 debug 成本從秒級升到分鐘級</strong>。如果 ConnectionManager 在企劃階段就設計了「Step 1: biometric → Step 2: credential → Step 3: WS connect → Step 4: auth token → Step 5: listen stream」五步 log，W2-002 的 auth token 問題在第一次連線就能從 log 看到「Step 3 完成，Step 4 未執行」。</p>
</li>
<li>
<p><strong>「事後補 log」的 log 品質較低</strong>。W2 修復時補的 <code>developer.log</code> 格式不統一（有的帶 <code>name:</code>，有的不帶；有的用 <code>// i18n-exempt</code> 標記，有的忘了），沒有統一的 log 層級，沒有結構化欄位。事後補的 log 是救火工具，不是可觀測性設計。</p>
</li>
<li>
<p><strong>自用工具最適合自架 log 收集</strong>。app_tunnel 的 server 和 client 都在同一台機器上（或同一個 Tailscale tailnet），client 可以直接打 HTTP POST 到本機的 log endpoint，不需要 Sentry 或 Crashlytics。一個 Go 寫的 JSON log receiver（20 行）+ grep 就是完整的 debug 工具鏈。</p>
</li>
<li>
<p><strong>Log 設計是功能規格的一部分</strong>。「連線到 ttyd 終端機」這個功能的規格不只是「建立 WS 連線」，還包含「每步有 log、失敗有 log、成功有 log」。跟 API 規格需要定義 request/response 一樣，連線功能需要定義 log 點。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>功能規格階段列出 log 點清單</strong>：每個功能的規格文件新增「可觀測性」欄位，列出啟動/步驟/錯誤/完成四類 log 點。</li>
<li><strong>建立統一 log 層</strong>：封裝 <code>developer.log</code> 為 <code>AppLogger</code>，統一 name、level、格式。開發期用 <code>developer.log</code>，後續可切換到 HTTP log endpoint。</li>
<li><strong>自架 log endpoint 方案</strong>：本機 Go server 開一個 <code>/log</code> POST endpoint，接收 JSON log，寫入檔案。Client 端 <code>AppLogger</code> 在 debug mode 同時寫 console + POST 到 endpoint。開發期 grep 查詢，不需要 dashboard。</li>
<li><strong>Protocol log 獨立一層</strong>：WebSocket frame type、payload 前綴、auth handshake 結果獨立記錄，跟 business log 分開。這層 log 在 release mode 應該能關閉。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計客戶端 log 方案 → <a href="/blog/testing/02-client-observability/" data-link-title="模組二：客戶端可觀測性" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — log 設計是功能規格的一部分">模組二：客戶端可觀測性</a></li>
<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 endpoint → <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>
</ul>
]]></content:encoded></item><item><title>4.C4 AWS：X-Ray 到 OpenTelemetry 轉換</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/</guid><description>&lt;p>這個案例的核心責任是把觀測遷移從工具替換，提升為標準化策略。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>AWS 已明確提出 X-Ray SDK/Daemon 的維護時程，並提供遷移到 OpenTelemetry 的路徑。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當 observability agent 與 SDK 受限於單一供應商，轉向 OTel 可以降低未來轉移成本，但需要治理採集、匯出與語意對齊。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先盤點現有 instrumentation 與依賴 SDK。&lt;/li>
&lt;li>先換 collector/agent，再逐步改應用端 instrumentation。&lt;/li>
&lt;li>把 trace/metric 的等價驗證納入 release gate。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-migration.html">X-Ray to OpenTelemetry migration guide&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把觀測遷移從工具替換，提升為標準化策略。</p>
<h2 id="觀察">觀察</h2>
<p>AWS 已明確提出 X-Ray SDK/Daemon 的維護時程，並提供遷移到 OpenTelemetry 的路徑。</p>
<h2 id="判讀">判讀</h2>
<p>當 observability agent 與 SDK 受限於單一供應商，轉向 OTel 可以降低未來轉移成本，但需要治理採集、匯出與語意對齊。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先盤點現有 instrumentation 與依賴 SDK。</li>
<li>先換 collector/agent，再逐步改應用端 instrumentation。</li>
<li>把 trace/metric 的等價驗證納入 release gate。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 與 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-migration.html">X-Ray to OpenTelemetry migration guide</a></li>
</ul>
]]></content:encoded></item><item><title>Datadog</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/</guid><description>&lt;p>Datadog 是 all-in-one SaaS observability 平台、承擔三個責任：覆蓋 APM / logs / metrics / RUM / synthetics / security / CI visibility 全訊號類型、auto-instrumentation 廣度業界第一、跟 600+ integrations 即插即用。設計取捨偏向「turnkey + 廣度 + integration」、成本是主要取捨點。&lt;/p>
&lt;p>對「想要 turnkey 體驗、不想自管 observability、多訊號類型統一平台、團隊規模可承擔成本」這條路徑、Datadog 是首選。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>安裝 Datadog Agent、配置 APM auto-instrumentation&lt;/li>
&lt;li>用 Datadog Logs / Metrics / APM 三大查詢介面&lt;/li>
&lt;li>控制 cost（log indexing / metric cardinality / APM trace sampling）&lt;/li>
&lt;li>寫 Monitor as code（Terraform）&lt;/li>
&lt;li>評估 OTLP ingestion 跟 Datadog SDK 的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-datadog-跑起來">最短路徑：5 分鐘把 Datadog 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 安裝 Agent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: DD_API_KEY=&amp;lt;key&amp;gt; DD_SITE=&amp;#34;datadoghq.com&amp;#34; bash -c &amp;#34;$(curl -L ...)&amp;#34;&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="c1"># 2. 啟用 APM&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"># TODO: 在 Agent config 加 apm_config.enabled: true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 應用程式加 ddtrace-run / dd-trace-py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證 Agent + APM 上線&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 在 Datadog UI 看 Host map + APM Service List&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="agent-安裝與配置">Agent 安裝與配置&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>安裝方式：package（apt/yum）/ container / K8s DaemonSet / Lambda extension&lt;/li>
&lt;li>Agent config：core / APM / Logs / NetFlow / SNMP 各 sub-config&lt;/li>
&lt;li>DogStatsD：應用層 custom metrics 入口&lt;/li>
&lt;li>對應指令：&lt;code>datadog-agent status&lt;/code>、&lt;code>/etc/datadog-agent/datadog.yaml&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="apm-自動-instrumentation">APM 自動 instrumentation&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>各語言 tracer：dd-trace-java / dd-trace-py / dd-trace-js / dd-trace-go&lt;/li>
&lt;li>Auto-instrumentation 廣度（業界最廣）&lt;/li>
&lt;li>Service / Resource / Operation 三層 trace 結構&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="logs-配置">Logs 配置&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>採集方式：Agent 採集 / Fluent Bit / Vector → Datadog&lt;/li>
&lt;li>Indexing vs Archives：indexing 費錢但可查、archives 便宜但只能 rehydrate&lt;/li>
&lt;li>Log Pipeline：parsing / enrichment / sensitive data scrubbing&lt;/li>
&lt;li>對應 cost 控制：indexing rate / retention&lt;/li>
&lt;/ul>
&lt;h3 id="metrics">Metrics&lt;/h3>
&lt;p>子議題:&lt;/p></description><content:encoded><![CDATA[<p>Datadog 是 all-in-one SaaS observability 平台、承擔三個責任：覆蓋 APM / logs / metrics / RUM / synthetics / security / CI visibility 全訊號類型、auto-instrumentation 廣度業界第一、跟 600+ integrations 即插即用。設計取捨偏向「turnkey + 廣度 + integration」、成本是主要取捨點。</p>
<p>對「想要 turnkey 體驗、不想自管 observability、多訊號類型統一平台、團隊規模可承擔成本」這條路徑、Datadog 是首選。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>安裝 Datadog Agent、配置 APM auto-instrumentation</li>
<li>用 Datadog Logs / Metrics / APM 三大查詢介面</li>
<li>控制 cost（log indexing / metric cardinality / APM trace sampling）</li>
<li>寫 Monitor as code（Terraform）</li>
<li>評估 OTLP ingestion 跟 Datadog SDK 的取捨</li>
</ol>
<h2 id="最短路徑5-分鐘把-datadog-跑起來">最短路徑：5 分鐘把 Datadog 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 安裝 Agent</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: DD_API_KEY=&lt;key&gt; DD_SITE=&#34;datadoghq.com&#34; bash -c &#34;$(curl -L ...)&#34;</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="c1"># 2. 啟用 APM</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: 在 Agent config 加 apm_config.enabled: true</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># TODO: 應用程式加 ddtrace-run / dd-trace-py</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"># 3. 驗證 Agent + APM 上線</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: 在 Datadog UI 看 Host map + APM Service List</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="agent-安裝與配置">Agent 安裝與配置</h3>
<p>子議題：</p>
<ul>
<li>安裝方式：package（apt/yum）/ container / K8s DaemonSet / Lambda extension</li>
<li>Agent config：core / APM / Logs / NetFlow / SNMP 各 sub-config</li>
<li>DogStatsD：應用層 custom metrics 入口</li>
<li>對應指令：<code>datadog-agent status</code>、<code>/etc/datadog-agent/datadog.yaml</code></li>
</ul>
<h3 id="apm-自動-instrumentation">APM 自動 instrumentation</h3>
<p>子議題：</p>
<ul>
<li>各語言 tracer：dd-trace-java / dd-trace-py / dd-trace-js / dd-trace-go</li>
<li>Auto-instrumentation 廣度（業界最廣）</li>
<li>Service / Resource / Operation 三層 trace 結構</li>
<li>對應 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a></li>
</ul>
<h3 id="logs-配置">Logs 配置</h3>
<p>子議題：</p>
<ul>
<li>採集方式：Agent 採集 / Fluent Bit / Vector → Datadog</li>
<li>Indexing vs Archives：indexing 費錢但可查、archives 便宜但只能 rehydrate</li>
<li>Log Pipeline：parsing / enrichment / sensitive data scrubbing</li>
<li>對應 cost 控制：indexing rate / retention</li>
</ul>
<h3 id="metrics">Metrics</h3>
<p>子議題:</p>
<ul>
<li>Custom metrics（DogStatsD / Agent / API）</li>
<li>Metric Type：count / gauge / histogram / distribution</li>
<li>Cardinality 控制：每 metric 收 tags 數限制</li>
<li>對應 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming cardinality</a></li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="cost-governance-agent-config/">Datadog 成本治理與 Agent 配置</a>：計價模型、custom metrics 成本控制、Agent 部署配置與常見故障</li>
<li><a href="otlp-ingestion-otel-integration/">OTLP Ingestion 與 OTel 整合</a>：Agent OTLP receiver 配置、OTel SDK feature parity、resource mapping 與故障判讀</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="成本治理">成本治理</h3>
<p>子議題：</p>
<ul>
<li>Hosts pricing（vs APM / Logs / Custom Metrics 各自獨立）</li>
<li>Log indexing rate 控制（Exclusion Filters）</li>
<li>Custom metrics 計費（per metric per host）</li>
<li>APM trace sampling</li>
<li>對應 Datadog Usage Attribution</li>
</ul>
<h3 id="otlp-ingestion">OTLP ingestion</h3>
<p>子議題：</p>
<ul>
<li>Datadog Agent 接受 OTLP（gRPC + HTTP）</li>
<li>對 OTel SDK 用戶的優勢（avoid Datadog SDK lock-in）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a></li>
<li>Datadog 自家 SDK vs OTel：feature parity 取捨</li>
</ul>
<h3 id="monitor-as-code">Monitor as code</h3>
<p>子議題：</p>
<ul>
<li>Terraform Datadog provider：dashboard / monitor / SLO / synthetic</li>
<li>跟 IaC pipeline 整合</li>
<li>多環境（dev / staging / prod）配置</li>
</ul>
<h3 id="apm-trace-sampling">APM Trace Sampling</h3>
<p>子議題：</p>
<ul>
<li>Head-based sampling（rate-based）</li>
<li>Tail-based（Datadog 新功能、需 Agent 支援）</li>
<li>Ingestion vs Indexing sampling 兩層</li>
<li>對應 cost 控制</li>
</ul>
<h3 id="rum--synthetics">RUM / Synthetics</h3>
<p>子議題：</p>
<ul>
<li>RUM（Real User Monitoring）：前端用戶體驗</li>
<li>Synthetics：browser test / API test 主動探測</li>
<li>Session Replay</li>
<li>跟 APM 關聯：frontend trace → backend trace</li>
</ul>
<h3 id="security-monitoring">Security Monitoring</h3>
<p>子議題：</p>
<ul>
<li>Cloud SIEM</li>
<li>ASM（Application Security Management、wAF/RASP）</li>
<li>Cloud Security Posture Management</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security 模組</a> 對照</li>
</ul>
<h2 id="跟-monitoring-模組的分工">跟 Monitoring 模組的分工</h2>
<p>本頁從 server-side APM 平台角度說明 Datadog — agent 部署、cost governance、OTel 遷移、跟 Grafana Stack 的對照。Client-side 的 RUM 體驗（RUM SDK 四種事件、session replay、全棧追蹤的 client 端視角）見 <a href="/blog/monitoring/06-commercial-comparison/datadog-rum/" data-link-title="Datadog RUM" data-link-desc="全棧 APM 的 client-side 觀點 — client action 到 server trace 的完整鏈路追蹤">Monitoring 模組 Datadog RUM</a>。</p>
<p>兩者的交叉點是 trace context — RUM SDK 注入的 trace header 讓 client action 跟 server span 串在同一個 trace。沒有 server-side APM 的團隊用 RUM 也有價值（client-side error + performance），但全棧追蹤需要兩邊都部署。</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="agent-連不上-datadog">Agent 連不上 Datadog</h3>
<p>操作原則：先 <code>datadog-agent status</code> 看 connectivity、再看 API key + region。</p>
<h3 id="apm-trace-缺失">APM trace 缺失</h3>
<p>操作原則：trace context propagation 在跨 service / 跨 thread 邊界丟失。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: dd-trace-py debug mode / `DD_TRACE_DEBUG=true`</span></span></span></code></pre></div><h3 id="log-indexing-cost-爆">Log indexing cost 爆</h3>
<p>操作原則：indexed log 量超預期、用 Exclusion Filter 過濾不必要 log。判讀：Datadog Usage page 看每 day indexed log。</p>
<h3 id="custom-metrics-爆預算">Custom metrics 爆預算</h3>
<p>操作原則：每 host 每 metric 計費、cardinality 高（per-user / per-request label）會爆。判讀：Metrics Summary 看 metric volume。</p>
<h3 id="monitor-noise">Monitor noise</h3>
<p>操作原則：alert 太多、低品質、用 Composite Monitor + Recovery / No data threshold。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>預算敏感</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（OSS）/ Cloud（cheaper）</td>
      </tr>
      <tr>
          <td>需要 OSS / self-host</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> + <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></td>
      </tr>
      <tr>
          <td>High-cardinality debug 深度</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>AWS-only + 成本</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a></td>
      </tr>
      <tr>
          <td>純 error tracking</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
      <tr>
          <td>多 vendor 標準化</td>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a> + 任一 backend</td>
      </tr>
      <tr>
          <td>Logs full-text 為主</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 dd-trace SDK 完整 API</li>
<li>Datadog UI 操作詳細</li>
<li>Pricing 詳細計算（用 Datadog Usage page）</li>
<li>600+ integrations 各自設定</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a></td>
          <td>OTLP ingestion + SDK 移轉</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Datadog 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>Datadog Logs Indexing / Archives 作為審計證據面</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming cardinality</a></td>
          <td>Custom metrics cardinality 治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>（反例）Datadog SDK ↔ OTLP 雙軌語意漂移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>中大型常選 Datadog turnkey</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Datadog 案例</strong>：客戶 cost optimization stories、large scale 部署（Shopify / Coinbase / Zoom 等）engineering blog。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>、<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>模組四：可觀測性平台</title><link>https://tarrragon.github.io/blog/backend/04-observability/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/</guid><description>&lt;p>可觀測性模組的核心目標是說明服務如何把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 轉成可操作的診斷系統。語言教材會處理標準 logger、執行環境訊號、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 邊界；本模組負責平台、資料流與操作規則。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 OpenTelemetry / Prometheus / Grafana Stack / Datadog / Elastic Stack / Honeycomb / AWS CloudWatch / GCP Cloud Operations / Sentry，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。Error tracking 是獨立子維度（Sentry），跟 metrics / logs / traces 三角互補。&lt;/p>
&lt;p>進入 vendor 比較前，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">觀測、可靠性與事故服務選型&lt;/a> 判斷目前缺的是訊號層、驗證層、響應層還是閉環層。可觀測性 vendor 選型只處理訊號層與部分告警入口；可靠性驗證與事故協作要交給可靠性與事故流程。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分類&lt;/th>
 &lt;th>內容方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">Log&lt;/a> aggregation&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>、索引、查詢、保留策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics&lt;/a>&lt;/td>
 &lt;td>counter、gauge、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality&lt;/a>、Prometheus&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tracing&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a>、OpenTelemetry&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard&lt;/a>&lt;/td>
 &lt;td>SLI、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a>、容量趨勢、服務健康&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert&lt;/a>&lt;/td>
 &lt;td>alert rule、noise control、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> workflow&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="選型入口">選型入口&lt;/h2>
&lt;p>可觀測性選型的核心判斷是團隊缺少哪一種操作訊號。當工程師需要還原事件脈絡時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>；需要趨勢與容量判斷時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>；需要跨服務路徑時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>；需要共同操作入口時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>；需要主動通知時先看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>可觀測性模組的核心目標是說明服務如何把 <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 與 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 轉成可操作的診斷系統。語言教材會處理標準 logger、執行環境訊號、<a href="/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">Diagnostic Endpoint</a> 與 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 邊界；本模組負責平台、資料流與操作規則。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors</a> — T1 收錄 OpenTelemetry / Prometheus / Grafana Stack / Datadog / Elastic Stack / Honeycomb / AWS CloudWatch / GCP Cloud Operations / Sentry，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。Error tracking 是獨立子維度（Sentry），跟 metrics / logs / traces 三角互補。</p>
<p>進入 vendor 比較前，先回到 <a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">觀測、可靠性與事故服務選型</a> 判斷目前缺的是訊號層、驗證層、響應層還是閉環層。可觀測性 vendor 選型只處理訊號層與部分告警入口；可靠性驗證與事故協作要交給可靠性與事故流程。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">Log</a> aggregation</td>
          <td><a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、索引、查詢、保留策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics</a></td>
          <td>counter、gauge、<a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a>、<a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a>、Prometheus</td>
      </tr>
      <tr>
          <td>Tracing</td>
          <td><a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a>、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a>、OpenTelemetry</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">Dashboard</a></td>
          <td>SLI、<a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a>、容量趨勢、服務健康</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert</a></td>
          <td>alert rule、noise control、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> workflow</td>
      </tr>
  </tbody>
</table>
<h2 id="選型入口">選型入口</h2>
<p>可觀測性選型的核心判斷是團隊缺少哪一種操作訊號。當工程師需要還原事件脈絡時先看 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>；需要趨勢與容量判斷時先看 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>；需要跨服務路徑時先看 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>；需要共同操作入口時先看 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>；需要主動通知時先看 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>。</p>
<p>Log aggregation 適合查單一事件與錯誤脈絡；metrics 適合觀察 error rate、latency、<a href="/blog/backend/knowledge-cards/throughput/" data-link-title="Throughput" data-link-desc="整理系統單位時間內可處理的工作量">throughput</a> 與 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> lag；tracing 適合拆解跨服務 request path；dashboard 適合整合 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO</a> 與容量趨勢；alert 適合把需要動作的異常送到負責者面前，並連到 <a href="/blog/backend/knowledge-cards/alert-runbook/" data-link-title="Alert Runbook" data-link-desc="說明告警如何連到可執行的排障與恢復流程">alert runbook</a>。</p>
<p>接近真實網路服務的例子包括 checkout 變慢、queue lag 上升、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> 斷線增加、Redis <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 增加與下游 API 錯誤率上升。這些場景的共同問題是從症狀回到原因，因此本模組會先處理欄位、關聯、<a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">metric cardinality</a>、查詢、視覺化與告警規則。</p>
<h2 id="訊號情境庫">訊號情境庫</h2>
<p>本模組收的是可重複套用的訊號情境，不收服務級案例庫。服務的長期時間線與事故史，留給可靠性驗證與事故處理兩個模組；可觀測性平台只保留能反覆套用在不同服務上的觀測判讀樣式，讓讀者先知道「該看哪種訊號、如何辨識失真、下一步交給誰」。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>先看訊號</th>
          <th>判讀重點</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>checkout 變慢</td>
          <td>latency <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>、downstream error rate</td>
          <td>先分辨是 app latency、DB wait、cache miss 還是外部依賴慢</td>
          <td>需要驗證回歸時回到 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
      </tr>
      <tr>
          <td>queue lag 上升</td>
          <td><a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、<a href="/blog/backend/knowledge-cards/retry-policy/" data-link-title="Retry Policy" data-link-desc="說明重試策略如何區分暫時性錯誤、永久錯誤與副作用風險">retry policy</a>、<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a> count</td>
          <td>先判斷是 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 不足、downstream 變慢，還是 <a href="/blog/backend/knowledge-cards/redelivery/" data-link-title="Redelivery" data-link-desc="說明 broker 重新投遞訊息時 consumer 需要承擔的重入責任">redelivery</a></td>
          <td>需要壓力驗證與回放時回到 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
      </tr>
      <tr>
          <td>metric cardinality 爆掉</td>
          <td>label explosion、cardinality growth、query latency</td>
          <td>先看是否為維度設計失控、tenant label 過細，或聚合點過多</td>
          <td>需要訊號治理與告警修正時回到 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a></td>
      </tr>
      <tr>
          <td>trace 斷鏈</td>
          <td>missing <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a>、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> propagation error、sample gap</td>
          <td>先看 context 是否跨 thread / task / process 正確傳遞</td>
          <td>需要補 instrumentation 時回到 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
      </tr>
      <tr>
          <td>alert 太吵但真正事件沒被抓到</td>
          <td>alert volume、<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、<a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based alert</a> mismatch</td>
          <td>先判斷是閾值太低、維度太窄，還是只盯症狀而沒盯服務健康指標</td>
          <td>需要事故演練與回寫時回到 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a></td>
      </tr>
  </tbody>
</table>
<p>這種情境庫的責任是定位訊號，服務史由可靠性驗證與事故處理承接。當讀者需要的是平台能力與判讀路由，可觀測性模組的範圍就夠了；當需要的是某個服務怎麼一路演進、怎麼歷次驗證與恢復，那是可靠性與事故模組的工作。</p>
<h2 id="跟可靠性與事故模組的串接">跟可靠性與事故模組的串接</h2>
<p>可觀測性是「觀測 → 驗證 → 事故」閉環的起點，但閉環是雙向的：</p>
<ul>
<li><strong>觀測 → 事故</strong>：訊號（log spike、SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、error rate）觸發告警、進入事故響應流程。判讀邊界由可觀測性定義、響應節奏由事故處理定義。</li>
<li><strong>觀測 → 驗證</strong>：SLO / SLI 量測由可觀測性提供、是 SLO 政策與 chaos hypothesis 的 baseline。沒有可信訊號就沒有可信驗證。</li>
<li><strong>驗證 → 觀測</strong>：驗證需求驅動訊號設計 — chaos experiment 需要新 metric、load test 需要新 dashboard、SLO 政策需要新 alert rule。</li>
<li><strong>事故 → 觀測</strong>：每次事故 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 揭露偵測缺口（symptom-based alert 缺、訊號太晚、cardinality 不足），回寫到訊號治理。</li>
<li><strong>資安 → 觀測</strong>：資安偵測、稽核證據與資料外洩風險會形成新的 log schema、audit log、alert 與 evidence chain 需求。尤其 <a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">偵測覆蓋率與訊號治理</a> 會回寫到訊號治理閉環。</li>
<li><strong>觀測 → 資安</strong>：log、trace、audit log 與 service topology 提供資安 triage 的事實基礎，讓 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">稽核追蹤與責任邊界</a> 能把責任鏈落到可查證資料。</li>
<li><strong>詳細閉環說明</strong>：見 <a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">Observability / Reliability / Incident Response 閉環</a>。</li>
</ul>
<h2 id="跟-monitoring-模組的串接">跟 Monitoring 模組的串接</h2>
<p><a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 模組</a> 聚焦非 server 端 runtime — mobile app、web 頁面、本機腳本的行為蒐集、錯誤回報與 SDK 設計。本模組聚焦 server-side observability。兩者的交叉點是 trace context propagation 和 event transport format。</p>
<ul>
<li><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side / Synthetic / RUM</a>：概念定位、RUM 與 synthetic 的 server-side 整合</li>
<li><a href="/blog/backend/04-observability/client-server-trace-integration/" data-link-title="4.24 Client-to-Server 端到端觀測串接" data-link-desc="用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路">4.24 Client-to-Server 觀測串接</a>：從 browser click 到 server span 的完整 trace 鏈路</li>
<li><a href="/blog/monitoring/telemetry-data-dual-use/" data-link-title="監控資料的雙重用途：行為分析與訊號治理" data-link-desc="同一份 event data 如何同時服務行為分析（funnel / cohort / attribution）和訊號治理（cardinality / cost / signal governance）— 格式交叉、治理衝突與分流架構">監控資料的雙重用途</a>：同一份 event data 如何同時服務行為分析（monitoring/08）與訊號治理（04）</li>
<li><a href="/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15 跨模組 Checkout Episode</a>：從 DB write 到 observability evidence 的四層端到端串聯</li>
</ul>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理如何產生穩定欄位與執行環境訊號。Backend observability 模組處理收集、儲存、查詢、視覺化、告警與跨服務關聯。</p>
<h2 id="企業案例補充">企業案例補充</h2>
<p>可觀測性的案例補充重點是「訊號平台為什麼這樣設計」，不是工具比較表。閱讀時先抓資料規模、查詢延遲、保留策略與多租戶治理，再對照本模組章節。</p>
<table>
  <thead>
      <tr>
          <th>企業案例</th>
          <th>主要觀測選型問題</th>
          <th>優先回讀章節</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://www.uber.com/en-GB/blog/m3/">M3: Uber’s Open Source, Large-scale Metrics Platform for Prometheus</a></td>
          <td>單機 Prometheus 不足時如何擴成平台層</td>
          <td><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2</a>、<a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11</a></td>
      </tr>
      <tr>
          <td><a href="https://blog.cloudflare.com/building-cloudflare-on-cloudflare/">Building Cloudflare on Cloudflare</a></td>
          <td>大規模系統內部如何同時做 logs/metrics/traces</td>
          <td><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1</a>、<a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3</a></td>
      </tr>
      <tr>
          <td><a href="https://blog.cloudflare.com/vision-for-observability/">Cloudflare Observability</a></td>
          <td>監控、分析、鑑識三層能力如何組合</td>
          <td><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
      <tr>
          <td><a href="https://discord.com/blog/how-discord-stores-trillions-of-messages">How Discord Stores Trillions of Messages</a></td>
          <td>成長後如何從儲存問題回推觀測缺口</td>
          <td><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a>、<a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a></td>
      </tr>
  </tbody>
</table>
<p>若要擴充企業案例，先到 <a href="/blog/backend/00-service-selection/enterprise-selection-case-atlas/" data-link-title="0.14 企業選型案例圖譜" data-link-desc="蒐集不同類型與不同規模企業的技術選型案例，作為後端選型判讀的跨情境補充。">0.14 企業選型案例圖譜</a> 依「企業型態 × 規模階段」挑樣本，再把觀測面教訓回寫到 4.16-4.21。這樣案例擴充會先補齊覆蓋度，再補單點技巧。</p>
<p>第一批缺口回填建議先做三條觀測題目：FinTech 補 audit log completeness 與 evidence traceability（回寫 4.12、4.20）；Gaming 補高峰時段 signal freshness 與 cardinality guardrail（回寫 4.7、4.17）；Healthcare 補資料主權相關的 access evidence 與留存邊界（回寫 4.12、4.18）。</p>
<table>
  <thead>
      <tr>
          <th>產業案例類型</th>
          <th>觀測回寫重點</th>
          <th>章節路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FinTech</td>
          <td>金流與帳務事件的 evidence chain、審計 log 完整性</td>
          <td><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
      <tr>
          <td>Gaming</td>
          <td>高峰流量下的訊號新鮮度、cardinality 膨脹與警示品質</td>
          <td><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a></td>
      </tr>
      <tr>
          <td>Healthcare</td>
          <td>存取軌跡可追溯性、資料留存邊界與跨團隊 ownership</td>
          <td><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12</a>、<a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a></td>
      </tr>
  </tbody>
</table>
<p>第一批案例正文入口見 <a href="/blog/backend/04-observability/cases/" data-link-title="可觀測性案例正文" data-link-desc="模組四案例正文入口，將企業案例補充轉成可回寫的訊號判讀文章。">可觀測性案例正文</a>，可直接對應 <code>4.12 / 4.17 / 4.18 / 4.20</code> 的回寫欄位。</p>
<p>第二批觀測遷移案例已補： <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray 到 OTel 轉換</a> 與 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP 導入</a>。兩者可直接回寫到 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a> 與 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>。</p>
<p>反例與規模對照入口： <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 反例</a> / <a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 對照</a>。</p>
<p>回退判讀寫法見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/#%e5%9b%9e%e9%80%80%e5%88%a4%e8%ae%80%e5%af%ab%e6%b3%95" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 回退判讀寫法</a>，觀測案例要優先保留訊號語意、採樣策略、告警偏差與 SLO 判讀差異。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>可觀測性使用方式會受語言的 logger 生態、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a>、exception/error model、執行環境 metrics 與 instrumentation SDK 影響。同步 runtime 要保留 request context 與 thread-local 邊界；async runtime 要確認 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 能跨 task 傳遞；輕量並發 runtime 要觀察 task/goroutine 數量、queue lag 與下游等待。動態語言要特別管理 log schema 穩定性；強型別語言則要避免過度包裝導致 trace 與 error chain 斷裂。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1</a></td>
          <td>log schema 與搜尋規劃</td>
          <td>設計欄位、索引與查詢方式</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2</a></td>
          <td>metrics 與 SLI/SLO</td>
          <td>用 counter、gauge、histogram 描述服務健康</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3</a></td>
          <td>tracing 與 context link</td>
          <td>追蹤跨服務 request path</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a></td>
          <td>dashboard 與 alert 設計</td>
          <td>讓告警能對應 runbook 與容量趨勢</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/attacker-view-observability-risks/" data-link-title="4.5 可觀測性威脅建模（Threat Modeling）" data-link-desc="從觀測盲區、告警失真與資料暴露風險，盤點 observability 的主要弱點">4.5</a></td>
          <td>可觀測性威脅建模（Threat Modeling）</td>
          <td>用盲區、告警失真與資料暴露風險盤點觀測系統</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6</a></td>
          <td>SLI 量測與 SLO 訊號設計</td>
          <td>把可靠性目標轉成可量測訊號、餵給 6.6 SLO 政策</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a></td>
          <td>Cardinality 治理與成本邊界</td>
          <td>把 cardinality 與保留階梯作為平台一級治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8</a></td>
          <td>訊號治理閉環</td>
          <td>把 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 偵測缺口回寫成新訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9</a></td>
          <td>Continuous Profiling</td>
          <td>把 CPU / heap / lock profile 升級為持續訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10</a></td>
          <td>Client-side / Synthetic / RUM</td>
          <td>補 server-side 看不到的 user perceived 訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11</a></td>
          <td>Telemetry Pipeline 架構</td>
          <td>把採集到查詢分層治理、定位 pipeline 失敗</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12</a></td>
          <td>Audit Log 邊界與 PII 治理</td>
          <td>把稽核訊號從 operational log 拆出、按法規治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13</a></td>
          <td>Service Topology 與 Dependency Map</td>
          <td>把跨服務依賴變成自動發現的觀測訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14</a></td>
          <td>Anomaly Detection</td>
          <td>ML / statistical baseline alert 跟 rule-based 整合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15</a></td>
          <td>Cost Attribution / Chargeback</td>
          <td>把 observability 成本拆到團隊 / 服務維度</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">4.16</a></td>
          <td>Observability Readiness Review</td>
          <td>在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a></td>
          <td>Telemetry Data Quality</td>
          <td>把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a></td>
          <td>Observability Operating Model</td>
          <td>定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/debuggability-by-design/" data-link-title="4.19 Debuggability by Design" data-link-desc="把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計">4.19</a></td>
          <td>Debuggability by Design</td>
          <td>把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
          <td>Observability Evidence Package</td>
          <td>把 log、metric、trace、audit 與資料品質限制包成可交接證據</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21</a></td>
          <td>Rule-level CPU Signal Governance</td>
          <td>把規則執行成本變成可觀測訊號，避免小變更在全域 rollout 後形成 CPU 熱點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22</a></td>
          <td>Checkout API Evidence Package 實作示範</td>
          <td>以 checkout 路徑示範 evidence package 如何交接到 gate 與 incident</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23</a></td>
          <td>觀測查詢設計</td>
          <td>把讀取路徑當系統設計問題：三種查詢模式、storage tiering、pre-aggregation 與資源治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/client-server-trace-integration/" data-link-title="4.24 Client-to-Server 端到端觀測串接" data-link-desc="用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路">4.24</a></td>
          <td>Client-to-Server 端到端觀測串接</td>
          <td>用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>註：4.1-4.24 已完成概念層、實作示範與端到端串接正文，案例庫可支援 06 與 08 的路由引用。後續工作重點為案例深挖與跨模組回寫密度提升，而非章節補齊。</p></blockquote>
<h2 id="個案前拓展空間">個案前拓展空間</h2>
<p>個案前拓展的責任是補足讀案例時需要的判讀框架。04 適合補「訊號是否足以支援判讀」這類跨服務能力，不適合展開單一服務的事故史。</p>
<table>
  <thead>
      <tr>
          <th>拓展方向</th>
          <th>補充理由</th>
          <th>先放位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Observability Readiness Review</td>
          <td>服務上線前需要先知道訊號是否支援事故分級與驗證</td>
          <td>4.16</td>
      </tr>
      <tr>
          <td>Telemetry Data Quality</td>
          <td>觀測資料本身也會缺漏、漂移、偏誤與時間錯位</td>
          <td>4.17</td>
      </tr>
      <tr>
          <td>Observability Operating Model</td>
          <td>dashboard、alert、成本與淘汰需要明確 owner</td>
          <td>4.18</td>
      </tr>
      <tr>
          <td>Debuggability by Design</td>
          <td>診斷能力需要進入 API / async / dependency 設計</td>
          <td>4.19</td>
      </tr>
  </tbody>
</table>
<p>本輪先完成這四個前置控制面，讓後續 06 與 08 文章有穩定的訊號前提可引用。若服務案例暴露的是訊號分類問題，回寫 4.16；若暴露的是資料品質問題，回寫 4.17；若暴露的是 owner 與治理問題，回寫 4.18；若暴露的是架構本身難以診斷，回寫 4.19。</p>
<h2 id="後續深化方向">後續深化方向</h2>
<p>04 後續深化以「案例反例補強、跨模組回寫、證據欄位對齊」為主。可觀測性是 06 與 08 的輸入層，重點在提高 evidence package、data quality 與 incident write-back 的銜接精度。</p>
<table>
  <thead>
      <tr>
          <th>深化方向</th>
          <th>主要責任</th>
          <th>回寫路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>案例反例補強</td>
          <td>補齊遷移失敗與訊號失真案例</td>
          <td><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
      </tr>
      <tr>
          <td>跨模組對位</td>
          <td>把觀測欄位對齊 release/incident 決策欄位</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
      </tr>
      <tr>
          <td>成本與治理</td>
          <td>把採樣、cardinality、chargeback 連到 owner 決策</td>
          <td><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>、<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15</a></td>
      </tr>
  </tbody>
</table>
<h2 id="實作探討入口">實作探討入口</h2>
<p>進入實作層時，04 建議先從一條最小切片開始：同一個 user journey 建立 <code>SLI + dashboard + alert + evidence query</code> 四件組，再把欄位直接接到 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 Verification Evidence Handoff</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>
<p>首篇示範已完成： <a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22 Checkout API Evidence Package 實作示範</a>。</p>
<p>完成條件是每篇都能回答四件事：判讀訊號、風險代價、控制面邊界與下一步路由。這樣 06 的 SLO / readiness / experiment safety 與 08 的 intake / decision log / impact assessment 才能引用 04，而不需要在各自章節重寫觀測前提。</p>
<h2 id="跟-infra-可觀測性的分界">跟 Infra 可觀測性的分界</h2>
<p><a href="/blog/infra/06-observability-logging/" data-link-title="模組六：可觀測性與 log 一併寫進 code" data-link-desc="log group、metric、alarm 跟基礎設施同生命週期管理，出事時追得到查得到">Infra 模組六：可觀測性與 log</a> 處理基礎設施層的訊號 — log group、CloudWatch metric、alarm 跟資源同生命週期的 IaC 管理。本模組處理應用層的訊號 — 服務的延遲、錯誤率、trace、業務指標。分界的判讀是：這個訊號是「資源建立時就該存在」還是「功能開發時才埋」——前者進 infra 的 IaC，後者進本模組的應用程式碼。事故排查時兩層合流：infra alarm 告訴你哪個資源異常，本模組的 trace 告訴你哪個請求路徑受影響。</p>
]]></content:encoded></item><item><title>4.5 可觀測性威脅建模（Threat Modeling）</title><link>https://tarrragon.github.io/blog/backend/04-observability/attacker-view-observability-risks/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/attacker-view-observability-risks/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>觀測系統為什麼需要威脅建模&lt;/li>
&lt;li>三類弱點：觀測盲區、告警失真、資料暴露&lt;/li>
&lt;li>每類弱點的判讀流程與修復方向&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安&lt;/a> 的分工&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>可觀測性威脅建模的判讀目標是「觀測系統本身有哪些弱點會讓事故更難處理、更慢收斂、或擴大成資安事件」。觀測系統是事故處理的核心工具 — 工具失靈時，事故的 MTTD（偵測時間）跟 MTTR（修復時間）都會被拉長。&lt;/p>
&lt;p>本章用三類弱點盤點觀測系統：觀測盲區（看不到問題）、告警失真（看到錯的東西）、資料暴露（觀測資料本身變成風險）。每類弱點有各自的判讀流程跟修復方向。&lt;/p>
&lt;p>跟傳統資安威脅建模的差異：資安威脅建模聚焦「攻擊者怎麼入侵系統」；觀測威脅建模聚焦「觀測系統的設計缺陷怎麼讓事故更難處理」。兩者的交叉點在資料暴露 — 觀測資料含 secret 或 PII 時，觀測弱點直接成為資安弱點。&lt;/p>
&lt;h2 id="哪些服務要先做觀測弱點盤點">哪些服務要先做觀測弱點盤點&lt;/h2>
&lt;p>下列情境同時出現時，觀測弱點會快速放大：&lt;/p>
&lt;ul>
&lt;li>服務數量增加，跨服務呼叫變深 — &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 斷鏈的影響面擴大&lt;/li>
&lt;li>值班依賴告警，但告警常常失真或過量 — &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a> 讓真正的問題被淹沒&lt;/li>
&lt;li>調查事故高度依賴人工搜尋 log — 缺少結構化查詢入口&lt;/li>
&lt;li>支援工具與觀測平台可接觸敏感資料 — 觀測資料的存取控制不足&lt;/li>
&lt;/ul>
&lt;h2 id="弱點一觀測盲區">弱點一：觀測盲區&lt;/h2>
&lt;p>觀測盲區是「問題存在但觀測系統看不到」的狀態。盲區的危險在於它讓團隊對系統狀態的判斷建立在不完整的資訊上 — 看起來一切正常，但其實有路徑沒被觀測到。&lt;/p>
&lt;h3 id="常見盲區">常見盲區&lt;/h3>
&lt;p>&lt;strong>Sampling 導致的盲區&lt;/strong>：head sampling 按固定比例丟棄 trace，低流量服務的錯誤樣本可能全部被丟。事故時查 trace 查不到，因為 sampling 把剛好那些 request 的 trace 丟了。修復方向是 tail sampling 或 minimum sample floor（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 sampling 策略&lt;/a>）。&lt;/p>
&lt;p>&lt;strong>Uninstrumented 路徑&lt;/strong>：新上線的服務沒加 instrumentation、async worker 沒有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a>、third-party SDK 的 HTTP call 沒被攔截。這些路徑在 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">service graph&lt;/a> 上不存在，事故時團隊甚至不知道有這條依賴。修復方向是把 instrumentation coverage 作為 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">readiness review&lt;/a> 的檢查項。&lt;/p>
&lt;p>&lt;strong>Context 斷鏈形成的局部盲區&lt;/strong>：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 在 queue、thread pool、background job 邊界斷掉後，下游的 span 成為孤兒。團隊可以看到下游服務有問題，但看不到跟上游 request 的因果關係。修復策略見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing&lt;/a>。&lt;/p>
&lt;p>&lt;strong>Log schema 漂移&lt;/strong>：不同服務的 log 用不同欄位名稱記錄同一個概念（&lt;code>request_id&lt;/code> vs &lt;code>req_id&lt;/code> vs &lt;code>requestId&lt;/code>）。查詢時用 &lt;code>request_id&lt;/code> 搜尋會漏掉用其他名稱的服務。修復方向是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">log schema&lt;/a> 的跨服務統一。&lt;/p>
&lt;h3 id="盲區的判讀方式">盲區的判讀方式&lt;/h3>
&lt;ul>
&lt;li>列出所有服務，標記哪些有 trace instrumentation、哪些沒有&lt;/li>
&lt;li>檢查 service graph 跟已知 architecture diagram 的差異 — 差異就是盲區&lt;/li>
&lt;li>用已知的跨服務 request 做 end-to-end trace 驗證，看有沒有斷點&lt;/li>
&lt;li>檢查 sampling policy，確認低流量服務跟 error sample 的保留率&lt;/li>
&lt;/ul>
&lt;h2 id="弱點二告警失真">弱點二：告警失真&lt;/h2>
&lt;p>告警失真是「觀測系統看到了、但告訴你的是錯的或沒用的」。失真比盲區更危險 — 盲區至少讓團隊知道「這裡沒資料、要用其他方式查」；失真讓團隊基於錯誤訊號做判斷。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>觀測系統為什麼需要威脅建模</li>
<li>三類弱點：觀測盲區、告警失真、資料暴露</li>
<li>每類弱點的判讀流程與修復方向</li>
<li>跟 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a> 跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a> 的分工</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>可觀測性威脅建模的判讀目標是「觀測系統本身有哪些弱點會讓事故更難處理、更慢收斂、或擴大成資安事件」。觀測系統是事故處理的核心工具 — 工具失靈時，事故的 MTTD（偵測時間）跟 MTTR（修復時間）都會被拉長。</p>
<p>本章用三類弱點盤點觀測系統：觀測盲區（看不到問題）、告警失真（看到錯的東西）、資料暴露（觀測資料本身變成風險）。每類弱點有各自的判讀流程跟修復方向。</p>
<p>跟傳統資安威脅建模的差異：資安威脅建模聚焦「攻擊者怎麼入侵系統」；觀測威脅建模聚焦「觀測系統的設計缺陷怎麼讓事故更難處理」。兩者的交叉點在資料暴露 — 觀測資料含 secret 或 PII 時，觀測弱點直接成為資安弱點。</p>
<h2 id="哪些服務要先做觀測弱點盤點">哪些服務要先做觀測弱點盤點</h2>
<p>下列情境同時出現時，觀測弱點會快速放大：</p>
<ul>
<li>服務數量增加，跨服務呼叫變深 — <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 斷鏈的影響面擴大</li>
<li>值班依賴告警，但告警常常失真或過量 — <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 讓真正的問題被淹沒</li>
<li>調查事故高度依賴人工搜尋 log — 缺少結構化查詢入口</li>
<li>支援工具與觀測平台可接觸敏感資料 — 觀測資料的存取控制不足</li>
</ul>
<h2 id="弱點一觀測盲區">弱點一：觀測盲區</h2>
<p>觀測盲區是「問題存在但觀測系統看不到」的狀態。盲區的危險在於它讓團隊對系統狀態的判斷建立在不完整的資訊上 — 看起來一切正常，但其實有路徑沒被觀測到。</p>
<h3 id="常見盲區">常見盲區</h3>
<p><strong>Sampling 導致的盲區</strong>：head sampling 按固定比例丟棄 trace，低流量服務的錯誤樣本可能全部被丟。事故時查 trace 查不到，因為 sampling 把剛好那些 request 的 trace 丟了。修復方向是 tail sampling 或 minimum sample floor（見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 sampling 策略</a>）。</p>
<p><strong>Uninstrumented 路徑</strong>：新上線的服務沒加 instrumentation、async worker 沒有 <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a>、third-party SDK 的 HTTP call 沒被攔截。這些路徑在 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">service graph</a> 上不存在，事故時團隊甚至不知道有這條依賴。修復方向是把 instrumentation coverage 作為 <a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">readiness review</a> 的檢查項。</p>
<p><strong>Context 斷鏈形成的局部盲區</strong>：<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 在 queue、thread pool、background job 邊界斷掉後，下游的 span 成為孤兒。團隊可以看到下游服務有問題，但看不到跟上游 request 的因果關係。修復策略見 <a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing</a>。</p>
<p><strong>Log schema 漂移</strong>：不同服務的 log 用不同欄位名稱記錄同一個概念（<code>request_id</code> vs <code>req_id</code> vs <code>requestId</code>）。查詢時用 <code>request_id</code> 搜尋會漏掉用其他名稱的服務。修復方向是 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">log schema</a> 的跨服務統一。</p>
<h3 id="盲區的判讀方式">盲區的判讀方式</h3>
<ul>
<li>列出所有服務，標記哪些有 trace instrumentation、哪些沒有</li>
<li>檢查 service graph 跟已知 architecture diagram 的差異 — 差異就是盲區</li>
<li>用已知的跨服務 request 做 end-to-end trace 驗證，看有沒有斷點</li>
<li>檢查 sampling policy，確認低流量服務跟 error sample 的保留率</li>
</ul>
<h2 id="弱點二告警失真">弱點二：告警失真</h2>
<p>告警失真是「觀測系統看到了、但告訴你的是錯的或沒用的」。失真比盲區更危險 — 盲區至少讓團隊知道「這裡沒資料、要用其他方式查」；失真讓團隊基於錯誤訊號做判斷。</p>
<h3 id="常見失真模式">常見失真模式</h3>
<p><strong>Threshold drift</strong>：alert 的閾值在設定時是合理的（error rate &gt; 1%），但服務改版後基線變了（正常 error rate 從 0.1% 變成 0.5%），閾值沒跟著調。結果是 alert 頻繁觸發但團隊知道是 false alarm — <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 開始累積。</p>
<p><strong>Aggregation 掩蓋</strong>：用 average latency 做 alert，tail latency 被掩蓋。Average 200ms 但 p99 是 5 秒 — 1% 的使用者體驗極差但 alert 沒觸發。修復方向是 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a> 跟 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a>。</p>
<p><strong>Alert storm</strong>：單一根因觸發大量 alert（database 慢 → 所有依賴該 database 的服務都觸發 latency alert + error alert + timeout alert）。On-call 收到 20 則通知，分不清哪個是因、哪個是果。修復方向是 alert grouping 跟 inhibition（見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>）。</p>
<p><strong>Stale dashboard</strong>：Dashboard 的 panel 引用的 metric name 已改名、panel 的 query 因 label 變更而回空值。Dashboard 看起來正常（曲線是平的），但其實是 no data 被渲染成 zero。修復方向是 dashboard 的 no-data alert 跟定期審視。</p>
<h3 id="失真的判讀方式">失真的判讀方式</h3>
<ul>
<li>追蹤 alert noise rate（每月有多少 alert 是 actionable 的）</li>
<li>檢查 alert rule 的 threshold 跟當前 baseline 是否對齊</li>
<li>確認 SLI 用 percentile 而非 average</li>
<li>事故復盤時問「這次的事故，alert 有沒有在對的時間告訴我們對的事」</li>
</ul>
<h2 id="弱點三資料暴露">弱點三：資料暴露</h2>
<p>觀測資料本身是風險資產。Log 可能含 secret（API key、token、password）、trace 可能含 PII（使用者 email、電話號碼在 span attribute 中）、dashboard 可能對所有人開放且顯示敏感業務指標。</p>
<h3 id="常見暴露路徑">常見暴露路徑</h3>
<p><strong>Log 含 secret</strong>：SDK 或框架在 error 發生時把完整 request body 寫進 log，body 中的 API key、token、password 跟著進入 log storage。Log storage 的存取控制通常比 secret manager 寬鬆 — 有 log 讀取權限的人都能看到 secret。</p>
<p><strong>Trace attribute 含 PII</strong>：<code>http.url</code> attribute 帶完整 URL（含 query parameter 裡的 email 或 token）、<code>db.statement</code> attribute 帶完整 SQL（含 WHERE 子句的使用者 ID）。Trace storage 的保留期可能比業務資料庫長，PII 在 trace 裡存活的時間超過必要範圍。</p>
<p><strong>Dashboard 權限過寬</strong>：所有工程師都能看所有服務的 dashboard，包含財務相關的 metric（營收、訂單金額分布）。Dashboard 的存取控制粒度通常是「整個 Grafana instance」而非「per-dashboard」。</p>
<p><strong>Collector / pipeline 有管理員權限</strong>：OTel Collector 或 log aggregator 以 admin 權限部署，可以讀寫 secret、修改配置、存取所有資料。Collector 被入侵時，攻擊者可以把 redaction 規則關掉、讓後續的 log 全量暴露。</p>
<h3 id="暴露的修復方向">暴露的修復方向</h3>
<ul>
<li>SDK 端做 redaction（在送出前掃描已知 secret pattern 並替換成 <code>[REDACTED]</code>）</li>
<li>Collector 端做 attribute 過濾（在 pipeline 中移除敏感 attribute）</li>
<li>Log / trace storage 做存取控制（RBAC、per-team 隔離）</li>
<li>Dashboard 做權限分層（業務 dashboard 需要額外授權）</li>
<li>定期掃描 log storage 檢查是否有未 redact 的 secret pattern</li>
</ul>
<p>詳見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安與資料保護</a> 跟 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 audit log governance</a>。</p>
<h2 id="設計取捨訊號完整度與成本控制">設計取捨：訊號完整度與成本控制</h2>
<p>觀測覆蓋越完整，盲區越少、事故定位越快。同時儲存、查詢與維護成本也會上升。穩定做法是先定義核心訊號與最低欄位（<a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">log schema</a> 的 correlation fields、<a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">SLI</a> 的 availability + latency），再按高風險路徑逐步加深觀測。</p>
<p>「全收」的成本問題見 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a>；「選擇性收」的品質問題見 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀觀測弱點時，按三類依序盤點：</p>
<ol>
<li><strong>盲區</strong>：哪些服務或路徑沒有被觀測到？Sampling 是否丟掉高價值樣本？</li>
<li><strong>失真</strong>：Alert noise rate 有多高？Threshold 跟 baseline 是否對齊？SLI 用的是 average 還是 percentile？</li>
<li><strong>暴露</strong>：Log / trace 是否含 secret 或 PII？Dashboard 權限是否過寬？Collector 的存取權限是否最小化？</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故時查 trace 查不到（sampling 丟掉）</li>
<li>Service graph 跟 architecture diagram 有明顯差異（uninstrumented 服務）</li>
<li>Alert noise rate &gt; 30%（threshold drift 或 aggregation 掩蓋）</li>
<li>同一事故觸發 10+ 個 alert（alert storm、缺 grouping / inhibition）</li>
<li>Log grep 到 API key 或 token（redaction 缺失）</li>
<li>Dashboard 對所有人開放且顯示營收指標（權限過寬）</li>
</ul>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing</a>：context 斷鏈的修復策略</li>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：alert noise control、grouping、inhibition</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a>：sampling 策略與保留決策</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal governance</a>：alert / dashboard 的定期審視</li>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 audit log</a>：觀測資料的存取控制與稽核</li>
<li><a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">4.16 readiness review</a>：instrumentation coverage 的上線前檢查</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：sampling bias 跟 schema drift 的品質問題</li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a>：secret management、data masking、存取控制</li>
</ul>
]]></content:encoded></item><item><title>4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/</guid><description>&lt;p>這個案例的核心責任是說明 observability 平台轉換常來自資料通道標準化需求。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Google Cloud 在 Cloud Trace 提供 OTLP 支援，降低應用程式對特定傳輸介面的綁定。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當團隊要跨多環境與多工具，標準化傳輸協定能減少重複 instrumentation 與遷移摩擦。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>將 collector 與 in-process exporter 對齊 OTLP。&lt;/li>
&lt;li>把 trace schema 與 sampling 規則集中治理。&lt;/li>
&lt;li>在遷移期保留舊通道與新通道比對。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 observability operating model&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://cloud.google.com/blog/products/management-tools/opentelemetry-now-in-google-cloud-observability">OTLP in Google Cloud Observability&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 observability 平台轉換常來自資料通道標準化需求。</p>
<h2 id="觀察">觀察</h2>
<p>Google Cloud 在 Cloud Trace 提供 OTLP 支援，降低應用程式對特定傳輸介面的綁定。</p>
<h2 id="判讀">判讀</h2>
<p>當團隊要跨多環境與多工具，標準化傳輸協定能減少重複 instrumentation 與遷移摩擦。</p>
<h2 id="策略">策略</h2>
<ol>
<li>將 collector 與 in-process exporter 對齊 OTLP。</li>
<li>把 trace schema 與 sampling 規則集中治理。</li>
<li>在遷移期保留舊通道與新通道比對。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 與 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 observability operating model</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/management-tools/opentelemetry-now-in-google-cloud-observability">OTLP in Google Cloud Observability</a></li>
</ul>
]]></content:encoded></item><item><title>Elastic Stack</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/</guid><description>&lt;p>Elastic Stack（前 ELK）是 logs-heavy observability 棧、承擔三個責任：Elasticsearch 搜尋與分析（full-text + structured query）、Beats / Logstash 採集 pipeline、Kibana 視覺化 + Elastic APM（traces）。設計取捨偏向「搜尋為核心 + 統一搜尋介面 + Elastic Security SIEM 整合」。AWS 因 2021 license 變動 fork OpenSearch、提供 Apache 2.0 替代。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>部署 Elasticsearch + Kibana + Beats 基本棧&lt;/li>
&lt;li>用 KQL / Lucene 查詢 logs、用 ES DSL 寫進階搜尋&lt;/li>
&lt;li>設計 index lifecycle（hot / warm / cold / frozen）&lt;/li>
&lt;li>評估 Beats / Logstash / Fluent Bit / Vector 的採集選擇&lt;/li>
&lt;li>評估 Elastic License vs OpenSearch fork 的取捨&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-elastic-stack-跑起來">最短路徑：5 分鐘把 Elastic Stack 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 用 docker-compose 跑 ES + Kibana&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker-compose.yml with elasticsearch + kibana&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="c1"># 2. 用 Filebeat 採集 host logs&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"># TODO: filebeat.yml with inputs + output.elasticsearch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 在 Kibana 查詢驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: KQL: `@timestamp &amp;gt;= now-15m AND log.level: &amp;#34;error&amp;#34;`&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="採集-pipeline">採集 pipeline&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Beats（Filebeat / Metricbeat / Packetbeat / Heartbeat / Auditbeat）：輕量、各自專屬&lt;/li>
&lt;li>Logstash：重型 ETL（grok parsing / enrichment / 多 output）&lt;/li>
&lt;li>Fluent Bit / Vector：替代採集 agent（更輕量、OSS）&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS&lt;/a> 對照&lt;/li>
&lt;/ul>
&lt;h3 id="查詢語法">查詢語法&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>KQL（Kibana Query Language）：直覺、適合日常查詢&lt;/li>
&lt;li>Lucene query string：複雜搜尋、boolean operators&lt;/li>
&lt;li>ES DSL（JSON）：API 級進階查詢&lt;/li>
&lt;li>ES|QL（Elastic Query Language、ES 8.11+）：類 SQL pipeline 語法&lt;/li>
&lt;/ul>
&lt;h3 id="index-設計">Index 設計&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Index template（mapping / settings）&lt;/li>
&lt;li>Data streams（time-series log / metrics）&lt;/li>
&lt;li>Field types：keyword / text / date / numeric / object / nested&lt;/li>
&lt;li>Dynamic mapping 風險：unbounded field 爆 index&lt;/li>
&lt;/ul>
&lt;h3 id="index-lifecycle-managementilm">Index Lifecycle Management（ILM）&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Elastic Stack（前 ELK）是 logs-heavy observability 棧、承擔三個責任：Elasticsearch 搜尋與分析（full-text + structured query）、Beats / Logstash 採集 pipeline、Kibana 視覺化 + Elastic APM（traces）。設計取捨偏向「搜尋為核心 + 統一搜尋介面 + Elastic Security SIEM 整合」。AWS 因 2021 license 變動 fork OpenSearch、提供 Apache 2.0 替代。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>部署 Elasticsearch + Kibana + Beats 基本棧</li>
<li>用 KQL / Lucene 查詢 logs、用 ES DSL 寫進階搜尋</li>
<li>設計 index lifecycle（hot / warm / cold / frozen）</li>
<li>評估 Beats / Logstash / Fluent Bit / Vector 的採集選擇</li>
<li>評估 Elastic License vs OpenSearch fork 的取捨</li>
</ol>
<h2 id="最短路徑5-分鐘把-elastic-stack-跑起來">最短路徑：5 分鐘把 Elastic Stack 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 用 docker-compose 跑 ES + Kibana</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: docker-compose.yml with elasticsearch + kibana</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="c1"># 2. 用 Filebeat 採集 host logs</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: filebeat.yml with inputs + output.elasticsearch</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 3. 在 Kibana 查詢驗證</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: KQL: `@timestamp &gt;= now-15m AND log.level: &#34;error&#34;`</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="採集-pipeline">採集 pipeline</h3>
<p>子議題：</p>
<ul>
<li>Beats（Filebeat / Metricbeat / Packetbeat / Heartbeat / Auditbeat）：輕量、各自專屬</li>
<li>Logstash：重型 ETL（grok parsing / enrichment / 多 output）</li>
<li>Fluent Bit / Vector：替代採集 agent（更輕量、OSS）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a> 對照</li>
</ul>
<h3 id="查詢語法">查詢語法</h3>
<p>子議題：</p>
<ul>
<li>KQL（Kibana Query Language）：直覺、適合日常查詢</li>
<li>Lucene query string：複雜搜尋、boolean operators</li>
<li>ES DSL（JSON）：API 級進階查詢</li>
<li>ES|QL（Elastic Query Language、ES 8.11+）：類 SQL pipeline 語法</li>
</ul>
<h3 id="index-設計">Index 設計</h3>
<p>子議題：</p>
<ul>
<li>Index template（mapping / settings）</li>
<li>Data streams（time-series log / metrics）</li>
<li>Field types：keyword / text / date / numeric / object / nested</li>
<li>Dynamic mapping 風險：unbounded field 爆 index</li>
</ul>
<h3 id="index-lifecycle-managementilm">Index Lifecycle Management（ILM）</h3>
<p>子議題：</p>
<ul>
<li>Hot phase：active write</li>
<li>Warm phase：read-only、查詢頻率低</li>
<li>Cold phase：searchable snapshot（S3 / object storage）</li>
<li>Frozen phase（ES 7.12+）：searchable snapshot + minimal cluster resource</li>
<li>Delete phase</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="ilm-log-pipeline/">Index Lifecycle Management 與 Log Pipeline</a>：ILM policy 設計、data stream / rollover、Beats vs Elastic Agent 採集選擇、ingest pipeline 與 shard sizing、cost governance</li>
</ul>
<h2 id="migration-playbook">Migration Playbook</h2>
<ul>
<li><a href="migrate-to-elastic-cloud/">Elastic Cloud 遷移</a>：自管 Elastic Stack 遷移到 Elastic Cloud</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="elastic-apm">Elastic APM</h3>
<p>子議題：</p>
<ul>
<li>APM Server 接收 trace data</li>
<li>各語言 APM agent（Java / Python / Node / .NET / Go / Ruby / PHP）</li>
<li>接受 OTLP（ES 7.16+）</li>
<li>Service map / dependency 視覺化</li>
</ul>
<h3 id="elastic-securitysiem">Elastic Security（SIEM）</h3>
<p>子議題：</p>
<ul>
<li>SIEM dashboard / detection rule</li>
<li>ECS（Elastic Common Schema）跨資料統一 field naming</li>
<li>Sigma rule import</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security 模組</a> 對照</li>
</ul>
<h3 id="cluster-scaling">Cluster scaling</h3>
<p>子議題：</p>
<ul>
<li>Node roles：master / data / ingest / coordinating / ML / transform</li>
<li>Hot-warm-cold architecture</li>
<li>Shard sizing（推薦 20-40GB per shard）</li>
<li>Cross-cluster search / replication</li>
</ul>
<h3 id="elastic-license-vs-opensearch-fork">Elastic License vs OpenSearch fork</h3>
<p>子議題：</p>
<ul>
<li>2021 Elastic 改 ELv2 / SSPL（非 OSI 認可）— AWS 不能提供「Elasticsearch as a Service」</li>
<li>AWS fork OpenSearch（Apache 2.0、基於 ES 7.10）</li>
<li>OpenSearch 持續演進、跟 ES 功能逐漸分歧</li>
<li>選擇判讀：合規 → OpenSearch；要最新 ES feature → Elastic</li>
</ul>
<h3 id="searchable-snapshots">Searchable Snapshots</h3>
<p>子議題：</p>
<ul>
<li>把 cold/frozen index 存 S3 / GCS / Azure Blob</li>
<li>查詢時動態 hydrate、成本降 80%+</li>
<li>適合 logs retention 長但查詢頻率低</li>
<li>對應 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></li>
</ul>
<h3 id="vector--fluent-bit-採集替代">Vector / Fluent Bit 採集替代</h3>
<p>子議題：</p>
<ul>
<li>為何用 Vector / Fluent Bit：更輕、resource 用量低</li>
<li>Beats 在 K8s 跑起來資源耗較大</li>
<li>對應 cost 跟 maintainability 取捨</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="index-mapping-explosion">Index mapping explosion</h3>
<p>操作原則：dynamic mapping 對未知 field 自動建 index、大量 field 爆 ES。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: GET /_cat/indices?v 看 field count</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: PUT index/_mapping 鎖定 fields</span></span></span></code></pre></div><h3 id="cluster-yellow--red">Cluster yellow / red</h3>
<p>操作原則：cluster status 影響 query。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: GET /_cluster/health</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: GET /_cat/shards?v 看 unassigned shards</span></span></span></code></pre></div><h3 id="query-過慢">Query 過慢</h3>
<p>操作原則：query 結果 &gt; 10K → 用 search_after / scroll；text field 上做 aggregation → 改 keyword field。</p>
<h3 id="disk-pressure">Disk pressure</h3>
<p>操作原則：cluster disk &gt; 85% → ES 進 read-only 模式。判讀：cluster.routing.allocation.disk.watermark。</p>
<h3 id="logstash-backpressure">Logstash backpressure</h3>
<p>操作原則：Logstash queue full → upstream Beats 累積 backpressure。判讀：Logstash monitoring page。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pure metrics</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> / Mimir</td>
      </tr>
      <tr>
          <td>純 logs 但 less search</td>
          <td>Loki（<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>）— 更便宜</td>
      </tr>
      <tr>
          <td>SaaS turnkey APM</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></td>
      </tr>
      <tr>
          <td>AWS-managed Elastic</td>
          <td>OpenSearch on AWS（Apache 2.0）</td>
      </tr>
      <tr>
          <td>Cloud-native logs</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch Logs</a> / <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Logging</a></td>
      </tr>
      <tr>
          <td>多 tier observability</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></td>
      </tr>
      <tr>
          <td>Enterprise SIEM</td>
          <td>Splunk / Microsoft Sentinel</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>ES query DSL 完整 reference</li>
<li>Lucene scoring 演算法</li>
<li>Kibana dashboard 美術</li>
<li>Elastic ML / Anomaly Detection 細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>Logs 作為 audit evidence</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></td>
          <td>Index Lifecycle / retention</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Elastic Stack 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS pipeline</a></td>
          <td>Beats / Logstash ↔ OTel Collector 採集 pipeline 對照</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>小型 single-node / 中型 hot-warm / 大型 hot-warm-cold-frozen</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（Loki 對照）、<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>6.5 如何新增結構化記錄欄位</title><link>https://tarrragon.github.io/blog/go/06-practical/structured-recording/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/go/06-practical/structured-recording/</guid><description>&lt;p>新增結構化記錄欄位的核心規則是先判斷這筆資訊是給工程師除錯、給系統重播，還是給使用者查詢。不同用途對應不同記錄邊界，資料應依用途進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log&lt;/a> 或 repository。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>分辨 structured log、domain event log 與 state repository&lt;/li>
&lt;li>設計穩定的 log 欄位名稱&lt;/li>
&lt;li>判斷哪些資料不應寫進 log&lt;/li>
&lt;li>用 &lt;code>EventLog.Append&lt;/code> 表達事件記錄邊界&lt;/li>
&lt;li>測試穩定欄位，而不是測自由文字&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察先判斷記錄用途">【觀察】先判斷記錄用途&lt;/h2>
&lt;p>記錄邊界的核心問題是資料要服務誰。工程師除錯、系統重播、使用者查詢是三種不同用途，對應三種不同儲存與格式責任。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>記錄類型&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>structured log&lt;/td>
 &lt;td>操作診斷、除錯、聚合查詢&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> full、event rejected、worker failed&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>domain event log&lt;/td>
 &lt;td>記錄已發生事實、audit、replay&lt;/td>
 &lt;td>&lt;code>notification.created&lt;/code>、&lt;code>job.failed&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>state repository&lt;/td>
 &lt;td>查詢目前狀態或投影&lt;/td>
 &lt;td>job current status、notification summary&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>structured log 服務操作診斷，event log 保存 normalized fact，state repository 回答目前狀態。先分清楚用途，才知道欄位該放哪裡。這個用途判斷比選擇哪個 logging package 更關鍵 — 工具決定怎麼寫，用途決定寫什麼、放哪裡。&lt;/p>
&lt;h2 id="判讀structured-log-是操作訊號">【判讀】structured log 是操作訊號&lt;/h2>
&lt;p>structured log 的核心用途是讓工程師知道系統正在發生什麼，並且能用欄位查詢。它應該記錄操作訊號，而不是完整業務資料。&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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&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="s">&amp;#34;event accepted&amp;#34;&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="s">&amp;#34;layer&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;adapter&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="s">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Type&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="s">&amp;#34;event_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&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="s">&amp;#34;subject_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SubjectID&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="s">&amp;#34;correlation_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">CorrelationID&lt;/span>&lt;span class="p">,&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;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>message&lt;/code> 給人讀，欄位給查詢工具使用。若未來要查某種事件是否大量進入系統，&lt;code>event_type&lt;/code> 欄位比文字搜尋更可靠。&lt;/p>
&lt;p>常見 log 欄位可以先定義成 helper，避免不同地方拼出不同名稱：&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">LogAttrsForEvent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span> &lt;span class="nx">DomainEvent&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">any&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="p">[]&lt;/span>&lt;span class="kt">any&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="s">&amp;#34;event_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ID&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="s">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Type&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="s">&amp;#34;subject_kind&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SubjectKind&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="s">&amp;#34;subject_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SubjectID&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="s">&amp;#34;correlation_id&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">CorrelationID&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="s">&amp;#34;schema_version&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">SchemaVersion&lt;/span>&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;span class="line">&lt;span class="ln">10&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;/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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;event accepted&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nf">LogAttrsForEvent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個 helper 保護的是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>。欄位名稱穩定，查詢與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 才能穩定。&lt;/p>
&lt;h2 id="策略reason-欄位要像-enum">【策略】reason 欄位要像 enum&lt;/h2>
&lt;p>&lt;code>reason&lt;/code> 的核心語意是可聚合的原因分類。它應使用小集合穩定值；完整錯誤訊息則放在 &lt;code>error&lt;/code> 欄位協助診斷。&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">const&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="nx">ReasonInvalidPayload&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;invalid_payload&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">ReasonQueueFull&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;queue_full&amp;#34;&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">ReasonDuplicateEvent&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;duplicate_event&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nx">ReasonTimeout&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;timeout&amp;#34;&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;p>記錄拒絕事件時：&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">logger&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Warn&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="s">&amp;#34;event rejected&amp;#34;&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="s">&amp;#34;layer&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;adapter&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="s">&amp;#34;reason&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ReasonInvalidPayload&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="s">&amp;#34;event_type&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nb">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Type&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="s">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&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="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>reason&lt;/code> 用來統計，&lt;code>error&lt;/code> 用來診斷，message 用來讓人快速理解。這三者不要混成一個大字串。&lt;/p>
&lt;h2 id="判讀event-log-記錄-normalized-fact">【判讀】event log 記錄 normalized fact&lt;/h2>
&lt;p>domain event log 的核心責任是保存已正規化的 domain event。它記錄的是系統承認的事實；raw request、debug log 與目前狀態分別屬於不同記錄邊界。&lt;/p>
&lt;p>先定義 port：&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">type&lt;/span> &lt;span class="nx">EventLog&lt;/span> &lt;span class="kd">interface&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="nf">Append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span> &lt;span class="nx">DomainEvent&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>memory implementation 可以先這樣寫：&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">type&lt;/span> &lt;span class="nx">InMemoryEventLog&lt;/span> &lt;span class="kd">struct&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="nx">mu&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Mutex&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="nx">events&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="nx">DomainEvent&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">NewInMemoryEventLog&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">InMemoryEventLog&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="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">InMemoryEventLog&lt;/span>&lt;span class="p">{}&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>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">l&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">InMemoryEventLog&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span> &lt;span class="nx">DomainEvent&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Lock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">mu&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Unlock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">events&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">l&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">events&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nf">cloneDomainEvent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>event log 應該保存 &lt;code>DomainEvent&lt;/code> envelope 中的穩定欄位，例如 event ID、type、subject、schema version、occurred/received time。它不需要保存 adapter 的 raw input，除非你已經明確設計 raw &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>新增結構化記錄欄位的核心規則是先判斷這筆資訊是給工程師除錯、給系統重播，還是給使用者查詢。不同用途對應不同記錄邊界，資料應依用途進入 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、<a href="/blog/backend/knowledge-cards/event-log/" data-link-title="Event Log" data-link-desc="說明事件歷史如何保存、重播與支援跨服務資料重建">event log</a> 或 repository。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>分辨 structured log、domain event log 與 state repository</li>
<li>設計穩定的 log 欄位名稱</li>
<li>判斷哪些資料不應寫進 log</li>
<li>用 <code>EventLog.Append</code> 表達事件記錄邊界</li>
<li>測試穩定欄位，而不是測自由文字</li>
</ol>
<hr>
<h2 id="觀察先判斷記錄用途">【觀察】先判斷記錄用途</h2>
<p>記錄邊界的核心問題是資料要服務誰。工程師除錯、系統重播、使用者查詢是三種不同用途，對應三種不同儲存與格式責任。</p>
<table>
  <thead>
      <tr>
          <th>記錄類型</th>
          <th>用途</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>structured log</td>
          <td>操作診斷、除錯、聚合查詢</td>
          <td><a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> full、event rejected、worker failed</td>
      </tr>
      <tr>
          <td>domain event log</td>
          <td>記錄已發生事實、audit、replay</td>
          <td><code>notification.created</code>、<code>job.failed</code></td>
      </tr>
      <tr>
          <td>state repository</td>
          <td>查詢目前狀態或投影</td>
          <td>job current status、notification summary</td>
      </tr>
  </tbody>
</table>
<p>structured log 服務操作診斷，event log 保存 normalized fact，state repository 回答目前狀態。先分清楚用途，才知道欄位該放哪裡。這個用途判斷比選擇哪個 logging package 更關鍵 — 工具決定怎麼寫，用途決定寫什麼、放哪裡。</p>
<h2 id="判讀structured-log-是操作訊號">【判讀】structured log 是操作訊號</h2>
<p>structured log 的核心用途是讓工程師知道系統正在發生什麼，並且能用欄位查詢。它應該記錄操作訊號，而不是完整業務資料。</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">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s">&#34;event accepted&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;layer&#34;</span><span class="p">,</span> <span class="s">&#34;adapter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s">&#34;event_type&#34;</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">Type</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;event_id&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="s">&#34;subject_id&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">SubjectID</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="s">&#34;correlation_id&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">CorrelationID</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p><code>message</code> 給人讀，欄位給查詢工具使用。若未來要查某種事件是否大量進入系統，<code>event_type</code> 欄位比文字搜尋更可靠。</p>
<p>常見 log 欄位可以先定義成 helper，避免不同地方拼出不同名稱：</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">LogAttrsForEvent</span><span class="p">(</span><span class="nx">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="p">[]</span><span class="kt">any</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="p">[]</span><span class="kt">any</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="s">&#34;event_id&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="s">&#34;event_type&#34;</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">Type</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="s">&#34;subject_kind&#34;</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">SubjectKind</span><span class="p">),</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="s">&#34;subject_id&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">SubjectID</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="s">&#34;correlation_id&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">CorrelationID</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="s">&#34;schema_version&#34;</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">SchemaVersion</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>使用時可以展開欄位：</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">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;event accepted&#34;</span><span class="p">,</span> <span class="nf">LogAttrsForEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="o">...</span><span class="p">)</span></span></span></code></pre></div><p>這個 helper 保護的是 <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>。欄位名稱穩定，查詢與 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 才能穩定。</p>
<h2 id="策略reason-欄位要像-enum">【策略】reason 欄位要像 enum</h2>
<p><code>reason</code> 的核心語意是可聚合的原因分類。它應使用小集合穩定值；完整錯誤訊息則放在 <code>error</code> 欄位協助診斷。</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">const</span> <span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">ReasonInvalidPayload</span> <span class="p">=</span> <span class="s">&#34;invalid_payload&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nx">ReasonQueueFull</span>      <span class="p">=</span> <span class="s">&#34;queue_full&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nx">ReasonDuplicateEvent</span> <span class="p">=</span> <span class="s">&#34;duplicate_event&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nx">ReasonTimeout</span>        <span class="p">=</span> <span class="s">&#34;timeout&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>記錄拒絕事件時：</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">logger</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s">&#34;event rejected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;layer&#34;</span><span class="p">,</span> <span class="s">&#34;adapter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s">&#34;reason&#34;</span><span class="p">,</span> <span class="nx">ReasonInvalidPayload</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;event_type&#34;</span><span class="p">,</span> <span class="nb">string</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">Type</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="s">&#34;error&#34;</span><span class="p">,</span> <span class="nx">err</span><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><code>reason</code> 用來統計，<code>error</code> 用來診斷，message 用來讓人快速理解。這三者不要混成一個大字串。</p>
<h2 id="判讀event-log-記錄-normalized-fact">【判讀】event log 記錄 normalized fact</h2>
<p>domain event log 的核心責任是保存已正規化的 domain event。它記錄的是系統承認的事實；raw request、debug log 與目前狀態分別屬於不同記錄邊界。</p>
<p>先定義 port：</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">type</span> <span class="nx">EventLog</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nf">Append</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>memory implementation 可以先這樣寫：</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">type</span> <span class="nx">InMemoryEventLog</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">mu</span>     <span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">events</span> <span class="p">[]</span><span class="nx">DomainEvent</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">func</span> <span class="nf">NewInMemoryEventLog</span><span class="p">()</span> <span class="o">*</span><span class="nx">InMemoryEventLog</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="o">&amp;</span><span class="nx">InMemoryEventLog</span><span class="p">{}</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></span><span class="line"><span class="ln">10</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">l</span> <span class="o">*</span><span class="nx">InMemoryEventLog</span><span class="p">)</span> <span class="nf">Append</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">l</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">defer</span> <span class="nx">l</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="nx">l</span><span class="p">.</span><span class="nx">events</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">l</span><span class="p">.</span><span class="nx">events</span><span class="p">,</span> <span class="nf">cloneDomainEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>event log 應該保存 <code>DomainEvent</code> envelope 中的穩定欄位，例如 event ID、type、subject、schema version、occurred/received time。它不需要保存 adapter 的 raw input，除非你已經明確設計 raw <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a>。</p>
<h2 id="執行event-log-要保護-copy-boundary">【執行】event log 要保護 copy boundary</h2>
<p>event log 的核心資料也是內部狀態。若 event 包含 slice、map 或 <code>json.RawMessage</code>，append 與讀取時都要避免外部修改內部資料。</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">cloneDomainEvent</span><span class="p">(</span><span class="nx">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="nx">DomainEvent</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">cloned</span> <span class="o">:=</span> <span class="nx">event</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">if</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Payload</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">cloned</span><span class="p">.</span><span class="nx">Payload</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">json</span><span class="p">.</span><span class="nf">RawMessage</span><span class="p">(</span><span class="kc">nil</span><span class="p">),</span> <span class="nx">event</span><span class="p">.</span><span class="nx">Payload</span><span class="o">...</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="k">return</span> <span class="nx">cloned</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>若要提供查詢方法，也要回傳複製資料：</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="p">(</span><span class="nx">l</span> <span class="o">*</span><span class="nx">InMemoryEventLog</span><span class="p">)</span> <span class="nf">List</span><span class="p">()</span> <span class="p">[]</span><span class="nx">DomainEvent</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">l</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Lock</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">l</span><span class="p">.</span><span class="nx">mu</span><span class="p">.</span><span class="nf">Unlock</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="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="nx">DomainEvent</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">l</span><span class="p">.</span><span class="nx">events</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="nx">i</span><span class="p">,</span> <span class="nx">event</span> <span class="o">:=</span> <span class="k">range</span> <span class="nx">l</span><span class="p">.</span><span class="nx">events</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">result</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="p">=</span> <span class="nf">cloneDomainEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">)</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="k">return</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這裡展示的是教學用記錄邊界。真正 event store 還需要持久化、排序、[schema <a href="/blog/backend/knowledge-cards/migration/" data-link-title="Migration" data-link-desc="說明系統如何把資料、流量或結構從舊狀態移到新狀態">migration</a>](/go/backend/knowledge-cards/schema-migration)、重播策略與交易語意。</p>
<h2 id="策略state-repository-保存目前狀態">【策略】state repository 保存目前狀態</h2>
<p>state repository 的核心責任是回答目前狀態。它可以由 event 更新，但用途不同於保存所有歷史事實的 event log。</p>
<p>例如：</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">type</span> <span class="nx">JobRepository</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nf">Apply</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="kt">error</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nf">Get</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">id</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="nx">JobProjection</span><span class="p">,</span> <span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>event log 和 state repository 可以在 processor 中各自被呼叫：</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">type</span> <span class="nx">RecordingEventProcessor</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">eventLog</span>   <span class="nx">EventLog</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">repository</span> <span class="nx">JobRepository</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nx">logger</span>     <span class="o">*</span><span class="nx">slog</span><span class="p">.</span><span class="nx">Logger</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></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">p</span> <span class="o">*</span><span class="nx">RecordingEventProcessor</span><span class="p">)</span> <span class="nf">Process</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">event</span> <span class="nx">DomainEvent</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">p</span><span class="p">.</span><span class="nx">eventLog</span><span class="p">.</span><span class="nf">Append</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;append event log: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">p</span><span class="p">.</span><span class="nx">repository</span><span class="p">.</span><span class="nf">Apply</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">event</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="nx">fmt</span><span class="p">.</span><span class="nf">Errorf</span><span class="p">(</span><span class="s">&#34;apply state projection: %w&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">p</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;event processed&#34;</span><span class="p">,</span> <span class="nf">LogAttrsForEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這段程式展示三種記錄邊界：event log 保存事實，repository 更新目前狀態，structured log 記錄操作訊號。</p>
<h2 id="判讀記錄位置要跟錯誤發生層一致">【判讀】記錄位置要跟錯誤發生層一致</h2>
<p>記錄位置的核心規則是在哪一層能提供最多上下文，就在哪一層記錄。同一個錯誤通常選擇一個主要層次記錄，避免 log 被重複訊號淹沒。</p>
<p>常見位置：</p>
<table>
  <thead>
      <tr>
          <th>發生位置</th>
          <th>應記錄內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>adapter</td>
          <td>raw input decode/normalize 失敗</td>
      </tr>
      <tr>
          <td>router/usecase</td>
          <td>command 被拒絕、權限不足、狀態不允許</td>
      </tr>
      <tr>
          <td>processor</td>
          <td>event validation、dedup、<a href="/blog/backend/knowledge-cards/projection/" data-link-title="Projection" data-link-desc="說明從事件流或資料變更推算出查詢用讀取視圖的轉換機制">projection</a> apply 結果</td>
      </tr>
      <tr>
          <td>worker</td>
          <td>queue full、外部來源失敗、重試結果</td>
      </tr>
  </tbody>
</table>
<p>例如 adapter 解碼失敗：</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">logger</span><span class="p">.</span><span class="nf">Warn</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s">&#34;callback rejected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;layer&#34;</span><span class="p">,</span> <span class="s">&#34;adapter&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s">&#34;reason&#34;</span><span class="p">,</span> <span class="nx">ReasonInvalidPayload</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;payload_bytes&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">body</span><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>這裡記錄 payload 大小即可診斷資料是否異常；完整 payload 可能包含敏感資料或過大內容。</p>
<h2 id="策略敏感資料預設不進-log">【策略】敏感資料預設不進 log</h2>
<p>敏感資料邊界的核心規則是 log 會被保存、轉發與搜尋，所以 token、password、完整 payload、完整個資應排除在 log 之外。</p>
<p>可以記錄：</p>
<ul>
<li>ID 或 opaque identifier</li>
<li>payload byte length</li>
<li>schema version</li>
<li>欄位是否存在</li>
<li>hash 或 checksum</li>
</ul>
<p>不應記錄：</p>
<ul>
<li>password</li>
<li>access token</li>
<li>cookie</li>
<li>完整 request body</li>
<li>完整 personal data</li>
</ul>
<p>若需要追蹤同一筆資料，可以記錄安全識別碼：</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">logger</span><span class="p">.</span><span class="nf">Debug</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s">&#34;payload received&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;payload_bytes&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s">&#34;payload_sha256&#34;</span><span class="p">,</span> <span class="nf">sha256Hex</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>debug log 也需要遵守同樣規則；只要可能被集中收集，就要先控制敏感資料。</p>
<h2 id="執行log-helper-測試只測穩定欄位">【執行】log helper 測試只測穩定欄位</h2>
<p>log helper 測試的核心目標是保護欄位名稱與值。log message 文案是給人讀的內容，通常保留調整空間。</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">TestLogAttrsForEvent</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">event</span> <span class="o">:=</span> <span class="nx">DomainEvent</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">ID</span><span class="p">:</span>            <span class="s">&#34;evt_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">Type</span><span class="p">:</span>          <span class="nx">EventNotificationCreated</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="nx">SubjectKind</span><span class="p">:</span>   <span class="nx">SubjectNotification</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">SubjectID</span><span class="p">:</span>     <span class="s">&#34;ntf_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">CorrelationID</span><span class="p">:</span> <span class="s">&#34;corr_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">SchemaVersion</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">attrs</span> <span class="o">:=</span> <span class="nf">LogAttrsForEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">got</span> <span class="o">:=</span> <span class="nf">attrsToMap</span><span class="p">(</span><span class="nx">attrs</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">if</span> <span class="nx">got</span><span class="p">[</span><span class="s">&#34;event_id&#34;</span><span class="p">]</span> <span class="o">!=</span> <span class="s">&#34;evt_1&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;event_id = %v, want evt_1&#34;</span><span class="p">,</span> <span class="nx">got</span><span class="p">[</span><span class="s">&#34;event_id&#34;</span><span class="p">])</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="k">if</span> <span class="nx">got</span><span class="p">[</span><span class="s">&#34;event_type&#34;</span><span class="p">]</span> <span class="o">!=</span> <span class="nb">string</span><span class="p">(</span><span class="nx">EventNotificationCreated</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;event_type = %v, want %s&#34;</span><span class="p">,</span> <span class="nx">got</span><span class="p">[</span><span class="s">&#34;event_type&#34;</span><span class="p">],</span> <span class="nx">EventNotificationCreated</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>測試輔助函式可以把 key-value slice 轉成 map：</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">attrsToMap</span><span class="p">(</span><span class="nx">attrs</span> <span class="p">[]</span><span class="kt">any</span><span class="p">)</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">any</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">result</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">any</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">for</span> <span class="nx">i</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span><span class="o">+</span><span class="mi">1</span> <span class="p">&lt;</span> <span class="nb">len</span><span class="p">(</span><span class="nx">attrs</span><span class="p">);</span> <span class="nx">i</span> <span class="o">+=</span> <span class="mi">2</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="nx">key</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">attrs</span><span class="p">[</span><span class="nx">i</span><span class="p">].(</span><span class="kt">string</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="k">continue</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">result</span><span class="p">[</span><span class="nx">key</span><span class="p">]</span> <span class="p">=</span> <span class="nx">attrs</span><span class="p">[</span><span class="nx">i</span><span class="o">+</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="nx">result</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>這個測試直接檢查 helper 輸出，不需要真的寫 log 或解析 logger output。</p>
<h2 id="執行event-log-測試要保護-append-與-copy">【執行】event log 測試要保護 append 與 copy</h2>
<p>event log 測試的核心目標是確認事件被 append，且外部無法透過原始 payload 或回傳值修改內部紀錄。</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">TestInMemoryEventLogAppendCopiesPayload</span><span class="p">(</span><span class="nx">t</span> <span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">log</span> <span class="o">:=</span> <span class="nf">NewInMemoryEventLog</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="nx">payload</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">RawMessage</span><span class="p">(</span><span class="s">`{&#34;topic&#34;:&#34;deployments&#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="nx">event</span> <span class="o">:=</span> <span class="nx">DomainEvent</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="nx">ID</span><span class="p">:</span>            <span class="s">&#34;evt_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nx">Type</span><span class="p">:</span>          <span class="nx">EventNotificationCreated</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="nx">SubjectKind</span><span class="p">:</span>   <span class="nx">SubjectNotification</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nx">SubjectID</span><span class="p">:</span>     <span class="s">&#34;ntf_1&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nx">OccurredAt</span><span class="p">:</span>    <span class="nx">time</span><span class="p">.</span><span class="nf">Date</span><span class="p">(</span><span class="mi">2026</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">22</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">UTC</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="nx">ReceivedAt</span><span class="p">:</span>    <span class="nx">time</span><span class="p">.</span><span class="nf">Date</span><span class="p">(</span><span class="mi">2026</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">22</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">time</span><span class="p">.</span><span class="nx">UTC</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">SchemaVersion</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="nx">Payload</span><span class="p">:</span>       <span class="nx">payload</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">log</span><span class="p">.</span><span class="nf">Append</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">event</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;append event: %v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">payload</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">=</span> <span class="sc">&#39;[&#39;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="nx">events</span> <span class="o">:=</span> <span class="nx">log</span><span class="p">.</span><span class="nf">List</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="k">if</span> <span class="nb">string</span><span class="p">(</span><span class="nx">events</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">Payload</span><span class="p">)</span> <span class="o">!=</span> <span class="s">`{&#34;topic&#34;:&#34;deployments&#34;}`</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="nx">t</span><span class="p">.</span><span class="nf">Fatalf</span><span class="p">(</span><span class="s">&#34;payload was modified through original slice&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>json.RawMessage</code> 本質是 <code>[]byte</code>，所以需要 copy。這類細節很容易被忽略，測試可以把邊界固定下來。</p>
<h2 id="實作檢查清單">實作檢查清單</h2>
<p>新增結構化記錄欄位時，可以依序檢查：</p>
<ol>
<li>這筆資料是給除錯、重播，還是查詢</li>
<li>structured log 是否只保存操作訊號與安全欄位</li>
<li>event log 是否保存 normalized domain event</li>
<li>state repository 是否只保存目前 projection</li>
<li>log 欄位名稱是否穩定</li>
<li><code>reason</code> 是否是小集合分類</li>
<li>是否避免完整 payload 與敏感資料</li>
<li>event log 是否保護 copy boundary</li>
<li>測試是否檢查穩定欄位，而不是自由文字</li>
</ol>
<h2 id="設計檢查">設計檢查</h2>
<h3 id="檢查一log-服務操作診斷">檢查一：log 服務操作診斷</h3>
<p>log 是操作診斷訊號，不是穩定查詢 API。需要使用者查詢的目前狀態，應該進 repository 或 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a>。</p>
<h3 id="檢查二event-log-保存-normalized-fact">檢查二：event log 保存 normalized fact</h3>
<p>event log 記錄的是 normalized fact。若把暫時性錯誤、debug 訊息與 raw payload 全塞進 event log，重播與 audit 會變得不可信。</p>
<h3 id="檢查三欄位名稱維持一致">檢查三：欄位名稱維持一致</h3>
<p><code>event_id</code>、<code>eventID</code>、<code>id</code> 混用會讓查詢失效。欄位 schema 要像 API 一樣維持穩定。</p>
<h3 id="檢查四完整-payload-需要明確策略">檢查四：完整 payload 需要明確策略</h3>
<p>完整 payload 可能包含敏感資料，也可能非常大。除非有明確安全與保存策略，否則只記錄大小、hash、ID 與必要欄位。</p>
<h2 id="本章不處理">本章不處理</h2>
<p>本章先處理 log、event log 與 repository 的分工；集中式 log 平台與可重播事件系統，會在下列章節再往外延伸：</p>
<ul>
<li><a href="/blog/go-advanced/07-distributed-operations/outbox-idempotency/" data-link-title="7.2 Durable queue、outbox 與 idempotency" data-link-desc="設計跨 process 事件傳遞的可靠性與去重邊界">Go 進階：Durable queue、outbox 與 idempotency</a></li>
<li><a href="/blog/go-advanced/07-distributed-operations/observability-pipeline/" data-link-title="7.4 Observability pipeline、metrics 與 tracing" data-link-desc="把 structured log、metric、trace 與 profile 組成可操作的診斷系統">Go 進階：Observability pipeline、metrics 與 tracing</a></li>
<li><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Backend：可觀測性平台</a></li>
</ul>
<h2 id="和-go-教材的關係">和 Go 教材的關係</h2>
<p>這一章承接的是 event log、state repository 與 log schema；如果你要先回看語言教材，可以讀：</p>
<ul>
<li><a href="/blog/go/06-practical/new-event-type/" data-link-title="6.2 如何新增一種 domain event" data-link-desc="擴展事件常數、輸入驗證與處理流程">Go：如何新增一種 domain event</a></li>
<li><a href="/blog/go/06-practical/repository-port/" data-link-title="6.6 如何新增 repository port" data-link-desc="先建立儲存邊界，再決定 memory、SQLite 或外部資料庫實作">Go：如何新增 repository port</a></li>
<li><a href="/blog/go/07-refactoring/interface-boundary/" data-link-title="7.2 用 interface 隔離外部依賴" data-link-desc="建立小而穩定的測試替身">Go：用 interface 隔離外部依賴</a></li>
<li><a href="/blog/go-advanced/06-production-operations/log-fields/" data-link-title="6.3 結構化日誌欄位設計" data-link-desc="讓 log 可 grep、可聚合、可追蹤">Go：結構化日誌欄位設計</a></li>
</ul>
]]></content:encoded></item><item><title>可除錯的 bootstrap：把可觀測性內建進安裝腳本</title><link>https://tarrragon.github.io/blog/linux/install/observable-bootstrap/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/observable-bootstrap/</guid><description>&lt;p>Bootstrap 腳本失敗是常態，所以它的設計目標之一應該是「失敗時可診斷」：把失敗當成會發生的事來設計，預先留好定位問題的痕跡。一支自動化安裝腳本要跨越的環境差異很多——機器缺某個工具、套件清單有筆誤、某個指令在這個發行版的行為跟預期不同——任何一處都可能讓它中斷。決定你是「三分鐘看出哪裡錯」還是「對著終端機捲半天瞎猜」的，是這支腳本有沒有在設計時就把可觀測性內建進去，跟運氣無關。&lt;/p>
&lt;p>可觀測性要事先設計，是因為失敗發生的當下，你能拿到的資訊就已經定型了。如果腳本只把輸出丟到終端機、失敗時只留下一句通用的錯誤，那當下你就只有那句話可看；如果它一路把帶時間戳的紀錄寫進檔案、失敗時主動印出出錯的位置，那同一個失敗就變得可定位。差別不在失敗本身，在失敗前你準備了什麼。如果你寫的是自己的 bootstrap（例如部署 dotfile 的那支 &lt;code>install.sh&lt;/code>），這層要在你第一次跑它之前就設計進去，而不是等它出事才回頭加；就算腳本不是你寫的、你只是來 debug 一次失敗，下一段「找程式自己的 log」一樣適用。&lt;/p>
&lt;h2 id="為什麼會瞎找">為什麼會瞎找&lt;/h2>
&lt;p>不可觀測的腳本失敗時，你手上只有終端機捲動過的那些輸出，而那往往不足以定位真正的原因。終端機的輸出是易逝的、會被後續輸出沖掉、多個來源的訊息交錯在一起；更麻煩的是，很多失敗的「表面錯誤」離「真正原因」隔了好幾層。一個指令因為前面某個變數是空的而失敗，但它報出來的錯可能完全沒提到那個空變數——你看著一個誤導性的症狀，往上游找不到源頭。&lt;/p>
&lt;p>破解這種瞎找的，常常是一份你一開始沒看的 log。很多程式在終端機只印一段摘要，卻同時把詳細的執行紀錄寫進一個 log 檔；當終端機的訊息不足以定位時，那份程式自己寫的 log 裡往往就有答案。除錯時養成「找程式自己的 log，而不是只盯著終端機捲動」的習慣，是把瞎找變成定位的關鍵一步——這也是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七日誌判讀&lt;/a> 的核心。而對你自己寫的 bootstrap，你可以更進一步：在設計時就讓它產生這樣一份 log。&lt;/p>
&lt;h2 id="三個內建可觀測性的手法">三個內建可觀測性的手法&lt;/h2>
&lt;p>讓一支 bootstrap 腳本可診斷，有三個低成本、效果明顯的手法，它們合起來把「失敗了」變成「失敗在第幾行、哪個指令、什麼狀態」。&lt;/p>
&lt;h3 id="log-落地把全部輸出-tee-進帶時間戳的檔案">log 落地：把全部輸出 tee 進帶時間戳的檔案&lt;/h3>
&lt;p>第一個手法是讓腳本的全部輸出同時進終端機跟一個 log 檔，而不是只進終端機。終端機的捲動是易逝的，log 檔是持久的——可以事後 &lt;code>grep&lt;/code>、可以貼給別人看、可以比對前後兩次跑的差異。在 bash 裡，一行 &lt;code>exec&lt;/code> 就能把後續所有 stdout 與 stderr 都導去 &lt;code>tee&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nv">LOG_DIR&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">XDG_STATE_HOME&lt;/span>&lt;span class="k">:-&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="p">/.local/state&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">/dotfiles&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">mkdir -p &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_DIR&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="nv">LOG_FILE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_DIR&lt;/span>&lt;span class="s2">/install-&lt;/span>&lt;span class="k">$(&lt;/span>date +%Y%m%d-%H%M%S&lt;span class="k">)&lt;/span>&lt;span class="s2">.log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nb">exec&lt;/span> &amp;gt; &amp;gt;&lt;span class="o">(&lt;/span>tee -a &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$LOG_FILE&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="o">)&lt;/span> 2&amp;gt;&lt;span class="p">&amp;amp;&lt;/span>&lt;span class="m">1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>帶時間戳的檔名讓每次跑各留一份、不互相覆蓋，事後可以回溯「上一次成功跟這次失敗差在哪」。log 檔放在 &lt;code>XDG_STATE_HOME&lt;/code>（狀態資料的標準位置）底下，符合慣例、也不污染家目錄。&lt;/p>
&lt;h3 id="錯誤定位用-err-trap-印出出錯的行與指令">錯誤定位：用 ERR trap 印出出錯的行與指令&lt;/h3>
&lt;p>第二個手法是讓腳本在中斷的瞬間，主動報出「是哪一行、哪個指令、什麼結束碼」失敗的。配合 &lt;code>set -e&lt;/code>（出錯即停）的腳本，預設只會默默地停，不告訴你停在哪。加一個 &lt;code>ERR&lt;/code> trap，就能在 &lt;code>set -e&lt;/code> 中斷之前先印出定位資訊：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">set&lt;/span> -Eeuo pipefail &lt;span class="c1"># -E 讓 ERR trap 在函式/子 shell 也生效&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nb">trap&lt;/span> &lt;span class="s1">&amp;#39;log &amp;#34;ERROR line $LINENO: [$BASH_COMMAND] exit=$?&amp;#34;&amp;#39;&lt;/span> ERR&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>$LINENO&lt;/code> 是出錯的行號、&lt;code>$BASH_COMMAND&lt;/code> 是當下正在執行的那條指令、&lt;code>$?&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">[00:06:51] ERROR line 40: [sudo pacman -S --needed stow git zsh] exit=1&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>範例裡的 &lt;code>pacman&lt;/code> 換發行版會不同，這裡只是示意 trap 輸出的格式——手法本身（行號 + 指令 + 結束碼）跟發行版無關。這一行直接點名元兇。前面提過的那類「表面錯誤離真正原因隔好幾層」的情況——例如某個指令因為 &lt;code>which&lt;/code> 不存在而拿到空字串、最後報一個看似無關的錯——有了這行，你會直接看到是哪一行的哪條指令掛了，不必從誤導性的症狀往回猜。&lt;code>set -E&lt;/code>（&lt;code>-E&lt;/code> 旗標）是為了讓 trap 在函式跟子 shell 裡也照樣觸發，少了它，包在函式裡的錯誤會漏掉。&lt;/p>
&lt;h3 id="步驟標記用帶時間戳的-log-函式標出進度">步驟標記：用帶時間戳的 log 函式標出進度&lt;/h3>
&lt;p>第三個手法是在關鍵步驟前印一行帶時間戳的標記，讓你能看出腳本跑到哪、哪一步慢。一個極簡的 log 函式就夠：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">log&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;[%s] %s\n&amp;#39;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date +%H:%M:%S&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$*&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;install.sh start | OS=&lt;/span>&lt;span class="nv">$OS&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;Installing base packages...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">log &lt;span class="s2">&amp;#34;Stowing configs...&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>時間戳的價值在於它同時給你「進度」跟「效能」兩種資訊：失敗時，最後一行成功的 log 告訴你它跨過了哪些步驟、卡在哪一步之後；正常時，相鄰兩行的時間差告訴你哪一步耗時最久。這比沒有標記、只能從一堆套件下載輸出裡猜「現在到底在幹嘛」清楚得多。&lt;/p>
&lt;h2 id="失敗可診斷是設計目標">失敗可診斷是設計目標&lt;/h2>
&lt;p>把這三個手法合起來，一支原本「失敗時只留一句通用錯誤」的腳本，會變成「每次跑都留一份完整 log、失敗時直接點名第幾行哪個指令、過程中每步都有時間戳」。成本是腳本開頭多幾行，回報是把未來每一次除錯從瞎找變成定位。這層可觀測性是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八 bootstrap script 設計&lt;/a> 的延伸——那篇給安裝腳本的骨架與套件清單，這篇給它加上失敗時的診斷能力，兩篇處理的是同一支腳本的兩個層面。&lt;/p>
&lt;p>這是設計階段的決定，不是事後能補的。當一支沒有可觀測性的腳本在一台陌生機器上失敗，你沒辦法回到過去讓它記錄當時的狀態——資訊在失敗的瞬間就已經流失了。所以「失敗可診斷」要跟功能一起設計進去，把它當成 bootstrap 的基本屬性，而不是出事之後才想加的補丁。&lt;/p>
&lt;h2 id="回到系列">回到系列&lt;/h2>
&lt;p>這幾篇合起來，是把一台機器從「空的」帶到「能接收 dotfile、且部署過程可診斷」的完整地基：&lt;a href="../install-option-decisions/">安裝選項判讀&lt;/a> 處理 OS 怎麼裝、&lt;a href="../minimal-install-verify/">工具驗證與補足&lt;/a> 處理裝完缺什麼、&lt;a href="../ssh-keyless-bootstrap/">外部連入與無 key bootstrap&lt;/a> 處理怎麼連進去把 dotfile 弄進來，這一篇處理當部署失敗時怎麼快速看出原因。再往前一步，把這套地基用在無人值守的長任務上、讓機器在你離開後自己跑完工作，見 &lt;a href="../unattended-remote-work/">讓機器跑無人值守的長任務&lt;/a>——無人盯著的任務尤其依賴這篇談的可觀測性。地基打好，後面 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到八&lt;/a> 的 dotfile 管理才有立足點。&lt;/p></description><content:encoded><![CDATA[<p>Bootstrap 腳本失敗是常態，所以它的設計目標之一應該是「失敗時可診斷」：把失敗當成會發生的事來設計，預先留好定位問題的痕跡。一支自動化安裝腳本要跨越的環境差異很多——機器缺某個工具、套件清單有筆誤、某個指令在這個發行版的行為跟預期不同——任何一處都可能讓它中斷。決定你是「三分鐘看出哪裡錯」還是「對著終端機捲半天瞎猜」的，是這支腳本有沒有在設計時就把可觀測性內建進去，跟運氣無關。</p>
<p>可觀測性要事先設計，是因為失敗發生的當下，你能拿到的資訊就已經定型了。如果腳本只把輸出丟到終端機、失敗時只留下一句通用的錯誤，那當下你就只有那句話可看；如果它一路把帶時間戳的紀錄寫進檔案、失敗時主動印出出錯的位置，那同一個失敗就變得可定位。差別不在失敗本身，在失敗前你準備了什麼。如果你寫的是自己的 bootstrap（例如部署 dotfile 的那支 <code>install.sh</code>），這層要在你第一次跑它之前就設計進去，而不是等它出事才回頭加；就算腳本不是你寫的、你只是來 debug 一次失敗，下一段「找程式自己的 log」一樣適用。</p>
<h2 id="為什麼會瞎找">為什麼會瞎找</h2>
<p>不可觀測的腳本失敗時，你手上只有終端機捲動過的那些輸出，而那往往不足以定位真正的原因。終端機的輸出是易逝的、會被後續輸出沖掉、多個來源的訊息交錯在一起；更麻煩的是，很多失敗的「表面錯誤」離「真正原因」隔了好幾層。一個指令因為前面某個變數是空的而失敗，但它報出來的錯可能完全沒提到那個空變數——你看著一個誤導性的症狀，往上游找不到源頭。</p>
<p>破解這種瞎找的，常常是一份你一開始沒看的 log。很多程式在終端機只印一段摘要，卻同時把詳細的執行紀錄寫進一個 log 檔；當終端機的訊息不足以定位時，那份程式自己寫的 log 裡往往就有答案。除錯時養成「找程式自己的 log，而不是只盯著終端機捲動」的習慣，是把瞎找變成定位的關鍵一步——這也是 <a href="/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">模組七日誌判讀</a> 的核心。而對你自己寫的 bootstrap，你可以更進一步：在設計時就讓它產生這樣一份 log。</p>
<h2 id="三個內建可觀測性的手法">三個內建可觀測性的手法</h2>
<p>讓一支 bootstrap 腳本可診斷，有三個低成本、效果明顯的手法，它們合起來把「失敗了」變成「失敗在第幾行、哪個指令、什麼狀態」。</p>
<h3 id="log-落地把全部輸出-tee-進帶時間戳的檔案">log 落地：把全部輸出 tee 進帶時間戳的檔案</h3>
<p>第一個手法是讓腳本的全部輸出同時進終端機跟一個 log 檔，而不是只進終端機。終端機的捲動是易逝的，log 檔是持久的——可以事後 <code>grep</code>、可以貼給別人看、可以比對前後兩次跑的差異。在 bash 裡，一行 <code>exec</code> 就能把後續所有 stdout 與 stderr 都導去 <code>tee</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">LOG_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">XDG_STATE_HOME</span><span class="k">:-</span><span class="nv">$HOME</span><span class="p">/.local/state</span><span class="si">}</span><span class="s2">/dotfiles&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="nv">$LOG_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nv">LOG_FILE</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$LOG_DIR</span><span class="s2">/install-</span><span class="k">$(</span>date +%Y%m%d-%H%M%S<span class="k">)</span><span class="s2">.log&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">exec</span> &gt; &gt;<span class="o">(</span>tee -a <span class="s2">&#34;</span><span class="nv">$LOG_FILE</span><span class="s2">&#34;</span><span class="o">)</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span></span></span></code></pre></div><p>帶時間戳的檔名讓每次跑各留一份、不互相覆蓋，事後可以回溯「上一次成功跟這次失敗差在哪」。log 檔放在 <code>XDG_STATE_HOME</code>（狀態資料的標準位置）底下，符合慣例、也不污染家目錄。</p>
<h3 id="錯誤定位用-err-trap-印出出錯的行與指令">錯誤定位：用 ERR trap 印出出錯的行與指令</h3>
<p>第二個手法是讓腳本在中斷的瞬間，主動報出「是哪一行、哪個指令、什麼結束碼」失敗的。配合 <code>set -e</code>（出錯即停）的腳本，預設只會默默地停，不告訴你停在哪。加一個 <code>ERR</code> trap，就能在 <code>set -e</code> 中斷之前先印出定位資訊：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">set</span> -Eeuo pipefail   <span class="c1"># -E 讓 ERR trap 在函式/子 shell 也生效</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">trap</span> <span class="s1">&#39;log &#34;ERROR line $LINENO: [$BASH_COMMAND] exit=$?&#34;&#39;</span> ERR</span></span></code></pre></div><p><code>$LINENO</code> 是出錯的行號、<code>$BASH_COMMAND</code> 是當下正在執行的那條指令、<code>$?</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">[00:06:51] ERROR line 40: [sudo pacman -S --needed stow git zsh] exit=1</span></span></code></pre></div><p>範例裡的 <code>pacman</code> 換發行版會不同，這裡只是示意 trap 輸出的格式——手法本身（行號 + 指令 + 結束碼）跟發行版無關。這一行直接點名元兇。前面提過的那類「表面錯誤離真正原因隔好幾層」的情況——例如某個指令因為 <code>which</code> 不存在而拿到空字串、最後報一個看似無關的錯——有了這行，你會直接看到是哪一行的哪條指令掛了，不必從誤導性的症狀往回猜。<code>set -E</code>（<code>-E</code> 旗標）是為了讓 trap 在函式跟子 shell 裡也照樣觸發，少了它，包在函式裡的錯誤會漏掉。</p>
<h3 id="步驟標記用帶時間戳的-log-函式標出進度">步驟標記：用帶時間戳的 log 函式標出進度</h3>
<p>第三個手法是在關鍵步驟前印一行帶時間戳的標記，讓你能看出腳本跑到哪、哪一步慢。一個極簡的 log 函式就夠：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">log<span class="o">()</span> <span class="o">{</span> <span class="nb">printf</span> <span class="s1">&#39;[%s] %s\n&#39;</span> <span class="s2">&#34;</span><span class="k">$(</span>date +%H:%M:%S<span class="k">)</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$*</span><span class="s2">&#34;</span><span class="p">;</span> <span class="o">}</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">log <span class="s2">&#34;install.sh start | OS=</span><span class="nv">$OS</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">log <span class="s2">&#34;Installing base packages...&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">log <span class="s2">&#34;Stowing configs...&#34;</span></span></span></code></pre></div><p>時間戳的價值在於它同時給你「進度」跟「效能」兩種資訊：失敗時，最後一行成功的 log 告訴你它跨過了哪些步驟、卡在哪一步之後；正常時，相鄰兩行的時間差告訴你哪一步耗時最久。這比沒有標記、只能從一堆套件下載輸出裡猜「現在到底在幹嘛」清楚得多。</p>
<h2 id="失敗可診斷是設計目標">失敗可診斷是設計目標</h2>
<p>把這三個手法合起來，一支原本「失敗時只留一句通用錯誤」的腳本，會變成「每次跑都留一份完整 log、失敗時直接點名第幾行哪個指令、過程中每步都有時間戳」。成本是腳本開頭多幾行，回報是把未來每一次除錯從瞎找變成定位。這層可觀測性是 <a href="/blog/linux/dotfile/08-sync-bootstrap/bootstrap-script-packages/" data-link-title="Bootstrap Script 與套件清單管理" data-link-desc="寫 dotfile 的 install script、或整理「這台機器裝了什麼」的套件清單時回來讀">模組八 bootstrap script 設計</a> 的延伸——那篇給安裝腳本的骨架與套件清單，這篇給它加上失敗時的診斷能力，兩篇處理的是同一支腳本的兩個層面。</p>
<p>這是設計階段的決定，不是事後能補的。當一支沒有可觀測性的腳本在一台陌生機器上失敗，你沒辦法回到過去讓它記錄當時的狀態——資訊在失敗的瞬間就已經流失了。所以「失敗可診斷」要跟功能一起設計進去，把它當成 bootstrap 的基本屬性，而不是出事之後才想加的補丁。</p>
<h2 id="回到系列">回到系列</h2>
<p>這幾篇合起來，是把一台機器從「空的」帶到「能接收 dotfile、且部署過程可診斷」的完整地基：<a href="../install-option-decisions/">安裝選項判讀</a> 處理 OS 怎麼裝、<a href="../minimal-install-verify/">工具驗證與補足</a> 處理裝完缺什麼、<a href="../ssh-keyless-bootstrap/">外部連入與無 key bootstrap</a> 處理怎麼連進去把 dotfile 弄進來，這一篇處理當部署失敗時怎麼快速看出原因。再往前一步，把這套地基用在無人值守的長任務上、讓機器在你離開後自己跑完工作，見 <a href="../unattended-remote-work/">讓機器跑無人值守的長任務</a>——無人盯著的任務尤其依賴這篇談的可觀測性。地基打好，後面 <a href="/blog/linux/dotfile/01-dotfile-management/" data-link-title="模組一：管理工具與目錄結構" data-link-desc="要把散落在家目錄的配置檔集中版控時，選 bare repo、stow 還是 chezmoi、目錄該怎麼組織">模組一到八</a> 的 dotfile 管理才有立足點。</p>
]]></content:encoded></item><item><title>模組六：可觀測性與 log 一併寫進 code</title><link>https://tarrragon.github.io/blog/infra/06-observability-logging/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/06-observability-logging/</guid><description>&lt;p>可觀測性要跟它監控的資源同生命週期：log group、metric 與 alarm 寫進建立資源的同一套 IaC，資源開出來的那一刻監控就在線，而非等出事才補。少了這條規則的代價很具體：凌晨資料庫 CPU 飆到 100%、API 開始逾時，值班工程師打開 console 想看 log，卻發現那個服務根本沒接 log group、metric 也只有 vendor 預設的幾條粗線，追不到呼叫鏈、查不到錯誤訊息，只能靠重啟賭它恢復。&lt;/p>
&lt;h2 id="observability-跟-infra-同一套-code同生命週期">observability 跟 infra 同一套 code、同生命週期&lt;/h2>
&lt;p>可觀測性是基礎設施的一部分，承擔「讓資源在出事時可被追查」的責任，因此它的建立、變更與銷毀要跟被監控的資源綁在同一個生命週期裡。一個 RDS 實例、一個 Lambda、一個 ECS service 被 IaC 建立時，它的 log group、它的關鍵 metric alarm 應該在同一份 plan 裡一起 apply；這個資源被 destroy 時，對應的 alarm 也一起收掉，不留下對著空資源狂叫的孤兒告警。&lt;/p>
&lt;p>把監控外掛在資源之外會製造兩種漂移。第一種是新資源沒有監控：service 透過 PR 加上去了，但 alarm 要某人事後手動進 console 點，於是有些 service 有 alarm、有些沒有，覆蓋率取決於誰記得。第二種是死資源留下殘響：資源砍了但 alarm 還在，半夜對著不存在的 target 噴 &lt;code>INSUFFICIENT_DATA&lt;/code>，值班的人學會忽略它，告警疲勞讓真的事故也被一起忽略。兩種漂移的共同根因都是監控跟資源不在同一個 apply 單位裡。&lt;/p>
&lt;p>判讀訊號很直接：如果有人能回答「這個服務有沒有 alarm」要去翻 console 而不是讀 code，監控就已經跟資源脫鉤了。修法是把監控宣告收進該資源的 module——模組四（環境分離與模組化）談的模組化在這裡延伸成「每個服務模組自帶它的 observability 宣告」，模組五（核心服務上 IaC）談的每個核心服務也應該在同一個 module 裡帶上自己的 log 與 alarm。&lt;/p>
&lt;h2 id="log-group-與-retention-設計">log group 與 retention 設計&lt;/h2>
&lt;p>Log group 是日誌的歸屬與保存單位，它要回答兩個治理問題：留多久、誰能讀。這兩個問題寫進 IaC 才能稽核，而非依賴 vendor 的隱性預設。許多雲端服務在你沒宣告 log group 時會自動建一個、套上「永久保留」的預設值，於是日誌無限堆積、帳單緩慢長大，而真正敏感的內容反而沒人管控存取。&lt;/p>
&lt;p>Retention 是成本、合規與除錯需求的三方取捨。除錯通常只需要近幾天到幾週的熱資料；合規（如稽核軌跡、金流紀錄）可能要求保留數年；而每多留一天就多一天的儲存費。划算的做法是按日誌類型分層：高頻、除錯用的 application log 設短 retention（例如 14 到 30 天），稽核相關的 access log 按合規要求設長期保留，必要時再把冷資料歸檔到更便宜的物件儲存。把這些值寫進 IaC，讓「為什麼這條 log 留 90 天」是一個能在 PR 上被討論的決定。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_log_group&amp;#34; &amp;#34;api&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/app/${var.env}/api&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n"> retention_in_days&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> var.env&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;prod&amp;#34;&lt;/span> &lt;span class="err">?&lt;/span> &lt;span class="m">30&lt;/span> &lt;span class="err">:&lt;/span> &lt;span class="m">7&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> kms_key_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_kms_key&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">logs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「誰能讀」是 retention 之外的另一半，因為 log 經常夾帶 PII、token 或內部結構，讀取權限要跟身分地基一起管。存取控制掛在模組二（身分與憑證地基）建立的 IAM 角色上，加密金鑰則對應模組三、模組七一路延伸的金鑰治理。常見陷阱是 log 在傳輸與儲存都加密了，卻對整個團隊開放讀取，等於把敏感資料攤在所有人面前；read 權限應該縮到值班與稽核需要的最小集合。應用層該怎麼決定哪些欄位根本不該進 log，屬於資料保護的範圍，可往 &lt;code>/backend/07-security-data-protection/&lt;/code> 對齊。&lt;/p>
&lt;h2 id="metric-與-alarm-寫進-iac">metric 與 alarm 寫進 IaC&lt;/h2>
&lt;p>Metric 與 alarm 寫進 IaC，目的是讓「資源被建立的同時就帶著它的健康判準」。Alarm 不只是一個閾值，它是一份對「這個資源什麼狀態算不正常」的成文約定：哪條 metric、跨多長的評估窗口、超過什麼值要通知誰。把這份約定寫進 code，它就能被 review、被版本控制、被跨環境複用，而不是散落在某個人腦中或 console 的某個角落。&lt;/p>
&lt;p>Alarm 的價值在於它連到動作，而非只是亮一盞燈。一條有用的 alarm 至少要綁定通知去向（on-call 的 SNS topic、PagerDuty、Slack），並寫清楚 &lt;code>INSUFFICIENT_DATA&lt;/code> 怎麼處理——資料不足到底算正常還是異常，取決於這條 metric 平常是否持續有資料。閾值設計是訊號與雜訊的取捨：設太敏感會頻繁誤報、養出告警疲勞，設太鈍則錯過真正的劣化。划算的起點是針對「使用者已經受影響」的症狀型 metric 設 alarm（錯誤率、p99 延遲、佇列積壓），而把成因型指標（CPU、記憶體）留作 dashboard 上的診斷線索，避免每個成因都獨立告警。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_metric_alarm&amp;#34; &amp;#34;api_5xx&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n"> alarm_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${var.env}-api-5xx-rate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> comparison_operator&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;GreaterThanThreshold&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> evaluation_periods&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="n"> metric_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;5XXError&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> namespace&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;AWS/ApiGateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> period&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">60&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> statistic&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Sum&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> threshold&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="n"> treat_missing_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;notBreaching&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n"> alarm_actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">aws_sns_topic&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">oncall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀訊號是：每次新服務上線都要有人「記得」去加 alarm，代表 alarm 還沒進 module 模板。修法是把基礎告警（錯誤率、延遲、健康檢查失敗）做成服務模組的預設輸出，讓開新服務時 alarm 跟著資源一起生出來，調整閾值才是該服務 owner 的選配。&lt;/p></description><content:encoded><![CDATA[<p>可觀測性要跟它監控的資源同生命週期：log group、metric 與 alarm 寫進建立資源的同一套 IaC，資源開出來的那一刻監控就在線，而非等出事才補。少了這條規則的代價很具體：凌晨資料庫 CPU 飆到 100%、API 開始逾時，值班工程師打開 console 想看 log，卻發現那個服務根本沒接 log group、metric 也只有 vendor 預設的幾條粗線，追不到呼叫鏈、查不到錯誤訊息，只能靠重啟賭它恢復。</p>
<h2 id="observability-跟-infra-同一套-code同生命週期">observability 跟 infra 同一套 code、同生命週期</h2>
<p>可觀測性是基礎設施的一部分，承擔「讓資源在出事時可被追查」的責任，因此它的建立、變更與銷毀要跟被監控的資源綁在同一個生命週期裡。一個 RDS 實例、一個 Lambda、一個 ECS service 被 IaC 建立時，它的 log group、它的關鍵 metric alarm 應該在同一份 plan 裡一起 apply；這個資源被 destroy 時，對應的 alarm 也一起收掉，不留下對著空資源狂叫的孤兒告警。</p>
<p>把監控外掛在資源之外會製造兩種漂移。第一種是新資源沒有監控：service 透過 PR 加上去了，但 alarm 要某人事後手動進 console 點，於是有些 service 有 alarm、有些沒有，覆蓋率取決於誰記得。第二種是死資源留下殘響：資源砍了但 alarm 還在，半夜對著不存在的 target 噴 <code>INSUFFICIENT_DATA</code>，值班的人學會忽略它，告警疲勞讓真的事故也被一起忽略。兩種漂移的共同根因都是監控跟資源不在同一個 apply 單位裡。</p>
<p>判讀訊號很直接：如果有人能回答「這個服務有沒有 alarm」要去翻 console 而不是讀 code，監控就已經跟資源脫鉤了。修法是把監控宣告收進該資源的 module——模組四（環境分離與模組化）談的模組化在這裡延伸成「每個服務模組自帶它的 observability 宣告」，模組五（核心服務上 IaC）談的每個核心服務也應該在同一個 module 裡帶上自己的 log 與 alarm。</p>
<h2 id="log-group-與-retention-設計">log group 與 retention 設計</h2>
<p>Log group 是日誌的歸屬與保存單位，它要回答兩個治理問題：留多久、誰能讀。這兩個問題寫進 IaC 才能稽核，而非依賴 vendor 的隱性預設。許多雲端服務在你沒宣告 log group 時會自動建一個、套上「永久保留」的預設值，於是日誌無限堆積、帳單緩慢長大，而真正敏感的內容反而沒人管控存取。</p>
<p>Retention 是成本、合規與除錯需求的三方取捨。除錯通常只需要近幾天到幾週的熱資料；合規（如稽核軌跡、金流紀錄）可能要求保留數年；而每多留一天就多一天的儲存費。划算的做法是按日誌類型分層：高頻、除錯用的 application log 設短 retention（例如 14 到 30 天），稽核相關的 access log 按合規要求設長期保留，必要時再把冷資料歸檔到更便宜的物件儲存。把這些值寫進 IaC，讓「為什麼這條 log 留 90 天」是一個能在 PR 上被討論的決定。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_log_group&#34; &#34;api&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;/app/${var.env}/api&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  retention_in_days</span> <span class="o">=</span><span class="n"> var.env</span> <span class="o">==</span> <span class="s2">&#34;prod&#34;</span> <span class="err">?</span> <span class="m">30</span> <span class="err">:</span> <span class="m">7</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  kms_key_id</span>        <span class="o">=</span> <span class="k">aws_kms_key</span><span class="p">.</span><span class="k">logs</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">}</span></span></code></pre></div><p>「誰能讀」是 retention 之外的另一半，因為 log 經常夾帶 PII、token 或內部結構，讀取權限要跟身分地基一起管。存取控制掛在模組二（身分與憑證地基）建立的 IAM 角色上，加密金鑰則對應模組三、模組七一路延伸的金鑰治理。常見陷阱是 log 在傳輸與儲存都加密了，卻對整個團隊開放讀取，等於把敏感資料攤在所有人面前；read 權限應該縮到值班與稽核需要的最小集合。應用層該怎麼決定哪些欄位根本不該進 log，屬於資料保護的範圍，可往 <code>/backend/07-security-data-protection/</code> 對齊。</p>
<h2 id="metric-與-alarm-寫進-iac">metric 與 alarm 寫進 IaC</h2>
<p>Metric 與 alarm 寫進 IaC，目的是讓「資源被建立的同時就帶著它的健康判準」。Alarm 不只是一個閾值，它是一份對「這個資源什麼狀態算不正常」的成文約定：哪條 metric、跨多長的評估窗口、超過什麼值要通知誰。把這份約定寫進 code，它就能被 review、被版本控制、被跨環境複用，而不是散落在某個人腦中或 console 的某個角落。</p>
<p>Alarm 的價值在於它連到動作，而非只是亮一盞燈。一條有用的 alarm 至少要綁定通知去向（on-call 的 SNS topic、PagerDuty、Slack），並寫清楚 <code>INSUFFICIENT_DATA</code> 怎麼處理——資料不足到底算正常還是異常，取決於這條 metric 平常是否持續有資料。閾值設計是訊號與雜訊的取捨：設太敏感會頻繁誤報、養出告警疲勞，設太鈍則錯過真正的劣化。划算的起點是針對「使用者已經受影響」的症狀型 metric 設 alarm（錯誤率、p99 延遲、佇列積壓），而把成因型指標（CPU、記憶體）留作 dashboard 上的診斷線索，避免每個成因都獨立告警。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_metric_alarm&#34; &#34;api_5xx&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  alarm_name</span>          <span class="o">=</span> <span class="s2">&#34;${var.env}-api-5xx-rate&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  comparison_operator</span> <span class="o">=</span> <span class="s2">&#34;GreaterThanThreshold&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  evaluation_periods</span>  <span class="o">=</span> <span class="m">3</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">  metric_name</span>         <span class="o">=</span> <span class="s2">&#34;5XXError&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  namespace</span>           <span class="o">=</span> <span class="s2">&#34;AWS/ApiGateway&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  period</span>              <span class="o">=</span> <span class="m">60</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  statistic</span>           <span class="o">=</span> <span class="s2">&#34;Sum&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  threshold</span>           <span class="o">=</span> <span class="m">10</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  treat_missing_data</span>  <span class="o">=</span> <span class="s2">&#34;notBreaching&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  alarm_actions</span>       <span class="o">=</span> <span class="p">[</span><span class="k">aws_sns_topic</span><span class="p">.</span><span class="k">oncall</span><span class="p">.</span><span class="k">arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>判讀訊號是：每次新服務上線都要有人「記得」去加 alarm，代表 alarm 還沒進 module 模板。修法是把基礎告警（錯誤率、延遲、健康檢查失敗）做成服務模組的預設輸出，讓開新服務時 alarm 跟著資源一起生出來，調整閾值才是該服務 owner 的選配。</p>
<h2 id="跟-monitoring-系列的分工基礎設施訊號-vs-客戶端行為訊號">跟 monitoring 系列的分工：基礎設施訊號 vs 客戶端行為訊號</h2>
<p>本模組的可觀測性處理基礎設施訊號，monitoring 系列處理客戶端與業務行為訊號，兩者觀測的對象不同、生命週期也不同，因此分屬不同的 code 與不同的章節。基礎設施訊號是資源層的健康狀態：log group、CPU、佇列深度、5xx 比例、實例存活，它們跟著資源被 IaC 建立與銷毀，回答「這個系統還活著嗎、哪裡壞了」。</p>
<p>客戶端行為訊號則是 SDK、Collector、業務埋點那一層：使用者點了什麼、轉換漏斗、前端錯誤、自訂事件，它們跟著產品功能演進、不跟著基礎設施資源同生共滅，所以放在 <code>/monitoring/</code>。判讀分界的問法是：這個訊號是「資源建立時就該存在」還是「功能開發時才埋」。前者進本模組的 IaC，後者進 monitoring 那層的應用程式碼。兩者在事故排查時會合流——基礎設施 alarm 告訴你哪個資源異常，客戶端訊號告訴你使用者實際受了什麼影響——但它們的擁有者、變更節奏與部署管道不同，混在一起會讓「誰負責這條訊號」變模糊。</p>
<p>收斂成一句判準：資源建立時就該存在的訊號歸本模組的 IaC，功能開發時才埋的客戶端行為訊號歸另一層；各條延伸章節見下方跨分類引用。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/06-observability-logging/log-metric-alarm-lifecycle/" data-link-title="可觀測性與 log 同生命週期管理" data-link-desc="log group、metric、alarm 寫進建立資源的同一套 IaC，讓監控跟資源同生共滅，出事時追得到查得到">可觀測性與 log 同生命週期管理</a></td>
          <td>log group、metric、alarm 寫進同一套 IaC，讓監控跟資源同生共滅，出事時追得到查得到</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 監控體系</a>：客戶端 SDK / Collector 那層的監控</li>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>：每個核心服務帶自己的 log 與 alarm</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：observability 變更也走 PR 與自動化護欄</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：哪些欄位不該進 log、PII 處理</li>
</ul>
]]></content:encoded></item><item><title>4.6 SLI 量測與 SLO 訊號設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>SLI 設計起點：user-journey 而非 system metric&lt;/li>
&lt;li>量測點選擇：edge / gateway / service / dependency 各自代表什麼&lt;/li>
&lt;li>Ratio metric vs latency &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a>：何時用哪種&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">Burn rate&lt;/a> 訊號：multi-window multi-burn-rate alert&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error budget&lt;/a> 計算所需的 metric 結構&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics&lt;/a> 的分工：4.2 是 counter/gauge/histogram 基礎、4.6 是 SLI 化的設計&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert&lt;/a> 的分工：4.4 是 alert 規則治理、4.6 是 alert 的訊號源頭&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>SLI 訊號設計是把可靠性目標轉成可量測資料的步驟，責任是讓 SLO 政策建立在使用者旅程與服務結果上。&lt;/p>
&lt;p>CPU、memory、queue depth 可以提供系統背景，但 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a> 需要回答的是使用者層面的問題：request 是否成功、回應是否夠快、結果是否正確。SLI 量測的位置跟算式決定了 SLO 反映的是「使用者體驗」還是「基礎設施健康」— 兩者的判讀意義不同。&lt;/p>
&lt;p>本章處理的是 metric 到 SLI 的轉換。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2&lt;/a> 定義 counter / gauge / histogram 的基礎型別；本章定義怎麼用這些型別組出代表使用者體驗的 SLI，並設計 burn rate alert 的訊號結構。SLO 政策本身（error budget freeze、release gate 決策）由 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.6 SLO 政策&lt;/a> 處理。&lt;/p>
&lt;h2 id="sli-設計起點user-journey">SLI 設計起點：User Journey&lt;/h2>
&lt;h3 id="從使用者操作推導-sli">從使用者操作推導 SLI&lt;/h3>
&lt;p>SLI 的設計起點是「使用者在做什麼、期待什麼結果」，不是「系統有什麼 metric 可以用」。&lt;/p>
&lt;p>一個 checkout 流程的使用者期待：request 成功（不會看到 error page）、回應夠快（不會等超過 3 秒）、結果正確（扣款金額正確）。對應三種 SLI：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Availability SLI&lt;/strong>：成功 request 的比例（&lt;code>successful_requests / total_requests&lt;/code>）&lt;/li>
&lt;li>&lt;strong>Latency SLI&lt;/strong>：回應時間在閾值內的比例（&lt;code>requests_under_3s / total_requests&lt;/code>）&lt;/li>
&lt;li>&lt;strong>Correctness SLI&lt;/strong>：結果正確的比例（需要業務邏輯判定，通常用特定 error code 或 reconciliation 結果）&lt;/li>
&lt;/ul>
&lt;p>每個 user journey 不需要三種 SLI 都有。Checkout 的 availability 跟 latency 是核心；correctness 靠事後對帳驗證。搜尋頁面的 latency 比 availability 更關鍵 — 使用者容忍偶發的「搜不到結果」但不容忍 5 秒的載入。&lt;/p>
&lt;h3 id="system-metric-跟-sli-的差異">System metric 跟 SLI 的差異&lt;/h3>
&lt;p>CPU &amp;gt; 90% 不是 SLI — 它是 cause signal。CPU 高但 latency 正常，使用者沒受影響。Disk usage &amp;gt; 85% 也不是 SLI — 它是 capacity signal，需要處理但不代表當下使用者體驗退化。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>SLI 設計起點：user-journey 而非 system metric</li>
<li>量測點選擇：edge / gateway / service / dependency 各自代表什麼</li>
<li>Ratio metric vs latency <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a>：何時用哪種</li>
<li><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">Burn rate</a> 訊號：multi-window multi-burn-rate alert</li>
<li><a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error budget</a> 計算所需的 metric 結構</li>
<li>跟 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a> 的分工：4.2 是 counter/gauge/histogram 基礎、4.6 是 SLI 化的設計</li>
<li>跟 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a> 的分工：4.4 是 alert 規則治理、4.6 是 alert 的訊號源頭</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>SLI 訊號設計是把可靠性目標轉成可量測資料的步驟，責任是讓 SLO 政策建立在使用者旅程與服務結果上。</p>
<p>CPU、memory、queue depth 可以提供系統背景，但 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a> 需要回答的是使用者層面的問題：request 是否成功、回應是否夠快、結果是否正確。SLI 量測的位置跟算式決定了 SLO 反映的是「使用者體驗」還是「基礎設施健康」— 兩者的判讀意義不同。</p>
<p>本章處理的是 metric 到 SLI 的轉換。<a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2</a> 定義 counter / gauge / histogram 的基礎型別；本章定義怎麼用這些型別組出代表使用者體驗的 SLI，並設計 burn rate alert 的訊號結構。SLO 政策本身（error budget freeze、release gate 決策）由 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.6 SLO 政策</a> 處理。</p>
<h2 id="sli-設計起點user-journey">SLI 設計起點：User Journey</h2>
<h3 id="從使用者操作推導-sli">從使用者操作推導 SLI</h3>
<p>SLI 的設計起點是「使用者在做什麼、期待什麼結果」，不是「系統有什麼 metric 可以用」。</p>
<p>一個 checkout 流程的使用者期待：request 成功（不會看到 error page）、回應夠快（不會等超過 3 秒）、結果正確（扣款金額正確）。對應三種 SLI：</p>
<ul>
<li><strong>Availability SLI</strong>：成功 request 的比例（<code>successful_requests / total_requests</code>）</li>
<li><strong>Latency SLI</strong>：回應時間在閾值內的比例（<code>requests_under_3s / total_requests</code>）</li>
<li><strong>Correctness SLI</strong>：結果正確的比例（需要業務邏輯判定，通常用特定 error code 或 reconciliation 結果）</li>
</ul>
<p>每個 user journey 不需要三種 SLI 都有。Checkout 的 availability 跟 latency 是核心；correctness 靠事後對帳驗證。搜尋頁面的 latency 比 availability 更關鍵 — 使用者容忍偶發的「搜不到結果」但不容忍 5 秒的載入。</p>
<h3 id="system-metric-跟-sli-的差異">System metric 跟 SLI 的差異</h3>
<p>CPU &gt; 90% 不是 SLI — 它是 cause signal。CPU 高但 latency 正常，使用者沒受影響。Disk usage &gt; 85% 也不是 SLI — 它是 capacity signal，需要處理但不代表當下使用者體驗退化。</p>
<p>System metric 的價值在 root cause analysis，不在 SLI。事故中先看 SLI 判斷「使用者是否受影響」，確認受影響後再看 system metric 判斷「原因是什麼」。把 system metric 當 SLI 會讓 SLO 反映基礎設施噪音而非使用者體驗。</p>
<h2 id="量測點選擇">量測點選擇</h2>
<p>SLI 的量測點影響「看到的是誰的觀點」。同一個 request 在不同位置量測會得到不同的 latency 跟 success rate。</p>
<h3 id="edge--load-balancer">Edge / Load Balancer</h3>
<p>最貼近使用者的量測點。量到的 latency 包含 network round-trip + TLS handshake + 所有 backend 處理時間。Availability 反映的是使用者實際看到的 success rate（包含 load balancer 自身的 502/503）。</p>
<p>優點是最能代表使用者體驗。缺點是 load balancer 的 metric 粒度有限 — 通常只有 status code 跟 latency，不帶 service-level 的維度切分。</p>
<h3 id="api-gateway">API Gateway</h3>
<p>比 edge 更有應用層上下文。可以按 route / method / tenant 切分 SLI。量到的 latency 不含 network round-trip（已經進入服務網路），但包含 authentication、rate limiting 跟所有下游處理。</p>
<p>API gateway 是多數團隊的 SLI 量測起點 — 粒度足夠、位置夠近使用者、通常已有 instrumentation。</p>
<h3 id="service-level">Service level</h3>
<p>每個服務的 handler-level metric。可以看到單一服務的 latency 跟 error rate，但不含上下游的影響。適合做 service-level SLO（「order service 的 p99 latency &lt; 200ms」），但不直接代表 user-journey SLO。</p>
<p>Service-level SLI 的價值在於 SLO 階層化 — user-journey SLO 拆分成每個服務的 SLO，事故時能快速定位是哪個服務的 SLO 被打破。</p>
<h3 id="dependency-level">Dependency level</h3>
<p>量測外部依賴（database、cache、third-party API）的回應時間跟 error rate。Dependency metric 的角色是 SLI 退化時的歸因訊號，用來追溯因果鏈而非直接代表使用者體驗。Database latency 上升 → service latency 上升 → user-journey latency SLO 被打破 — dependency metric 幫助追溯因果鏈。</p>
<h2 id="sli-的-metric-結構">SLI 的 Metric 結構</h2>
<h3 id="ratio-metricavailability-跟-correctness">Ratio metric：availability 跟 correctness</h3>
<p>Availability SLI 的 metric 結構需要兩個 counter：total requests 跟 successful requests（或 failed requests）。SLI = good / total。</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"># Availability SLI
</span></span><span class="line"><span class="ln">2</span><span class="cl">http_requests_total{service=&#34;checkout&#34;, status=&#34;2xx&#34;} / http_requests_total{service=&#34;checkout&#34;}</span></span></code></pre></div><p>定義「good」的邊界需要明確。5xx 算 bad，4xx 呢？Client error（400）通常不算服務失敗；authentication failure（401/403）也不算。但 429（rate limit）可能代表服務容量不足，視情境可能算 bad。這個邊界要在 SLI 定義時明確寫下來。</p>
<h3 id="latency-metricthreshold-based-ratio">Latency metric：threshold-based ratio</h3>
<p>Latency SLI 用 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 量測，SLI 值是「在閾值內的 request 比例」。</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"># Latency SLI：p99 &lt; 500ms 的比例
</span></span><span class="line"><span class="ln">2</span><span class="cl">histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service=&#34;checkout&#34;}[5m])) &lt; 0.5
</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"># 或用 ratio 形式
</span></span><span class="line"><span class="ln">5</span><span class="cl">sum(rate(http_request_duration_seconds_bucket{le=&#34;0.5&#34;,service=&#34;checkout&#34;}[5m]))
</span></span><span class="line"><span class="ln">6</span><span class="cl">/ sum(rate(http_request_duration_seconds_count{service=&#34;checkout&#34;}[5m]))</span></span></code></pre></div><p>Latency 閾值的選擇要對齊使用者期待而非系統能力。使用者期待 checkout 在 3 秒內完成 — 這是閾值的來源，不是「系統平均 latency 是 200ms 所以閾值設 500ms」。</p>
<h3 id="label-設計">Label 設計</h3>
<p>SLI metric 的 label 需要足夠的切分能力（by service、by endpoint、by tenant），但受 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a> 預算約束。</p>
<p>最小 label set：service name + method（GET/POST）+ status class（2xx/4xx/5xx）。這組 label 支撐 service-level SLO 計算。</p>
<p>擴展 label：endpoint path（normalize 後，例如 <code>/api/orders/{id}</code> → <code>/api/orders/:id</code>）、tenant（多租戶場景）。每增加一個 label 維度，series 數量乘法增長 — 在 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a> 的 label 白名單中管理。</p>
<h2 id="burn-rate-與-multi-window-alert">Burn Rate 與 Multi-window Alert</h2>
<h3 id="burn-rate-的概念">Burn rate 的概念</h3>
<p><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">Burn rate</a> 是「error budget 被消耗的速度」。Burn rate = 1 代表按 SLO 允許的速度正常消耗；burn rate = 10 代表消耗速度是允許值的 10 倍 — 如果持續下去，error budget 會在 SLO 週期的 1/10 內耗盡。</p>
<p>用 burn rate alert 取代固定閾值 alert 的好處：burn rate 自動適應流量。低流量時段的幾筆 error 可能 burn rate 很低（因為 total 也少、對 error budget 影響小）；高流量時段的相同 error rate 可能 burn rate 很高（因為 total 多、影響的使用者量大）。</p>
<h3 id="multi-window-multi-burn-rate">Multi-window multi-burn-rate</h3>
<p>單一時間窗口的 burn rate alert 會太吵（短窗口）或太晚（長窗口）。Multi-window 策略組合兩者：</p>
<table>
  <thead>
      <tr>
          <th>視窗組合</th>
          <th>Burn rate 閾值</th>
          <th>偵測速度</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5min + 1hr</td>
          <td>14.4x</td>
          <td>快</td>
          <td>急性問題、page</td>
      </tr>
      <tr>
          <td>30min + 6hr</td>
          <td>6x</td>
          <td>中</td>
          <td>持續退化</td>
      </tr>
      <tr>
          <td>2hr + 3day</td>
          <td>1x</td>
          <td>慢</td>
          <td>慢性消耗</td>
      </tr>
  </tbody>
</table>
<p>14.4x 的來源：若 SLO 週期是 30 天、要在 1 小時內偵測到會耗盡 2% error budget 的問題，burn rate = (30 × 24) / 1 × 0.02 ≈ 14.4。6x 跟 1x 依此邏輯調整消耗比例跟偵測窗口。</p>
<p>短窗口（5min）抓急性：error rate 突然飆高、burn rate 衝到 14.4x。長窗口（1hr）做確認：退化確實持續、排除瞬間 spike。兩個窗口都超過閾值才觸發 alert，減少單一 spike 的 false alarm。</p>
<h3 id="recording-rule-支撐-burn-rate-計算">Recording rule 支撐 burn rate 計算</h3>
<p>Burn rate 的計算涉及多個時間窗口的 ratio metric。每次 alert evaluate 都重算會給 TSDB 帶來查詢壓力。用 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 把每個窗口的 error ratio 預計算，alert rule 讀 recording rule 的輸出：</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"># Recording rule：5 分鐘窗口的 error ratio
</span></span><span class="line"><span class="ln">2</span><span class="cl">- record: slo:checkout:error_ratio:rate5m
</span></span><span class="line"><span class="ln">3</span><span class="cl">  expr: sum(rate(http_requests_total{service=&#34;checkout&#34;,status=~&#34;5..&#34;}[5m]))
</span></span><span class="line"><span class="ln">4</span><span class="cl">      / sum(rate(http_requests_total{service=&#34;checkout&#34;}[5m]))</span></span></code></pre></div><p>Alert rule 讀 recording rule 比每次重算 raw series 高效，也讓 burn rate 的計算邏輯集中管理。</p>
<h2 id="error-budget-的-metric-結構">Error Budget 的 Metric 結構</h2>
<p><a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error budget</a> 是 SLO 週期內允許的錯誤量。SLO = 99.9% 代表 30 天內允許 0.1% 的 request 失敗。Error budget = total requests × 0.001。</p>
<p>Error budget 的 metric 結構需要：</p>
<ul>
<li><strong>Total requests（rolling window）</strong>：過去 30 天的 total request count</li>
<li><strong>Failed requests（rolling window）</strong>：過去 30 天的 failed request count</li>
<li><strong>Budget consumed</strong>：failed / (total × (1 - SLO target))</li>
<li><strong>Budget remaining</strong>：1 - budget consumed</li>
</ul>
<p>Budget remaining 作為 dashboard panel 跟 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 的輸入 — 餘額低於閾值時 freeze deployment。這個計算的 rolling window 用 recording rule 維護，避免每次查詢掃描 30 天的 raw data。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 SLI 設計時，先看量測點是否貼近使用者，再看算式是否能穩定支援 error budget。</p>
<p>重點訊號包括：</p>
<ul>
<li>Edge / gateway / service / dependency 的量測點是否各自有清楚責任</li>
<li>Latency percentile 與 ratio metric 是否對應不同使用者體驗</li>
<li><a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">Burn rate</a> 是否使用多時間窗，避免太吵或太晚</li>
<li>SLI label 是否有足夠切分能力，同時受 cardinality 預算約束</li>
<li>Error budget 的 rolling window 是否用 recording rule 維護</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Alert 用 system metric（CPU / memory）而非 user-facing 訊號</li>
<li>Burn rate 只有單窗、噪音多或偵測太晚</li>
<li>SLI 計算用平均、不用 percentile</li>
<li>Error budget 算式分母不穩（流量低時誤觸發、高時稀釋）</li>
<li>SLI 量測點離使用者太遠（內部 service 而非 edge/gateway）</li>
<li>SLI 沒有定義「什麼算 good request」的邊界（4xx 算不算 bad）</li>
<li>Burn rate 計算每次重算 raw series、沒有 recording rule</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>System metric 當 SLI</td>
          <td>CPU/memory alert 頻繁但使用者沒受影響</td>
          <td>改用 user-facing ratio / latency SLI</td>
      </tr>
      <tr>
          <td>Burn rate 單窗</td>
          <td>短窗太吵或長窗太晚、alert 價值低</td>
          <td>組合 5min+1hr / 30min+6hr 多窗策略</td>
      </tr>
      <tr>
          <td>SLI 用 average latency</td>
          <td>Tail latency 被掩蓋、p99 使用者體驗失真</td>
          <td>改用 histogram percentile</td>
      </tr>
      <tr>
          <td>Good request 邊界不明</td>
          <td>4xx 算不算 bad、SLI 值忽高忽低</td>
          <td>明確定義 good/bad 分類、寫進 SLI spec</td>
      </tr>
      <tr>
          <td>Error budget 無 rolling</td>
          <td>月初 budget 就耗盡、剩下 20 天沒有保護機制</td>
          <td>用 rolling window 持續計算、預警消耗速度</td>
      </tr>
      <tr>
          <td>SLI label 無界</td>
          <td>每個 URL path 都是獨立 SLI、series 爆炸</td>
          <td>Normalize path、label 白名單、cardinality 預算</td>
      </tr>
      <tr>
          <td>SLO 無 owner</td>
          <td>沒人維護 SLI 定義跟閾值、退化時無人負責</td>
          <td>每個 SLO 帶 owner、定期審視</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a>：counter / gauge / histogram 基礎型別</li>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：burn rate alert 的 noise control 跟 runbook</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：SLI metric 的 cardinality 預算</li>
<li><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 client-side / RUM</a>：user-journey-centric SLI 的前端訊號來源</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：recording rule 支撐 burn rate 計算</li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.6 SLO 政策</a>：error budget 餘額作為 freeze 條件</li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.8 release gate</a>：burn rate 觸發 freeze</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.1 incident severity</a>：burn rate 對應 severity 門檻</li>
<li><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14 anomaly detection</a>：跟 SLO threshold 的訊號分工</li>
</ul>
]]></content:encoded></item><item><title>4.C6 AWS：ADOT on EKS 管線遷移</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/</guid><description>&lt;p>這個案例的核心責任是把 observability 遷移做成管線治理，而不是單點 agent 替換。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>AWS ADOT on EKS 的實務把 metrics、traces 採集策略整合到可管理的 collector pipeline。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>多代理混用雖然能運作，但在規模化時會放大配置漂移與維運成本。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先統一 collector 部署模式。&lt;/li>
&lt;li>將 exporter 與 sampling 規則集中管理。&lt;/li>
&lt;li>以資料品質指標驗證遷移成效。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 observability operating model&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://aws-otel.github.io/docs/getting-started/adot-eks-add-on/">AWS Distro for OpenTelemetry on EKS&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 observability 遷移做成管線治理，而不是單點 agent 替換。</p>
<h2 id="觀察">觀察</h2>
<p>AWS ADOT on EKS 的實務把 metrics、traces 採集策略整合到可管理的 collector pipeline。</p>
<h2 id="判讀">判讀</h2>
<p>多代理混用雖然能運作，但在規模化時會放大配置漂移與維運成本。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先統一 collector 部署模式。</li>
<li>將 exporter 與 sampling 規則集中管理。</li>
<li>以資料品質指標驗證遷移成效。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 與 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 observability operating model</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws-otel.github.io/docs/getting-started/adot-eks-add-on/">AWS Distro for OpenTelemetry on EKS</a></li>
</ul>
]]></content:encoded></item><item><title>Honeycomb</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/</guid><description>&lt;p>Honeycomb 是 high-cardinality observability SaaS、承擔三個責任：events-based 資料模型（不是 metrics aggregation）、unknown-unknowns 偵錯能力（BubbleUp / Heatmap）、observability-driven SRE 文化代表平台。設計取捨偏向「深度優於廣度」、不追求 Datadog 的 integration 廣度、專注於 high-cardinality + distributed system debugging。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 Honeycomb SDK 或 OTel 送 events 到 Honeycomb&lt;/li>
&lt;li>用 BubbleUp 找 outlier 模式（unknown-unknowns）&lt;/li>
&lt;li>設計 SLO + burn rate alert&lt;/li>
&lt;li>配置 Refinery（tail-based sampling）&lt;/li>
&lt;li>評估 Honeycomb vs Datadog 的選用判讀&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-honeycomb-跑起來">最短路徑：5 分鐘把 Honeycomb 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 應用程式加 instrumentation（Honeycomb SDK 或 OTel SDK）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: HONEYCOMB_API_KEY + dataset 設定&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 用 Beeline SDK 或 OTel + OTLP exporter&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 送 sample events&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 觀察 trace 出現在 Honeycomb UI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 用 query 介面查詢&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: SELECT count + visualize by service.name&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="events-vs-metrics-心智模型">Events vs metrics 心智模型&lt;/h3>
&lt;p>Honeycomb 跟 metrics-aggregation 平台不同。子議題：&lt;/p>
&lt;ul>
&lt;li>Event = 一個 trace span（包含 dozens of attributes）&lt;/li>
&lt;li>不預先 aggregate、查詢時 group by 任意 attribute&lt;/li>
&lt;li>High-cardinality 不是問題、是設計目標&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="instrumentation">Instrumentation&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Honeycomb SDK&lt;/strong>（Beeline）：簡單、Honeycomb-specific、auto-instrumentation 部分&lt;/li>
&lt;li>&lt;strong>OTel SDK + OTLP&lt;/strong>：標準、vendor-neutral、推薦新部署用&lt;/li>
&lt;li>Manual attribute：對 business / domain context attribute 不省略&lt;/li>
&lt;li>Refinery：tail-based sampling proxy&lt;/li>
&lt;/ul>
&lt;h3 id="query-介面">Query 介面&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Visualize：count / count_distinct / heatmap / p50 / p95 / p99&lt;/li>
&lt;li>Group by：任意 attribute（user_id / region / version 等）&lt;/li>
&lt;li>Filter：WHERE clause&lt;/li>
&lt;li>對應 SLO query：&lt;code>heatmap(duration_ms) GROUP BY service.name WHERE http.status_code = 500&lt;/code>&lt;/li>
&lt;/ul>
&lt;h2 id="deep-article">Deep Article&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="high-cardinality-query-bubbleup/">High-Cardinality Query Model 與 BubbleUp&lt;/a>：event-based 資料模型、high-cardinality 查詢設計、BubbleUp 異常偵測、SLO / burn rate、derived columns、dataset 設計與 OTLP ingestion&lt;/li>
&lt;/ul>
&lt;h2 id="migration-playbook">Migration Playbook&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="migrate-from-sentry/">Sentry 遷移到 Honeycomb&lt;/a>：error tracking 轉 event-based observability&lt;/li>
&lt;/ul>
&lt;h2 id="進階主題按需閱讀">進階主題（按需閱讀）&lt;/h2>
&lt;h3 id="bubbleup-分析">BubbleUp 分析&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Honeycomb 是 high-cardinality observability SaaS、承擔三個責任：events-based 資料模型（不是 metrics aggregation）、unknown-unknowns 偵錯能力（BubbleUp / Heatmap）、observability-driven SRE 文化代表平台。設計取捨偏向「深度優於廣度」、不追求 Datadog 的 integration 廣度、專注於 high-cardinality + distributed system debugging。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 Honeycomb SDK 或 OTel 送 events 到 Honeycomb</li>
<li>用 BubbleUp 找 outlier 模式（unknown-unknowns）</li>
<li>設計 SLO + burn rate alert</li>
<li>配置 Refinery（tail-based sampling）</li>
<li>評估 Honeycomb vs Datadog 的選用判讀</li>
</ol>
<h2 id="最短路徑5-分鐘把-honeycomb-跑起來">最短路徑：5 分鐘把 Honeycomb 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 應用程式加 instrumentation（Honeycomb SDK 或 OTel SDK）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: HONEYCOMB_API_KEY + dataset 設定</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># TODO: 用 Beeline SDK 或 OTel + OTLP exporter</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"># 2. 送 sample events</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># TODO: 觀察 trace 出現在 Honeycomb UI</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"># 3. 用 query 介面查詢</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="c1"># TODO: SELECT count + visualize by service.name</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="events-vs-metrics-心智模型">Events vs metrics 心智模型</h3>
<p>Honeycomb 跟 metrics-aggregation 平台不同。子議題：</p>
<ul>
<li>Event = 一個 trace span（包含 dozens of attributes）</li>
<li>不預先 aggregate、查詢時 group by 任意 attribute</li>
<li>High-cardinality 不是問題、是設計目標</li>
<li>對應 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a></li>
</ul>
<h3 id="instrumentation">Instrumentation</h3>
<p>子議題：</p>
<ul>
<li><strong>Honeycomb SDK</strong>（Beeline）：簡單、Honeycomb-specific、auto-instrumentation 部分</li>
<li><strong>OTel SDK + OTLP</strong>：標準、vendor-neutral、推薦新部署用</li>
<li>Manual attribute：對 business / domain context attribute 不省略</li>
<li>Refinery：tail-based sampling proxy</li>
</ul>
<h3 id="query-介面">Query 介面</h3>
<p>子議題：</p>
<ul>
<li>Visualize：count / count_distinct / heatmap / p50 / p95 / p99</li>
<li>Group by：任意 attribute（user_id / region / version 等）</li>
<li>Filter：WHERE clause</li>
<li>對應 SLO query：<code>heatmap(duration_ms) GROUP BY service.name WHERE http.status_code = 500</code></li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="high-cardinality-query-bubbleup/">High-Cardinality Query Model 與 BubbleUp</a>：event-based 資料模型、high-cardinality 查詢設計、BubbleUp 異常偵測、SLO / burn rate、derived columns、dataset 設計與 OTLP ingestion</li>
</ul>
<h2 id="migration-playbook">Migration Playbook</h2>
<ul>
<li><a href="migrate-from-sentry/">Sentry 遷移到 Honeycomb</a>：error tracking 轉 event-based observability</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="bubbleup-分析">BubbleUp 分析</h3>
<p>子議題：</p>
<ul>
<li>給定 heatmap 異常區、自動找區隔 outlier 跟 baseline 的 attribute</li>
<li>適合「我看到 latency spike、但不知道哪個維度造成」</li>
<li>Unknown-unknowns 偵錯模式</li>
<li>跟 Datadog APM 的 service map 對照</li>
</ul>
<h3 id="slo-與-burn-rate-alert">SLO 與 burn rate alert</h3>
<p>子議題：</p>
<ul>
<li>SLO 配置（service + indicator + objective + window）</li>
<li>Burn rate calculation：multi-window multi-burn-rate alert</li>
<li>跟 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate</a> 對照</li>
<li>對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></li>
</ul>
<h3 id="refinerytail-based-sampling">Refinery（tail-based sampling）</h3>
<p>子議題：</p>
<ul>
<li>為什麼需要 tail-based：保留有錯 / 高延遲 trace、丟正常 trace</li>
<li>Refinery 部署模式（gateway in front of Honeycomb）</li>
<li>Sampling rule：error / latency / per-service / dynamic</li>
<li>對應成本：100% ingestion 太貴、tail-based 平衡</li>
</ul>
<h3 id="otlp-integration">OTLP integration</h3>
<p>子議題：</p>
<ul>
<li>Honeycomb 接受 OTLP（gRPC / HTTP）</li>
<li>應用層用 OTel SDK、傳給 Honeycomb 不用改 SDK</li>
<li>Multi-backend 支援：同一份 OTel data 送 Honeycomb + 其他</li>
<li>對應 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a></li>
</ul>
<h3 id="結構化-events-設計">結構化 events 設計</h3>
<p>子議題：</p>
<ul>
<li>哪些 attribute 應加（user_id / request_id / business 維度）</li>
<li>哪些 attribute 不該加（PII / secrets）</li>
<li>Wide events 哲學：一個 event 帶 dozens of attributes、不分散到多 metric</li>
<li>對應 PII redaction strategy</li>
</ul>
<h3 id="observability-driven-development">Observability-driven development</h3>
<p>子議題：</p>
<ul>
<li>Charity Majors 提的 SDLC 模式：production debug 是常態</li>
<li>TDD + observability：寫 code 同時思考可觀測性</li>
<li>跟 SRE 文化整合</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="events-沒到-honeycomb">Events 沒到 Honeycomb</h3>
<p>操作原則：先看 SDK 配置（API key + dataset）、再看 network、最後看 Honeycomb status page。</p>
<h3 id="query-timeout">Query timeout</h3>
<p>操作原則：query window 過大或 attribute cardinality 過高造成 backend slow。判讀：縮 time window、簡化 group by。</p>
<h3 id="sampling-過頭-vs-不足">Sampling 過頭 vs 不足</h3>
<p>操作原則：debug 時找不到 trace（sampling 過頭）vs cost 爆（sampling 不足）。Refinery 提供 dynamic sampling 解決靜態 rate 的不足。</p>
<h3 id="burn-rate-alert-noise">Burn rate alert noise</h3>
<p>操作原則：multi-window 設計避免「短暫 spike 觸發 alert」、低 burn rate window 給長期趨勢。</p>
<h3 id="跟其他-backend-dual-ship-不一致">跟其他 backend dual ship 不一致</h3>
<p>對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a>。判讀：兩個 backend 數據不對齊、看 SDK 是否 dual export、attribute mapping 是否一致。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>廣度大、要 600+ integrations</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></td>
      </tr>
      <tr>
          <td>預算敏感</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（OSS）</td>
      </tr>
      <tr>
          <td>Pure metrics</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></td>
      </tr>
      <tr>
          <td>Logs full-text</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a></td>
      </tr>
      <tr>
          <td>Error tracking 為主</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
      <tr>
          <td>Cloud-native (AWS / GCP)</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> / <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Ops</a></td>
      </tr>
      <tr>
          <td>Self-hosted</td>
          <td>OSS observability（Honeycomb 是 SaaS only）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Honeycomb SDK 完整 API</li>
<li>BubbleUp 內部演算法</li>
<li>Refinery 詳細配置</li>
<li>Honeycomb pricing 詳細</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a></td>
          <td>High-cardinality debug pattern</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel signal drift</a></td>
          <td>（反例）Refinery / dual ship 對齊驗證</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Honeycomb 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a></td>
          <td>從 Datadog APM 遷出時 Honeycomb 是 events 替代</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s scale signals</a></td>
          <td>動態叢集下 wide events 補 metrics 維度不足</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>Honeycomb 適合中大型 + observability-driven team</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Honeycomb 案例</strong>：Charity Majors 的 production talks、Honeycomb customer engineering blog、Refinery scale-up case。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>、<a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></li>
<li>下游能力：<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">06 reliability 模組</a>（SLO / burn rate）、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>5.6 Hook 系統可觀測性設計</title><link>https://tarrragon.github.io/blog/python/05-error-testing/observability-design/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/python/05-error-testing/observability-design/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">上一章&lt;/a>介紹了 &lt;code>run_hook_safely&lt;/code> 這個頂層例外處理器，解決了「44 個 Hook 各自處理錯誤」的問題。但「捕獲錯誤」只是可觀測性的第一步。真正的問題是：&lt;/p>
&lt;blockquote>
&lt;p>當 44 個 Hook 每天執行數百次，你怎麼知道它們運行正常？出了問題你怎麼找到原因？&lt;/p>&lt;/blockquote>
&lt;p>本章從三個維度建立 Hook 系統的可觀測性：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>解決的問題&lt;/th>
 &lt;th>核心機制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>日誌架構&lt;/td>
 &lt;td>每次執行的痕跡在哪裡？&lt;/td>
 &lt;td>Structured Logging + Log Rotation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>錯誤可見性&lt;/td>
 &lt;td>出錯了誰來告訴用戶？&lt;/td>
 &lt;td>stderr 輸出 + Fallback 策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>健康監控&lt;/td>
 &lt;td>系統整體是否正常？&lt;/td>
 &lt;td>執行時間追蹤 + 日誌清理&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="一日誌架構設計">一、日誌架構設計&lt;/h2>
&lt;h3 id="11-需求分析">1.1 需求分析&lt;/h3>
&lt;p>Hook 日誌系統和一般應用程式的日誌有兩個根本差異：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>差異&lt;/th>
 &lt;th>一般應用程式&lt;/th>
 &lt;th>Hook 系統&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>生命週期&lt;/td>
 &lt;td>長時間運行&lt;/td>
 &lt;td>每次觸發執行一次（秒級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實例數量&lt;/td>
 &lt;td>1-3 個服務&lt;/td>
 &lt;td>44 個獨立腳本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>日誌量&lt;/td>
 &lt;td>大量、持續&lt;/td>
 &lt;td>少量、離散&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>讀者&lt;/td>
 &lt;td>運維團隊&lt;/td>
 &lt;td>開發者自己&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些差異決定了日誌架構的選擇：不需要集中式日誌服務，但需要&lt;strong>按 Hook 名稱隔離&lt;/strong>和&lt;strong>按時間自動清理&lt;/strong>。&lt;/p>
&lt;h3 id="12-目錄結構設計">1.2 目錄結構設計&lt;/h3>





&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">.claude/hook-logs/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">├── acceptance-gate-hook/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">│ ├── acceptance-gate-hook-20260304-091523.log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">│ ├── acceptance-gate-hook-20260304-091845.log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">│ └── .cleanup_trigger # 清理觸發計數器
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">├── command-entrance-gate-hook/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">│ ├── command-entrance-gate-hook-20260304-091523.log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">│ └── ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">└── phase-completion-gate-hook/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> └── ...&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>每個 Hook 有獨立的日誌目錄。每次執行產生一個獨立的日誌檔案，檔名包含時間戳。這個設計的好處：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>隔離性&lt;/strong>：排查問題時只需看特定 Hook 的目錄&lt;/li>
&lt;li>&lt;strong>時間線&lt;/strong>：按檔名排序就能看到執行歷史&lt;/li>
&lt;li>&lt;strong>清理&lt;/strong>：按目錄或按時間清理都很容易&lt;/li>
&lt;/ul>
&lt;h3 id="13-日誌系統初始化">1.3 日誌系統初始化&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">setup_hook_logging&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Logger&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="s2">&amp;#34;&amp;#34;&amp;#34;建立並設定 Hook 日誌系統&amp;#34;&amp;#34;&amp;#34;&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">if&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">hook_name&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="n">hook_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">DEFAULT_HOOK_NAME&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="n">sanitized_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_sanitize_hook_name&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&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="n">root_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">_find_project_root&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="n">log_base_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">root_dir&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s2">&amp;#34;.claude&amp;#34;&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s2">&amp;#34;hook-logs&amp;#34;&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">sanitized_name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="c1"># 建立日誌目錄（失敗時降級，不拋出異常）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="n">log_base_dir&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mkdir&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parents&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">exist_ok&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="ne">OSError&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">_create_fallback_logger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getLogger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hook_name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl"> &lt;span class="n">_clear_logger_handlers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setLevel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">DEBUG&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl"> &lt;span class="n">is_debug&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">os&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getenv&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;HOOK_DEBUG&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">lower&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;true&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> &lt;span class="n">_setup_logger_handlers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">log_base_dir&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">sanitized_name&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">is_debug&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">logger&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段程式碼有幾個值得注意的設計決策。&lt;/p>
&lt;p>&lt;strong>Named Logger&lt;/strong>：使用 &lt;code>logging.getLogger(hook_name)&lt;/code> 取得 named logger，而非 root logger。這確保每個 Hook 的日誌設定互不干擾：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 每個 Hook 有自己的 logger 實例&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n">logger_a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getLogger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;acceptance-gate-hook&amp;#34;&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="n">logger_b&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">getLogger&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;command-entrance-gate-hook&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="c1"># 兩者的 handlers、level、format 完全獨立&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Handler 清理&lt;/strong>：每次初始化前先清除舊的 handlers。這防止同一個 logger 被重複配置（例如在測試中多次呼叫 &lt;code>setup_hook_logging&lt;/code>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">_clear_logger_handlers&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">logger&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">logging&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">Logger&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">-&amp;gt;&lt;/span> &lt;span class="kc">None&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="s2">&amp;#34;&amp;#34;&amp;#34;清除 logger 的所有 handlers&amp;#34;&amp;#34;&amp;#34;&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">for&lt;/span> &lt;span class="n">handler&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">handlers&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="n">logger&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">removeHandler&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">handler&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="n">handler&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close&lt;/span>&lt;span class="p">()&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意 &lt;code>logger.handlers[:]&lt;/code> 的切片複製。直接遍歷 &lt;code>logger.handlers&lt;/code> 並在迴圈中 &lt;code>removeHandler&lt;/code> 會修改列表長度，導致跳過元素。這是 Python 中遍歷時修改集合的經典陷阱。&lt;/p>
&lt;p>&lt;strong>環境變數控制&lt;/strong>：透過 &lt;code>HOOK_DEBUG&lt;/code> 環境變數切換日誌詳細程度，不需要修改程式碼：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 正常模式：stdout 只顯示 WARNING 以上&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">python3 my-hook.py
&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="c1"># 除錯模式：stdout 顯示所有等級&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nv">HOOK_DEBUG&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> python3 my-hook.py&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="14-雙通道輸出">1.4 雙通道輸出&lt;/h3>
&lt;p>每個 logger 配置兩個 handler，分別負責不同用途：&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">上一章</a>介紹了 <code>run_hook_safely</code> 這個頂層例外處理器，解決了「44 個 Hook 各自處理錯誤」的問題。但「捕獲錯誤」只是可觀測性的第一步。真正的問題是：</p>
<blockquote>
<p>當 44 個 Hook 每天執行數百次，你怎麼知道它們運行正常？出了問題你怎麼找到原因？</p></blockquote>
<p>本章從三個維度建立 Hook 系統的可觀測性：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>解決的問題</th>
          <th>核心機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>日誌架構</td>
          <td>每次執行的痕跡在哪裡？</td>
          <td>Structured Logging + Log Rotation</td>
      </tr>
      <tr>
          <td>錯誤可見性</td>
          <td>出錯了誰來告訴用戶？</td>
          <td>stderr 輸出 + Fallback 策略</td>
      </tr>
      <tr>
          <td>健康監控</td>
          <td>系統整體是否正常？</td>
          <td>執行時間追蹤 + 日誌清理</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="一日誌架構設計">一、日誌架構設計</h2>
<h3 id="11-需求分析">1.1 需求分析</h3>
<p>Hook 日誌系統和一般應用程式的日誌有兩個根本差異：</p>
<table>
  <thead>
      <tr>
          <th>差異</th>
          <th>一般應用程式</th>
          <th>Hook 系統</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>生命週期</td>
          <td>長時間運行</td>
          <td>每次觸發執行一次（秒級）</td>
      </tr>
      <tr>
          <td>實例數量</td>
          <td>1-3 個服務</td>
          <td>44 個獨立腳本</td>
      </tr>
      <tr>
          <td>日誌量</td>
          <td>大量、持續</td>
          <td>少量、離散</td>
      </tr>
      <tr>
          <td>讀者</td>
          <td>運維團隊</td>
          <td>開發者自己</td>
      </tr>
  </tbody>
</table>
<p>這些差異決定了日誌架構的選擇：不需要集中式日誌服務，但需要<strong>按 Hook 名稱隔離</strong>和<strong>按時間自動清理</strong>。</p>
<h3 id="12-目錄結構設計">1.2 目錄結構設計</h3>





<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">.claude/hook-logs/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── acceptance-gate-hook/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">│   ├── acceptance-gate-hook-20260304-091523.log
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">│   ├── acceptance-gate-hook-20260304-091845.log
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">│   └── .cleanup_trigger           # 清理觸發計數器
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">├── command-entrance-gate-hook/
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│   ├── command-entrance-gate-hook-20260304-091523.log
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│   └── ...
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">└── phase-completion-gate-hook/
</span></span><span class="line"><span class="ln">10</span><span class="cl">    └── ...</span></span></code></pre></div><p>每個 Hook 有獨立的日誌目錄。每次執行產生一個獨立的日誌檔案，檔名包含時間戳。這個設計的好處：</p>
<ul>
<li><strong>隔離性</strong>：排查問題時只需看特定 Hook 的目錄</li>
<li><strong>時間線</strong>：按檔名排序就能看到執行歷史</li>
<li><strong>清理</strong>：按目錄或按時間清理都很容易</li>
</ul>
<h3 id="13-日誌系統初始化">1.3 日誌系統初始化</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="k">def</span> <span class="nf">setup_hook_logging</span><span class="p">(</span><span class="n">hook_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">logging</span><span class="o">.</span><span class="n">Logger</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;&#34;&#34;建立並設定 Hook 日誌系統&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">hook_name</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">hook_name</span> <span class="o">=</span> <span class="n">DEFAULT_HOOK_NAME</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">sanitized_name</span> <span class="o">=</span> <span class="n">_sanitize_hook_name</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">root_dir</span> <span class="o">=</span> <span class="n">_find_project_root</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">log_base_dir</span> <span class="o">=</span> <span class="n">root_dir</span> <span class="o">/</span> <span class="s2">&#34;.claude&#34;</span> <span class="o">/</span> <span class="s2">&#34;hook-logs&#34;</span> <span class="o">/</span> <span class="n">sanitized_name</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="c1"># 建立日誌目錄（失敗時降級，不拋出異常）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">log_base_dir</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">except</span> <span class="ne">OSError</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">return</span> <span class="n">_create_fallback_logger</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">_clear_logger_handlers</span><span class="p">(</span><span class="n">logger</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">setLevel</span><span class="p">(</span><span class="n">logging</span><span class="o">.</span><span class="n">DEBUG</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="n">is_debug</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;HOOK_DEBUG&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;true&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="n">_setup_logger_handlers</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">log_base_dir</span><span class="p">,</span> <span class="n">sanitized_name</span><span class="p">,</span> <span class="n">is_debug</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">return</span> <span class="n">logger</span></span></span></code></pre></div><p>這段程式碼有幾個值得注意的設計決策。</p>
<p><strong>Named Logger</strong>：使用 <code>logging.getLogger(hook_name)</code> 取得 named logger，而非 root logger。這確保每個 Hook 的日誌設定互不干擾：</p>





<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="c1"># 每個 Hook 有自己的 logger 實例</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">logger_a</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s2">&#34;acceptance-gate-hook&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">logger_b</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s2">&#34;command-entrance-gate-hook&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 兩者的 handlers、level、format 完全獨立</span></span></span></code></pre></div><p><strong>Handler 清理</strong>：每次初始化前先清除舊的 handlers。這防止同一個 logger 被重複配置（例如在測試中多次呼叫 <code>setup_hook_logging</code>）：</p>





<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="k">def</span> <span class="nf">_clear_logger_handlers</span><span class="p">(</span><span class="n">logger</span><span class="p">:</span> <span class="n">logging</span><span class="o">.</span><span class="n">Logger</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="s2">&#34;&#34;&#34;清除 logger 的所有 handlers&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">for</span> <span class="n">handler</span> <span class="ow">in</span> <span class="n">logger</span><span class="o">.</span><span class="n">handlers</span><span class="p">[:]:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">removeHandler</span><span class="p">(</span><span class="n">handler</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">handler</span><span class="o">.</span><span class="n">close</span><span class="p">()</span></span></span></code></pre></div><p>注意 <code>logger.handlers[:]</code> 的切片複製。直接遍歷 <code>logger.handlers</code> 並在迴圈中 <code>removeHandler</code> 會修改列表長度，導致跳過元素。這是 Python 中遍歷時修改集合的經典陷阱。</p>
<p><strong>環境變數控制</strong>：透過 <code>HOOK_DEBUG</code> 環境變數切換日誌詳細程度，不需要修改程式碼：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 正常模式：stdout 只顯示 WARNING 以上</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">python3 my-hook.py
</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="c1"># 除錯模式：stdout 顯示所有等級</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nv">HOOK_DEBUG</span><span class="o">=</span><span class="nb">true</span> python3 my-hook.py</span></span></code></pre></div><h3 id="14-雙通道輸出">1.4 雙通道輸出</h3>
<p>每個 logger 配置兩個 handler，分別負責不同用途：</p>





<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="k">def</span> <span class="nf">_setup_logger_handlers</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">log_base_dir</span><span class="p">,</span> <span class="n">sanitized_name</span><span class="p">,</span> <span class="n">is_debug</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;&#34;&#34;為 logger 配置 handlers&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="c1"># 檔案 handler：記錄所有等級，供事後分析</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">timestamp</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&#34;%Y%m</span><span class="si">%d</span><span class="s2">-%H%M%S&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">log_file_path</span> <span class="o">=</span> <span class="n">log_base_dir</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">sanitized_name</span><span class="si">}</span><span class="s2">-</span><span class="si">{</span><span class="n">timestamp</span><span class="si">}</span><span class="s2">.log&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">file_handler</span> <span class="o">=</span> <span class="n">_create_file_handler</span><span class="p">(</span><span class="n">log_file_path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="n">file_handler</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">addHandler</span><span class="p">(</span><span class="n">file_handler</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="c1"># 控制台 handler：正常模式只顯示 WARNING+，除錯模式顯示全部</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">addHandler</span><span class="p">(</span><span class="n">_create_stream_handler</span><span class="p">(</span><span class="n">is_debug</span><span class="p">))</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>Handler</th>
          <th>輸出目標</th>
          <th>等級</th>
          <th>格式</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FileHandler</td>
          <td>日誌檔案</td>
          <td>DEBUG</td>
          <td><code>[2026-03-04 09:15:23] DEBUG - message</code></td>
          <td>事後分析</td>
      </tr>
      <tr>
          <td>StreamHandler</td>
          <td>stdout</td>
          <td>WARNING（正常）/ DEBUG（除錯）</td>
          <td><code>[WARNING] message</code></td>
          <td>即時回饋</td>
      </tr>
  </tbody>
</table>
<p>為什麼 StreamHandler 輸出到 <strong>stdout</strong> 而非 stderr？這和 Claude Code 的 Hook 系統規則有關：</p>
<table>
  <thead>
      <tr>
          <th>輸出管道</th>
          <th>Claude Code 的解讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>stdout</td>
          <td>正常訊息，顯示為 <code>hook success</code></td>
      </tr>
      <tr>
          <td>stderr</td>
          <td>錯誤訊息，顯示為 <code>hook error</code></td>
      </tr>
  </tbody>
</table>
<p>日誌中的 WARNING 訊息是給開發者的提醒，不是 Hook 執行失敗。如果把 WARNING 輸出到 stderr，Claude Code 會把它當成錯誤。所以 StreamHandler 必須走 stdout。</p>
<h3 id="15-hook-名稱淨化">1.5 Hook 名稱淨化</h3>
<p>Hook 名稱會用於檔案系統路徑（目錄名和檔名），所以需要淨化：</p>





<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="k">def</span> <span class="nf">_sanitize_hook_name</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;&#34;&#34;淨化 hook 名稱，移除無法用於檔案系統的字元&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">name</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">return</span> <span class="n">DEFAULT_HOOK_NAME</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">for</span> <span class="n">char</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">&#34;&lt;&#34;</span><span class="p">,</span> <span class="s2">&#34;&gt;&#34;</span><span class="p">,</span> <span class="s2">&#34;|&#34;</span><span class="p">]:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">char</span><span class="p">,</span> <span class="s2">&#34;-&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;/&#34;</span><span class="p">,</span> <span class="s2">&#34;-&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\\</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;-&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="c1"># 合併連續 &#34;-&#34; 並移除前後</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">while</span> <span class="s2">&#34;--&#34;</span> <span class="ow">in</span> <span class="n">name</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&#34;--&#34;</span><span class="p">,</span> <span class="s2">&#34;-&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="n">name</span><span class="o">.</span><span class="n">strip</span><span class="p">(</span><span class="s2">&#34;-&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">return</span> <span class="n">name</span> <span class="k">if</span> <span class="n">name</span> <span class="k">else</span> <span class="n">DEFAULT_HOOK_NAME</span></span></span></code></pre></div><p>這是防禦性程式設計的典型例子。雖然目前所有 Hook 的名稱都是合法的檔案名（像 <code>acceptance-gate-hook</code>），但<strong>不能假設呼叫端一定傳入合法名稱</strong>。淨化函式確保即使傳入 <code>&lt;invalid|name&gt;</code> 也能產生合法的目錄名 <code>invalid-name</code>。</p>
<h3 id="16-專案根目錄定位">1.6 專案根目錄定位</h3>
<p>日誌目錄在專案根目錄下的 <code>.claude/hook-logs/</code>。但 Hook 可能從不同的工作目錄被執行，所以需要動態定位：</p>





<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="k">def</span> <span class="nf">_find_project_root</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">Path</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;&#34;&#34;查詢專案根目錄
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">    優先順序：
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">    1. 環境變數 CLAUDE_PROJECT_DIR
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">    2. 從 cwd 向上搜尋 CLAUDE.md（最多 5 層）
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">    3. os.getcwd() fallback（永不失敗）
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">    &#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">env_dir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;CLAUDE_PROJECT_DIR&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">if</span> <span class="n">env_dir</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="n">Path</span><span class="p">(</span><span class="n">env_dir</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">current_dir</span> <span class="o">=</span> <span class="n">Path</span><span class="o">.</span><span class="n">cwd</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">CLAUDE_MD_SEARCH_DEPTH</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">current_dir</span> <span class="o">/</span> <span class="s2">&#34;CLAUDE.md&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">            <span class="k">return</span> <span class="n">current_dir</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">parent</span> <span class="o">=</span> <span class="n">current_dir</span><span class="o">.</span><span class="n">parent</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="k">if</span> <span class="n">parent</span> <span class="o">==</span> <span class="n">current_dir</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="k">break</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="n">current_dir</span> <span class="o">=</span> <span class="n">parent</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">return</span> <span class="n">Path</span><span class="o">.</span><span class="n">cwd</span><span class="p">()</span></span></span></code></pre></div><p>三層 fallback 的設計邏輯：</p>
<table>
  <thead>
      <tr>
          <th>優先級</th>
          <th>方式</th>
          <th>適用場景</th>
          <th>失敗條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>環境變數</td>
          <td>Claude Code 啟動時自動設定</td>
          <td>手動執行時未設定</td>
      </tr>
      <tr>
          <td>2</td>
          <td>向上搜尋 CLAUDE.md</td>
          <td>手動執行、測試</td>
          <td>在非專案目錄執行</td>
      </tr>
      <tr>
          <td>3</td>
          <td>cwd</td>
          <td>最後手段</td>
          <td>永不失敗</td>
      </tr>
  </tbody>
</table>
<p>注意搜尋深度限制 <code>CLAUDE_MD_SEARCH_DEPTH = 5</code>。不做深度限制的話，在 <code>/</code> 目錄執行時會遍歷整個檔案系統。5 層足以覆蓋大多數專案結構（<code>/Users/user/projects/my-app/.claude/hooks/</code> 需要 4 層）。</p>
<hr>
<h2 id="二錯誤可見性設計">二、錯誤可見性設計</h2>
<h3 id="21-核心問題靜默失敗">2.1 核心問題：靜默失敗</h3>
<p>IMP-003 事件是錯誤可見性設計的直接動機。7 個 Hook 因為變數作用域問題（<code>NameError</code>）靜默失敗了至少 2 個 session。失敗的流程是：</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">Hook 執行 → NameError → run_hook_safely 捕獲 → 寫入日誌檔案 → 返回 EXIT_ERROR
</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></span></code></pre></div><p>問題出在 <code>_log_exception</code> 的初版只寫入日誌檔案：</p>





<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="c1"># W25-005 之前的版本（有缺陷）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">_log_exception</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">hook_name</span><span class="p">,</span> <span class="n">tb_str</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">critical</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Unhandled exception in </span><span class="si">{</span><span class="n">hook_name</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">critical</span><span class="p">(</span><span class="n">tb_str</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="c1"># 到這裡就結束了 -- 用戶完全不知道出錯</span></span></span></code></pre></div><h3 id="22-修正stderr-強制可見">2.2 修正：stderr 強制可見</h3>
<p>W25-005 在日誌寫入之後加了一行 stderr 輸出：</p>





<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="k">def</span> <span class="nf">_log_exception</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">hook_name</span><span class="p">,</span> <span class="n">tb_str</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;&#34;&#34;記錄異常 traceback 到日誌&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="c1"># 1. 寫入日誌檔案（完整 traceback，供事後分析）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">critical</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Unhandled exception in </span><span class="si">{</span><span class="n">hook_name</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">critical</span><span class="p">(</span><span class="n">tb_str</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">logging_error</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="c1"># 日誌系統本身也可能失敗（磁碟滿了、權限問題）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Failed to log exception: </span><span class="si">{</span><span class="n">logging_error</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">tb_str</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="c1"># 2. 輸出到 stderr，讓 Claude Code 顯示 &#34;hook error&#34;（W25-005 新增）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nb">print</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="sa">f</span><span class="s2">&#34;[Hook Error] </span><span class="si">{</span><span class="n">hook_name</span><span class="si">}</span><span class="s2"> failed unexpectedly. &#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="sa">f</span><span class="s2">&#34;Check hook logs for details.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="p">)</span></span></span></code></pre></div><p>這個設計的關鍵在於<strong>兩層輸出各司其職</strong>：</p>
<table>
  <thead>
      <tr>
          <th>輸出</th>
          <th>目標</th>
          <th>內容</th>
          <th>讀者</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>日誌檔案</td>
          <td><code>.claude/hook-logs/{name}/</code></td>
          <td>完整 traceback</td>
          <td>開發者（事後分析）</td>
      </tr>
      <tr>
          <td>stderr</td>
          <td>Claude Code UI</td>
          <td>簡短錯誤提示</td>
          <td>用戶（即時感知）</td>
      </tr>
  </tbody>
</table>
<p><strong>為什麼不把完整 traceback 輸出到 stderr？</strong> 因為 stderr 的內容會直接顯示在 Claude Code 的對話介面中。一段 20 行的 Python traceback 對用戶來說是噪音。只需要告訴用戶「哪個 Hook 出錯了」和「去哪裡看詳情」就夠了。</p>
<h3 id="23-日誌系統自身的-fallback">2.3 日誌系統自身的 Fallback</h3>
<p>如果日誌系統本身出了問題（例如磁碟已滿，無法寫入日誌檔案），怎麼辦？</p>





<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="c1"># 目錄建立失敗時的 Fallback</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">log_base_dir</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</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="k">except</span> <span class="ne">OSError</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">return</span> <span class="n">_create_fallback_logger</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>  <span class="c1"># 降級為純 stdout 輸出</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">_create_fallback_logger</span><span class="p">(</span><span class="n">hook_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">logging</span><span class="o">.</span><span class="n">Logger</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="s2">&#34;&#34;&#34;建立 Fallback Logger（僅 StreamHandler）&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">_clear_logger_handlers</span><span class="p">(</span><span class="n">logger</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">setLevel</span><span class="p">(</span><span class="n">logging</span><span class="o">.</span><span class="n">DEBUG</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">logger</span><span class="o">.</span><span class="n">addHandler</span><span class="p">(</span><span class="n">_create_stream_handler</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">return</span> <span class="n">logger</span></span></span></code></pre></div><p>Fallback Logger 只有 StreamHandler（stdout），沒有 FileHandler。這表示日誌不會被儲存到檔案，但<strong>至少 Hook 能正常運行</strong>，而且重要訊息仍然會出現在控制台。</p>
<p>這體現了一個重要的設計原則：<strong>可觀測性基礎設施的故障不應該導致業務功能中斷</strong>。日誌系統壞了，Hook 仍然要能工作。</p>
<h3 id="24-imp-005-的教訓import-階段的可見性">2.4 IMP-005 的教訓：Import 階段的可見性</h3>
<p>IMP-005 暴露了另一個可見性盲區：<strong>import 階段的錯誤</strong>。當模組遷移後 import 路徑沒更新，<code>ModuleNotFoundError</code> 在 <code>run_hook_safely</code> 之前就發生了：</p>





<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="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</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"># 這一行在 run_hook_safely 之前執行</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 如果失敗，run_hook_safely 根本不會被呼叫</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kn">from</span> <span class="nn">lib.common_functions</span> <span class="kn">import</span> <span class="n">hook_output</span>  <span class="c1"># ModuleNotFoundError!</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kn">from</span> <span class="nn">hook_utils</span> <span class="kn">import</span> <span class="n">run_hook_safely</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">run_hook_safely</span><span class="p">(</span><span class="n">main</span><span class="p">,</span> <span class="s2">&#34;my-hook&#34;</span><span class="p">))</span></span></span></code></pre></div><p><code>run_hook_safely</code> 的保護範圍是 <code>main()</code> 函式內部，但 import 發生在模組載入階段。解決方案是在 import 處加入 try-except 防護：</p>





<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="ch">#!/usr/bin/env python3</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</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"># Import 防護：確保失敗時有明確的 stderr 輸出</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="kn">from</span> <span class="nn">hook_utils</span> <span class="kn">import</span> <span class="n">run_hook_safely</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="kn">from</span> <span class="nn">lib.common_functions</span> <span class="kn">import</span> <span class="n">hook_output</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">except</span> <span class="ne">ImportError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Hook Import Error] </span><span class="si">{</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>沒有 Import 防護</th>
          <th>有 Import 防護</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Code 顯示 <code>hook error</code></td>
          <td>Claude Code 顯示 <code>hook error</code></td>
      </tr>
      <tr>
          <td>無法得知是哪個 Hook</td>
          <td><code>[Hook Import Error] my-hook.py: No module named 'common_functions'</code></td>
      </tr>
      <tr>
          <td>無法得知什麼原因</td>
          <td>精確到模組名稱和檔案名稱</td>
      </tr>
  </tbody>
</table>
<h3 id="25-imp-006-的教訓兩條錯誤路徑">2.5 IMP-006 的教訓：兩條錯誤路徑</h3>
<p>IMP-006 案例 D 揭示了一個更隱蔽的問題：Hook 有兩條不同的「失敗路徑」，但只有一條有 stderr 輸出。</p>





<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="k">def</span> <span class="nf">main</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="c1"># ...驗證邏輯...</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="k">if</span> <span class="n">should_block</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="c1"># 路徑 1：業務邏輯拒絕（有意阻止）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;error&#34;</span><span class="p">:</span> <span class="n">error_message</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">result</span><span class="p">),</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">return</span> <span class="mi">2</span>  <span class="c1"># 只有 stdout，沒有 stderr！</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># run_hook_safely 包裝</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 路徑 2：未預期異常 -- _log_exception 已有 stderr 輸出</span></span></span></code></pre></div><p>開發者只考慮了「未預期異常」這條路徑（由 <code>_log_exception</code> 處理），忘了「有意阻止」也需要 stderr 輸出。修復：</p>





<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="k">if</span> <span class="n">should_block</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;error&#34;</span><span class="p">:</span> <span class="n">error_message</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">result</span><span class="p">),</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1"># 新增：確保用戶在 Claude Code UI 能看到拒絕原因</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Agent Ticket Validation] blocked: </span><span class="si">{</span><span class="n">error_message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="mi">2</span></span></span></code></pre></div><p>教訓歸納為一條規則：<strong>Hook 的所有非成功路徑都必須有 stderr 輸出</strong>。不只是 exception，業務邏輯的拒絕也算。</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">Hook 執行結果
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── 成功（return 0）→ stdout 正常訊息
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── 未預期異常（Exception）→ stderr 由 _log_exception 處理
</span></span><span class="line"><span class="ln">4</span><span class="cl">└── 有意阻止（return 非 0）→ stderr 必須有原因說明  ← 容易遺漏</span></span></code></pre></div><hr>
<h2 id="三健康監控設計">三、健康監控設計</h2>
<h3 id="31-執行時間追蹤">3.1 執行時間追蹤</h3>
<p><code>run_hook_safely</code> 記錄每次執行的耗時：</p>





<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="k">def</span> <span class="nf">run_hook_safely</span><span class="p">(</span><span class="n">main_func</span><span class="p">,</span> <span class="n">hook_name</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">logger</span> <span class="o">=</span> <span class="n">setup_hook_logging</span><span class="p">(</span><span class="n">hook_name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</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="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">exit_code</span> <span class="o">=</span> <span class="n">main_func</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">elapsed_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Hook execution time: </span><span class="si">{</span><span class="n">elapsed_time</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">s&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="n">exit_code</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">except</span> <span class="p">(</span><span class="ne">KeyboardInterrupt</span><span class="p">,</span> <span class="ne">SystemExit</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">raise</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">elapsed_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">start_time</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Hook execution time before failure: </span><span class="si">{</span><span class="n">elapsed_time</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">s&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="n">tb_str</span> <span class="o">=</span> <span class="n">traceback</span><span class="o">.</span><span class="n">format_exc</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="n">_log_exception</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">hook_name</span><span class="p">,</span> <span class="n">tb_str</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="k">return</span> <span class="n">EXIT_ERROR</span></span></span></code></pre></div><p>注意兩處 <code>elapsed_time</code> 的記錄位置——成功和失敗路徑各記一次。失敗時記錄「失敗前的執行時間」，可以判斷是立即失敗（import 錯誤，&lt; 0.01s）還是在執行過程中失敗（邏輯錯誤，可能數秒）。</p>
<p>日誌檔案中的記錄：</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-03-04 09:15:23] DEBUG - Hook execution time: 0.05s       # 正常
</span></span><span class="line"><span class="ln">2</span><span class="cl">[2026-03-04 09:15:24] DEBUG - Hook execution time: 2.34s       # 偏慢，值得關注
</span></span><span class="line"><span class="ln">3</span><span class="cl">[2026-03-04 09:15:25] DEBUG - Hook execution time before failure: 0.00s  # import 階段就失敗了</span></span></code></pre></div><p>這些數據在 IMP-006 案例 C 的排查中發揮了作用。hookify plugin 的 timeout 設定為 10ms，而 Python 啟動需要約 24ms。比對 Hook 執行時間和 timeout 設定，就能定位超時問題。</p>
<h3 id="32-日誌自動清理log-rotation">3.2 日誌自動清理（Log Rotation）</h3>
<p>44 個 Hook 每天執行數百次，日誌檔案會快速累積。自動清理機制避免磁碟空間被耗盡：</p>





<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="n">LOG_RETENTION_DAYS</span> <span class="o">=</span> <span class="mi">7</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">LOG_CLEANUP_TRIGGER_FREQUENCY</span> <span class="o">=</span> <span class="mi">10</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="k">def</span> <span class="nf">_cleanup_old_logs</span><span class="p">(</span><span class="n">log_base_dir</span><span class="p">:</span> <span class="n">Path</span><span class="p">,</span> <span class="n">retention_days</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="n">LOG_RETENTION_DAYS</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="s2">&#34;&#34;&#34;清理超期日誌檔案&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">cutoff_time</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span> <span class="o">-</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="n">retention_days</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">for</span> <span class="n">log_file</span> <span class="ow">in</span> <span class="n">log_base_dir</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">&#34;*.log&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">                <span class="n">mtime</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromtimestamp</span><span class="p">(</span><span class="n">log_file</span><span class="o">.</span><span class="n">stat</span><span class="p">()</span><span class="o">.</span><span class="n">st_mtime</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">                <span class="k">if</span> <span class="n">mtime</span> <span class="o">&lt;</span> <span class="n">cutoff_time</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">                    <span class="n">log_file</span><span class="o">.</span><span class="n">unlink</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="k">except</span> <span class="p">(</span><span class="ne">OSError</span><span class="p">,</span> <span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">                <span class="k">pass</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">except</span> <span class="ne">OSError</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">pass</span></span></span></code></pre></div><h4 id="為什麼不用-python-標準庫的-rotatingfilehandler">為什麼不用 Python 標準庫的 RotatingFileHandler</h4>
<p><code>RotatingFileHandler</code> 按照<strong>單一檔案大小</strong>輪轉，適合長時間運行的服務。但 Hook 系統的日誌模式是每次執行一個新檔案，需要的是按<strong>時間</strong>清理舊檔案。兩者的需求場景不同：</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>適用場景</th>
          <th>Hook 系統需求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>RotatingFileHandler</td>
          <td>單一長期運行程序，同一個日誌檔</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>TimedRotatingFileHandler</td>
          <td>單一程序按時間分割日誌</td>
          <td>部分適用</td>
      </tr>
      <tr>
          <td>自訂清理</td>
          <td>多程序、每次新檔案、按時間保留</td>
          <td>適用</td>
      </tr>
  </tbody>
</table>
<h3 id="33-清理頻率控制">3.3 清理頻率控制</h3>
<p>每次 Hook 執行都檢查是否需要清理，這本身也有成本。所以用一個 <code>.cleanup_trigger</code> 檔案作為計數器，每 N 次呼叫才真正執行清理：</p>





<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="k">def</span> <span class="nf">_setup_logger_handlers</span><span class="p">(</span><span class="n">logger</span><span class="p">,</span> <span class="n">log_base_dir</span><span class="p">,</span> <span class="n">sanitized_name</span><span class="p">,</span> <span class="n">is_debug</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="s2">&#34;&#34;&#34;為 logger 配置 handlers&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="c1"># 觸發日誌清理（降低頻率）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">cleanup_marker</span> <span class="o">=</span> <span class="n">log_base_dir</span> <span class="o">/</span> <span class="s2">&#34;.cleanup_trigger&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">if</span> <span class="n">cleanup_marker</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="n">count</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">cleanup_marker</span><span class="o">.</span><span class="n">read_text</span><span class="p">()</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="ow">or</span> <span class="s2">&#34;0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">if</span> <span class="n">count</span> <span class="o">&gt;=</span> <span class="n">LOG_CLEANUP_TRIGGER_FREQUENCY</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                <span class="n">_cleanup_old_logs</span><span class="p">(</span><span class="n">log_base_dir</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">                <span class="n">cleanup_marker</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="s2">&#34;0&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">                <span class="n">cleanup_marker</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">count</span> <span class="o">+</span> <span class="mi">1</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="n">cleanup_marker</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="s2">&#34;1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="k">except</span> <span class="p">(</span><span class="ne">OSError</span><span class="p">,</span> <span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">pass</span>  <span class="c1"># 清理失敗不影響日誌功能</span></span></span></code></pre></div><p><code>LOG_CLEANUP_TRIGGER_FREQUENCY = 10</code> 表示每 10 次執行才清理一次。這是一個權衡：</p>
<table>
  <thead>
      <tr>
          <th>頻率</th>
          <th>好處</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每次（1）</td>
          <td>日誌目錄永遠乾淨</td>
          <td>每次 Hook 都多一次目錄掃描</td>
      </tr>
      <tr>
          <td>每 10 次</td>
          <td>幾乎感覺不到開銷</td>
          <td>最多累積 10 個多餘檔案</td>
      </tr>
      <tr>
          <td>每 100 次</td>
          <td>開銷最小</td>
          <td>可能累積數百個多餘檔案</td>
      </tr>
  </tbody>
</table>
<p><strong>為什麼用檔案而不用記憶體計數器？</strong> 因為 Hook 是獨立程序，每次執行都是新進程。記憶體中的計數器在進程結束後就消失了。檔案是跨進程持久化的最簡單方式。</p>
<p>注意最外層的 <code>except (OSError, ValueError): pass</code>。清理機制本身的故障（例如檔案被鎖定、計數器檔案損壞）不應該影響日誌功能。這和 Fallback Logger 的設計原則一致：<strong>輔助功能的故障不阻擋核心功能</strong>。</p>
<hr>
<h2 id="四三個錯誤模式的可觀測性教訓">四、三個錯誤模式的可觀測性教訓</h2>
<p>前面三個維度的設計，很大程度源自三個真實錯誤模式（IMP-003、IMP-005、IMP-006）的教訓。把它們放在一起看，可以提煉出可觀測性設計的通用原則。</p>
<h3 id="41-imp-003作用域迴歸--靜默失敗的代價">4.1 IMP-003：作用域迴歸 &ndash; 靜默失敗的代價</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>事件</strong></td>
          <td>7 個 Hook 因 <code>NameError</code> 靜默失敗 2+ session</td>
      </tr>
      <tr>
          <td><strong>根因</strong></td>
          <td>logger 從全域移入 main()，引用者未更新</td>
      </tr>
      <tr>
          <td><strong>可觀測性缺陷</strong></td>
          <td><code>_log_exception</code> 只寫檔案日誌，不輸出 stderr</td>
      </tr>
      <tr>
          <td><strong>修正</strong></td>
          <td>新增 stderr 輸出（W25-005）</td>
      </tr>
      <tr>
          <td><strong>通用原則</strong></td>
          <td><strong>錯誤必須有用戶可感知的通知管道</strong></td>
      </tr>
  </tbody>
</table>
<p>詳細的作用域分析見<a href="/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究</a>。</p>
<h3 id="42-imp-005import-未同步--保護範圍的盲區">4.2 IMP-005：Import 未同步 &ndash; 保護範圍的盲區</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>事件</strong></td>
          <td>5 個 Hook 因 <code>ModuleNotFoundError</code> 啟動失敗</td>
      </tr>
      <tr>
          <td><strong>根因</strong></td>
          <td>模組遷移後 import 路徑未更新</td>
      </tr>
      <tr>
          <td><strong>可觀測性缺陷</strong></td>
          <td><code>run_hook_safely</code> 無法保護 import 階段</td>
      </tr>
      <tr>
          <td><strong>修正</strong></td>
          <td>在 import 處加入 try-except + stderr</td>
      </tr>
      <tr>
          <td><strong>通用原則</strong></td>
          <td><strong>頂層保護的範圍必須覆蓋所有執行階段</strong></td>
      </tr>
  </tbody>
</table>
<h3 id="43-imp-006隱性故障--錯誤路徑的完整性">4.3 IMP-006：隱性故障 &ndash; 錯誤路徑的完整性</h3>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>事件</strong></td>
          <td>多種不同根因的 hook error 無法區分</td>
      </tr>
      <tr>
          <td><strong>案例 A</strong></td>
          <td>函式參數遺漏（部分 call site 缺少 logger）</td>
      </tr>
      <tr>
          <td><strong>案例 C</strong></td>
          <td>Plugin timeout 10ms，Python 啟動需 24ms</td>
      </tr>
      <tr>
          <td><strong>案例 D</strong></td>
          <td>有意阻止路徑缺少 stderr</td>
      </tr>
      <tr>
          <td><strong>通用原則</strong></td>
          <td><strong>所有非成功路徑都需要可區分的錯誤輸出</strong></td>
      </tr>
  </tbody>
</table>
<h3 id="44-共通教訓">4.4 共通教訓</h3>
<p>三個錯誤模式的共通點，提煉為三條可觀測性設計規則：</p>
<h4 id="規則-1錯誤不可靜默">規則 1：錯誤不可靜默</h4>





<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="c1"># 錯誤做法：只寫日誌，用戶不知道</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">logger</span><span class="o">.</span><span class="n">critical</span><span class="p">(</span><span class="n">tb_str</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="c1"># 正確做法：日誌 + 用戶通知</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">logger</span><span class="o">.</span><span class="n">critical</span><span class="p">(</span><span class="n">tb_str</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Hook Error] </span><span class="si">{</span><span class="n">hook_name</span><span class="si">}</span><span class="s2"> failed&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span></span></span></code></pre></div><h4 id="規則-2保護必須完整">規則 2：保護必須完整</h4>





<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="c1"># 錯誤做法：只保護 main()</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">run_hook_safely</span><span class="p">(</span><span class="n">main</span><span class="p">,</span> <span class="s2">&#34;hook&#34;</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="c1"># 正確做法：import 也要保護</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="kn">from</span> <span class="nn">lib.module</span> <span class="kn">import</span> <span class="n">function</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">except</span> <span class="ne">ImportError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Hook Import Error] </span><span class="si">{</span><span class="vm">__file__</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="n">run_hook_safely</span><span class="p">(</span><span class="n">main</span><span class="p">,</span> <span class="s2">&#34;hook&#34;</span><span class="p">))</span></span></span></code></pre></div><h4 id="規則-3錯誤要可區分">規則 3：錯誤要可區分</h4>





<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="c1"># 錯誤做法：所有錯誤用同一種訊息</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;hook error&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</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="c1"># 正確做法：包含 Hook 名稱和錯誤類型</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Hook Error] </span><span class="si">{</span><span class="n">hook_name</span><span class="si">}</span><span class="s2"> failed unexpectedly&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Hook Import Error] </span><span class="si">{</span><span class="n">filename</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">error</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;[Agent Validation] blocked: </span><span class="si">{</span><span class="n">reason</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span></span></span></code></pre></div><hr>
<h2 id="五完整的可觀測性架構">五、完整的可觀測性架構</h2>
<p>把前面的設計串在一起，一個 Hook 的完整執行路徑和可觀測性覆蓋如下：</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">Hook 被觸發
</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">├─ [階段 1] Import 載入
</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">│  └─ 失敗 → try-except 捕獲
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">│            ├─ stderr: [Hook Import Error] hook.py: error
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│            └─ sys.exit(1)
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">│
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">├─ [階段 2] setup_hook_logging
</span></span><span class="line"><span class="ln">10</span><span class="cl">│  ├─ 成功 → Logger 就緒（FileHandler + StreamHandler）
</span></span><span class="line"><span class="ln">11</span><span class="cl">│  └─ 失敗 → Fallback Logger（僅 StreamHandler）
</span></span><span class="line"><span class="ln">12</span><span class="cl">│
</span></span><span class="line"><span class="ln">13</span><span class="cl">├─ [階段 3] main() 執行
</span></span><span class="line"><span class="ln">14</span><span class="cl">│  ├─ 成功 → logger.debug(&#34;execution time: Xs&#34;)
</span></span><span class="line"><span class="ln">15</span><span class="cl">│  │         return exit_code
</span></span><span class="line"><span class="ln">16</span><span class="cl">│  ├─ 業務拒絕 → stderr: [Hook Name] blocked: reason
</span></span><span class="line"><span class="ln">17</span><span class="cl">│  │             return 2
</span></span><span class="line"><span class="ln">18</span><span class="cl">│  └─ 未預期異常 → logger.critical(traceback)
</span></span><span class="line"><span class="ln">19</span><span class="cl">│                   stderr: [Hook Error] hook failed
</span></span><span class="line"><span class="ln">20</span><span class="cl">│                   return 1
</span></span><span class="line"><span class="ln">21</span><span class="cl">│
</span></span><span class="line"><span class="ln">22</span><span class="cl">└─ [階段 4] 日誌清理（每 10 次觸發）
</span></span><span class="line"><span class="ln">23</span><span class="cl">   └─ 清理 7 天前的日誌檔案</span></span></code></pre></div><p>每個階段都有對應的可觀測性機制。沒有任何執行路徑是「靜默」的。</p>
<hr>
<h2 id="思考題">思考題</h2>
<ol>
<li>
<p>為什麼 <code>_cleanup_old_logs</code> 使用 <code>mtime</code>（修改時間）而非 <code>ctime</code>（建立時間）來判斷過期？在什麼情況下兩者會不同？</p>
</li>
<li>
<p>如果兩個 Hook 同時執行（例如同時觸發的 PreToolUse Hook），它們的日誌會互相干擾嗎？提示：思考 <code>logging.getLogger(hook_name)</code> 的行為。</p>
</li>
<li>
<p>目前的清理計數器用檔案系統實作。如果改用原子操作（例如 <code>os.rename</code>），能否解決並行存取的 race condition？值得嗎？</p>
</li>
</ol>
<h2 id="實作練習">實作練習</h2>
<ol>
<li>
<p><strong>寫一個日誌分析腳本</strong>：掃描 <code>.claude/hook-logs/</code> 目錄，統計每個 Hook 的平均執行時間、失敗次數、最後一次執行時間。</p>
</li>
<li>
<p><strong>實作 RotatingFileHandler 版本</strong>：修改 <code>setup_hook_logging</code>，改用單一日誌檔 + <code>RotatingFileHandler</code>（按大小輪轉），並比較和目前方案的優缺點。</p>
</li>
<li>
<p><strong>加入健康檢查端點</strong>：寫一個 <code>hook-health-check.py</code> 腳本，檢查每個 Hook 目錄的最新日誌是否包含 <code>CRITICAL</code> 等級的記錄，輸出健康報告。</p>
</li>
</ol>
<hr>
<p><em>上一章：<a href="/blog/python/05-error-testing/error-infrastructure/" data-link-title="5.5 頂層例外處理機制" data-link-desc="run_hook_safely 與統一錯誤基礎設施">頂層例外處理機制</a></em>
<em>相關：<a href="/blog/python/07-refactoring/refactoring-pitfalls/" data-link-title="重構陷阱與防護" data-link-desc="三個真實重構事故的共通模式：部分更新問題與系統性防護方法">重構陷阱與防護</a> &ndash; IMP-003/005/006 的重構角度分析</em>
<em>相關：<a href="/blog/python/07-refactoring/scope-regression/" data-link-title="作用域迴歸案例研究" data-link-desc="從 IMP-003 事件學習 Python 變數作用域的陷阱">作用域迴歸案例研究</a> &ndash; IMP-003 的完整技術分析</em></p>
]]></content:encoded></item><item><title>4.C7 Datadog：OTel 相容遷移實務</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/datadog-otel-migration-practice/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/datadog-otel-migration-practice/</guid><description>&lt;p>這個案例的核心責任是把 observability 遷移做成可逐步替換的技術路線。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Datadog 與 OTel 生態整合的做法，顯示團隊可在不一次重寫下逐步切換採集管線。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>觀測遷移的主要風險是資料語意漂移與管線雙軌期成本，而非單一 agent 安裝。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先建立雙軌採集的對照驗證。&lt;/li>
&lt;li>把 schema 與 sampling 政策版本化。&lt;/li>
&lt;li>用品質指標決定何時關閉舊管線。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.datadoghq.com/blog/instrument-python-apps-with-datadog-and-opentelemetry/">Datadog and OpenTelemetry&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把 observability 遷移做成可逐步替換的技術路線。</p>
<h2 id="觀察">觀察</h2>
<p>Datadog 與 OTel 生態整合的做法，顯示團隊可在不一次重寫下逐步切換採集管線。</p>
<h2 id="判讀">判讀</h2>
<p>觀測遷移的主要風險是資料語意漂移與管線雙軌期成本，而非單一 agent 安裝。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先建立雙軌採集的對照驗證。</li>
<li>把 schema 與 sampling 政策版本化。</li>
<li>用品質指標決定何時關閉舊管線。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11</a> 與 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.datadoghq.com/blog/instrument-python-apps-with-datadog-and-opentelemetry/">Datadog and OpenTelemetry</a></li>
</ul>
]]></content:encoded></item><item><title>4.7 Cardinality 治理與成本邊界</title><link>https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>cardinality 為何爆：unbounded label（user_id / request_id / url path）&lt;/li>
&lt;li>metrics 的 cardinality 影響：時序資料庫 series 爆炸、查詢退化&lt;/li>
&lt;li>log 的 cardinality 影響：索引膨脹、保留成本&lt;/li>
&lt;li>trace 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> 策略：head sampling vs tail sampling、tradeoff&lt;/li>
&lt;li>cost-aware observability：成本作為治理輸入而非事後賬單&lt;/li>
&lt;li>governance 控制面：label 白名單、ingestion quota、保留階梯&lt;/li>
&lt;li>高峰場景：流量尖峰時 cardinality slope 是 leading indicator&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a> 的分工：4.1 設計欄位、4.7 設邊界&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics&lt;/a> 的分工：4.2 是 metric 種類、4.7 是 label 治理&lt;/li>
&lt;li>反模式：所有事件都打高 cardinality label、預算耗盡才砍訊號、保留策略無階梯&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Cardinality 治理是把觀測維度當成有限資源管理的流程，責任是讓訊號足夠可切分，同時不讓儲存、查詢與告警成本失控。&lt;/p>
&lt;p>這一頁處理的是成本邊界。可觀測性需要有選擇地收集訊號；它把高價值維度留在可查詢路徑，把低價值或無界維度放到更合適的資料層。&lt;/p>
&lt;p>Cardinality 跟成本的關係是非線性的。Label 數目每增加一倍，metric series 數目可能呈乘法增長；查詢延遲、儲存大小、索引重建時間都會跟著放大。把 cardinality 視為一級治理項目，能避免「收得越多越好」的直覺推著成本上升。&lt;/p>
&lt;h2 id="cardinality-在不同訊號的失分模式">Cardinality 在不同訊號的失分模式&lt;/h2>
&lt;p>Cardinality 在 metric、log、trace 三類訊號的影響機制不同，失分模式也不同。把三者用同一套治理規則處理，會在某類訊號上過度限制、在另一類上失控。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號類型&lt;/th>
 &lt;th>主要失分機制&lt;/th>
 &lt;th>控制手段&lt;/th>
 &lt;th>典型 trigger&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Metric&lt;/td>
 &lt;td>TSDB series 爆炸、查詢退化&lt;/td>
 &lt;td>label 白名單、bucketize、aggregation&lt;/td>
 &lt;td>user_id / request_id 進 label&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Log&lt;/td>
 &lt;td>索引膨脹、保留成本暴增&lt;/td>
 &lt;td>索引欄位限制、結構化分層、分流&lt;/td>
 &lt;td>完整 URL / payload 進索引欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Trace&lt;/td>
 &lt;td>sampling 後遺失高價值樣本&lt;/td>
 &lt;td>tail sampling、minimum sample floor、 exemplar&lt;/td>
 &lt;td>head sampling 比例固定&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Metric cardinality 是最敏感的維度。Prometheus 等 pull-based TSDB 在 series 數超過數百萬時查詢退化、aggregation 失準、recording rule 跑不完。Cloud 託管型 TSDB 雖然容量更大，但每個 active series 的單價非常具體，cardinality 直接對應 vendor 月帳單。&lt;/p>
&lt;p>Log cardinality 的失分比較緩慢。Log 的 unique 值多本身不會立即崩潰，但全文索引 + 結構化欄位索引會持續膨脹，到某個臨界點查詢從毫秒退化到秒、再到分鐘。一般診斷不易察覺，要靠 query latency 跟 index size 的長期趨勢才能發現。&lt;/p>
&lt;p>Trace cardinality 的問題是另一種：sampling 過於粗暴會丟失高價值樣本。低流量服務、錯誤樣本、長尾延遲樣本若被 head sampling 平均稀釋，事故時無 trace 可看。Trace 的治理重點是 sampling 策略而非單純限制 cardinality。&lt;/p>
&lt;h2 id="高-cardinality-的常見來源">高 cardinality 的常見來源&lt;/h2>
&lt;p>無界維度進入可查詢路徑是 cardinality 失控的最大來源。常見的「無意中變成 label」：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>User / tenant identifier&lt;/strong>：把 user_id 當 label 時，每個用戶都產生一條 series。10 萬用戶 = 10 萬條 series 乘以其他 label 的笛卡爾積。&lt;/li>
&lt;li>&lt;strong>Request / session identifier&lt;/strong>：request_id、session_id、trace_id 本質是無界的，進入 metric label 後 series 無限增長。&lt;/li>
&lt;li>&lt;strong>完整 URL / path parameters&lt;/strong>：&lt;code>/users/123/orders/456&lt;/code> 這類 path 進入 label，每個 unique URL 都是新 series。&lt;/li>
&lt;li>&lt;strong>錯誤訊息 / stack trace&lt;/strong>：把 raw error message 當 label 時，每次新錯誤 = 新 series。&lt;/li>
&lt;li>&lt;strong>時間戳跟亂數&lt;/strong>：偶發出現的 bug，把 timestamp、uuid 寫進 label。&lt;/li>
&lt;/ul>
&lt;p>這些都應該進 &lt;em>log&lt;/em> 或 &lt;em>trace&lt;/em> 的欄位，不該進 &lt;em>metric&lt;/em> 的 label。Metric 的 label 應該是有界的維度：service name、environment、region、status code、http method、error class。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>cardinality 為何爆：unbounded label（user_id / request_id / url path）</li>
<li>metrics 的 cardinality 影響：時序資料庫 series 爆炸、查詢退化</li>
<li>log 的 cardinality 影響：索引膨脹、保留成本</li>
<li>trace 的 <a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 策略：head sampling vs tail sampling、tradeoff</li>
<li>cost-aware observability：成本作為治理輸入而非事後賬單</li>
<li>governance 控制面：label 白名單、ingestion quota、保留階梯</li>
<li>高峰場景：流量尖峰時 cardinality slope 是 leading indicator</li>
<li>跟 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a> 的分工：4.1 設計欄位、4.7 設邊界</li>
<li>跟 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a> 的分工：4.2 是 metric 種類、4.7 是 label 治理</li>
<li>反模式：所有事件都打高 cardinality label、預算耗盡才砍訊號、保留策略無階梯</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Cardinality 治理是把觀測維度當成有限資源管理的流程，責任是讓訊號足夠可切分，同時不讓儲存、查詢與告警成本失控。</p>
<p>這一頁處理的是成本邊界。可觀測性需要有選擇地收集訊號；它把高價值維度留在可查詢路徑，把低價值或無界維度放到更合適的資料層。</p>
<p>Cardinality 跟成本的關係是非線性的。Label 數目每增加一倍，metric series 數目可能呈乘法增長；查詢延遲、儲存大小、索引重建時間都會跟著放大。把 cardinality 視為一級治理項目，能避免「收得越多越好」的直覺推著成本上升。</p>
<h2 id="cardinality-在不同訊號的失分模式">Cardinality 在不同訊號的失分模式</h2>
<p>Cardinality 在 metric、log、trace 三類訊號的影響機制不同，失分模式也不同。把三者用同一套治理規則處理，會在某類訊號上過度限制、在另一類上失控。</p>
<table>
  <thead>
      <tr>
          <th>訊號類型</th>
          <th>主要失分機制</th>
          <th>控制手段</th>
          <th>典型 trigger</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Metric</td>
          <td>TSDB series 爆炸、查詢退化</td>
          <td>label 白名單、bucketize、aggregation</td>
          <td>user_id / request_id 進 label</td>
      </tr>
      <tr>
          <td>Log</td>
          <td>索引膨脹、保留成本暴增</td>
          <td>索引欄位限制、結構化分層、分流</td>
          <td>完整 URL / payload 進索引欄位</td>
      </tr>
      <tr>
          <td>Trace</td>
          <td>sampling 後遺失高價值樣本</td>
          <td>tail sampling、minimum sample floor、 exemplar</td>
          <td>head sampling 比例固定</td>
      </tr>
  </tbody>
</table>
<p>Metric cardinality 是最敏感的維度。Prometheus 等 pull-based TSDB 在 series 數超過數百萬時查詢退化、aggregation 失準、recording rule 跑不完。Cloud 託管型 TSDB 雖然容量更大，但每個 active series 的單價非常具體，cardinality 直接對應 vendor 月帳單。</p>
<p>Log cardinality 的失分比較緩慢。Log 的 unique 值多本身不會立即崩潰，但全文索引 + 結構化欄位索引會持續膨脹，到某個臨界點查詢從毫秒退化到秒、再到分鐘。一般診斷不易察覺，要靠 query latency 跟 index size 的長期趨勢才能發現。</p>
<p>Trace cardinality 的問題是另一種：sampling 過於粗暴會丟失高價值樣本。低流量服務、錯誤樣本、長尾延遲樣本若被 head sampling 平均稀釋，事故時無 trace 可看。Trace 的治理重點是 sampling 策略而非單純限制 cardinality。</p>
<h2 id="高-cardinality-的常見來源">高 cardinality 的常見來源</h2>
<p>無界維度進入可查詢路徑是 cardinality 失控的最大來源。常見的「無意中變成 label」：</p>
<ul>
<li><strong>User / tenant identifier</strong>：把 user_id 當 label 時，每個用戶都產生一條 series。10 萬用戶 = 10 萬條 series 乘以其他 label 的笛卡爾積。</li>
<li><strong>Request / session identifier</strong>：request_id、session_id、trace_id 本質是無界的，進入 metric label 後 series 無限增長。</li>
<li><strong>完整 URL / path parameters</strong>：<code>/users/123/orders/456</code> 這類 path 進入 label，每個 unique URL 都是新 series。</li>
<li><strong>錯誤訊息 / stack trace</strong>：把 raw error message 當 label 時，每次新錯誤 = 新 series。</li>
<li><strong>時間戳跟亂數</strong>：偶發出現的 bug，把 timestamp、uuid 寫進 label。</li>
</ul>
<p>這些都應該進 <em>log</em> 或 <em>trace</em> 的欄位，不該進 <em>metric</em> 的 label。Metric 的 label 應該是有界的維度：service name、environment、region、status code、http method、error class。</p>
<h2 id="高峰場景的-cardinality-失控">高峰場景的 cardinality 失控</h2>
<p>高峰場景的 cardinality 治理責任是讓「平時可控的 series 上限」在尖峰時仍能維持決策可用。平時 cardinality 看似穩定，高峰時可能突然出現新 tenant、新 endpoint、新 error class 的湧入，把 series 推到平台極限；治理重點是把「成長斜率」「容量緩衝」「dry-run」「freshness gap」變成預先設計的訊號、而非高峰中即興救火。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming 高峰流量下的訊號新鮮度與 Cardinality</a>：揭露「ingestion lag、cardinality growth slope、alert freshness gap」是高峰場景的核心治理項目（三個訊號名稱屬 case 直接列出）；以下做法基於通用工程知識展開。</p>
<p>高峰場景的可操作做法：</p>
<ol>
<li><strong>把 cardinality growth slope 視為 leading indicator</strong>：series 數目的成長斜率比絕對值更早反映異常。突然出現的快速上升通常意味著新 label 值湧入或既有 label 失控。</li>
<li><strong>預設容量 buffer</strong>：日常使用容量設在平台上限的 50-60%，留高峰時 cardinality 突發空間。把容量推到 90% 才追加治理會在高峰時來不及。</li>
<li><strong>高峰前的 dry-run</strong>：把預期高峰流量的 cardinality 估算進 capacity model，找出可能的 unbounded label。對應 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>。</li>
<li><strong>Alert freshness gap 也要監控</strong>：高峰時 ingestion lag 上升、告警延遲、值班決策落在過期資料上的風險。把 alert freshness（資料時間 vs 當前時間）變成 dashboard 訊號。</li>
</ol>
<p>高峰結束後做 retrospective：哪些 label 在高峰時超出預期、哪些 alert 因延遲沒及時觸發、哪些 series 應該下次提前 bucketize。這個 retrospective 是治理閉環的一部分，由 <a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal-governance-loop</a> 處理長期回寫。</p>
<h2 id="sampling-策略">Sampling 策略</h2>
<p>本章是 04 模組的 sampling 策略 SSoT — Head / Tail / Adaptive / Exemplar 四類策略集中在此；sampling 對資料品質的失真風險（low-traffic bias、error sample loss、tail latency loss）由 <a href="/blog/backend/04-observability/telemetry-data-quality/#sampling-%e8%88%87%e4%bb%a3%e8%a1%a8%e6%80%a7" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Sampling 與代表性</a> 處理；trace context 層的 sampling 配置由 <a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing context</a> 處理。</p>
<p>Sampling 策略的核心責任是控制觀測成本、同時保留足以判讀的高價值樣本。固定比例 head sampling 是最常見、也是最容易丟失高價值樣本的策略。</p>
<table>
  <thead>
      <tr>
          <th>策略類型</th>
          <th>機制</th>
          <th>適用場景</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Head sampling</td>
          <td>在 trace 開始時決定是否採樣</td>
          <td>簡單、低延遲、collector 端低資源</td>
          <td>不知道 trace 結果就決定、可能丟錯誤</td>
      </tr>
      <tr>
          <td>Tail sampling</td>
          <td>等 trace 結束後再決定（看是否錯誤、長延遲）</td>
          <td>保留錯誤、保留 outlier</td>
          <td>collector 要 buffer 整條 trace、資源高</td>
      </tr>
      <tr>
          <td>Adaptive sampling</td>
          <td>按服務、tenant、流量動態調整比例</td>
          <td>多租戶、流量差異大</td>
          <td>規則複雜、需要監控 sampling rate</td>
      </tr>
      <tr>
          <td>Exemplar attachment</td>
          <td>metric 帶代表性 trace id 樣本</td>
          <td>從 metric 跳到 trace</td>
          <td>不解決 sampling 本身、是補充</td>
      </tr>
  </tbody>
</table>
<p>實務上常用組合：低流量服務用接近 100% 採樣（minimum sample floor）、高流量服務用 tail sampling 保留錯誤跟長尾、metric 帶 exemplar 讓從 dashboard 跳到 trace。</p>
<p>四類策略各自的適用情境：</p>
<ul>
<li><strong>Head sampling</strong> 適合單體應用、延遲敏感、collector 端資源吃緊的場景。代價是 trace 開始時無法判斷是否錯誤、會等比例丟掉錯誤樣本。</li>
<li><strong>Tail sampling</strong> 適合微服務、需保留錯誤跟長尾的場景。代價是 collector 要 buffer 整條 trace、記憶體跟 CPU 用量明顯增加、對 cluster gateway 容量規劃壓力大。</li>
<li><strong>Adaptive sampling</strong> 適合多租戶、流量差異大的場景。風險是規則複雜化會造成 sampling rate 漂移、必須持續監控每個 service / tenant 的實際保留比例、否則治理會失控。</li>
<li><strong>Exemplar attachment</strong> 補強 metric → trace 跳轉、不解決 sampling 本身。在已有 head/tail sampling 的場景上加 exemplar 是低成本高價值的做法。</li>
</ul>
<p>關鍵是 sampling policy 本身要可被服務團隊理解跟調整。把 sampling 規則寫在 collector 配置裡、版本化、跟著 release 一起管理；把當前 sampling rate 跟保留分布暴露在 dashboard 上。當服務團隊發現某段時間 trace 殘缺、要能直接查到 sampling policy 的當下值跟變更紀錄。</p>
<h2 id="控制面與保留階梯">控制面與保留階梯</h2>
<p>可操作的 cardinality / 成本治理控制面有四層，從預防到事後審計都要覆蓋。</p>
<ol>
<li><strong>設計時 label 白名單</strong>：服務團隊新增 metric 時要 review label 是否在白名單內。白名單列出有界維度（service、env、region、status_code、error_class、http_method），明確排除 user_id、request_id、完整 URL。</li>
<li><strong>Ingestion 層 quota 與 cardinality limit</strong>：collector 或 vendor 端設定每服務、每 tenant 的 series 上限。超過上限時觸發告警，並啟動 graceful 降級（保留高優先 series、其他暫停）。</li>
<li><strong>保留階梯</strong>：依資料熱度跟法規責任分層保留。熱資料（最近 7 天）full granularity、溫資料（7-30 天）aggregated、冷資料（30+ 天）長期歸檔。階梯設計要結合 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 audit log governance</a> 的法規保留期。</li>
<li><strong>成本歸屬到 owner</strong>：把 ingestion、storage、query 成本拆到服務或團隊維度。沒有歸屬的成本會被視為平台問題，治理動力不會傳到產生成本的團隊。詳見 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>。</li>
</ol>
<p>保留階梯的另一個價值是事故時的容量保護。當熱資料儲存接近滿載、可以加速冷化、主動釋放容量給當下事件、避免被動等保留期到再恢復。</p>
<h2 id="storage-tiering-對查詢能力的影響"><a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering</a> 對查詢能力的影響</h2>
<p>保留階梯不只是成本工具，它直接決定不同時間範圍的查詢能力。每一層的儲存介質、索引密度、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 精度決定了該層能回答什麼問題、不能回答什麼問題。</p>
<h3 id="每一層能回答什麼">每一層能回答什麼</h3>
<p>Hot tier 保留完整精度與完整索引，能支援即席診斷的所有維度切片（by service、by tenant、by error code、by request id）。當資料從 hot 移到 warm，部分索引可能被移除、精度可能被 rollup 降低，能做的查詢從「特定 request id 的完整事件鏈」退化為「某服務過去兩週的 error rate 趨勢」。到 cold tier，通常只剩 timestamp + 少數結構化欄位的最小索引，細節查詢需要先 rehydrate 回 warm 或 hot 層。</p>
<p>這個退化是設計選擇，但需要被使用者感知。事故復盤時，如果團隊想查兩週前的特定 request 但資料已在 warm tier 且 request id 索引被移除，他們需要知道「不是沒有資料，而是需要 rehydrate 才能查」。</p>
<h3 id="跨層查詢的延遲跳變">跨層查詢的延遲跳變</h3>
<p>Dashboard 的時間範圍選擇直接觸發跨層查詢。使用者從「最近 1 小時」（全部在 hot tier）拉到「最近 7 天」（hot + warm tier），查詢延遲從毫秒跳到秒級。再拉到「最近 90 天」（hot + warm + cold tier），延遲可能跳到十秒甚至分鐘級。</p>
<p>這種延遲跳變在事故中的影響是：incident commander 想看長期趨勢來判斷異常是突發還是漸進時，dashboard 卡在載入。應對方式是在 dashboard 設計時就把「長時間趨勢」panel 指向 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 或 rollup series，讓它讀取預聚合資料而非跨層掃描 raw data。</p>
<h3 id="tier-邊界依訊號類型差異化">Tier 邊界依訊號類型差異化</h3>
<p>不同訊號類型的 tier 邊界應該不同。Error log 跟 trace 的事故診斷價值比 debug log 高，hot tier 保留期應該更長。<a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">Audit log</a> 因合規要求可能需要長期可查詢而非純歸檔。SLO-critical 的 metric series 可能需要 hot tier 保留 30 天來支援 monthly burn rate 計算，而 debug-level 的 metric 只需要 7 天 hot tier。</p>
<p>把所有訊號用同一個 tier 邊界管理（「全部 7 天 hot、30 天 warm、1 年 cold」）會讓高價值訊號過早退化、低價值訊號佔用過多 hot tier 容量。依訊號優先級設定差異化的 tier 邊界是保留階梯設計的進階步驟。</p>
<p>詳細的跨訊號查詢設計見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 cardinality 時，先看維度是否有決策價值，再看它是否有上界。</p>
<p>重點訊號包括：</p>
<ul>
<li>user id、request id、完整 URL 是否進入不該承受的 metric label</li>
<li>log index 是否只索引常用查詢欄位</li>
<li>trace <a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 是否能優先保留高價值樣本</li>
<li><a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 是否依資料熱度與法規責任分層</li>
<li>cardinality growth slope 是否被監控為 leading indicator</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>metric series 數量曲線陡升、TSDB 查詢退化</li>
<li>log ingestion 成本月對月雙位數成長</li>
<li>label 含 user_id / request_id / 完整 URL 直接送到 metric</li>
<li>ingestion quota 觸發時靠砍訊號救火、無 graceful 降階</li>
<li>保留策略全平、無冷熱分層、舊資料拖累查詢</li>
<li>高峰時 alert freshness gap 擴大、值班用過期資料</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>無界 label 進 metric</td>
          <td>user_id / request_id 在 label 中</td>
          <td>label 白名單、把細粒度放到 log / trace</td>
      </tr>
      <tr>
          <td>預算耗盡才砍訊號</td>
          <td>quota 觸發後緊急砍 series</td>
          <td>平時設成長告警、緩衝容量 50-60%</td>
      </tr>
      <tr>
          <td>保留策略全平</td>
          <td>所有 log / metric 都留 30 天</td>
          <td>依熱度跟法規分階、結合 audit retention</td>
      </tr>
      <tr>
          <td>Sampling 比例固定</td>
          <td>head sampling 10% 套全部服務</td>
          <td>低流量 100%、錯誤強制保留、tail sampling</td>
      </tr>
      <tr>
          <td>成本無歸屬</td>
          <td>平台付帳、團隊無動力治理</td>
          <td>歸屬到 service owner、進 cost attribution</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO</a>：SLI metric 的 cardinality 上限</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal-governance-loop</a>：高峰 retrospective 回寫治理</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a>：pipeline 層 quota 執行</li>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 audit log governance</a>：audit 保留期銜接</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>：成本治理的責任分配層</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：storage tiering 對查詢能力的完整設計</li>
<li><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量成本</a>：observability 成本作為容量規劃輸入</li>
<li><a href="/blog/backend/04-observability/vendors/" data-link-title="可觀測性 Vendor 清單" data-link-desc="規劃 telemetry standard、metrics、logs、traces、APM 與 error tracking 的服務頁撰寫順序與判準">vendors</a>：各平台的 ingestion / query quota 模型</li>
</ul>
]]></content:encoded></item><item><title>AWS CloudWatch</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/</guid><description>&lt;p>CloudWatch 是 AWS 原生 observability 服務、承擔三個責任：AWS 服務內建 metrics / logs / alarms（無需配置）、跨 AWS 服務統一觀測平面、X-Ray + Container Insights + Lambda Insights 等專用擴展。設計取捨偏向「AWS 生態深度整合 + 不用第三方 vendor + 預設 turnkey」、跨雲跟成本是主要限制。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 AWS CLI / Console 查 CloudWatch metrics / logs / alarms&lt;/li>
&lt;li>用 CloudWatch Logs Insights 查詢結構化 logs&lt;/li>
&lt;li>配置 alarm + composite alarm + EventBridge integration&lt;/li>
&lt;li>用 X-Ray 追蹤 distributed tracing&lt;/li>
&lt;li>控制 CloudWatch cost（log ingestion / metric / API call）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-cloudwatch-跑起來">最短路徑：5 分鐘把 CloudWatch 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 用 CloudWatch Agent 採集 EC2 metrics + logs&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: aws-cli + cloudwatch-agent.json config&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="c1"># 2. 查詢 metric&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"># TODO: aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 用 Logs Insights 查詢&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: fields @timestamp, @message | filter @message like /ERROR/ | sort @timestamp desc&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="metrics--logs--alarms-整合">Metrics / Logs / Alarms 整合&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Namespace + Dimension + Metric 三層&lt;/li>
&lt;li>Custom metric（CLI / SDK / Agent）&lt;/li>
&lt;li>Logs group + Log stream + Log event&lt;/li>
&lt;li>Alarm + Composite alarm + EventBridge rule&lt;/li>
&lt;/ul>
&lt;h3 id="logs-insights-query">Logs Insights query&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Query syntax：fields / filter / parse / stats / sort&lt;/li>
&lt;li>跟 KQL / LogQL 對照（CloudWatch 自家 syntax）&lt;/li>
&lt;li>對應指令：&lt;code>aws logs start-query&lt;/code>、&lt;code>aws logs get-query-results&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="metrics-math">Metrics Math&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>跨 metric 算術運算（rate / sum / avg）&lt;/li>
&lt;li>適合 dashboard / alarm 不直接 metric 表達的計算&lt;/li>
&lt;li>對比 PromQL：CloudWatch Math 較弱、無 label join 能力&lt;/li>
&lt;/ul>
&lt;h3 id="x-ray-tracing">X-Ray tracing&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>CloudWatch 是 AWS 原生 observability 服務、承擔三個責任：AWS 服務內建 metrics / logs / alarms（無需配置）、跨 AWS 服務統一觀測平面、X-Ray + Container Insights + Lambda Insights 等專用擴展。設計取捨偏向「AWS 生態深度整合 + 不用第三方 vendor + 預設 turnkey」、跨雲跟成本是主要限制。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 AWS CLI / Console 查 CloudWatch metrics / logs / alarms</li>
<li>用 CloudWatch Logs Insights 查詢結構化 logs</li>
<li>配置 alarm + composite alarm + EventBridge integration</li>
<li>用 X-Ray 追蹤 distributed tracing</li>
<li>控制 CloudWatch cost（log ingestion / metric / API call）</li>
</ol>
<h2 id="最短路徑5-分鐘把-cloudwatch-跑起來">最短路徑：5 分鐘把 CloudWatch 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 用 CloudWatch Agent 採集 EC2 metrics + logs</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: aws-cli + cloudwatch-agent.json config</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="c1"># 2. 查詢 metric</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 3. 用 Logs Insights 查詢</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: fields @timestamp, @message | filter @message like /ERROR/ | sort @timestamp desc</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="metrics--logs--alarms-整合">Metrics / Logs / Alarms 整合</h3>
<p>子議題：</p>
<ul>
<li>Namespace + Dimension + Metric 三層</li>
<li>Custom metric（CLI / SDK / Agent）</li>
<li>Logs group + Log stream + Log event</li>
<li>Alarm + Composite alarm + EventBridge rule</li>
</ul>
<h3 id="logs-insights-query">Logs Insights query</h3>
<p>子議題：</p>
<ul>
<li>Query syntax：fields / filter / parse / stats / sort</li>
<li>跟 KQL / LogQL 對照（CloudWatch 自家 syntax）</li>
<li>對應指令：<code>aws logs start-query</code>、<code>aws logs get-query-results</code></li>
</ul>
<h3 id="metrics-math">Metrics Math</h3>
<p>子議題：</p>
<ul>
<li>跨 metric 算術運算（rate / sum / avg）</li>
<li>適合 dashboard / alarm 不直接 metric 表達的計算</li>
<li>對比 PromQL：CloudWatch Math 較弱、無 label join 能力</li>
</ul>
<h3 id="x-ray-tracing">X-Ray tracing</h3>
<p>子議題：</p>
<ul>
<li>各語言 X-Ray SDK</li>
<li>Sampling rule（rate-based / reservoir）</li>
<li>Service map 自動 build</li>
<li>對應 <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OpenTelemetry</a> 遷移案例</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="logs-insights-governance/">Logs Insights 查詢與日誌治理</a>：log group 設計、query syntax、retention policy、cross-account aggregation、subscription filter 與 cost governance</li>
<li><a href="alarms-composite-operations/">Alarms 與 Composite Alarms 操作實務</a>：Metric Alarm、Anomaly Detection、Composite Alarm 設計、alarm actions、missing data 處理與 cost</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="container-insights--lambda-insights">Container Insights / Lambda Insights</h3>
<p>子議題：</p>
<ul>
<li>Container Insights：EKS / ECS metrics + logs 自動採集</li>
<li>Lambda Insights：Lambda runtime metrics + cold start visibility</li>
<li>跟 Prometheus + Grafana 的 K8s 模式對照</li>
</ul>
<h3 id="cloudwatch-synthetics--rum">CloudWatch Synthetics / RUM</h3>
<p>子議題：</p>
<ul>
<li>Synthetics：canary script 定期 probe</li>
<li>RUM：前端用戶體驗</li>
<li>跟 Datadog Synthetics / RUM 對照</li>
</ul>
<h3 id="logs-lifecycle">Logs lifecycle</h3>
<p>子議題：</p>
<ul>
<li>Retention（1 day to never expire）</li>
<li>Subscription filter：把 logs 送到 Lambda / Kinesis / S3</li>
<li>Logs to S3 archive</li>
<li>對應 cost 控制</li>
</ul>
<h3 id="cost-控制">Cost 控制</h3>
<p>子議題：</p>
<ul>
<li>Logs ingestion charge（per GB）</li>
<li>Metrics storage charge（custom metrics + high-resolution）</li>
<li>API call charge（GetMetricData / Logs Insights query）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></li>
</ul>
<h3 id="cloudwatch-managed-prometheusamp">CloudWatch Managed Prometheus（AMP）</h3>
<p>子議題：</p>
<ul>
<li>AMP：AWS managed Prometheus、scrape EKS / ECS</li>
<li>跟 CloudWatch 互補（CloudWatch 是 AWS-native、AMP 是 OSS standard）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a></li>
</ul>
<h3 id="aws-distro-for-opentelemetryadot">AWS Distro for OpenTelemetry（ADOT）</h3>
<p>子議題：</p>
<ul>
<li>AWS-supported OTel distribution</li>
<li>跟 X-Ray / AMP / CloudWatch 都整合</li>
<li>推薦的 OTel adoption 路徑</li>
<li>對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="logs-insights-query-過慢">Logs Insights query 過慢</h3>
<p>操作原則：query 範圍 + 結果集大時、用 sample 縮範圍。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: fields @timestamp, @message | limit 100（先測 logic）</span></span></span></code></pre></div><h3 id="metric-not-found">Metric not found</h3>
<p>操作原則：metric namespace / dimension 對應錯。判讀：用 <code>aws cloudwatch list-metrics --namespace ...</code> 確認。</p>
<h3 id="alarm-沒觸發">Alarm 沒觸發</h3>
<p>操作原則：alarm period / evaluation period / datapoints 配置造成延遲或忽略。</p>
<h3 id="x-ray-trace-incomplete">X-Ray trace incomplete</h3>
<p>操作原則：sampling rule 過頭、subseg context propagation 失敗。判讀：X-Ray console 看 trace timeline。</p>
<h3 id="cost-爆">Cost 爆</h3>
<p>操作原則：log ingestion 多、custom metric 多、Logs Insights query 量大都會貢獻。判讀：Cost Explorer 看 CloudWatch service breakdown。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多雲 / 跨雲統一</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> / <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OTel</a></td>
      </tr>
      <tr>
          <td>進階 APM 體驗</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> / <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>高頻 query / 大量 log</td>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（Loki）/ <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a></td>
      </tr>
      <tr>
          <td>OTel standard</td>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OTel</a> + ADOT / AMP</td>
      </tr>
      <tr>
          <td>GCP / Azure 生態</td>
          <td><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Operations</a> / Azure Monitor</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各 AWS 服務的 CloudWatch metric 名稱列表</li>
<li>CloudWatch Synthetics canary script 語法</li>
<li>Logs Insights 完整 query syntax reference</li>
<li>AWS IAM 跟 CloudWatch 的細部權限</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray to OTel</a></td>
          <td>X-Ray 遷出到 OTel</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT EKS pipeline</a></td>
          <td>AWS Distro + EKS 觀測</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 CloudWatch 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>CloudWatch Logs / S3 archive 作為 audit evidence</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></td>
          <td>Logs lifecycle / retention 對應資料主權限制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>AWS-only 場景優先 CloudWatch</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>、<a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">Cloud Operations</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>4.8 訊號治理閉環</title><link>https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何訊號需要治理閉環：alert / metric / dashboard 是會老化的資產&lt;/li>
&lt;li>偵測缺口的來源：post-incident review、chaos test、日常 noise&lt;/li>
&lt;li>訊號生命週期：新增 → 調整 → 淘汰&lt;/li>
&lt;li>Alert 健康度量測&lt;/li>
&lt;li>Dashboard 健康度量測&lt;/li>
&lt;li>治理節奏與 ownership&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>訊號治理閉環是把事故、演練與日常使用經驗回寫到觀測系統的流程，責任是讓 alert、metric 與 dashboard 隨服務變化而更新。&lt;/p>
&lt;p>觀測資產會老化：服務拓撲會變、流量型態會變、告警接收者會離職或轉組。設定一次就不再動的 alert rule 會在數月後變成 noise 來源；建立一次就不再看的 dashboard 會累積成系統負擔。訊號治理把觀測系統當成需要持續維護的產品，而非建好就完成的基礎設施。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert&lt;/a> 的分工：4.4 處理設計（怎麼設計好的 dashboard 跟 alert），4.8 處理維運與淘汰（設計好之後怎麼讓它們持續有效）。&lt;/p>
&lt;h2 id="偵測缺口的來源">偵測缺口的來源&lt;/h2>
&lt;h3 id="post-incident-review">Post-incident review&lt;/h3>
&lt;p>每次事故的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 都可能揭露偵測缺口 — 事故發生到被偵測到的時間太長、alert 觸發了但指向錯誤的方向、或根本沒有 alert 觸發。&lt;/p>
&lt;p>偵測缺口的分類：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>缺口類型&lt;/th>
 &lt;th>典型表現&lt;/th>
 &lt;th>回寫方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>訊號缺失&lt;/td>
 &lt;td>問題存在但沒有對應的 metric 或 trace&lt;/td>
 &lt;td>新增 metric / span&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert 太晚&lt;/td>
 &lt;td>Alert 在使用者投訴後才觸發&lt;/td>
 &lt;td>調整閾值或加短窗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert 指向錯誤&lt;/td>
 &lt;td>Alert 觸發了但指向不相關的服務&lt;/td>
 &lt;td>修正 alert rule&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dashboard 沒有對應視圖&lt;/td>
 &lt;td>事故中需要看某個維度但現有 dashboard 沒有&lt;/td>
 &lt;td>新增 panel&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>關聯性斷裂&lt;/td>
 &lt;td>Log / trace / metric 無法用同一個 ID 串連&lt;/td>
 &lt;td>補 correlation field&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Post-incident review 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action items&lt;/a> 中標記為「detection gap」的項目，應該指派給觀測系統的 owner，帶明確的 metric / alert / dashboard 變更規格。&lt;/p>
&lt;h3 id="chaos-test-與演練">Chaos test 與演練&lt;/h3>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">Chaos test&lt;/a> 跟災難恢復演練會在受控條件下暴露觀測盲區。注入 dependency failure 後，觀測系統是否在預期時間內觸發 alert？Alert 是否指向正確的方向？Dashboard 是否有足夠的 panel 支援診斷？&lt;/p>
&lt;p>演練揭露的盲區跟事故揭露的盲區性質相同，但成本更低 — 在受控環境發現的缺口不會拉長真實事故的 MTTR。&lt;/p>
&lt;h3 id="日常-noise-累積">日常 noise 累積&lt;/h3>
&lt;p>Alert noise 的日常累積是漸進式的退化 — 每個月新增幾個 alert rule 但沒有淘汰舊的，noise rate 從 10% 慢慢升到 30% 再到 50%。退化的訊號是 on-call 工程師開始忽略某些 alert（先 ack 再看、或直接 resolve 不看）。&lt;/p>
&lt;h2 id="訊號生命週期">訊號生命週期&lt;/h2>
&lt;h3 id="新增">新增&lt;/h3>
&lt;p>新訊號的來源：新服務上線時的 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">readiness review&lt;/a> 檢查、post-incident review 的 detection gap、chaos test 暴露的盲區、新功能上線時的 SLI 定義。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何訊號需要治理閉環：alert / metric / dashboard 是會老化的資產</li>
<li>偵測缺口的來源：post-incident review、chaos test、日常 noise</li>
<li>訊號生命週期：新增 → 調整 → 淘汰</li>
<li>Alert 健康度量測</li>
<li>Dashboard 健康度量測</li>
<li>治理節奏與 ownership</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>訊號治理閉環是把事故、演練與日常使用經驗回寫到觀測系統的流程，責任是讓 alert、metric 與 dashboard 隨服務變化而更新。</p>
<p>觀測資產會老化：服務拓撲會變、流量型態會變、告警接收者會離職或轉組。設定一次就不再動的 alert rule 會在數月後變成 noise 來源；建立一次就不再看的 dashboard 會累積成系統負擔。訊號治理把觀測系統當成需要持續維護的產品，而非建好就完成的基礎設施。</p>
<p>跟 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a> 的分工：4.4 處理設計（怎麼設計好的 dashboard 跟 alert），4.8 處理維運與淘汰（設計好之後怎麼讓它們持續有效）。</p>
<h2 id="偵測缺口的來源">偵測缺口的來源</h2>
<h3 id="post-incident-review">Post-incident review</h3>
<p>每次事故的 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 都可能揭露偵測缺口 — 事故發生到被偵測到的時間太長、alert 觸發了但指向錯誤的方向、或根本沒有 alert 觸發。</p>
<p>偵測缺口的分類：</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>典型表現</th>
          <th>回寫方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊號缺失</td>
          <td>問題存在但沒有對應的 metric 或 trace</td>
          <td>新增 metric / span</td>
      </tr>
      <tr>
          <td>Alert 太晚</td>
          <td>Alert 在使用者投訴後才觸發</td>
          <td>調整閾值或加短窗</td>
      </tr>
      <tr>
          <td>Alert 指向錯誤</td>
          <td>Alert 觸發了但指向不相關的服務</td>
          <td>修正 alert rule</td>
      </tr>
      <tr>
          <td>Dashboard 沒有對應視圖</td>
          <td>事故中需要看某個維度但現有 dashboard 沒有</td>
          <td>新增 panel</td>
      </tr>
      <tr>
          <td>關聯性斷裂</td>
          <td>Log / trace / metric 無法用同一個 ID 串連</td>
          <td>補 correlation field</td>
      </tr>
  </tbody>
</table>
<p>Post-incident review 的 <a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action items</a> 中標記為「detection gap」的項目，應該指派給觀測系統的 owner，帶明確的 metric / alert / dashboard 變更規格。</p>
<h3 id="chaos-test-與演練">Chaos test 與演練</h3>
<p><a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">Chaos test</a> 跟災難恢復演練會在受控條件下暴露觀測盲區。注入 dependency failure 後，觀測系統是否在預期時間內觸發 alert？Alert 是否指向正確的方向？Dashboard 是否有足夠的 panel 支援診斷？</p>
<p>演練揭露的盲區跟事故揭露的盲區性質相同，但成本更低 — 在受控環境發現的缺口不會拉長真實事故的 MTTR。</p>
<h3 id="日常-noise-累積">日常 noise 累積</h3>
<p>Alert noise 的日常累積是漸進式的退化 — 每個月新增幾個 alert rule 但沒有淘汰舊的，noise rate 從 10% 慢慢升到 30% 再到 50%。退化的訊號是 on-call 工程師開始忽略某些 alert（先 ack 再看、或直接 resolve 不看）。</p>
<h2 id="訊號生命週期">訊號生命週期</h2>
<h3 id="新增">新增</h3>
<p>新訊號的來源：新服務上線時的 <a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">readiness review</a> 檢查、post-incident review 的 detection gap、chaos test 暴露的盲區、新功能上線時的 SLI 定義。</p>
<p>新增訊號時要同時定義：metric / alert 的 owner、預期的 noise rate baseline、review 週期、淘汰條件。沒有 owner 跟 review 週期的訊號會在累積後變成治理負擔。</p>
<h3 id="調整">調整</h3>
<p>調整的觸發條件：alert threshold 跟當前 baseline 偏差過大、dashboard panel 的資料來源（metric name、label）已改變、alert 的 runbook link 過期、noise rate 超過團隊可接受的上限。</p>
<p>調整是訊號治理的主要日常工作。多數訊號不需要刪除，但需要隨服務演進跟著更新。</p>
<h3 id="淘汰">淘汰</h3>
<p>淘汰的觸發條件：alert rule 超過 N 天（例如 180 天）沒有觸發、dashboard 超過 N 天沒有人訪問、metric 被 recording rule 取代後原始查詢不再使用、服務已下線但 alert / dashboard 還在。</p>
<p>淘汰需要 owner 確認。自動淘汰（超過 180 天不觸發就自動刪除）風險太高 — 有些 alert 本來就是極低頻但極高價值（年度高峰才觸發的 capacity alert）。安全做法是自動標記候選淘汰，由 owner 在定期審視中決定保留或刪除。</p>
<h2 id="alert-健康度量測">Alert 健康度量測</h2>
<p>Alert 的健康度用四個指標追蹤：</p>
<p><strong>Noise rate</strong>：不需要行動的 alert / 總 alert。On-call 在 ack 時標記 actionable / noise。月度彙整。目標：&lt; 30%。</p>
<p><strong>MTTD（Mean Time to Detect）</strong>：事故開始到 alert 觸發的時間。從 incident timeline 回溯。目標：跟 SLO burn rate window 對齊（急性問題 &lt; 5 分鐘）。</p>
<p><strong>False positive rate</strong>：alert 觸發但事後確認沒有問題 / 總 alert。跟 noise rate 不同 — noise 包含 redundant alert（有問題但重複），false positive 是真的沒問題。</p>
<p><strong>Coverage</strong>：有 alert 覆蓋的 user journey / 總 user journey。未覆蓋的 user journey 代表潛在的偵測盲區。</p>
<h2 id="dashboard-健康度量測">Dashboard 健康度量測</h2>
<p>Dashboard 的健康度用三個指標追蹤：</p>
<p><strong>訪問頻率</strong>：每個 dashboard 的每週 / 每月訪問次數。Grafana 的 usage analytics 或 access log 可以提供。長期零訪問的 dashboard 是候選淘汰。</p>
<p><strong>Data freshness</strong>：Dashboard panel 是否顯示有效資料。Panel 因 metric name 改變或 label 漂移而回空值時，曲線看起來是平的零線 — 容易被誤讀成「一切正常」。定期掃描所有 panel 的 no-data 狀態。</p>
<p><strong>Owner coverage</strong>：有 owner 的 dashboard / 總 dashboard。沒有 owner 的 dashboard 沒人負責更新，退化只是時間問題。</p>
<h2 id="治理節奏">治理節奏</h2>
<p>訊號治理需要固定節奏，避免「只在事故後才補訊號、平時不管」的反應式治理。</p>
<p><strong>事故驅動（每次事故後）</strong>：Post-incident review 的 detection gap action items 在兩週內 close — 新增 / 調整的 metric、alert、dashboard 已部署並驗證。</p>
<p><strong>定期審視（每季）</strong>：</p>
<ul>
<li>Alert noise rate 報告：noise rate &gt; 30% 的 alert rule 進入調整或淘汰流程</li>
<li>Dashboard 訪問頻率報告：零訪問 dashboard 進入淘汰審視</li>
<li>Orphan alert / dashboard（owner 離職或轉組、未交接）指派新 owner</li>
</ul>
<p><strong>年度回顧</strong>：</p>
<ul>
<li>觀測覆蓋率（有 instrumentation 的服務 / 總服務）</li>
<li>SLI / SLO 的量測點跟閾值是否需要調整（業務變化、流量變化）</li>
<li>觀測成本 vs 事故成本的 ROI 評估</li>
</ul>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀訊號治理時，先看缺口是否有來源，再看改善項是否真的關閉。</p>
<p>重點訊號包括：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">Post-incident review</a> 是否把偵測缺口轉成具體 metric / alert / dashboard 變更</li>
<li><a href="/blog/backend/knowledge-cards/chaos-test/" data-link-title="Chaos Test" data-link-desc="說明透過受控故障注入驗證系統在異常條件下的恢復能力">Chaos test</a> 或 DR 演練是否暴露新的觀測盲區</li>
<li>Alert noise、ack time、false positive 是否有趨勢追蹤</li>
<li>Orphan dashboard 與過期 alert 是否有定期清理節奏</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Alert 數量只增不減、無淘汰流程</li>
<li>Alert noise rate &gt; 30%、ack 後無實際動作</li>
<li>Dashboard 半年無人訪問、仍存在於主目錄</li>
<li>Post-incident review action items 大半 open &gt; 90 天</li>
<li>同類事故重複發生、觀測系統無更新</li>
<li>Alert owner 離職後無人接手、alert 成為孤兒</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Alert 只增不減</td>
          <td>數百個 alert rule、多數是 noise</td>
          <td>定期審視 + 自動標記候選淘汰</td>
      </tr>
      <tr>
          <td>Dashboard 全是裝飾</td>
          <td>事故時沒人打開、只有 demo 時展示</td>
          <td>追蹤訪問頻率、零訪問的淘汰</td>
      </tr>
      <tr>
          <td>Post-incident action 永遠 open</td>
          <td>Detection gap 被記錄但半年沒 close</td>
          <td>兩週 close 期限、逾期自動升級</td>
      </tr>
      <tr>
          <td>治理只在事故後才啟動</td>
          <td>平時不管、出事才補</td>
          <td>建立每季定期審視節奏</td>
      </tr>
      <tr>
          <td>Orphan alert 無人負責</td>
          <td>Owner 離職後 alert 持續觸發但沒人處理</td>
          <td>交接流程 + orphan 掃描</td>
      </tr>
      <tr>
          <td>Chaos test 不看觀測面</td>
          <td>只看服務恢復、不看 alert 跟 dashboard 表現</td>
          <td>Chaos hypothesis 包含觀測預期</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：alert / dashboard 的設計原則</li>
<li><a href="/blog/backend/04-observability/attacker-view-observability-risks/" data-link-title="4.5 可觀測性威脅建模（Threat Modeling）" data-link-desc="從觀測盲區、告警失真與資料暴露風險，盤點 observability 的主要弱點">4.5 威脅建模</a>：告警失真作為觀測弱點</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a>：新訊號的成本邊界</li>
<li><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14 anomaly detection</a>：anomaly false positive 的淘汰</li>
<li><a href="/blog/backend/04-observability/observability-readiness-review/" data-link-title="4.16 Observability Readiness Review" data-link-desc="在服務上線、重大變更與演練前檢查 log / metric / trace / alert 是否可支援事故判讀">4.16 readiness review</a>：上線前的觀測覆蓋檢查</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：ownership 矩陣</li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 post-incident review</a>：action items 回寫機制</li>
<li><a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">8.11 閉環</a>：跨模組視角的閉環</li>
</ul>
]]></content:encoded></item><item><title>9.8 效能可觀測性</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>效能可觀測性的責任是讓容量決策有訊號基礎。沒有適當訊號時、就算有壓測結果跟容量計畫、也看不到「現在實際距離 saturation 多遠」、無法做即時調整。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 的關係：9.4 找到 saturation 點、9.8 定義持續監控這個點的訊號跟 dashboard。跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組&lt;/a> 是 sibling — 04 處理通用觀測、9.8 處理 &lt;em>容量規劃用&lt;/em> 的觀測。&lt;/p>
&lt;p>本章不重複 04 的訊號治理基礎、聚焦在 &lt;em>容量 / 效能 / 成本三條觀測線怎麼整合&lt;/em>。讀完後讀者能設計一個「容量 dashboard」、回答「現在距離 saturation 還有多遠、什麼時候該擴」。&lt;/p>
&lt;h2 id="use-method-在-production-持續監控">USE method 在 production 持續監控&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE method&lt;/a> 不只是壓測時用、production 也要持續監控。&lt;/p>
&lt;p>對每個資源（CPU / RAM / disk / network / DB connection / cache pool / file descriptor）量三個維度：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Utilization&lt;/strong>（使用率 0-100%）：直觀但會誤判&lt;/li>
&lt;li>&lt;strong>Saturation&lt;/strong>（queue depth）：早期警訊&lt;/li>
&lt;li>&lt;strong>Errors&lt;/strong>（資源層錯誤）：已經出事的訊號&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>為什麼不能只看 utilization&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>CPU 100% 但 run queue 空 → 還能撐（單純 CPU bound）&lt;/li>
&lt;li>CPU 80% 但 run queue 不斷增長 → 已 saturate（saturation 比 utilization 領先）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Saturation metric 是 capacity warning 的最早訊號&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>queue depth（每個 queue / pool）&lt;/li>
&lt;li>connection pool 使用率（最常見隱性 bottleneck）&lt;/li>
&lt;li>thread pool / coroutine count&lt;/li>
&lt;li>event loop lag（Node.js、async runtime）&lt;/li>
&lt;li>GC pause time / frequency&lt;/li>
&lt;li>cache hit rate / eviction rate&lt;/li>
&lt;li>replication lag&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Dashboard 設計&lt;/strong>：每個關鍵資源獨立 panel、同時顯示 utilization 跟 saturation。alert 在 &lt;em>saturation 起飛&lt;/em> 時觸發、不是 utilization 滿。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &amp;#43; AWS Media Services 撐 30 channels live &amp;#43; 5M MAU、工程工時下降 90%">Lemino connection limit&lt;/a> — connection saturation 是 RDB 的真正 bottleneck、不是 CPU；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato latency 降 90%&lt;/a> — 從 TiDB 換到 DynamoDB、saturation 行為完全不同、observability 也要跟著改。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>效能可觀測性的責任是讓容量決策有訊號基礎。沒有適當訊號時、就算有壓測結果跟容量計畫、也看不到「現在實際距離 saturation 多遠」、無法做即時調整。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的關係：9.4 找到 saturation 點、9.8 定義持續監控這個點的訊號跟 dashboard。跟 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 是 sibling — 04 處理通用觀測、9.8 處理 <em>容量規劃用</em> 的觀測。</p>
<p>本章不重複 04 的訊號治理基礎、聚焦在 <em>容量 / 效能 / 成本三條觀測線怎麼整合</em>。讀完後讀者能設計一個「容量 dashboard」、回答「現在距離 saturation 還有多遠、什麼時候該擴」。</p>
<h2 id="use-method-在-production-持續監控">USE method 在 production 持續監控</h2>
<p><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE method</a> 不只是壓測時用、production 也要持續監控。</p>
<p>對每個資源（CPU / RAM / disk / network / DB connection / cache pool / file descriptor）量三個維度：</p>
<ul>
<li><strong>Utilization</strong>（使用率 0-100%）：直觀但會誤判</li>
<li><strong>Saturation</strong>（queue depth）：早期警訊</li>
<li><strong>Errors</strong>（資源層錯誤）：已經出事的訊號</li>
</ul>
<p><strong>為什麼不能只看 utilization</strong>：</p>
<ul>
<li>CPU 100% 但 run queue 空 → 還能撐（單純 CPU bound）</li>
<li>CPU 80% 但 run queue 不斷增長 → 已 saturate（saturation 比 utilization 領先）</li>
</ul>
<p><strong>Saturation metric 是 capacity warning 的最早訊號</strong>：</p>
<ul>
<li>queue depth（每個 queue / pool）</li>
<li>connection pool 使用率（最常見隱性 bottleneck）</li>
<li>thread pool / coroutine count</li>
<li>event loop lag（Node.js、async runtime）</li>
<li>GC pause time / frequency</li>
<li>cache hit rate / eviction rate</li>
<li>replication lag</li>
</ul>
<p><strong>Dashboard 設計</strong>：每個關鍵資源獨立 panel、同時顯示 utilization 跟 saturation。alert 在 <em>saturation 起飛</em> 時觸發、不是 utilization 滿。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino connection limit</a> — connection saturation 是 RDB 的真正 bottleneck、不是 CPU；<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato latency 降 90%</a> — 從 TiDB 換到 DynamoDB、saturation 行為完全不同、observability 也要跟著改。</p>
<h2 id="red-method請求層的容量訊號">RED method：請求層的容量訊號</h2>
<p><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED method</a> 跟 USE 互補、從請求層看容量。</p>
<ul>
<li><strong>Rate</strong>：requests per second（每個 service / endpoint）</li>
<li><strong>Errors</strong>：error rate</li>
<li><strong>Duration</strong>：latency distribution（histogram、不是單一 percentile）</li>
</ul>
<p><strong>Duration 比 Errors 早</strong>：duration p99 飆通常先於 error rate 上升、是 saturation 的早期警訊。</p>
<p><strong>每個 endpoint 都要有 RED</strong>：不能只看全站 average、要分 endpoint。登入 endpoint 跟結帳 endpoint 的 saturation 行為不同、混在一起看不到 issue。</p>
<p><strong>Histogram 是必須、不是 nice-to-have</strong>：</p>
<ul>
<li>只記 p99 → 看不到 p999、看不到 distribution shape</li>
<li>記 histogram → 可以隨時算任何 percentile、可以做 long-tail 分析</li>
<li>Prometheus histogram、OpenMetrics histogram 是現代標準</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 25ms p95</a> — p95 是業務 KPI、不是技術指標、每個 endpoint 都有獨立 SLO。</p>
<h2 id="p50--p95--p99--p999-的取捨">p50 / p95 / p99 / p999 的取捨</h2>
<p>不同 percentile 反映不同問題、選錯 percentile 會錯失 issue。</p>
<ul>
<li><strong>p50（中位數）</strong>：整體狀況、感覺正常的指標、對長尾不敏感</li>
<li><strong>p95</strong>：日常 user-perceived experience、大多數用戶感受到的延遲</li>
<li><strong>p99</strong>：minority but critical 用戶體驗、SLO 常訂在這</li>
<li><strong>p999</strong>：極端長尾、受 GC pause / leader election / retry storm 影響、internal critical 系統訂在這</li>
</ul>
<p><strong>業務 SLO 通常訂 p99</strong>：「99% 用戶 request &lt; 500ms」是常見承諾、合約 SLA 也通常基於 p99。
<strong>Internal critical 系統訂 p99.9</strong>：金融交易、即時配對、客服 SaaS（5 個 9 可用性對應 5 個 9 latency 期待）。</p>
<p><strong>紀錄分布、不只紀錄 percentile</strong>：</p>
<ul>
<li>gauge p99 → 看不到 distribution shape、看不到 multimodal 分布</li>
<li>histogram → 可以重新計算任何 percentile、可以對比 distribution、可以找 anomaly</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi p99 &lt; 10ms</a> — ML inference 在 p99 才能控制用戶體驗、p50 沒意義；<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> — 必須關注 p999、RAFT 系統長尾顯著。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency 卡片</a>。</p>
<h2 id="cost-dashboard">Cost dashboard</h2>
<p>成本訊號跟容量訊號要 <em>並列顯示</em>、不要分開看。</p>
<p><strong>Per-service / per-endpoint cost attribution</strong>：</p>
<ul>
<li>每個 service 自己的雲端成本</li>
<li>拆到每個 endpoint</li>
<li>跟 RPS / latency 並列、看「成本上升是因為流量還是低效」</li>
</ul>
<p><strong>Cost per request 的時序變化</strong>：</p>
<ul>
<li>突然上升通常是 <em>退化</em> 訊號（新版本沒效率）</li>
<li>緩慢上升通常是 <em>規模</em> 訊號（用戶增加但 efficiency 沒變）</li>
</ul>
<p><strong>成本異常告警（vs 容量異常告警）</strong>：</p>
<ul>
<li>容量告警：utilization &gt; X% → 擴容</li>
<li>成本告警：cost spike &gt; X% → review</li>
<li>兩者可能同時觸發（autoscaler 擴容也擴 cost）、要區分</li>
</ul>
<p><strong>跟業務 metric 對齊</strong>：cost per active user、cost per transaction、cost per ML inference。業務 metric 級別的 cost 才能 review unit economics。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">Lyft 100+ 微服務各自 cost</a> — 微服務粒度的 cost attribution、找出哪個 service 過貴；對應 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04.14 cost attribution</a>。</p>
<h2 id="continuous-profiling">Continuous profiling</h2>
<p><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous profiling</a> 是現代效能 observability 的關鍵環節 — production 持續取 profile（CPU / heap / lock）、隨時可以做 diff 跟 root cause。</p>
<p><strong>工具生態</strong>：</p>
<ul>
<li>Datadog Continuous Profiler、Pyroscope（開源 + Grafana 整合）、Parca（CNCF）</li>
<li>GCP Cloud Profiler、Azure Application Insights Profiler、AWS CodeGuru Profiler</li>
<li>Overhead 通常 &lt; 1% CPU、放心開在 production</li>
</ul>
<p><strong>跟 distributed tracing 整合</strong>：trace → span → profile。一個 slow request 點下去、能看到對應 span、再下去看 profile。</p>
<p><strong>Profile diff 是 release gate 的核心訊號</strong>：每次 deploy 後自動對比 baseline、退化幅度過門檻 trigger alert。詳見 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a> 跟 <a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff 卡片</a>。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix 多 DB 統一後 profile 變單純</a> — DB 統一 → application 層 profile 噪音降低 → 退化定位更快。</p>
<h2 id="cardinality-cost-governance">Cardinality cost governance</h2>
<p>效能 observability 的成本經常爆炸、源頭通常是 high cardinality metric。</p>
<p><strong>高 cardinality 來源</strong>：</p>
<ul>
<li>per-user metric（user_id label）</li>
<li>per-request metric（request_id label）</li>
<li>per-trace metric（trace_id label）</li>
</ul>
<p><strong>為什麼會爆</strong>：Prometheus 等 metric system 為每個 label 組合存獨立 time series、cardinality = 所有 label value 的笛卡爾積。100 萬 user × 100 endpoint × 10 region = 10 億 time series、儲存爆炸。</p>
<p><strong>對策</strong>：</p>
<ul>
<li>high cardinality 資訊放 log / trace、不放 metric</li>
<li>metric label 限制在 low-cardinality 維度（service、endpoint、region、status）</li>
<li>真的需要 high-cardinality 分析、用 sampled trace + log query</li>
</ul>
<p>對應 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">04.10 cardinality cost governance</a>、跟 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">Metric Cardinality 卡片</a>。</p>
<h2 id="訊號跟-slo-對接">訊號跟 SLO 對接</h2>
<p>最後一層整合：每個 saturation metric 都要對應一個 SLO threshold、訊號驅動行動。</p>
<p><strong>訊號 → 行動鏈</strong>：</p>
<ul>
<li>saturation metric 超 threshold → trigger alert</li>
<li>alert 觸發 → trigger autoscaler / runbook / oncall</li>
<li>持續超 threshold → trigger <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> burn alert</li>
<li>error budget 用完 → trigger release freeze</li>
</ul>
<p><strong>Alert 不要太敏感</strong>：</p>
<ul>
<li>false positive 浪費 oncall、長期會 alert fatigue（<a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">Alert Fatigue 卡片</a>）</li>
<li>用 multi-window multi-burn-rate alert（Google SRE 推薦）</li>
<li>用 symptom-based alert（業務影響）而非 cause-based alert（單一資源）</li>
</ul>
<p>跟 <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a> 直接對接。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads 99.999%</a></td>
          <td>SLO 5 個 9 的訊號治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 12 個月 99.999%</a></td>
          <td>滾動 SLO 觀測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi p99 分解</a></td>
          <td>ML inference 多 stage latency budget</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech p95 是業務 KPI</a></td>
          <td>latency 不只是技術指標</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a></li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>（基礎訊號）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method</a></li>
<li><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED Method</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
<li><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling</a></li>
<li><a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request</a></li>
</ul>
]]></content:encoded></item><item><title>4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/</guid><description>&lt;p>這個案例的核心責任是把平台擴縮行為轉成可觀測治理問題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Airbnb 在 Kubernetes 規模化過程強調動態擴縮，代表觀測系統需要追上容量與拓撲變化。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>若訊號模型無法反映動態叢集，告警與容量判讀容易失真。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>將叢集層指標與服務層指標分開治理。&lt;/li>
&lt;li>在擴縮流程中保留關鍵健康訊號。&lt;/li>
&lt;li>用回溯報表驗證擴縮與事故關聯。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://airbnb.tech/infrastructure/dynamic-kubernetes-cluster-scaling-at-airbnb/">Dynamic Kubernetes Cluster Scaling at Airbnb&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把平台擴縮行為轉成可觀測治理問題。</p>
<h2 id="觀察">觀察</h2>
<p>Airbnb 在 Kubernetes 規模化過程強調動態擴縮，代表觀測系統需要追上容量與拓撲變化。</p>
<h2 id="判讀">判讀</h2>
<p>若訊號模型無法反映動態叢集，告警與容量判讀容易失真。</p>
<h2 id="策略">策略</h2>
<ol>
<li>將叢集層指標與服務層指標分開治理。</li>
<li>在擴縮流程中保留關鍵健康訊號。</li>
<li>用回溯報表驗證擴縮與事故關聯。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13</a> 與 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://airbnb.tech/infrastructure/dynamic-kubernetes-cluster-scaling-at-airbnb/">Dynamic Kubernetes Cluster Scaling at Airbnb</a></li>
</ul>
]]></content:encoded></item><item><title>GCP Cloud Operations</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/</guid><description>&lt;p>GCP Cloud Operations（前 Stackdriver）是 GCP 原生 observability 套件、承擔三個責任：GCP 服務內建 Cloud Logging / Monitoring / Trace（無需配置）、跟 GCP 資源 model 深度整合（project / folder / org）、BigQuery 匯出長期 logs 跟分析。設計取捨偏向「GCP 生態 turnkey + BigQuery 整合 + Cloud Profiler 持續 profiling」、跨雲跟進階 distributed tracing 是限制。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 gcloud / Console 查 Cloud Logging / Monitoring&lt;/li>
&lt;li>設計 structured logging + log-based metrics&lt;/li>
&lt;li>用 Cloud Monitoring uptime checks + SLO + alerting policy&lt;/li>
&lt;li>用 Cloud Trace + Cloud Profiler 做 application performance&lt;/li>
&lt;li>配置 BigQuery 匯出長期 logs 跟分析&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-cloud-operations-跑起來">最短路徑：5 分鐘把 Cloud Operations 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. GCP 預設啟用 Cloud Logging / Monitoring（free tier 額度）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: GKE / Cloud Run / Cloud Functions 自動 log + metric&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="c1"># 2. 查詢 logs&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"># TODO: gcloud logging read &amp;#39;resource.type=&amp;#34;gae_app&amp;#34; AND severity&amp;gt;=ERROR&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 用 Logs Explorer 視覺化查詢&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: Console → Logging → Logs Explorer&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cloud-logging-結構化-logs">Cloud Logging 結構化 logs&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>jsonPayload：結構化 log（推薦）&lt;/li>
&lt;li>Severity 7 級（DEBUG / INFO / NOTICE / WARNING / ERROR / CRITICAL / ALERT）&lt;/li>
&lt;li>Resource type / Resource labels：自動帶入&lt;/li>
&lt;li>對應 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="log-based-metrics">Log-based metrics&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Counter metric：log 出現次數&lt;/li>
&lt;li>Distribution metric：log field 數值分布&lt;/li>
&lt;li>適合：把 application log 轉成 metric trigger alert&lt;/li>
&lt;li>對應指令：&lt;code>gcloud logging metrics create&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="cloud-monitoring-uptime-checks--slo">Cloud Monitoring uptime checks / SLO&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>GCP Cloud Operations（前 Stackdriver）是 GCP 原生 observability 套件、承擔三個責任：GCP 服務內建 Cloud Logging / Monitoring / Trace（無需配置）、跟 GCP 資源 model 深度整合（project / folder / org）、BigQuery 匯出長期 logs 跟分析。設計取捨偏向「GCP 生態 turnkey + BigQuery 整合 + Cloud Profiler 持續 profiling」、跨雲跟進階 distributed tracing 是限制。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 gcloud / Console 查 Cloud Logging / Monitoring</li>
<li>設計 structured logging + log-based metrics</li>
<li>用 Cloud Monitoring uptime checks + SLO + alerting policy</li>
<li>用 Cloud Trace + Cloud Profiler 做 application performance</li>
<li>配置 BigQuery 匯出長期 logs 跟分析</li>
</ol>
<h2 id="最短路徑5-分鐘把-cloud-operations-跑起來">最短路徑：5 分鐘把 Cloud Operations 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. GCP 預設啟用 Cloud Logging / Monitoring（free tier 額度）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: GKE / Cloud Run / Cloud Functions 自動 log + metric</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="c1"># 2. 查詢 logs</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: gcloud logging read &#39;resource.type=&#34;gae_app&#34; AND severity&gt;=ERROR&#39;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 3. 用 Logs Explorer 視覺化查詢</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: Console → Logging → Logs Explorer</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cloud-logging-結構化-logs">Cloud Logging 結構化 logs</h3>
<p>子議題：</p>
<ul>
<li>jsonPayload：結構化 log（推薦）</li>
<li>Severity 7 級（DEBUG / INFO / NOTICE / WARNING / ERROR / CRITICAL / ALERT）</li>
<li>Resource type / Resource labels：自動帶入</li>
<li>對應 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP</a></li>
</ul>
<h3 id="log-based-metrics">Log-based metrics</h3>
<p>子議題：</p>
<ul>
<li>Counter metric：log 出現次數</li>
<li>Distribution metric：log field 數值分布</li>
<li>適合：把 application log 轉成 metric trigger alert</li>
<li>對應指令：<code>gcloud logging metrics create</code></li>
</ul>
<h3 id="cloud-monitoring-uptime-checks--slo">Cloud Monitoring uptime checks / SLO</h3>
<p>子議題：</p>
<ul>
<li>Uptime check：HTTP / HTTPS / TCP / ICMP 多地點 probe</li>
<li>SLO：service indicator + objective + window + burn rate alert</li>
<li>Multi-window SLO alert（類 Honeycomb burn rate）</li>
<li>對應 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">knowledge cards burn-rate</a></li>
</ul>
<h3 id="cloud-trace">Cloud Trace</h3>
<p>子議題：</p>
<ul>
<li>接受 OTLP（Cloud Trace 2.0+）</li>
<li>自動採集 GCP service（Cloud Run / GKE / App Engine）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP adoption</a></li>
<li>跟 X-Ray 比、distributed tracing 較基礎</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="cloud-monitoring-mql/">Cloud Monitoring Metrics Model 與 MQL</a>：GCP metrics model、MQL vs PromQL、custom metrics 設計、alerting policy 與 Managed Prometheus 整合</li>
<li><a href="cloud-logging-export-compliance/">Cloud Logging 查詢、匯出與合規</a>：查詢語言、log router / sink 匯出、retention 設計、organization-level 聚合、audit log 與 PII / CMEK 合規治理</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="cloud-profiler">Cloud Profiler</h3>
<p>子議題：</p>
<ul>
<li>持續 profiling（CPU / Heap / Wall time / Mutex）</li>
<li>支援 Go / Java / Python / Node</li>
<li>Flame graph 視覺化</li>
<li>跟 Pyroscope / Datadog Profiler 對照</li>
</ul>
<h3 id="bigquery-匯出長期儲存">BigQuery 匯出長期儲存</h3>
<p>子議題：</p>
<ul>
<li>Log Router：定義 sink 把 logs 匯出 BigQuery / GCS / Pub/Sub</li>
<li>BigQuery 適合長期 + 分析查詢（SQL）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></li>
<li>Cost：BigQuery storage 比 Cloud Logging cheaper</li>
</ul>
<h3 id="error-reporting">Error Reporting</h3>
<p>子議題：</p>
<ul>
<li>自動聚合 application error</li>
<li>各語言 client library（Python / Java / Node / Go）</li>
<li>跟 Sentry 對照（Sentry 更深 / 更廣）</li>
</ul>
<h3 id="cloud-monitoring-agent">Cloud Monitoring agent</h3>
<p>子議題：</p>
<ul>
<li>Ops Agent（取代 Stackdriver agent）：統一 logs + metrics 採集</li>
<li>支援 GCE / Bare metal / AWS / on-prem</li>
<li>配置：YAML config + receivers / processors / exporters（類 OTel Collector）</li>
</ul>
<h3 id="multi-project--multi-region-治理">Multi-project / Multi-region 治理</h3>
<p>子議題：</p>
<ul>
<li>Aggregated logging sink：跨 project 集中 logs</li>
<li>Cross-project SLO</li>
<li>Workspace（前 Stackdriver workspace）已 deprecated、改用 Metrics Scope</li>
</ul>
<h3 id="otlp-integration">OTLP integration</h3>
<p>子議題：</p>
<ul>
<li>Cloud Trace 接受 OTLP（2024 GA）</li>
<li>Cloud Monitoring 接受 OTel metrics（via OTel Collector + GCP exporter）</li>
<li>Logs in OTel 跟 Cloud Logging 整合（成熟中）</li>
<li>對應 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="logs-沒出現">Logs 沒出現</h3>
<p>操作原則：先看 resource type / project 是否對、再看 IAM 權限。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: gcloud logging read --project=&lt;id&gt; --resource-type=...</span></span></span></code></pre></div><h3 id="monitoring-查不到-metric">Monitoring 查不到 metric</h3>
<p>操作原則：metric name + project + filter 是否對。對應 Metrics Explorer 確認 metric 存在。</p>
<h3 id="slo-alert-noise">SLO alert noise</h3>
<p>操作原則：multi-window burn rate 設計避免噪音。</p>
<h3 id="cloud-trace-太空">Cloud Trace 太空</h3>
<p>操作原則：sampling 不足或 SDK 沒配置。判讀：Cloud Trace 看 span count + 確認 SDK Cloud Trace exporter 設定。</p>
<h3 id="bigquery-匯出-cost-爆">BigQuery 匯出 cost 爆</h3>
<p>操作原則：sink filter 沒收斂、所有 logs 都匯。判讀：Cloud Logging usage 看 export volume。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多雲統一觀測</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> / <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OTel</a></td>
      </tr>
      <tr>
          <td>進階 APM 廣度</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></td>
      </tr>
      <tr>
          <td>High-cardinality debug</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>Logs full-text 進階</td>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a> / Loki</td>
      </tr>
      <tr>
          <td>AWS / Azure 生態</td>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a> / Azure Monitor</td>
      </tr>
      <tr>
          <td>Error tracking 進階</td>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>gcloud / Cloud Console UI 操作詳細</li>
<li>各 GCP 服務的內建 metric 完整列表</li>
<li>Cloud Trace span structure 細節</li>
<li>BigQuery SQL syntax</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>主討論議題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP</a></td>
          <td>OTLP 在 GCP 的採用路徑</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Cloud Operations 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>Cloud Logging + BigQuery 作為審計證據與長期分析</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a></td>
          <td>BigQuery 匯出長期 retention</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>（反例）Cloud Trace ↔ OTLP 雙軌語意對齊</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>GCP-only 場景優先 Cloud Operations</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>、<a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch</a></li>
<li>下游能力：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
</ul>
]]></content:encoded></item><item><title>4.9 Continuous Profiling</title><link>https://tarrragon.github.io/blog/backend/04-observability/continuous-profiling/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/continuous-profiling/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>Continuous profiling 的定位：metrics / logs / traces 之外的第四角&lt;/li>
&lt;li>Profile 維度：CPU、heap、allocations、lock contention、goroutine / async task&lt;/li>
&lt;li>Always-on vs on-demand：何時用哪種&lt;/li>
&lt;li>Flame graph 與版本差異比較&lt;/li>
&lt;li>Overhead 控制&lt;/li>
&lt;li>Vendor 定位&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Continuous profiling 是把 CPU、memory、allocation 與 lock contention 變成長期可比較的 production 訊號，責任是補上 metrics、logs、traces 看不到的 callstack 成本。&lt;/p>
&lt;p>Metrics 會告訴你「CPU usage 上升了」，trace 會告訴你「這條 request 的 latency 從 200ms 變成 800ms」，profile 會告訴你「增加的 600ms 花在哪幾個 function call、哪幾行程式碼」。Profile 是唯一能精確到 callstack level 的觀測訊號。&lt;/p>
&lt;p>「Continuous」的關鍵差異是：傳統 profiling 是事故時才手動開啟，continuous profiling 是 production 常駐的低開銷採樣。事故時不需要重現問題 — baseline profile 已經在那裡，直接跟事故期間的 profile 做 diff。&lt;/p>
&lt;h2 id="profile-維度">Profile 維度&lt;/h2>
&lt;p>不同的 profile 維度回答不同的效能問題。服務的退化模式決定需要哪些維度。&lt;/p>
&lt;h3 id="cpu-profile">CPU profile&lt;/h3>
&lt;p>回答「CPU 時間花在哪些 function」。最常用的 profile 維度。適合診斷 latency 退化（某個 function 開始佔更多 CPU 時間）跟 CPU 利用率異常（某段程式碼意外進入 hot path）。&lt;/p>
&lt;p>CPU profile 用 sampling 方式採集 — 定期（例如每秒 100 次）記錄當前的 callstack。統計意義上，出現在 sample 中的次數跟實際 CPU 消耗成正比。Sampling 頻率越高精度越好，但 overhead 也越高。&lt;/p>
&lt;h3 id="heap--memory-profile">Heap / memory profile&lt;/h3>
&lt;p>回答「memory 被哪些 function 持有」。適合診斷 memory leak（allocation 持續增長、GC 回收不了）跟 GC pressure（大量短命物件導致 GC 頻繁）。&lt;/p>
&lt;p>Heap profile 記錄的是某個時間點的 live object 分布。Allocation profile 記錄的是一段時間內誰做了多少 allocation — 兩者互補。Memory leak 用 heap profile 的時間趨勢看；GC pressure 用 allocation profile 看。&lt;/p>
&lt;h3 id="lock-contention-profile">Lock contention profile&lt;/h3>
&lt;p>回答「哪些 lock 的等待時間最長」。適合診斷 mutex contention（多個 thread / goroutine 搶同一把 lock、等待時間累積成 latency）。&lt;/p>
&lt;p>Lock profile 在高並發服務的診斷中特別有用。Metrics 只能看到整體 latency 上升；trace 能看到某個 span 變慢；lock profile 能精確定位是哪把 lock 在哪個 callstack 被等待。&lt;/p>
&lt;h3 id="goroutine--async-task-profile">Goroutine / async task profile&lt;/h3>
&lt;p>Go 的 goroutine profile 回答「有多少 goroutine、它們在做什麼（running / waiting / blocked）」。Goroutine leak（goroutine 數量持續增長、都在等待某個 channel 或 lock）是 Go 服務常見的退化模式。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>Continuous profiling 的定位：metrics / logs / traces 之外的第四角</li>
<li>Profile 維度：CPU、heap、allocations、lock contention、goroutine / async task</li>
<li>Always-on vs on-demand：何時用哪種</li>
<li>Flame graph 與版本差異比較</li>
<li>Overhead 控制</li>
<li>Vendor 定位</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Continuous profiling 是把 CPU、memory、allocation 與 lock contention 變成長期可比較的 production 訊號，責任是補上 metrics、logs、traces 看不到的 callstack 成本。</p>
<p>Metrics 會告訴你「CPU usage 上升了」，trace 會告訴你「這條 request 的 latency 從 200ms 變成 800ms」，profile 會告訴你「增加的 600ms 花在哪幾個 function call、哪幾行程式碼」。Profile 是唯一能精確到 callstack level 的觀測訊號。</p>
<p>「Continuous」的關鍵差異是：傳統 profiling 是事故時才手動開啟，continuous profiling 是 production 常駐的低開銷採樣。事故時不需要重現問題 — baseline profile 已經在那裡，直接跟事故期間的 profile 做 diff。</p>
<h2 id="profile-維度">Profile 維度</h2>
<p>不同的 profile 維度回答不同的效能問題。服務的退化模式決定需要哪些維度。</p>
<h3 id="cpu-profile">CPU profile</h3>
<p>回答「CPU 時間花在哪些 function」。最常用的 profile 維度。適合診斷 latency 退化（某個 function 開始佔更多 CPU 時間）跟 CPU 利用率異常（某段程式碼意外進入 hot path）。</p>
<p>CPU profile 用 sampling 方式採集 — 定期（例如每秒 100 次）記錄當前的 callstack。統計意義上，出現在 sample 中的次數跟實際 CPU 消耗成正比。Sampling 頻率越高精度越好，但 overhead 也越高。</p>
<h3 id="heap--memory-profile">Heap / memory profile</h3>
<p>回答「memory 被哪些 function 持有」。適合診斷 memory leak（allocation 持續增長、GC 回收不了）跟 GC pressure（大量短命物件導致 GC 頻繁）。</p>
<p>Heap profile 記錄的是某個時間點的 live object 分布。Allocation profile 記錄的是一段時間內誰做了多少 allocation — 兩者互補。Memory leak 用 heap profile 的時間趨勢看；GC pressure 用 allocation profile 看。</p>
<h3 id="lock-contention-profile">Lock contention profile</h3>
<p>回答「哪些 lock 的等待時間最長」。適合診斷 mutex contention（多個 thread / goroutine 搶同一把 lock、等待時間累積成 latency）。</p>
<p>Lock profile 在高並發服務的診斷中特別有用。Metrics 只能看到整體 latency 上升；trace 能看到某個 span 變慢；lock profile 能精確定位是哪把 lock 在哪個 callstack 被等待。</p>
<h3 id="goroutine--async-task-profile">Goroutine / async task profile</h3>
<p>Go 的 goroutine profile 回答「有多少 goroutine、它們在做什麼（running / waiting / blocked）」。Goroutine leak（goroutine 數量持續增長、都在等待某個 channel 或 lock）是 Go 服務常見的退化模式。</p>
<p>其他語言有對應的概念：Java 的 thread dump、Node.js 的 async resource tracking、Python 的 asyncio task inspection。</p>
<h2 id="always-on-vs-on-demand">Always-on vs On-demand</h2>
<h3 id="always-oncontinuous">Always-on（continuous）</h3>
<p>Production 常駐的低開銷 profiling。CPU sampling 頻率降低（每秒 19 或 100 次，避免跟系統 timer 共振），heap sampling 用語言 runtime 內建機制（Go 的 <code>runtime/pprof</code>、Java 的 JFR）。</p>
<p>Always-on 的核心價值是 baseline — 平時就有 profile 資料，事故時可以跟 baseline 做 diff，看「哪些 function 的 CPU 消耗跟平時不同」。沒有 baseline 的 profiling 只能看「現在的 profile 長什麼樣」，無法判斷哪些是異常的。</p>
<h3 id="on-demand">On-demand</h3>
<p>事故中或效能調查時手動開啟的高精度 profiling。Sampling 頻率更高、涵蓋更多維度、但 overhead 也更高（可能影響 production 服務的 latency）。</p>
<p>On-demand profiling 適合在 always-on profile 定位到可疑 function 後，做更細粒度的 callstack 分析。兩者搭配使用 — always-on 做日常監控跟 baseline，on-demand 做事故深挖。</p>
<h3 id="overhead-控制">Overhead 控制</h3>
<p>Continuous profiling 的可行性取決於 overhead 是否夠低。目標是 CPU overhead &lt; 1%、memory overhead &lt; 10MB。</p>
<p>影響 overhead 的因素：</p>
<ul>
<li><strong>Sampling 頻率</strong>：CPU profile 每秒 100 次 vs 1000 次，overhead 差一個數量級</li>
<li><strong>採集機制</strong>：eBPF-based profiler（Parca、Pyroscope eBPF）在 kernel 層採集，overhead 比 language-level profiler 低；language runtime 內建機制（Go pprof、Java JFR）overhead 居中；instrumentation-based profiler overhead 最高</li>
<li><strong>資料傳輸</strong>：profile 資料定期傳到 backend 的網路跟序列化成本</li>
</ul>
<p>Production 部署前要用 benchmark 驗證 overhead。在 load test 環境開啟 profiling、比較開啟前後的 latency p99 跟 CPU usage — 差異超過 1% 要調整 sampling 頻率或換更輕量的 profiler。</p>
<h2 id="flame-graph-與版本差異比較">Flame Graph 與版本差異比較</h2>
<h3 id="flame-graph">Flame graph</h3>
<p>Flame graph 是 profile 資料的標準視覺化。X 軸是 callstack 的寬度（代表 sample 佔比 = 資源消耗佔比），Y 軸是 callstack 深度（底部是 root function、頂部是 leaf function）。寬的矩形代表消耗多、窄的代表消耗少。</p>
<p>讀 flame graph 的方式是「從寬的開始看」— 最寬的矩形是當前最大的資源消耗者。如果某個 function 佔整個 flame graph 的 40%，它就是最值得最佳化的候選。</p>
<h3 id="diff-flame-graph">Diff flame graph</h3>
<p>Diff flame graph 是兩個 profile 的差異視覺化。紅色代表新版本消耗增加、綠色代表減少。適合用在：</p>
<ul>
<li><strong>版本間比較</strong>：v1.2.3 vs v1.2.4 的 CPU profile diff，看新版本哪些 function 變慢</li>
<li><strong>Canary 對照</strong>：canary instance vs baseline instance 的即時 diff</li>
<li><strong>事故 vs baseline</strong>：事故期間的 profile vs 平時的 profile</li>
</ul>
<p>Diff flame graph 需要 profile 帶 version / deploy label。Profile 跟版本標記失聯時，跨版本比較只能靠手動對照時間範圍 — 精確度跟效率都會下降。</p>
<h2 id="vendor-定位">Vendor 定位</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>採集機制</th>
          <th>語言支援</th>
          <th>定位</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pyroscope</td>
          <td>SDK + eBPF</td>
          <td>Go, Java, Python, Ruby</td>
          <td>開源自架，Grafana 生態整合</td>
      </tr>
      <tr>
          <td>Parca</td>
          <td>eBPF</td>
          <td>語言無關（kernel 級）</td>
          <td>開源自架，零 instrumentation</td>
      </tr>
      <tr>
          <td>Datadog Profiler</td>
          <td>Agent + SDK</td>
          <td>Go, Java, Python, .NET</td>
          <td>託管，跟 APM trace 整合</td>
      </tr>
      <tr>
          <td>Polar Signals</td>
          <td>eBPF（Parca Cloud）</td>
          <td>語言無關</td>
          <td>託管 Parca</td>
      </tr>
  </tbody>
</table>
<p>選擇要點：如果已有 Grafana 生態（Prometheus + Loki + Tempo），Pyroscope 整合最自然。如果不想改 application code（零 instrumentation），eBPF-based 的 Parca 是選項。如果已用 Datadog APM，Datadog Profiler 跟 trace 的整合（從 trace span 跳到對應的 profile）是獨有優勢。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Continuous profiling 的持續價值取決於兩件事：profile 能否按版本做 diff（沒有 baseline 就無法判斷哪些 callstack 是異常的），以及 overhead 能否低到 production 常駐（overhead 過高等於回到「事故時才開」的模式）。</p>
<p>重點訊號包括：</p>
<ul>
<li>Profile 是否帶有 service、version、environment 與 deploy label</li>
<li>Flame graph diff 是否能對照 canary / baseline</li>
<li>CPU、heap、lock、allocation 是否覆蓋主要退化模式</li>
<li>Production sampling 是否足夠低成本且常駐穩定</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一段熱點程式碼反覆出現在事故 RCA 中、無 baseline profile</li>
<li>CPU / memory 異常時靠重現除錯、無 production profile 可對照</li>
<li>版本升級後 latency 退化、定位具體 callstack 需要重現環境</li>
<li>Profile 跟 commit / version label 失聯、跨版本 diff 需要人工對照</li>
<li>Profiling overhead 過高、production 環境常駐成本過高</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Profiling 只在事故時才開</td>
          <td>事故時開 profiler 需要時間、問題可能已消失</td>
          <td>Always-on continuous profiling</td>
      </tr>
      <tr>
          <td>Production sampling rate = 0</td>
          <td>Profile 只存在於 staging、production 沒資料</td>
          <td>調低 sampling 頻率到 overhead &lt; 1%</td>
      </tr>
      <tr>
          <td>Profile 跟 version 失聯</td>
          <td>Diff 只能靠時間範圍猜、無法精確比較</td>
          <td>Profile metadata 帶 version / commit hash label</td>
      </tr>
      <tr>
          <td>只看 CPU profile</td>
          <td>Memory leak 跟 lock contention 被忽略</td>
          <td>按服務退化模式選擇 profile 維度</td>
      </tr>
      <tr>
          <td>Profile 資料沒有保留策略</td>
          <td>儲存持續成長、舊 profile 佔空間但沒被查</td>
          <td>依版本保留（每版本保留 N 天）</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a>：metrics 是聚合訊號、profile 是 callstack 級別</li>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing</a>：trace 是 request 維度、profile 是 process 維度</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：profile 儲存量與保留策略</li>
<li><a href="/blog/backend/04-observability/rule-level-cpu-signal-governance/" data-link-title="4.21 Rule-level CPU Signal Governance" data-link-desc="把規則與策略執行成本變成可觀測訊號，避免控制面小變更在資料面形成 CPU 熱點。">4.21 rule-level CPU signal</a>：規則執行成本的 CPU 訊號治理</li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 post-incident review</a>：RCA 引用 profile flame graph</li>
</ul>
]]></content:encoded></item><item><title>4.C9 反例：OTel 遷移後訊號漂移</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/</guid><description>&lt;p>這個反例的核心責任是說明 observability 遷移失敗常以語意漂移形式出現，資料丟失反而少見。&lt;/p>
&lt;h2 id="事故長相">事故長相&lt;/h2>
&lt;p>OTel 切換後，儀表板看起來都有資料，但 on-call 開始收到不同告警，SLO burn rate 與舊系統長期對不上。同一個事故在新舊管線裡被歸到不同 service、不同 label 或不同 latency bucket。&lt;/p>
&lt;h2 id="為什麼會擴大">為什麼會擴大&lt;/h2>
&lt;p>觀測資料是事故判讀的入口。若 metric 名稱、label、sampling、aggregation 不一致，團隊會對同一個現象做出不同判斷，甚至在錯誤訊號上回退服務。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>觀測遷移的回退不一定是回到舊 agent。更重要的是保留新舊訊號對照，先停止讓新管線主導告警與 SLO 判定，再修正語意對齊。若直接關掉新管線，反而會失去分析漂移原因的證據。&lt;/p>
&lt;h2 id="觀測專屬告警條件">觀測專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>新舊管線對同一服務的 error rate 長期偏離&lt;/li>
&lt;li>missing span 或 missing metric 比例持續上升&lt;/li>
&lt;li>alert 噪音增加，但事故量沒有對應增加&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明 observability 遷移失敗常以語意漂移形式出現，資料丟失反而少見。</p>
<h2 id="事故長相">事故長相</h2>
<p>OTel 切換後，儀表板看起來都有資料，但 on-call 開始收到不同告警，SLO burn rate 與舊系統長期對不上。同一個事故在新舊管線裡被歸到不同 service、不同 label 或不同 latency bucket。</p>
<h2 id="為什麼會擴大">為什麼會擴大</h2>
<p>觀測資料是事故判讀的入口。若 metric 名稱、label、sampling、aggregation 不一致，團隊會對同一個現象做出不同判斷，甚至在錯誤訊號上回退服務。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>觀測遷移的回退不一定是回到舊 agent。更重要的是保留新舊訊號對照，先停止讓新管線主導告警與 SLO 判定，再修正語意對齊。若直接關掉新管線，反而會失去分析漂移原因的證據。</p>
<h2 id="觀測專屬告警條件">觀測專屬告警條件</h2>
<ul>
<li>新舊管線對同一服務的 error rate 長期偏離</li>
<li>missing span 或 missing metric 比例持續上升</li>
<li>alert 噪音增加，但事故量沒有對應增加</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a> 與 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11</a>。</p>
]]></content:encoded></item><item><title>Sentry</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/</guid><description>&lt;p>Sentry 是 error tracking 的事實標準、承擔三個責任：跨 frontend / backend / mobile 的 unhandled exception 自動聚合（issue grouping）、release-aware error tracking（regressed errors / source map）、延伸功能（APM / Continuous Profiling / Session Replay / Cron Monitoring）。設計取捨偏向「錯誤生命週期管理 + UX 強 + OSS self-host 雙軌」、不追求 metrics / logs 全面平台。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>整合 Sentry SDK（auto-instrumentation）到 frontend / backend / mobile&lt;/li>
&lt;li>配置 release + source map、追蹤 regressed errors&lt;/li>
&lt;li>設計 issue grouping / fingerprint 避免 noise&lt;/li>
&lt;li>用 Sentry Performance / Session Replay / Cron Monitoring&lt;/li>
&lt;li>評估 self-hosted vs SaaS、跟 IR 平台整合&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-sentry-跑起來">最短路徑：5 分鐘把 Sentry 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 註冊 Sentry / self-host、拿 DSN&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: 從 Console 拿 project DSN&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="c1"># 2. 整合 SDK（範例：Python）&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"># TODO: import sentry_sdk; sentry_sdk.init(dsn=..., traces_sample_rate=1.0)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 觸發 test exception 驗證&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: try: 1/0 / except: sentry_sdk.capture_exception()&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="sdk-整合auto-instrumentation">SDK 整合（auto-instrumentation）&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>各語言 SDK：Python / Node / Java / Go / Ruby / PHP / .NET / iOS / Android&lt;/li>
&lt;li>自動 framework instrumentation（Django / FastAPI / Express / Rails 等）&lt;/li>
&lt;li>Manual capture：&lt;code>capture_exception&lt;/code> / &lt;code>capture_message&lt;/code>&lt;/li>
&lt;li>對應 OTel integration（Sentry 接受 OTel context）&lt;/li>
&lt;/ul>
&lt;h3 id="release--source-map">Release / source map&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Release 標記每次部署（git SHA / version）&lt;/li>
&lt;li>Source map 上傳：minified frontend code → readable stack trace&lt;/li>
&lt;li>Regressed errors：之前 resolved 在新 release 又出現&lt;/li>
&lt;li>對應 release health metric&lt;/li>
&lt;/ul>
&lt;h3 id="issue-grouping--fingerprint">Issue grouping / fingerprint&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>Auto grouping：based on stack trace + exception type&lt;/li>
&lt;li>自訂 fingerprint：把不同 errors 聚成同 issue&lt;/li>
&lt;li>拆 issue：相同 stack 但需分開追蹤&lt;/li>
&lt;li>對應 noise 控制&lt;/li>
&lt;/ul>
&lt;h3 id="performance-monitoring">Performance monitoring&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Sentry 是 error tracking 的事實標準、承擔三個責任：跨 frontend / backend / mobile 的 unhandled exception 自動聚合（issue grouping）、release-aware error tracking（regressed errors / source map）、延伸功能（APM / Continuous Profiling / Session Replay / Cron Monitoring）。設計取捨偏向「錯誤生命週期管理 + UX 強 + OSS self-host 雙軌」、不追求 metrics / logs 全面平台。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>整合 Sentry SDK（auto-instrumentation）到 frontend / backend / mobile</li>
<li>配置 release + source map、追蹤 regressed errors</li>
<li>設計 issue grouping / fingerprint 避免 noise</li>
<li>用 Sentry Performance / Session Replay / Cron Monitoring</li>
<li>評估 self-hosted vs SaaS、跟 IR 平台整合</li>
</ol>
<h2 id="最短路徑5-分鐘把-sentry-跑起來">最短路徑：5 分鐘把 Sentry 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 註冊 Sentry / self-host、拿 DSN</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: 從 Console 拿 project DSN</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="c1"># 2. 整合 SDK（範例：Python）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: import sentry_sdk; sentry_sdk.init(dsn=..., traces_sample_rate=1.0)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 3. 觸發 test exception 驗證</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: try: 1/0 / except: sentry_sdk.capture_exception()</span></span></span></code></pre></div><h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="sdk-整合auto-instrumentation">SDK 整合（auto-instrumentation）</h3>
<p>子議題：</p>
<ul>
<li>各語言 SDK：Python / Node / Java / Go / Ruby / PHP / .NET / iOS / Android</li>
<li>自動 framework instrumentation（Django / FastAPI / Express / Rails 等）</li>
<li>Manual capture：<code>capture_exception</code> / <code>capture_message</code></li>
<li>對應 OTel integration（Sentry 接受 OTel context）</li>
</ul>
<h3 id="release--source-map">Release / source map</h3>
<p>子議題：</p>
<ul>
<li>Release 標記每次部署（git SHA / version）</li>
<li>Source map 上傳：minified frontend code → readable stack trace</li>
<li>Regressed errors：之前 resolved 在新 release 又出現</li>
<li>對應 release health metric</li>
</ul>
<h3 id="issue-grouping--fingerprint">Issue grouping / fingerprint</h3>
<p>子議題：</p>
<ul>
<li>Auto grouping：based on stack trace + exception type</li>
<li>自訂 fingerprint：把不同 errors 聚成同 issue</li>
<li>拆 issue：相同 stack 但需分開追蹤</li>
<li>對應 noise 控制</li>
</ul>
<h3 id="performance-monitoring">Performance monitoring</h3>
<p>子議題：</p>
<ul>
<li>Traces sampling rate</li>
<li>Transaction / span 結構（類 APM）</li>
<li>Web Vitals（前端 LCP / FID / CLS）</li>
<li>跟 OTel trace 互操作</li>
</ul>
<h2 id="deep-article">Deep Article</h2>
<ul>
<li><a href="error-grouping-fingerprinting/">Error Grouping 與 Fingerprinting 策略</a>：預設 grouping 演算法、自訂 fingerprint rules、merge/unmerge、grouping 不準的判讀與大量 unique errors 的治理</li>
<li><a href="release-tracking-session-replay/">Release Tracking 與 Session Replay</a>：release health、deploy tracking、session replay 隱私設定、performance monitoring 與 OTel 整合、self-hosted vs SaaS</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="session-replay">Session Replay</h3>
<p>子議題：</p>
<ul>
<li>前端用戶體驗錄影（含 error 前後操作）</li>
<li>隱私設定：mask PII / block element</li>
<li>Sample rate 控制</li>
<li>跟 LogRocket / FullStory 對照</li>
</ul>
<h3 id="cron-monitoringsentry-crons">Cron Monitoring（Sentry Crons）</h3>
<p>子議題：</p>
<ul>
<li>監控 scheduled job 是否準時跑 + 是否成功</li>
<li>Schedule 配置（crontab / interval）</li>
<li>Heartbeat ping / 自動 alert</li>
<li>對應 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response</a></li>
</ul>
<h3 id="continuous-profiling">Continuous Profiling</h3>
<p>子議題：</p>
<ul>
<li>各語言 profiler（Python / Node / Go）</li>
<li>CPU / memory flame graph</li>
<li>跟 Pyroscope / Datadog Profiler 對照</li>
</ul>
<h3 id="self-hosted-vs-saas">Self-hosted vs SaaS</h3>
<p>子議題：</p>
<ul>
<li>Self-hosted：Sentry OSS（docker-compose + 數十 service）</li>
<li>SaaS：sentry.io、5 levels（developer / team / business / enterprise）</li>
<li>規模化通常用 SaaS（self-host 維運成本高）</li>
<li>Privacy / compliance 場景：self-host</li>
</ul>
<h3 id="跟-ir-平台整合">跟 IR 平台整合</h3>
<p>子議題：</p>
<ul>
<li>跟 PagerDuty / Opsgenie / incident.io 整合</li>
<li>Alert routing：嚴重 issue → on-call</li>
<li>Issue 跟 incident ticket 關聯</li>
<li>對應 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response 模組</a></li>
</ul>
<h3 id="otel-integration">OTel integration</h3>
<p>子議題：</p>
<ul>
<li>Sentry SDK 接受 OTel context（trace_id / span_id）</li>
<li>跟其他 OTel backend dual ship</li>
<li>Sentry 自家 SDK feature 較深（vs 純 OTel）</li>
</ul>
<h2 id="跟-monitoring-模組的分工">跟 Monitoring 模組的分工</h2>
<p>本頁從 server-side 觀測平台角度說明 Sentry — error grouping 的告警整合、performance monitoring 的 SLI 指標設計、self-hosted vs SaaS 成本、跟 OTel 的 context 整合。Client-side 的使用體驗（SDK 自動攔截設計、error grouping 的 client 端行為、session replay 的操作重播、跟自架 monitor 的比較）見 <a href="/blog/monitoring/06-commercial-comparison/sentry-deep-dive/" data-link-title="Sentry 深入" data-link-desc="Error tracking &#43; performance monitoring &#43; session replay 的架構 — Sentry 從 error-first 出發如何擴展到全面可觀測性">Monitoring 模組 Sentry 深入</a>。</p>
<p>兩者的交叉點是 error event 的格式和 trace context propagation — client SDK 捕獲的 error 帶 trace context，server-side 的 Sentry 用同一個 trace 串接完整路徑。</p>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="issue-不出現">Issue 不出現</h3>
<p>操作原則：先確認 SDK 配置（DSN + initialization）、再看 sampling rate、最後看 ad blocker 等網路問題。</p>
<h3 id="issue-noise太多-issue">Issue noise（太多 issue）</h3>
<p>操作原則：用 fingerprint / inbound filter / rate limit 控制。判讀：Issue list 看哪些是噪音。</p>
<h3 id="release-沒對應">Release 沒對應</h3>
<p>操作原則：release tag 沒正確傳 SDK、或 source map 沒上傳。判讀：issue 沒有 release 資訊。</p>
<h3 id="performance-traces-缺失">Performance traces 缺失</h3>
<p>操作原則：sampling rate 過低或 SDK 沒啟用 performance。</p>
<h3 id="session-replay-不出現">Session Replay 不出現</h3>
<p>操作原則：sample rate 設定 + 隱私 setting 是否 block 過頭。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>完整 metrics / logs 平台</td>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> / <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> / ELK</td>
      </tr>
      <tr>
          <td>High-cardinality 分析</td>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
      </tr>
      <tr>
          <td>純 backend 已有 APM</td>
          <td>跟 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> APM 重疊、選一即可</td>
      </tr>
      <tr>
          <td>替代 error tracking</td>
          <td>Bugsnag / Rollbar / Raygun（T2 候選）</td>
      </tr>
      <tr>
          <td>Pure logs / metrics</td>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> / <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic</a> / Cloud-native</td>
      </tr>
      <tr>
          <td>OTel-only 標準</td>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OTel</a> + 任一 backend</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 Sentry SDK 完整 API</li>
<li>Sentry self-host 部署細節</li>
<li>各 framework integration 細節</li>
<li>Sentry pricing 詳細</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例待補-frontend-sentry-case">直接相關案例（待補 frontend Sentry case）</h3>
<p>Sentry 是 04 observability 模組第二大 SaaS（次 Datadog）、但 04 cases 庫主要聚焦 OTel / Prometheus / Grafana / ELK 等後端 telemetry pipeline 場景、Sentry 直接案例（frontend error / release health）待補。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Sentry 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit</a></td>
          <td>Issue 跟 audit evidence 串聯、release 對應監管要求</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak</a></td>
          <td>高峰下 issue noise / rate limit / inbound filter</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel migration signal drift</a></td>
          <td>Sentry SDK ↔ OTel context propagation 雙軌驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模對照</a></td>
          <td>Frontend / mobile-heavy team 通常選 Sentry</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 frontend Sentry case</strong>：大規模前端團隊（Shopify / Slack / GitHub frontend）error tracking 案例、release health 落地、跟 incident.io / PagerDuty 整合案例。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a>、<a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>下游能力：<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response 模組</a></li>
</ul>
]]></content:encoded></item><item><title>4.10 Client-side / Synthetic / RUM</title><link>https://tarrragon.github.io/blog/backend/04-observability/client-side-monitoring/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/client-side-monitoring/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>Server-side 觀測的盲區&lt;/li>
&lt;li>RUM（Real User Monitoring）：真實用戶端訊號&lt;/li>
&lt;li>Synthetic monitoring：主動探測&lt;/li>
&lt;li>Core Web Vitals 與 backend SLI 的整合&lt;/li>
&lt;li>Client trace 跟 server trace 的串接&lt;/li>
&lt;li>Vendor 定位&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Client-side、Synthetic 與 RUM 訊號是把使用者實際感知納入觀測系統的資料來源，責任是補上 server-side 指標看不到的網路、瀏覽器、地區與裝置差異。&lt;/p>
&lt;p>服務端 200 率正常只代表 backend 有回應。使用者是否真的能完成操作，還要看 DNS 解析、CDN 快取、ISP 路由、瀏覽器渲染與 client-side JavaScript 執行。這些環節每一個都可能讓使用者的體驗跟 server-side dashboard 顯示的完全不同。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">monitoring 模組&lt;/a> 的分工：monitoring 模組聚焦「非 server 端 runtime 的監控體系」（SDK 設計、collector 架構、rule engine）；本章聚焦「backend 觀測系統如何整合 client-side 訊號」。交叉點是事件格式跟 transport。&lt;/p>
&lt;h2 id="server-side-觀測的盲區">Server-side 觀測的盲區&lt;/h2>
&lt;p>Server-side 觀測能看到「request 到達 server 之後發生了什麼」，看不到「request 到達 server 之前」跟「response 離開 server 之後」的環節。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>環節&lt;/th>
 &lt;th>Server 能看到嗎&lt;/th>
 &lt;th>影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>DNS 解析&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>DNS 異常讓使用者完全到不了 server&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN / edge 故障&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>CDN 返回 stale 或 error、server 無感&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ISP 路由異常&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>特定地區使用者延遲暴增&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TLS handshake&lt;/td>
 &lt;td>部分看得到&lt;/td>
 &lt;td>Certificate 問題讓部分 client 連不上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Browser rendering&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>TTFB 正常但 LCP / CLS 很差&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Client-side JS error&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>功能壞了但 API call 正常&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>弱網 / offline&lt;/td>
 &lt;td>看不到&lt;/td>
 &lt;td>Request timeout 或完全沒發出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這些盲區意味著 server-side 的「一切正常」跟使用者的「用不了」可以同時存在。&lt;/p>
&lt;h2 id="rumreal-user-monitoring">RUM（Real User Monitoring）&lt;/h2>
&lt;p>RUM 在使用者的瀏覽器或 app 中嵌入監控 SDK，收集真實使用者的效能跟錯誤資料。跟 synthetic monitoring 的差異是 RUM 看的是真實流量，能反映真實的地理分布、裝置差異跟網路條件。&lt;/p>
&lt;h3 id="核心指標">核心指標&lt;/h3>
&lt;p>&lt;strong>頁面效能&lt;/strong>：First Contentful Paint（FCP）、Largest Contentful Paint（LCP）、Cumulative Layout Shift（CLS）、Interaction to Next Paint（INP）。這四個指標（Core Web Vitals 系列）是 Google 定義的使用者體驗量化標準。&lt;/p>
&lt;p>&lt;strong>JS error&lt;/strong>：未捕獲的 exception、promise rejection、resource loading failure。RUM SDK 自動攔截（&lt;code>window.onerror&lt;/code>、&lt;code>unhandledrejection&lt;/code>），帶 stack trace、browser info、page URL。&lt;/p>
&lt;p>&lt;strong>API call 效能&lt;/strong>：從 client 端量測的 API latency（包含 DNS + TCP + TLS + server processing + response download）。跟 server-side 量測的差異就是網路延遲跟 client 處理時間。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>Server-side 觀測的盲區</li>
<li>RUM（Real User Monitoring）：真實用戶端訊號</li>
<li>Synthetic monitoring：主動探測</li>
<li>Core Web Vitals 與 backend SLI 的整合</li>
<li>Client trace 跟 server trace 的串接</li>
<li>Vendor 定位</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Client-side、Synthetic 與 RUM 訊號是把使用者實際感知納入觀測系統的資料來源，責任是補上 server-side 指標看不到的網路、瀏覽器、地區與裝置差異。</p>
<p>服務端 200 率正常只代表 backend 有回應。使用者是否真的能完成操作，還要看 DNS 解析、CDN 快取、ISP 路由、瀏覽器渲染與 client-side JavaScript 執行。這些環節每一個都可能讓使用者的體驗跟 server-side dashboard 顯示的完全不同。</p>
<p>跟 <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">monitoring 模組</a> 的分工：monitoring 模組聚焦「非 server 端 runtime 的監控體系」（SDK 設計、collector 架構、rule engine）；本章聚焦「backend 觀測系統如何整合 client-side 訊號」。交叉點是事件格式跟 transport。</p>
<h2 id="server-side-觀測的盲區">Server-side 觀測的盲區</h2>
<p>Server-side 觀測能看到「request 到達 server 之後發生了什麼」，看不到「request 到達 server 之前」跟「response 離開 server 之後」的環節。</p>
<table>
  <thead>
      <tr>
          <th>環節</th>
          <th>Server 能看到嗎</th>
          <th>影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DNS 解析</td>
          <td>看不到</td>
          <td>DNS 異常讓使用者完全到不了 server</td>
      </tr>
      <tr>
          <td>CDN / edge 故障</td>
          <td>看不到</td>
          <td>CDN 返回 stale 或 error、server 無感</td>
      </tr>
      <tr>
          <td>ISP 路由異常</td>
          <td>看不到</td>
          <td>特定地區使用者延遲暴增</td>
      </tr>
      <tr>
          <td>TLS handshake</td>
          <td>部分看得到</td>
          <td>Certificate 問題讓部分 client 連不上</td>
      </tr>
      <tr>
          <td>Browser rendering</td>
          <td>看不到</td>
          <td>TTFB 正常但 LCP / CLS 很差</td>
      </tr>
      <tr>
          <td>Client-side JS error</td>
          <td>看不到</td>
          <td>功能壞了但 API call 正常</td>
      </tr>
      <tr>
          <td>弱網 / offline</td>
          <td>看不到</td>
          <td>Request timeout 或完全沒發出</td>
      </tr>
  </tbody>
</table>
<p>這些盲區意味著 server-side 的「一切正常」跟使用者的「用不了」可以同時存在。</p>
<h2 id="rumreal-user-monitoring">RUM（Real User Monitoring）</h2>
<p>RUM 在使用者的瀏覽器或 app 中嵌入監控 SDK，收集真實使用者的效能跟錯誤資料。跟 synthetic monitoring 的差異是 RUM 看的是真實流量，能反映真實的地理分布、裝置差異跟網路條件。</p>
<h3 id="核心指標">核心指標</h3>
<p><strong>頁面效能</strong>：First Contentful Paint（FCP）、Largest Contentful Paint（LCP）、Cumulative Layout Shift（CLS）、Interaction to Next Paint（INP）。這四個指標（Core Web Vitals 系列）是 Google 定義的使用者體驗量化標準。</p>
<p><strong>JS error</strong>：未捕獲的 exception、promise rejection、resource loading failure。RUM SDK 自動攔截（<code>window.onerror</code>、<code>unhandledrejection</code>），帶 stack trace、browser info、page URL。</p>
<p><strong>API call 效能</strong>：從 client 端量測的 API latency（包含 DNS + TCP + TLS + server processing + response download）。跟 server-side 量測的差異就是網路延遲跟 client 處理時間。</p>
<h3 id="切分維度">切分維度</h3>
<p>RUM 資料的價值在於可以按維度切分：地區（哪個國家 / 城市慢）、裝置（mobile vs desktop、iOS vs Android）、網路型態（4G vs wifi vs 3G）、瀏覽器（Chrome vs Safari vs Firefox）。</p>
<p>切分後的資料能回答 server-side 回答不了的問題：「為什麼巴西的使用者比美國慢 3 倍？」（CDN 沒覆蓋巴西）、「為什麼 Safari 的 error rate 比 Chrome 高？」（某個 JS API 在 Safari 的行為不同）。</p>
<h3 id="取樣與成本">取樣與成本</h3>
<p>RUM 的事件量跟使用者流量成正比。高流量網站的 RUM 資料量可能很大（每秒數千筆 page view + error + resource timing），成本隨之上升。</p>
<p>RUM 的取樣策略跟 server-side trace sampling 類似：可以全收（低流量網站）、按比例取樣（高流量）、或按條件取樣（error 全收、正常 page view 取樣）。取樣後的資料仍能看到趨勢跟 percentile，但個別 session 的完整 replay 需要該 session 被取樣到。</p>
<h2 id="synthetic-monitoring">Synthetic Monitoring</h2>
<p>Synthetic monitoring 用自動化的 <a href="/blog/backend/knowledge-cards/probe/" data-link-title="Probe" data-link-desc="說明平台如何透過 probe 判斷服務狀態與接流量條件">probe</a> 從外部網路定期發起請求，測量 availability 跟 latency。跟 RUM 的差異是 synthetic 是主動探測（沒有真實使用者也能跑），能 24/7 持續監控。</p>
<h3 id="適用場景">適用場景</h3>
<p><strong>Availability 探測</strong>：每分鐘從多個地區對關鍵頁面或 API endpoint 發 request，確認可達性。DNS 異常、CDN 故障、TLS 過期 — 這些 server-side 看不到的問題，synthetic probe 能第一時間抓到。</p>
<p><strong>SLO probe</strong>：用 synthetic probe 量測關鍵 user journey 的端到端 latency（login → homepage → checkout），作為 SLO 的 client-side 量測點。</p>
<p><strong>Third-party 依賴監控</strong>：探測 payment gateway、SSO provider、CDN 的可用性。這些外部依賴故障時 server-side 只能看到 timeout 或 error code，synthetic probe 能從使用者的角度看到完整影響。</p>
<h3 id="常見陷阱">常見陷阱</h3>
<p>Synthetic probe 的探測路徑必須跟真實使用者一致。Probe 從 datacenter 內部發 request、走內部 DNS、不經過 CDN — 這種 probe 量到的 latency 跟 availability 不代表真實使用者的體驗。</p>
<p>Probe 應該從外部網路、經過公開 DNS、經過 CDN / edge、用真實 browser（headless Chrome）渲染頁面。Catchpoint、Pingdom、Datadog Synthetic 都提供從多個公開地理位置發 probe 的能力。</p>
<h2 id="core-web-vitals-與-backend-sli-的整合">Core Web Vitals 與 Backend SLI 的整合</h2>
<p>Core Web Vitals（LCP、CLS、INP）是 client-side 的使用者體驗指標。Backend SLI（availability、latency p99）是 server-side 的服務健康指標。兩者各自反映不同層面、需要整合看才能得到完整圖像。</p>
<p>整合方式是在 dashboard 上並排顯示：backend SLI panel 旁邊放 RUM 的 LCP / INP panel。當 backend latency 正常但 LCP 退化，問題在 frontend rendering 或 CDN；當 backend latency 升高且 LCP 同步退化，問題在 backend。</p>
<p><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 設計</a> 的 user-journey-centric SLI 應該同時考慮 server-side 跟 client-side 的量測點。只看 server-side 的 SLI 會低估使用者實際感知的延遲。</p>
<h2 id="client-trace-跟-server-trace-的串接">Client Trace 跟 Server Trace 的串接</h2>
<p>RUM SDK 跟 backend 的 trace 串接讓一個 user action 的完整路徑可追蹤 — 從 button click 到 browser 發 API request 到 backend 處理到 response rendering。</p>
<p>串接方式是 RUM SDK 在發起 API request 時注入 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> header（W3C <code>traceparent</code>）。Backend 的 trace instrumentation 提取 header、建立 child span。完整的 trace waterfall 從 browser span 開始、經過 backend span、到 database span。</p>
<p>串接的條件是 RUM SDK 跟 backend SDK 使用相同的 trace context format。OTel 生態（browser SDK + backend SDK）天然支援；混用 vendor 時需要確認 header format 一致。</p>
<h2 id="vendor-定位">Vendor 定位</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>RUM</th>
          <th>Synthetic</th>
          <th>特點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog RUM</td>
          <td>有</td>
          <td>有</td>
          <td>跟 APM trace 整合、session replay</td>
      </tr>
      <tr>
          <td>Sentry</td>
          <td>有</td>
          <td>無</td>
          <td>Error tracking 為主、效能次之</td>
      </tr>
      <tr>
          <td>New Relic Browser</td>
          <td>有</td>
          <td>有</td>
          <td>全棧觀測整合</td>
      </tr>
      <tr>
          <td>Catchpoint</td>
          <td>無</td>
          <td>有</td>
          <td>Synthetic 專精、全球 probe 網路</td>
      </tr>
      <tr>
          <td>Pingdom</td>
          <td>無</td>
          <td>有</td>
          <td>簡單 availability probe</td>
      </tr>
      <tr>
          <td>Grafana Faro</td>
          <td>有</td>
          <td>無</td>
          <td>開源、Grafana 生態整合</td>
      </tr>
  </tbody>
</table>
<p>選擇要點：已有 APM vendor 的團隊優先用同 vendor 的 RUM（trace 串接最自然）。只需要 availability probe 的用 Pingdom 或 Synthetic 功能。需要 session replay（重現使用者操作序列）的選 Datadog RUM 或 Sentry。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 client-side monitoring 時，先看訊號是否代表真實使用者，再看 synthetic probe 是否覆蓋關鍵旅程。</p>
<p>重點訊號包括：</p>
<ul>
<li>RUM 是否能按地區、裝置、網路型態與瀏覽器切分</li>
<li>Synthetic probe 是否從外部網路與真實入口進入</li>
<li>Core Web Vitals 是否能和 backend SLI 並排比較</li>
<li>Client trace / session 是否能和 server trace 串接</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>使用者回報慢但 server-side latency 正常</li>
<li>CDN / edge 故障時內部 dashboard 全綠</li>
<li>行動弱網場景無 visibility、僅有 wifi 桌面端訊號</li>
<li>Synthetic probe 從 datacenter 內部跑、路徑跟真實使用者不同</li>
<li>客戶投訴定位耗時長、無 client 端 trace / RUM session</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLO 只看 server 200 率</td>
          <td>CDN / DNS 故障時 SLO 一切正常</td>
          <td>加 synthetic probe 跟 RUM 作為 SLI 來源</td>
      </tr>
      <tr>
          <td>Synthetic probe 走內部網路</td>
          <td>Probe latency 跟真實使用者差距大</td>
          <td>Probe 從外部公開網路、經 DNS / CDN 路徑</td>
      </tr>
      <tr>
          <td>RUM 無取樣策略</td>
          <td>高流量時 RUM 成本失控</td>
          <td>按條件取樣（error 全收、正常取樣）</td>
      </tr>
      <tr>
          <td>Client trace 跟 server 斷裂</td>
          <td>看不到 browser → server 的完整路徑</td>
          <td>RUM SDK 注入 W3C trace context header</td>
      </tr>
      <tr>
          <td>只看 overall LCP</td>
          <td>全球平均看起來好但特定地區體驗極差</td>
          <td>按地區 / 裝置 / 網路切分 RUM 資料</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO</a>：user-journey-centric SLI 需要 client-side 量測點</li>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing</a>：client trace 跟 server trace 的 context 串接</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a>：CDN / edge 配置變更影響 RUM 訊號</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 incident response</a>：客戶感知影響量化</li>
<li><a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 模組</a>：非 server 端的監控體系設計</li>
<li><a href="/blog/backend/04-observability/client-server-trace-integration/" data-link-title="4.24 Client-to-Server 端到端觀測串接" data-link-desc="用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路">4.24 Client-to-Server 觀測串接</a>：從 browser click 到 server span 的完整 trace 鏈路實作</li>
</ul>
]]></content:encoded></item><item><title>Cloud Monitoring Metrics Model 與 MQL</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/cloud-monitoring-mql/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/cloud-monitoring-mql/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations&lt;/a> 的 vendor deep article，深化 overview「Cloud Monitoring uptime checks / SLO」跟「OTLP integration」段。初次接觸 GCP 觀測的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>GCP 服務預設把 metrics 寫到 Cloud Monitoring，工程師打開 Metrics Explorer 就能看到 CPU、記憶體、request count。問題通常出在三個地方：GCP 內建 metrics 的 resource model 跟應用層的 business metrics 用不同語言描述同一件事，PromQL 使用者要重新學 MQL 語法，alerting policy 的 condition type 跟 notification channel 配置比預期複雜。理解 Cloud Monitoring 的 metrics model 才能避免 custom metrics 爆量、alert noise、跟 Prometheus 生態的銜接摩擦。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="monitored-resource-與-metric-descriptor">Monitored resource 與 metric descriptor&lt;/h3>
&lt;p>Cloud Monitoring 的資料模型有兩個軸：&lt;strong>monitored resource&lt;/strong> 描述「誰產生了這個 metric」，&lt;strong>metric descriptor&lt;/strong> 描述「這個 metric 量什麼」。&lt;/p>
&lt;p>Monitored resource 是 GCP 自動帶入的標籤集合。GKE pod 的 monitored resource type 是 &lt;code>k8s_pod&lt;/code>，帶 &lt;code>project_id&lt;/code>、&lt;code>location&lt;/code>、&lt;code>cluster_name&lt;/code>、&lt;code>namespace_name&lt;/code>、&lt;code>pod_name&lt;/code>。Cloud Run revision 是 &lt;code>cloud_run_revision&lt;/code>，帶 &lt;code>service_name&lt;/code>、&lt;code>revision_name&lt;/code>、&lt;code>location&lt;/code>。這層標籤不需要工程師手動設定，GCP agent 或 SDK 自動填入。&lt;/p>
&lt;p>Metric descriptor 定義 metric 的名稱、型別（GAUGE / DELTA / CUMULATIVE）、value type（INT64 / DOUBLE / DISTRIBUTION）與自訂 label。GCP 內建 metrics 用 &lt;code>compute.googleapis.com/instance/cpu/utilization&lt;/code> 這樣的命名空間格式；custom metrics 用 &lt;code>custom.googleapis.com/&amp;lt;your-name&amp;gt;&lt;/code> 或 &lt;code>workload.googleapis.com/&amp;lt;your-name&amp;gt;&lt;/code>（後者透過 OTel Collector 或 Managed Prometheus 寫入時使用）。&lt;/p>
&lt;p>兩個軸相乘就是 time series 的數量。Cardinality 管理在 GCP 上等同於控制 monitored resource × metric label 的組合數。GCP 對 custom metrics 有每個 project 的 time series 配額（預設 500 per metric descriptor、可申請提高），超過時寫入會被拒。&lt;/p>
&lt;h3 id="mql-vs-promql">MQL vs PromQL&lt;/h3>
&lt;p>Cloud Monitoring 有兩種查詢語言。MQL（Monitoring Query Language）是 GCP 自家設計的 pipeline 語法：&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">fetch k8s_container
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">| metric &amp;#39;kubernetes.io/container/cpu/core_usage_time&amp;#39;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">| align rate(1m)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">| every 1m
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">| group_by [resource.cluster_name, resource.namespace_name],
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> [value_cpu_usage: aggregate(value.core_usage_time)]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>PromQL 在 Cloud Monitoring 上也可用（透過 Managed Service for Prometheus）。兩者的核心差異：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations</a> 的 vendor deep article，深化 overview「Cloud Monitoring uptime checks / SLO」跟「OTLP integration」段。初次接觸 GCP 觀測的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>GCP 服務預設把 metrics 寫到 Cloud Monitoring，工程師打開 Metrics Explorer 就能看到 CPU、記憶體、request count。問題通常出在三個地方：GCP 內建 metrics 的 resource model 跟應用層的 business metrics 用不同語言描述同一件事，PromQL 使用者要重新學 MQL 語法，alerting policy 的 condition type 跟 notification channel 配置比預期複雜。理解 Cloud Monitoring 的 metrics model 才能避免 custom metrics 爆量、alert noise、跟 Prometheus 生態的銜接摩擦。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="monitored-resource-與-metric-descriptor">Monitored resource 與 metric descriptor</h3>
<p>Cloud Monitoring 的資料模型有兩個軸：<strong>monitored resource</strong> 描述「誰產生了這個 metric」，<strong>metric descriptor</strong> 描述「這個 metric 量什麼」。</p>
<p>Monitored resource 是 GCP 自動帶入的標籤集合。GKE pod 的 monitored resource type 是 <code>k8s_pod</code>，帶 <code>project_id</code>、<code>location</code>、<code>cluster_name</code>、<code>namespace_name</code>、<code>pod_name</code>。Cloud Run revision 是 <code>cloud_run_revision</code>，帶 <code>service_name</code>、<code>revision_name</code>、<code>location</code>。這層標籤不需要工程師手動設定，GCP agent 或 SDK 自動填入。</p>
<p>Metric descriptor 定義 metric 的名稱、型別（GAUGE / DELTA / CUMULATIVE）、value type（INT64 / DOUBLE / DISTRIBUTION）與自訂 label。GCP 內建 metrics 用 <code>compute.googleapis.com/instance/cpu/utilization</code> 這樣的命名空間格式；custom metrics 用 <code>custom.googleapis.com/&lt;your-name&gt;</code> 或 <code>workload.googleapis.com/&lt;your-name&gt;</code>（後者透過 OTel Collector 或 Managed Prometheus 寫入時使用）。</p>
<p>兩個軸相乘就是 time series 的數量。Cardinality 管理在 GCP 上等同於控制 monitored resource × metric label 的組合數。GCP 對 custom metrics 有每個 project 的 time series 配額（預設 500 per metric descriptor、可申請提高），超過時寫入會被拒。</p>
<h3 id="mql-vs-promql">MQL vs PromQL</h3>
<p>Cloud Monitoring 有兩種查詢語言。MQL（Monitoring Query Language）是 GCP 自家設計的 pipeline 語法：</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">fetch k8s_container
</span></span><span class="line"><span class="ln">2</span><span class="cl">| metric &#39;kubernetes.io/container/cpu/core_usage_time&#39;
</span></span><span class="line"><span class="ln">3</span><span class="cl">| align rate(1m)
</span></span><span class="line"><span class="ln">4</span><span class="cl">| every 1m
</span></span><span class="line"><span class="ln">5</span><span class="cl">| group_by [resource.cluster_name, resource.namespace_name],
</span></span><span class="line"><span class="ln">6</span><span class="cl">    [value_cpu_usage: aggregate(value.core_usage_time)]</span></span></code></pre></div><p>PromQL 在 Cloud Monitoring 上也可用（透過 Managed Service for Prometheus）。兩者的核心差異：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>MQL</th>
          <th>PromQL（via Managed Prometheus）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料來源</td>
          <td>所有 Cloud Monitoring metrics</td>
          <td>透過 Managed Prometheus 寫入的 metrics</td>
      </tr>
      <tr>
          <td>查詢介面</td>
          <td>Metrics Explorer / alerting condition</td>
          <td>Grafana / Prometheus UI / API</td>
      </tr>
      <tr>
          <td>Aggregation 語法</td>
          <td>pipe-style <code>group_by</code></td>
          <td>函式風格 <code>sum by (label)</code></td>
      </tr>
      <tr>
          <td>跨 GCP 與 custom</td>
          <td>原生支援 GCP 內建 metrics</td>
          <td>需要轉成 Prometheus 格式</td>
      </tr>
      <tr>
          <td>學習曲線</td>
          <td>GCP-specific、不可搬到其他平台</td>
          <td>跨平台標準、可搬到 Mimir / Thanos</td>
      </tr>
  </tbody>
</table>
<p>選擇判讀：純 GCP 環境且團隊沒有 Prometheus 經驗 → MQL 起步快。已有 Prometheus / Grafana 生態 → 用 Managed Prometheus + PromQL、把 GCP 內建 metrics 透過 Prometheus-compatible exporter 導入。混合環境 → 兩者並存、GCP 原生 metrics 用 MQL 做 alerting、application metrics 用 PromQL 查詢。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="custom-metrics-設計與寫入">Custom metrics 設計與寫入</h3>
<p>Custom metrics 的常見路徑有三條：</p>
<p><strong>路徑一：Cloud Monitoring API 直接寫入</strong>。應用程式用 Cloud Monitoring client library 建立 metric descriptor 並寫入 time series。適合 GCP-native 應用，不需要額外 agent。</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">metric type: custom.googleapis.com/checkout/latency_ms
</span></span><span class="line"><span class="ln">2</span><span class="cl">kind: GAUGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">value type: DISTRIBUTION
</span></span><span class="line"><span class="ln">4</span><span class="cl">labels: [service, region, status_code]</span></span></code></pre></div><p><strong>路徑二：OTel Collector + GCP exporter</strong>。應用程式用 OTel SDK 產生 metrics，OTel Collector 透過 <code>googlecloud</code> exporter 寫到 Cloud Monitoring。Metrics 命名空間是 <code>workload.googleapis.com/</code>。適合已有 OTel instrumentation 的服務。</p>
<p><strong>路徑三：Managed Service for Prometheus</strong>。部署 GCP 的 Managed Prometheus collector（或自管 Prometheus + remote write），metrics 存在 GCP 託管的 Monarch backend。查詢用 PromQL。適合 Kubernetes 環境且團隊熟悉 Prometheus 生態。</p>
<p>三條路徑可以共存。選擇判讀：先看團隊的 metrics 生態是 GCP-native 還是 Prometheus-native，再看 multi-cloud 需求。Managed Prometheus 的優勢是 PromQL 可搬、劣勢是 GCP 內建 metrics 需要額外整合。</p>
<h3 id="alerting-policy-配置">Alerting policy 配置</h3>
<p>Cloud Monitoring alerting policy 由三部分組成：condition、notification channel、documentation。</p>
<p>Condition types：</p>
<ul>
<li><strong>Metric threshold</strong>：metric 超過閾值 N 分鐘。適合「error rate &gt; 1% 持續 5 分鐘」。</li>
<li><strong>Metric absence</strong>：metric 消失。適合偵測 scrape 斷裂或服務停擺。</li>
<li><strong>Forecasting</strong>：預測 metric 在 N 小時後超過閾值。適合 disk 滿、quota 耗盡。</li>
<li><strong>Process health</strong>：GCE instance 的 process 是否存活。</li>
<li><strong>Log-based</strong>：Cloud Logging 出現特定 pattern 時觸發。適合把 error log 轉成 alert。</li>
<li><strong>SLO burn rate</strong>：SLO 設定後、burn rate 超過閾值。對應 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn-rate</a> 概念。</li>
</ul>
<p>Notification channels：Email / PagerDuty / Slack / Pub/Sub / Webhook / SMS。Pub/Sub channel 適合接自定義 automation（收到 alert → trigger Cloud Function）。</p>
<p>Snooze 與 maintenance window：暫時抑制特定 alerting policy。部署期間或已知維護時使用。</p>
<h3 id="managed-prometheus-整合">Managed Prometheus 整合</h3>
<p>GCP Managed Service for Prometheus 的部署模式：</p>
<ul>
<li><strong>GKE 模式</strong>：啟用 GKE monitoring、Managed Prometheus collector 自動部署。不需要自管 Prometheus server。</li>
<li><strong>Remote write 模式</strong>：自管 Prometheus server + <code>remote_write</code> 到 GCP Monarch endpoint。保留本地查詢能力，同時長期儲存在 GCP。</li>
<li><strong>OTel Collector 模式</strong>：OTel Collector 用 <code>googlemanagedprometheus</code> exporter 寫到 Monarch。</li>
</ul>
<p>查詢端：用 GCP Console 的 PromQL UI、或部署 Grafana + GMP datasource。PromQL 功能子集支援良好（rate / histogram_quantile / aggregation），少數進階功能（subquery）有限制。</p>
<h2 id="故障演練與邊界">故障演練與邊界</h2>
<h3 id="custom-metric-配額用盡">Custom metric 配額用盡</h3>
<p><strong>觸發條件</strong>：custom metric descriptor 數量超過 project 配額（預設 500），或單一 metric descriptor 的 time series 數量超過配額。</p>
<p><strong>表現</strong>：API 回傳 429 或 quota exceeded error。新 time series 寫不進去，既有的不受影響。</p>
<p><strong>修復</strong>：清理不再使用的 metric descriptor（describe → delete）、合併語意重疊的 metrics、減少 label cardinality。GCP Console → IAM → Quotas 可以申請提高配額，但先確認是設計問題而非真的需要那麼多 series。</p>
<h3 id="alerting-policy-觸發延遲">Alerting policy 觸發延遲</h3>
<p><strong>觸發條件</strong>：alerting policy 使用的 metrics 的 alignment period 或 duration 設定過長。</p>
<p><strong>表現</strong>：異常已經發生 10 分鐘，alert 才觸發。原因是 Cloud Monitoring 的 evaluation cycle 跟 metrics ingestion delay 相加。GCP 內建 metrics 的 ingestion delay 約 1-3 分鐘；custom metrics 透過 API 寫入的 delay 約 10-30 秒。</p>
<p><strong>修復</strong>：把 condition 的 alignment period 設短（1 分鐘）、duration 設短（但太短會造成 flapping）。Log-based alerting condition 的 delay 通常比 metric-based 短（秒級 vs 分鐘級），緊急異常考慮用 log-based condition。</p>
<h3 id="managed-prometheus-查詢與自管-prometheus-結果不一致">Managed Prometheus 查詢與自管 Prometheus 結果不一致</h3>
<p><strong>觸發條件</strong>：同一個 PromQL query 在本地 Prometheus 跟 GMP 的結果不同。</p>
<p><strong>表現</strong>：dashboard 數字對不上、alert 觸發行為不一致。</p>
<p><strong>修復</strong>：先確認 remote write 是否有 sample drop（看 <code>prometheus_remote_storage_samples_failed_total</code>）。再確認 GMP 的 PromQL 子集限制（部分 subquery 語法不支援）。最後確認 metric naming：local Prometheus 的 metric name 跟 GMP 儲存後的 naming convention 可能有差異（加了 <code>__name__</code> prefix 或 resource label）。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>Cloud Monitoring 的計費模型基於 <strong>ingested metrics volume</strong>（per million data points）。GCP 內建 metrics（agent metrics 除外）免費。Custom metrics 的前 150 MB per billing account 免費，超過後按 volume 計費。</p>
<p>成本治理的判讀：</p>
<ul>
<li>最大成本來源通常是高頻率的 custom metrics 或高 cardinality label</li>
<li>用 <code>monitoring.googleapis.com/billing/bytes_ingested</code> metric 追蹤 ingestion 量</li>
<li>減少 scrape interval（15s → 30s 或 60s）可以直接降低 ingestion 量</li>
<li>Managed Prometheus 的計費跟 custom metrics 分開計算（per samples ingested）</li>
</ul>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁</a>：overview 與日常操作</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality 治理</a>：cardinality 治理的完整策略</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO signal</a>：SLO burn rate alert 的訊號設計</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a>：Managed Prometheus 的上游概念</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>：OTel Collector + GCP exporter 整合</li>
<li><a href="../cloud-logging-export-compliance/">Cloud Logging 查詢、匯出與合規</a>：同 vendor 的 logs 面</li>
</ul>
]]></content:encoded></item><item><title>CloudWatch Logs Insights 查詢與日誌治理</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/logs-insights-governance/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/logs-insights-governance/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch&lt;/a> 的 vendor deep article，深化 overview「Logs Insights query」跟「Logs lifecycle」段。初次接觸 CloudWatch 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>CloudWatch Logs 的成本模型跟 self-hosted log stack 不同 — ingestion、storage 跟 query 分開計費，每一層都有明確的 cost lever。理解 log group 設計、retention 設定與 subscription filter 的組合，才能在 AWS-native 環境下控制日誌成本而不犧牲事故判讀能力。&lt;/p>
&lt;h2 id="log-group-設計">Log group 設計&lt;/h2>
&lt;h3 id="拆分粒度">拆分粒度&lt;/h3>
&lt;p>Log group 是 CloudWatch Logs 的計費與 retention 邊界。同一個 log group 內的所有 log stream 共用 retention policy 和 access control（IAM resource policy）。&lt;/p>
&lt;p>合理的拆分粒度是 &lt;strong>一個服務一個 log group&lt;/strong>，而非一個帳號一個或一個 container 一個。服務級拆分讓 retention、查詢範圍與 IAM 權限自然對齊服務 ownership。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>拆分策略&lt;/th>
 &lt;th>適合場景&lt;/th>
 &lt;th>風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>一個服務一個 log group&lt;/td>
 &lt;td>多數 production 服務&lt;/td>
 &lt;td>log group 數量增長需要 naming convention&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一個環境一個 log group&lt;/td>
 &lt;td>非常小的團隊、staging/dev 環境&lt;/td>
 &lt;td>混合多個服務的日誌，查詢時需要額外 filter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一個 Lambda function 一個 log group&lt;/td>
 &lt;td>Lambda 預設行為&lt;/td>
 &lt;td>Lambda 數量多時 log group 爆量，管理成本高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Lambda 的預設行為是每個 function 自動建一個 log group（&lt;code>/aws/lambda/&amp;lt;function-name&amp;gt;&lt;/code>）。function 數量超過數十個後，需要用 naming convention 加 tag 控制，否則 retention policy 難以統一套用。&lt;/p>
&lt;h3 id="naming-convention">Naming convention&lt;/h3>
&lt;p>推薦格式：&lt;code>/&amp;lt;environment&amp;gt;/&amp;lt;service&amp;gt;/&amp;lt;component&amp;gt;&lt;/code>，例如 &lt;code>/prod/checkout-api/app&lt;/code>、&lt;code>/prod/checkout-api/access-log&lt;/code>。統一前綴讓 Logs Insights 的 multi-log-group query 用 prefix matching 篩選。&lt;/p>
&lt;h2 id="logs-insights-查詢語法">Logs Insights 查詢語法&lt;/h2>
&lt;h3 id="核心語法">核心語法&lt;/h3>
&lt;p>Logs Insights 的查詢結構是 pipe-based：每行用 &lt;code>|&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">fields @timestamp, @message, @logStream
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">| filter @message like /ERROR/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">| parse @message &amp;#34;order_id=* status=*&amp;#34; as order_id, status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">| stats count(*) as error_count by status
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">| sort error_count desc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">| limit 20&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>常用 command 對照：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Command&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>注意事項&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>fields&lt;/code>&lt;/td>
 &lt;td>選擇要顯示的欄位&lt;/td>
 &lt;td>&lt;code>@timestamp&lt;/code>、&lt;code>@message&lt;/code> 是內建欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>filter&lt;/code>&lt;/td>
 &lt;td>條件篩選&lt;/td>
 &lt;td>支援 &lt;code>like /regex/&lt;/code>、&lt;code>=&lt;/code>、&lt;code>&amp;gt;&lt;/code>、&lt;code>in []&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>parse&lt;/code>&lt;/td>
 &lt;td>從非結構化 log 擷取欄位&lt;/td>
 &lt;td>glob pattern 用 &lt;code>*&lt;/code>、regex 用 &lt;code>/pattern/&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>stats&lt;/code>&lt;/td>
 &lt;td>聚合計算&lt;/td>
 &lt;td>&lt;code>count&lt;/code>、&lt;code>avg&lt;/code>、&lt;code>sum&lt;/code>、&lt;code>min&lt;/code>、&lt;code>max&lt;/code>、&lt;code>pct&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>sort&lt;/code>&lt;/td>
 &lt;td>排序&lt;/td>
 &lt;td>預設 &lt;code>@timestamp desc&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>display&lt;/code>&lt;/td>
 &lt;td>只顯示指定欄位（跟 &lt;code>fields&lt;/code> 互補）&lt;/td>
 &lt;td>用在 &lt;code>stats&lt;/code> 後只要看聚合結果&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="json-自動解析">JSON 自動解析&lt;/h3>
&lt;p>CloudWatch Logs 會自動辨識 JSON 格式的 log event。JSON 欄位用 dot notation 存取：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch</a> 的 vendor deep article，深化 overview「Logs Insights query」跟「Logs lifecycle」段。初次接觸 CloudWatch 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>CloudWatch Logs 的成本模型跟 self-hosted log stack 不同 — ingestion、storage 跟 query 分開計費，每一層都有明確的 cost lever。理解 log group 設計、retention 設定與 subscription filter 的組合，才能在 AWS-native 環境下控制日誌成本而不犧牲事故判讀能力。</p>
<h2 id="log-group-設計">Log group 設計</h2>
<h3 id="拆分粒度">拆分粒度</h3>
<p>Log group 是 CloudWatch Logs 的計費與 retention 邊界。同一個 log group 內的所有 log stream 共用 retention policy 和 access control（IAM resource policy）。</p>
<p>合理的拆分粒度是 <strong>一個服務一個 log group</strong>，而非一個帳號一個或一個 container 一個。服務級拆分讓 retention、查詢範圍與 IAM 權限自然對齊服務 ownership。</p>
<table>
  <thead>
      <tr>
          <th>拆分策略</th>
          <th>適合場景</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一個服務一個 log group</td>
          <td>多數 production 服務</td>
          <td>log group 數量增長需要 naming convention</td>
      </tr>
      <tr>
          <td>一個環境一個 log group</td>
          <td>非常小的團隊、staging/dev 環境</td>
          <td>混合多個服務的日誌，查詢時需要額外 filter</td>
      </tr>
      <tr>
          <td>一個 Lambda function 一個 log group</td>
          <td>Lambda 預設行為</td>
          <td>Lambda 數量多時 log group 爆量，管理成本高</td>
      </tr>
  </tbody>
</table>
<p>Lambda 的預設行為是每個 function 自動建一個 log group（<code>/aws/lambda/&lt;function-name&gt;</code>）。function 數量超過數十個後，需要用 naming convention 加 tag 控制，否則 retention policy 難以統一套用。</p>
<h3 id="naming-convention">Naming convention</h3>
<p>推薦格式：<code>/&lt;environment&gt;/&lt;service&gt;/&lt;component&gt;</code>，例如 <code>/prod/checkout-api/app</code>、<code>/prod/checkout-api/access-log</code>。統一前綴讓 Logs Insights 的 multi-log-group query 用 prefix matching 篩選。</p>
<h2 id="logs-insights-查詢語法">Logs Insights 查詢語法</h2>
<h3 id="核心語法">核心語法</h3>
<p>Logs Insights 的查詢結構是 pipe-based：每行用 <code>|</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">fields @timestamp, @message, @logStream
</span></span><span class="line"><span class="ln">2</span><span class="cl">| filter @message like /ERROR/
</span></span><span class="line"><span class="ln">3</span><span class="cl">| parse @message &#34;order_id=* status=*&#34; as order_id, status
</span></span><span class="line"><span class="ln">4</span><span class="cl">| stats count(*) as error_count by status
</span></span><span class="line"><span class="ln">5</span><span class="cl">| sort error_count desc
</span></span><span class="line"><span class="ln">6</span><span class="cl">| limit 20</span></span></code></pre></div><p>常用 command 對照：</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>用途</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>fields</code></td>
          <td>選擇要顯示的欄位</td>
          <td><code>@timestamp</code>、<code>@message</code> 是內建欄位</td>
      </tr>
      <tr>
          <td><code>filter</code></td>
          <td>條件篩選</td>
          <td>支援 <code>like /regex/</code>、<code>=</code>、<code>&gt;</code>、<code>in []</code></td>
      </tr>
      <tr>
          <td><code>parse</code></td>
          <td>從非結構化 log 擷取欄位</td>
          <td>glob pattern 用 <code>*</code>、regex 用 <code>/pattern/</code></td>
      </tr>
      <tr>
          <td><code>stats</code></td>
          <td>聚合計算</td>
          <td><code>count</code>、<code>avg</code>、<code>sum</code>、<code>min</code>、<code>max</code>、<code>pct</code></td>
      </tr>
      <tr>
          <td><code>sort</code></td>
          <td>排序</td>
          <td>預設 <code>@timestamp desc</code></td>
      </tr>
      <tr>
          <td><code>display</code></td>
          <td>只顯示指定欄位（跟 <code>fields</code> 互補）</td>
          <td>用在 <code>stats</code> 後只要看聚合結果</td>
      </tr>
  </tbody>
</table>
<h3 id="json-自動解析">JSON 自動解析</h3>
<p>CloudWatch Logs 會自動辨識 JSON 格式的 log event。JSON 欄位用 dot notation 存取：</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">fields @timestamp, requestId, level, message
</span></span><span class="line"><span class="ln">2</span><span class="cl">| filter level = &#34;ERROR&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">| stats count(*) by bin(5m)</span></span></code></pre></div><p>如果 log 是 JSON 格式，<code>parse</code> 通常不需要 — 直接用欄位名稱。混合格式（部分 JSON、部分 plain text）時，需要用 <code>isPresent()</code> 判斷欄位是否存在。</p>
<h3 id="效能考量">效能考量</h3>
<p>Logs Insights 的查詢成本按掃描的 data 量計費（每 GB scanned），不按結果數。減少掃描量的方式：</p>
<ul>
<li>縮短時間範圍：事故判讀先查最近 30 分鐘，確認 pattern 後再擴大</li>
<li>指定 log group：避免對所有 log group 做全域查詢</li>
<li>用 <code>limit</code> 限制結果集大小（不影響掃描量，但減少資料傳輸）</li>
</ul>
<p>跨 log group 查詢最多同時查 50 個 log group。超過時需要拆成多次查詢或用 subscription filter 把資料匯到集中儲存。</p>
<h2 id="retention-policy">Retention policy</h2>
<h3 id="設定方式">設定方式</h3>
<p>Retention policy 在 log group 級別設定。每個 log group 可以獨立選擇 1 天到 10 年、或永不過期。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws logs put-retention-policy <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --log-group-name /prod/checkout-api/app <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --retention-in-days <span class="m">30</span></span></span></code></pre></div><p>常見 retention 策略按服務性質分：</p>
<table>
  <thead>
      <tr>
          <th>服務類型</th>
          <th>建議 retention</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心交易路徑（checkout、payment）</td>
          <td>90-365 天</td>
          <td>事故回溯、合規稽核</td>
      </tr>
      <tr>
          <td>一般 API 服務</td>
          <td>30-90 天</td>
          <td>事故回溯足夠，cost 可控</td>
      </tr>
      <tr>
          <td>Background job / worker</td>
          <td>14-30 天</td>
          <td>失敗時看最近數天即可</td>
      </tr>
      <tr>
          <td>Lambda / short-lived function</td>
          <td>7-14 天</td>
          <td>高量低價值，過期快速清理</td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>365 天以上或永不過期</td>
          <td>法規要求，見 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a></td>
      </tr>
  </tbody>
</table>
<p>未設定 retention 的 log group 預設永不過期 — 這是 CloudWatch 日誌成本超支的常見原因。新 log group 建立後應立即設定 retention。</p>
<h3 id="fintech-合規場景的-log-group-分離">FinTech 合規場景的 log group 分離</h3>
<p><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">FinTech 審計證據案例</a>揭露一個常見問題：audit log 跟 operational log 混在同一個 log group，retention 只能統一設定。結果要嘛 operational log 為了合規被迫留太久（成本浪費）、要嘛 audit log 跟著 operational log 的短 retention 被刪掉（合規風險）。</p>
<p>CloudWatch 的 log group 設計天然支援這種分離 — audit log 跟 operational log 用不同 log group、各自設定 retention：</p>
<table>
  <thead>
      <tr>
          <th>Log 類型</th>
          <th>Log group 命名</th>
          <th>Retention</th>
          <th>Log class</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>交易 audit log</td>
          <td><code>/prod/checkout-api/audit</code></td>
          <td>2555 天（7 年）</td>
          <td>Infrequent Access</td>
      </tr>
      <tr>
          <td>Application operational log</td>
          <td><code>/prod/checkout-api/app</code></td>
          <td>30 天</td>
          <td>Standard</td>
      </tr>
      <tr>
          <td>Access log（ALB / API Gateway）</td>
          <td><code>/prod/checkout-api/access</code></td>
          <td>90 天</td>
          <td>Standard</td>
      </tr>
  </tbody>
</table>
<p>Audit log group 的額外治理：</p>
<ul>
<li><strong>IAM 權限分離</strong>：audit log group 的讀取權限（<code>logs:GetLogEvents</code>）限縮到 compliance team 跟 security team，application developer 只能讀 operational log group。避免 audit log 被隨意查詢或汙染</li>
<li><strong>Immutability</strong>：CloudWatch Logs 本身不支援 WORM（write once read many），合規要求 immutable 存檔時用 subscription filter 把 audit log 同步送到 S3 + Object Lock</li>
<li><strong>Cross-account 集中</strong>：audit log 的 cross-account aggregation（見下方段落）的 IAM 權限要比 operational log 嚴格 — aggregated sink 的 destination 只能由 security team 控制</li>
</ul>
<h3 id="infrequent-access-log-class">Infrequent Access log class</h3>
<p>CloudWatch Logs 提供兩種 log class：<strong>Standard</strong>（完整查詢、即時 subscription filter、metric filter）跟 <strong>Infrequent Access</strong>（僅支援 Logs Insights 查詢、不支援即時 subscription filter 跟 metric filter、ingestion 成本約降 50%）。</p>
<p>Audit log 的存取模式通常是「寫入頻繁、查詢極少（只在稽核或事故時才查）」— 正好符合 Infrequent Access 的定位。把 7 年 retention 的 audit log group 設成 Infrequent Access，ingestion 成本直接砍半。</p>
<p>注意 Infrequent Access 的限制：不能用 subscription filter 即時轉發到 Lambda 或 Kinesis，不能用 metric filter 從 log 產生 CloudWatch metric。如果 audit log 需要即時異常偵測（例如偵測大量失敗交易），要用 Standard class + subscription filter 做即時處理、再用 Lambda 寫到長期 audit log group（Infrequent Access）。</p>
<h3 id="自動化套用">自動化套用</h3>
<p>用 AWS Config rule 或 CloudFormation / CDK 的 log group 定義統一設定 retention。Lambda function 自動建立的 log group 不會自動套用 retention，需要額外自動化（Lambda post-hook 或 EventBridge rule + Lambda 設定 retention）。</p>
<h2 id="cross-account-log-aggregation">Cross-account log aggregation</h2>
<h3 id="架構模式">架構模式</h3>
<p>多帳號環境下，常見做法是設立一個「觀測帳號」（observability account），把其他帳號的 logs 匯入。</p>
<p>兩種匯入方式：</p>
<p><strong>Subscription filter + Kinesis Data Firehose</strong>：每個 source 帳號的 log group 設 subscription filter，把 log event 送到 observability 帳號的 Kinesis Data Firehose，再寫到 S3 或 OpenSearch。適合需要長期存檔或進階查詢的場景。</p>
<p><strong>CloudWatch cross-account observability</strong>：AWS 原生功能，在 monitoring account 直接查詢 source accounts 的 CloudWatch 資料（metrics、logs、traces）。設定較簡單，但查詢延遲較高，且 Logs Insights 的 cross-account 查詢有 region 限制。</p>
<table>
  <thead>
      <tr>
          <th>匯入方式</th>
          <th>適合場景</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Subscription filter + Firehose</td>
          <td>需要 S3 archive、OpenSearch 全文搜尋、離線分析</td>
          <td>每個 log group 最多 2 個 subscription filter</td>
      </tr>
      <tr>
          <td>Cross-account observability</td>
          <td>只需要 CloudWatch console 統一查詢</td>
          <td>同 region 限制、查詢延遲較高</td>
      </tr>
  </tbody>
</table>
<h3 id="subscription-filter-實務">Subscription filter 實務</h3>
<p>Subscription filter 可以把 log event 送到 Lambda（即時處理）、Kinesis Data Stream（緩衝）、Kinesis Data Firehose（直接寫 S3/OpenSearch）或另一個 log group。</p>
<p>每個 log group 最多 2 個 subscription filter — 這是硬限制。如果同一個 log group 需要同時送 S3 archive 跟即時 alerting，要用 Kinesis Data Stream 做 fan-out，讓 stream 下游各自消費。</p>
<p>filter pattern 語法支援 JSON 欄位匹配：</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">{ $.level = &#34;ERROR&#34; }</span></span></code></pre></div><p>只把 ERROR 級別的 log 送到 alerting pipeline，可以大幅降低下游處理量跟成本。</p>
<h2 id="cost-governance">Cost governance</h2>
<h3 id="計費結構">計費結構</h3>
<p>CloudWatch Logs 的成本由三個維度組成：</p>
<table>
  <thead>
      <tr>
          <th>計費項目</th>
          <th>計費方式</th>
          <th>常見比例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ingestion</td>
          <td>每 GB ingested</td>
          <td>通常佔 50-70%</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>每 GB-month stored</td>
          <td>通常佔 20-40%</td>
      </tr>
      <tr>
          <td>Query（Logs Insights）</td>
          <td>每 GB scanned</td>
          <td>通常佔 5-15%</td>
      </tr>
  </tbody>
</table>
<p>Ingestion 是最大成本。降低 ingestion 的手段：</p>
<ul>
<li><strong>調整 log level</strong>：production 只保留 INFO 以上，DEBUG 只在問題排查時短暫開啟</li>
<li><strong>去除重複資訊</strong>：access log 跟 application log 不要記錄相同欄位</li>
<li><strong>用 metric filter 替代 log query</strong>：高頻計數（error count、request count）用 CloudWatch Metric Filter 從 log 產生 metric，查詢成本從 log scan 轉成 metric query</li>
</ul>
<h3 id="成本觀測">成本觀測</h3>
<p>用 CloudWatch 自己的 metric 觀測 log 成本：</p>
<ul>
<li><code>IncomingBytes</code>（per log group）：監控哪個 log group ingestion 最大</li>
<li><code>IncomingLogEvents</code>（per log group）：監控 event 數量</li>
<li>AWS Cost Explorer 按 CloudWatch 拆分：看 log ingestion vs storage vs API call 的比例</li>
</ul>
<h3 id="降本決策樹">降本決策樹</h3>
<p>判斷成本是否合理的順序：</p>
<ol>
<li>最大 ingestion 的 log group 是哪個？是否合理（核心服務的 access log 量大是正常的）</li>
<li>Retention 是否都有設定？未設定的 log group 會持續累積 storage 成本</li>
<li>是否有 DEBUG 級別 log 在 production 長期開啟？</li>
<li>是否有 subscription filter 把全量 log 送到外部？能否加 filter pattern 只送需要的部分</li>
</ol>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>觀測管線整合：CloudWatch Logs → Subscription Filter → Kinesis Firehose → S3 / OpenSearch，見 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a></li>
<li>Audit log 治理：合規場景的 log retention 跟 access control，見 <a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a></li>
<li>Evidence package：把 Logs Insights query link 跟時間窗放進 evidence，見 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>OTel 整合：ADOT 可以把 log 送到 CloudWatch Logs 或其他 backend，見 <a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OpenTelemetry Collector 部署模式</a></li>
</ul>
]]></content:encoded></item><item><title>Datadog 成本治理與 Agent 配置</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/cost-governance-agent-config/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/cost-governance-agent-config/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog&lt;/a> 的 vendor deep article，深化 overview 的成本跟 Agent 段。初次接觸 Datadog 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Datadog 是全託管觀測平台，涵蓋 metrics、logs、traces、profiling、RUM、synthetic monitoring。託管方案的核心取捨是「零運維但成本跟用量成正比」— 用得越多付得越多，而且計價維度多（host、custom metric、log ingestion、span、indexed span），成本治理需要理解每個維度的計價模型。&lt;/p>
&lt;h2 id="計價模型概覽">計價模型概覽&lt;/h2>
&lt;p>Datadog 的主要計價維度：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>計價方式&lt;/th>
 &lt;th>常見失控來源&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Infrastructure host&lt;/td>
 &lt;td>每 host/月&lt;/td>
 &lt;td>Auto-scaling 造成 host 數量波動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Custom metrics&lt;/td>
 &lt;td>每 unique time series/月&lt;/td>
 &lt;td>Label 爆炸（同 cardinality 問題）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Log ingestion&lt;/td>
 &lt;td>每 GB ingested/月&lt;/td>
 &lt;td>Debug log level 忘記關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Log indexed retention&lt;/td>
 &lt;td>每 million events × 天/月&lt;/td>
 &lt;td>預設 retention 太長&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>APM host + indexed span&lt;/td>
 &lt;td>每 host/月 + 每 million span&lt;/td>
 &lt;td>Sampling 沒設、全收&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Profiling&lt;/td>
 &lt;td>每 host/月（APM 加購）&lt;/td>
 &lt;td>整體成本疊加&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多數 Datadog 成本失控的根因是 custom metrics 跟 log ingestion — 兩者跟 cardinality 跟 log volume 直接相關，成長可以很快。&lt;/p>
&lt;h2 id="custom-metrics-成本控制">Custom Metrics 成本控制&lt;/h2>
&lt;h3 id="什麼算-custom-metric">什麼算 custom metric&lt;/h3>
&lt;p>Datadog 把每個 unique 的 metric name + tag 組合算一個 time series。&lt;code>http_requests_total{service=checkout, method=GET, status=200}&lt;/code> 跟 &lt;code>http_requests_total{service=checkout, method=POST, status=500}&lt;/code> 是兩個 time series。&lt;/p>
&lt;p>Tag 的笛卡爾積決定 series 數量。5 個 service × 4 個 method × 5 個 status = 100 個 series。加一個 &lt;code>region&lt;/code> tag（3 個值）就變 300 個。加一個 &lt;code>endpoint&lt;/code> tag（50 個 normalized path）就變 15,000 個。&lt;/p>
&lt;h3 id="控制策略">控制策略&lt;/h3>
&lt;p>&lt;strong>Tag 白名單&lt;/strong>：跟 Prometheus 的 label 白名單邏輯相同。只保留有查詢價值的 tag — service、method、status_class（2xx/4xx/5xx）。移除 user_id、request_id、完整 URL。&lt;/p>
&lt;p>&lt;strong>Metrics without Limits&lt;/strong>：Datadog 的功能 — 在 ingestion 之後、query 之前過濾 tag。所有 tag 都收但只 index / 計費特定 tag。適合「收全量但只查部分維度」的場景。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> 的 vendor deep article，深化 overview 的成本跟 Agent 段。初次接觸 Datadog 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁</a>。</p></blockquote>
<h2 id="定位">定位</h2>
<p>Datadog 是全託管觀測平台，涵蓋 metrics、logs、traces、profiling、RUM、synthetic monitoring。託管方案的核心取捨是「零運維但成本跟用量成正比」— 用得越多付得越多，而且計價維度多（host、custom metric、log ingestion、span、indexed span），成本治理需要理解每個維度的計價模型。</p>
<h2 id="計價模型概覽">計價模型概覽</h2>
<p>Datadog 的主要計價維度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>計價方式</th>
          <th>常見失控來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infrastructure host</td>
          <td>每 host/月</td>
          <td>Auto-scaling 造成 host 數量波動</td>
      </tr>
      <tr>
          <td>Custom metrics</td>
          <td>每 unique time series/月</td>
          <td>Label 爆炸（同 cardinality 問題）</td>
      </tr>
      <tr>
          <td>Log ingestion</td>
          <td>每 GB ingested/月</td>
          <td>Debug log level 忘記關</td>
      </tr>
      <tr>
          <td>Log indexed retention</td>
          <td>每 million events × 天/月</td>
          <td>預設 retention 太長</td>
      </tr>
      <tr>
          <td>APM host + indexed span</td>
          <td>每 host/月 + 每 million span</td>
          <td>Sampling 沒設、全收</td>
      </tr>
      <tr>
          <td>Profiling</td>
          <td>每 host/月（APM 加購）</td>
          <td>整體成本疊加</td>
      </tr>
  </tbody>
</table>
<p>多數 Datadog 成本失控的根因是 custom metrics 跟 log ingestion — 兩者跟 cardinality 跟 log volume 直接相關，成長可以很快。</p>
<h2 id="custom-metrics-成本控制">Custom Metrics 成本控制</h2>
<h3 id="什麼算-custom-metric">什麼算 custom metric</h3>
<p>Datadog 把每個 unique 的 metric name + tag 組合算一個 time series。<code>http_requests_total{service=checkout, method=GET, status=200}</code> 跟 <code>http_requests_total{service=checkout, method=POST, status=500}</code> 是兩個 time series。</p>
<p>Tag 的笛卡爾積決定 series 數量。5 個 service × 4 個 method × 5 個 status = 100 個 series。加一個 <code>region</code> tag（3 個值）就變 300 個。加一個 <code>endpoint</code> tag（50 個 normalized path）就變 15,000 個。</p>
<h3 id="控制策略">控制策略</h3>
<p><strong>Tag 白名單</strong>：跟 Prometheus 的 label 白名單邏輯相同。只保留有查詢價值的 tag — service、method、status_class（2xx/4xx/5xx）。移除 user_id、request_id、完整 URL。</p>
<p><strong>Metrics without Limits</strong>：Datadog 的功能 — 在 ingestion 之後、query 之前過濾 tag。所有 tag 都收但只 index / 計費特定 tag。適合「收全量但只查部分維度」的場景。</p>
<p><strong>DogStatsD 聚合</strong>：Datadog Agent 的 DogStatsD 端在 Agent 層做 pre-aggregation，把客戶端的 per-request metric 聚合成 per-interval 的摘要。減少送到 Datadog 的 data point 數量。DogStatsD 聚合在 Agent 端執行，跟 TSDB 層的 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 是不同位置的 pre-aggregation 機制。</p>
<p><strong>Usage attribution</strong>：Datadog 的 <a href="https://docs.datadoghq.com/account_management/billing/usage_attribution/">Usage Attribution</a> 功能把 custom metric 成本拆到 service / team tag，讓團隊看到自己的 metric 成本。對應 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>。</p>
<h3 id="判讀指標">判讀指標</h3>
<p>Datadog UI 的 Metric Summary 頁面顯示每個 metric name 的 tag cardinality。定期（每月）檢查 top 20 高 cardinality metric，確認是否有意外的 tag 爆炸。</p>
<h2 id="log-ingestion-成本控制">Log Ingestion 成本控制</h2>
<h3 id="index-策略">Index 策略</h3>
<p>Datadog log 的計費分兩層：ingestion（進來就計費）跟 indexing（索引後按保留天數計費）。可以 ingest 所有 log 但只 index 部分 — 非 indexed 的 log 可以在 15 分鐘的 live tail 窗口查看，之後就看不到了（除非歸檔到 S3/GCS 做 rehydrate）。</p>
<p>可操作的分層：</p>
<ul>
<li><strong>Error / warning log</strong>：index，retention 30 天</li>
<li><strong>Info log（關鍵路徑）</strong>：index，retention 7 天</li>
<li><strong>Debug log</strong>：不 index、只 ingest（live tail 用）；或直接不送</li>
<li><strong>Access log（高量）</strong>：不 index、歸檔到 S3、需要時 rehydrate</li>
</ul>
<h3 id="exclusion-filter">Exclusion filter</h3>
<p>Datadog 的 index exclusion filter 讓特定 pattern 的 log 進入 ingestion pipeline 但跳過 index。例：health check 的 access log（<code>path:/health</code>）每秒數百筆但沒有 debug 價值，設 exclusion filter 讓它不佔 index quota。</p>
<h3 id="log-pipeline-跟-datadog-log-的對應">Log pipeline 跟 Datadog log 的對應</h3>
<p><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 的 collector 端可以在 log 送到 Datadog 之前做 filtering — 低價值 log 直接 drop、不進 Datadog ingestion（連 ingestion 費用都省）。這比 Datadog 的 exclusion filter 更節省成本（exclusion filter 仍然計 ingestion 費用）。</p>
<h2 id="agent-部署配置">Agent 部署配置</h2>
<h3 id="agent-部署模式">Agent 部署模式</h3>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>部署位置</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Host agent</td>
          <td>每台 VM 一個 agent</td>
          <td>傳統 VM 部署</td>
      </tr>
      <tr>
          <td>DaemonSet agent</td>
          <td>K8s 每個 node 一個 agent</td>
          <td>K8s 標準部署</td>
      </tr>
      <tr>
          <td>Sidecar agent</td>
          <td>每個 pod 一個 agent</td>
          <td>需要嚴格隔離時</td>
      </tr>
      <tr>
          <td>Cluster agent</td>
          <td>K8s cluster 一個</td>
          <td>收集 cluster-level metric</td>
      </tr>
  </tbody>
</table>
<p>多數 K8s 部署用 DaemonSet + Cluster Agent 組合。DaemonSet agent 收集 node-level 跟 pod-level 的 metric / log / trace；Cluster Agent 收集 cluster-level 的 metadata 跟 event。</p>
<h3 id="agent-健康判讀">Agent 健康判讀</h3>
<p>Agent 本身需要被監控 — Agent 故障時 Datadog 看到的是「資料消失」而非「Agent 掛了」。</p>
<p>判讀指標（Agent 自帶）：</p>
<ul>
<li><code>datadog.agent.running</code>：Agent process 是否存活</li>
<li><code>datadog.agent.check_run</code>：各 integration check 是否正常</li>
<li><code>datadog.dogstatsd.packets.dropped</code>：DogStatsD buffer 滿時丟棄的封包數</li>
</ul>
<p>Agent 掛掉時 dashboard 會出現 gap（資料斷層）。如果所有 host 同時斷層、問題在 Datadog backend；如果特定 host 斷層、問題在該 host 的 Agent。</p>
<h3 id="常見-agent-故障">常見 Agent 故障</h3>
<p><strong>CPU / memory over-consumption</strong>：Agent 開太多 integration check 或 DogStatsD 收太多 custom metric。修復：減少 check 數量、調整 DogStatsD 的 aggregation interval、或升級 Agent 版本（新版通常更節省資源）。</p>
<p><strong>Log collection 延遲</strong>：Agent 的 log tail 落後，log 到達 Datadog 的延遲增加。原因通常是 log rotation 設定跟 Agent 的 tail 設定不一致，或 log 量突然爆增超過 Agent 的處理能力。</p>
<p><strong>Network connectivity</strong>：Agent 到 Datadog intake endpoint 的網路問題。Agent 會 buffer 資料並重試，但 buffer 滿（預設 100MB）後會 drop。在網路不穩的環境（edge location、受限網路），需要加大 buffer 或設定 proxy。</p>
<h2 id="跟-otel-的整合">跟 OTel 的整合</h2>
<p>Datadog 支援 OpenTelemetry — 可以用 OTel SDK instrumentation + OTel Collector，把資料送到 Datadog backend。這種模式讓 instrumentation 跟 vendor 解耦，但犧牲部分 Datadog-native 功能（例如 Watchdog anomaly detection 需要 Datadog Agent 的 metadata）。</p>
<p>整合模式的選擇跟 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration practice</a> 的案例分析對應 — 雙軌期的成本跟語意對齊是主要挑戰。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁</a>：overview 跟日常操作</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a>：cardinality 治理的完整策略</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>：成本歸因的組織治理</li>
<li><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a>：Datadog 跟 OTel 的整合案例</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>：vendor-neutral instrumentation</li>
</ul>
]]></content:encoded></item><item><title>High-Cardinality Query Model 與 BubbleUp</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/high-cardinality-query-bubbleup/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/high-cardinality-query-bubbleup/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb&lt;/a> 的 vendor deep article，深化 overview「BubbleUp 分析」跟「Events vs metrics 心智模型」段。初次接觸 Honeycomb 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Metrics-based 觀測系統有一個結構性限制：metric 在寫入前就做了 aggregation，之後只能沿著預先定義的 label 維度查詢。當事故需要按 user_id、request_id、feature_flag_variant 或 deployment_version 定位時，metrics 系統要嘛沒有這些維度（label cardinality 會爆），要嘛需要事先知道要看哪個維度（但事故通常是 unknown-unknowns）。&lt;/p>
&lt;p>Honeycomb 用 event-based 模型解決這個問題 — 每一筆 event（通常是一個 trace span）帶幾十個 attribute，查詢時才決定 group by 哪些維度。BubbleUp 進一步自動找出區隔 outlier 跟 baseline 的 attribute，讓工程師不需要事先猜測問題維度。&lt;/p>
&lt;p>理解 Honeycomb 的資料模型、查詢設計跟 BubbleUp 的工作方式，才能判斷什麼場景下 Honeycomb 比 metrics-first 系統更有效、什麼場景下 metrics-first 仍然是對的選擇。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="event-based-資料模型">Event-based 資料模型&lt;/h3>
&lt;p>Honeycomb 的儲存引擎是 column store — 每一筆 event 是一列、每一個 attribute 是一欄。寫入時不做 aggregation，查詢時才 group by / filter / aggregate。&lt;/p>
&lt;p>跟 metrics-first 系統的根本差異：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Metrics-first（Prometheus）&lt;/th>
 &lt;th>Event-based（Honeycomb）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>寫入時&lt;/td>
 &lt;td>按 label 組合 aggregate 成 time series&lt;/td>
 &lt;td>存原始 event、帶所有 attribute&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢時&lt;/td>
 &lt;td>只能沿既有 label 維度查詢&lt;/td>
 &lt;td>任意 attribute 組合 group by&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cardinality&lt;/td>
 &lt;td>label 組合數 = time series 數、有上限&lt;/td>
 &lt;td>Attribute 組合數不影響儲存結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本模型&lt;/td>
 &lt;td>按 time series 數計費&lt;/td>
 &lt;td>按 events volume 計費&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合&lt;/td>
 &lt;td>已知維度的趨勢監控&lt;/td>
 &lt;td>unknown-unknowns 的事故偵錯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>一筆 checkout event 在 Honeycomb 可能帶 30+ 個 attribute：service.name、http.method、http.status_code、http.url、user_id、tenant_id、region、deployment_version、feature_flag.variant、db.duration_ms、cache.hit、payment.provider、error.message 等。在 Prometheus 上，user_id 跟 tenant_id 是不能當 label 的（cardinality 爆）；在 Honeycomb 上，它們只是多一欄。&lt;/p>
&lt;h3 id="bubbleup-的工作方式">BubbleUp 的工作方式&lt;/h3>
&lt;p>BubbleUp 是 Honeycomb 的自動異常歸因功能。操作流程：&lt;/p>
&lt;ol>
&lt;li>在 heatmap 上框選異常區域（例如 latency spike 的時間段跟數值範圍）&lt;/li>
&lt;li>BubbleUp 把框選區域的 events（outlier set）跟框外 events（baseline set）做統計比較&lt;/li>
&lt;li>對每一個 attribute，計算兩組 events 的分布差異（Honeycomb 使用 distribution divergence 量度）&lt;/li>
&lt;li>排序差異最大的 attribute 顯示在面板上&lt;/li>
&lt;/ol>
&lt;p>BubbleUp 的價值在於它跳過了「猜測哪個維度有問題」的步驟。傳統 metrics dashboarding 需要工程師先想到「可能是某個 region 的問題」→ 加 region filter → 確認。BubbleUp 直接告訴你「outlier set 跟 baseline set 在 region、deployment_version、payment.provider 三個維度上分布最不同」。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a> 的 vendor deep article，深化 overview「BubbleUp 分析」跟「Events vs metrics 心智模型」段。初次接觸 Honeycomb 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Metrics-based 觀測系統有一個結構性限制：metric 在寫入前就做了 aggregation，之後只能沿著預先定義的 label 維度查詢。當事故需要按 user_id、request_id、feature_flag_variant 或 deployment_version 定位時，metrics 系統要嘛沒有這些維度（label cardinality 會爆），要嘛需要事先知道要看哪個維度（但事故通常是 unknown-unknowns）。</p>
<p>Honeycomb 用 event-based 模型解決這個問題 — 每一筆 event（通常是一個 trace span）帶幾十個 attribute，查詢時才決定 group by 哪些維度。BubbleUp 進一步自動找出區隔 outlier 跟 baseline 的 attribute，讓工程師不需要事先猜測問題維度。</p>
<p>理解 Honeycomb 的資料模型、查詢設計跟 BubbleUp 的工作方式，才能判斷什麼場景下 Honeycomb 比 metrics-first 系統更有效、什麼場景下 metrics-first 仍然是對的選擇。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="event-based-資料模型">Event-based 資料模型</h3>
<p>Honeycomb 的儲存引擎是 column store — 每一筆 event 是一列、每一個 attribute 是一欄。寫入時不做 aggregation，查詢時才 group by / filter / aggregate。</p>
<p>跟 metrics-first 系統的根本差異：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Metrics-first（Prometheus）</th>
          <th>Event-based（Honeycomb）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫入時</td>
          <td>按 label 組合 aggregate 成 time series</td>
          <td>存原始 event、帶所有 attribute</td>
      </tr>
      <tr>
          <td>查詢時</td>
          <td>只能沿既有 label 維度查詢</td>
          <td>任意 attribute 組合 group by</td>
      </tr>
      <tr>
          <td>Cardinality</td>
          <td>label 組合數 = time series 數、有上限</td>
          <td>Attribute 組合數不影響儲存結構</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>按 time series 數計費</td>
          <td>按 events volume 計費</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>已知維度的趨勢監控</td>
          <td>unknown-unknowns 的事故偵錯</td>
      </tr>
  </tbody>
</table>
<p>一筆 checkout event 在 Honeycomb 可能帶 30+ 個 attribute：service.name、http.method、http.status_code、http.url、user_id、tenant_id、region、deployment_version、feature_flag.variant、db.duration_ms、cache.hit、payment.provider、error.message 等。在 Prometheus 上，user_id 跟 tenant_id 是不能當 label 的（cardinality 爆）；在 Honeycomb 上，它們只是多一欄。</p>
<h3 id="bubbleup-的工作方式">BubbleUp 的工作方式</h3>
<p>BubbleUp 是 Honeycomb 的自動異常歸因功能。操作流程：</p>
<ol>
<li>在 heatmap 上框選異常區域（例如 latency spike 的時間段跟數值範圍）</li>
<li>BubbleUp 把框選區域的 events（outlier set）跟框外 events（baseline set）做統計比較</li>
<li>對每一個 attribute，計算兩組 events 的分布差異（Honeycomb 使用 distribution divergence 量度）</li>
<li>排序差異最大的 attribute 顯示在面板上</li>
</ol>
<p>BubbleUp 的價值在於它跳過了「猜測哪個維度有問題」的步驟。傳統 metrics dashboarding 需要工程師先想到「可能是某個 region 的問題」→ 加 region filter → 確認。BubbleUp 直接告訴你「outlier set 跟 baseline set 在 region、deployment_version、payment.provider 三個維度上分布最不同」。</p>
<p>BubbleUp 的限制：它需要足夠的 event 量才能做統計比較。低 QPS 服務（&lt; 1 event/sec）在短時間窗內可能沒有足夠的 outlier events。它也不處理因果關係 — 分布差異最大的 attribute 不一定是 root cause，可能是 correlated symptom。</p>
<h3 id="slo-與-burn-rate-alert">SLO 與 Burn Rate Alert</h3>
<p>Honeycomb 的 SLO 功能把 service-level indicator 定義成一個 query、目標成功率定義成 SLO threshold、窗口跟 burn rate 用來觸發 alert。</p>
<p>SLO 設定要素：</p>
<ul>
<li><strong>SLI query</strong>：定義「成功」的條件。例如 <code>WHERE duration_ms &lt; 500 AND http.status_code &lt; 500</code>。</li>
<li><strong>SLO target</strong>：例如 99.9%。</li>
<li><strong>Window</strong>：通常 30 天 rolling window。</li>
<li><strong>Burn rate alert</strong>：multi-window multi-burn-rate。1 小時窗口看快速 burn（14.4x burn rate）、6 小時窗口看中速 burn（6x）、3 天窗口看慢速 burn（1x）。</li>
</ul>
<p>跟 Prometheus-based SLO 的差異：Prometheus SLO 通常用 recording rule 預先計算 error budget remaining，alert 基於 recording rule 結果。Honeycomb SLO 直接在 event 上做即時計算，不需要 recording rule。代價是 Honeycomb 的 SLO 計算跟平台綁定、不可搬。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn-rate</a> 概念跟 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO signal</a> 的訊號設計。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="derived-columns">Derived Columns</h3>
<p>Derived columns 是在 Honeycomb 查詢層建立的計算欄位，不改變原始 event。</p>
<p>常用場景：</p>
<ul>
<li><strong>Duration bucket</strong>：<code>IF(LTE($duration_ms, 100), &quot;fast&quot;, IF(LTE($duration_ms, 500), &quot;normal&quot;, &quot;slow&quot;))</code> — 把連續數值轉成 category、方便 group by</li>
<li><strong>Error classification</strong>：<code>IF(GTE($http.status_code, 500), &quot;server_error&quot;, IF(GTE($http.status_code, 400), &quot;client_error&quot;, &quot;ok&quot;))</code> — 對 status code 做語意分類</li>
<li><strong>Feature flag analysis</strong>：<code>CONCAT($service.name, &quot;-&quot;, $feature_flag.variant)</code> — 組合 attribute 做 A/B 比較</li>
</ul>
<p>Derived columns 的效能影響：它們在查詢時計算，不佔 ingestion 或 storage。但複雜的 derived column expression 會增加查詢 latency。</p>
<h3 id="dataset-設計">Dataset 設計</h3>
<p>Honeycomb 的 dataset 是資料隔離的單位。設計決策：</p>
<p><strong>Option A：per-environment dataset</strong>（production / staging / dev 各自獨立）。優點是查詢預設在單一環境、不需要每次加 environment filter。缺點是跨環境比較需要切換 dataset。</p>
<p><strong>Option B：per-service dataset</strong>（checkout-api / payment-adapter / notification-service 各自獨立）。優點是單一服務的查詢效能好（資料量小）。缺點是跨服務 trace 需要用 trace view 跨 dataset 查。</p>
<p><strong>Option C：single dataset per environment</strong>（production 一個大 dataset、所有服務混在一起）。優點是跨服務查詢不需切換、BubbleUp 能跨服務比較。缺點是資料量大、查詢稍慢、不同服務的 attribute 不一致可能造成混淆。</p>
<p>Honeycomb 推薦 Option C — 把同一環境的所有服務放同一個 dataset。理由是 BubbleUp 跟 trace view 的跨服務能力是 Honeycomb 的核心價值，拆太細會削弱這個優勢。用 <code>service.name</code> attribute 做 per-service filter。</p>
<h3 id="otlp-ingestion">OTLP Ingestion</h3>
<p>Honeycomb 原生接受 OTLP（gRPC 跟 HTTP）。應用程式用 OTel SDK 產生 traces / logs、設定 OTLP endpoint 為 <code>api.honeycomb.io:443</code>、帶 API key header。</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"># OTel Collector config example
</span></span><span class="line"><span class="ln">2</span><span class="cl">exporters:
</span></span><span class="line"><span class="ln">3</span><span class="cl">  otlp:
</span></span><span class="line"><span class="ln">4</span><span class="cl">    endpoint: &#34;api.honeycomb.io:443&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">    headers:
</span></span><span class="line"><span class="ln">6</span><span class="cl">      &#34;x-honeycomb-team&#34;: &#34;${HONEYCOMB_API_KEY}&#34;
</span></span><span class="line"><span class="ln">7</span><span class="cl">      &#34;x-honeycomb-dataset&#34;: &#34;production&#34;</span></span></code></pre></div><p>OTel SDK 跟 Honeycomb Beeline SDK 的選擇：新部署一律用 OTel SDK — vendor neutral、可搬。Beeline SDK 是 Honeycomb-specific，已進入維護模式。既有 Beeline 部署可以逐步遷移到 OTel SDK。</p>
<h2 id="故障演練與邊界">故障演練與邊界</h2>
<h3 id="sampling-不足導致成本失控">Sampling 不足導致成本失控</h3>
<p><strong>觸發條件</strong>：高 QPS 服務（&gt; 10K req/sec）不做 sampling、全量送 Honeycomb。</p>
<p><strong>表現</strong>：月帳單高於預期。Honeycomb 按 events volume 計費、高 QPS 服務全量 ingestion 的成本可能是 Prometheus 的數倍。</p>
<p><strong>修復</strong>：部署 Refinery（Honeycomb 的 tail-based sampling proxy）。Refinery 在 trace 完成後決定是否保留 — 保留所有 error trace、保留所有高 latency trace、對正常 trace 做 sampling（例如保留 10%）。Dynamic sampling 根據 traffic pattern 自動調整 sampling rate。</p>
<p>成本與可見度的取捨：1% sampling 意味著 99% 的正常 event 看不到。如果需要回答「過去一小時有多少 successful request」這種 count 問題，sampling 會引入統計誤差。Honeycomb 支援 sample rate annotation — query 結果會用 sample rate 做加權還原。</p>
<h3 id="bubbleup-結果不可行動">BubbleUp 結果不可行動</h3>
<p><strong>觸發條件</strong>：BubbleUp 顯示差異最大的 attribute 是「timestamp」或「trace_id」— 這些 attribute 天然在 outlier set 跟 baseline set 之間分布不同，不提供歸因資訊。</p>
<p><strong>修復</strong>：在 BubbleUp 設定中排除 high-entropy attribute（trace_id、span_id、timestamp）。Honeycomb 允許設定 BubbleUp 的 ignore list。另外確保 event 帶足夠的 business-context attribute — 如果 event 只有 infra-level attribute（CPU、memory），BubbleUp 能找到的 insight 有限。</p>
<h3 id="gaming-高峰的-cardinality-情境">Gaming 高峰的 cardinality 情境</h3>
<p><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">Gaming 案例</a>揭露了 metrics-first 跟 event-first 系統在高峰期的根本差異。線上遊戲的賽季開跑或限時活動會讓流量在 30 分鐘內暴增 10 倍，同時 per-player、per-match-id 的 label 組合讓 Prometheus 的 active series 從 50 萬爆到 500 萬。</p>
<p>Prometheus 在這個場景的痛點不只是容量 — 而是 cardinality 爆炸改變了系統行為：scrape 變慢導致 metric freshness 從 15 秒退化到數分鐘、recording rule evaluation 跟不上 interval、alert 基於過期數據判斷。修法是 drop per-player label 或做 pre-aggregation、但 drop 掉之後事故時就查不到「哪個玩家的 session 異常」。</p>
<p>Honeycomb 的 event model 在這個場景天然有優勢 — per-player、per-match 是 event 上的 attribute，不產生 series、不影響 ingestion 效能。活動開跑時 event volume 暴增，但 Honeycomb 的 column store 只是行數增加、查詢的 IO 成本線性增長而非指數。BubbleUp 可以在高峰期直接找出「哪些 player_region × match_type 的組合延遲最高」。</p>
<p>代價是成本 — 10 倍的流量意味著 10 倍的 events volume、10 倍的計費。Gaming 場景通常需要搭配動態 sampling：正常 gameplay event 做 1:100 sampling、error 跟 high-latency event 全量保留。Refinery 的 tail-based sampling 在這裡是必備元件。</p>
<h3 id="honeycomb-vs-prometheus-的共存">Honeycomb vs Prometheus 的共存</h3>
<p>Honeycomb 不取代 Prometheus — 兩者解決不同問題。Prometheus 適合已知維度的趨勢監控（error rate dashboard、capacity trending、SLO burn rate），Honeycomb 適合 unknown-unknowns 的事故偵錯。</p>
<p>共存模式：application 用 OTel SDK 同時產生 metrics（→ Prometheus）跟 traces（→ Honeycomb）。Alerting 在 Prometheus 側（因為 metrics aggregation 穩定且成本低），深度偵錯在 Honeycomb 側。</p>
<h3 id="雙工具成本治理模式">雙工具成本治理模式</h3>
<p><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">觀測成本治理案例</a>提出一個在中大型團隊反覆驗證的分工：Prometheus 負責 golden signals（低 cardinality、固定 recording rules、成本可預測），Honeycomb 負責 high-cardinality debug（按需查詢、pay per event）。</p>
<p>這個分工的成本結構：Prometheus 的成本隨 active series 數量增長（cardinality-driven）、Honeycomb 的成本隨 event volume 增長（traffic-driven）。兩者的成本 driver 不同、scaling curve 不同 — Prometheus 在 series 爆炸時成本失控、Honeycomb 在 QPS 暴增時成本失控。把兩者放在一起、用各自的成本 sweet spot 互補、比只買一家更能控制總成本。</p>
<p>判讀自己是否需要雙工具的訊號：Prometheus dashboard 已經穩定、但事故時仍需要 20+ 分鐘才能定位到具體 user / request / deployment_version — 這 20 分鐘就是 Honeycomb 的價值。如果事故定位都能在 5 分鐘內靠 Prometheus label 完成，不需要加 Honeycomb。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>Honeycomb 的計費基於 <strong>events volume</strong>（per million events ingested per month）。Event 的大小（attribute 數量）不直接影響計費（目前模型按 event 筆數、不按 payload size）。</p>
<p>成本治理手段：</p>
<ul>
<li><strong>Sampling</strong>：最直接。10% sampling = 成本降 90%。用 Refinery 做 tail-based sampling 保留重要 trace。</li>
<li><strong>Attribute 精簡</strong>：減少不需要的 attribute 不直接降成本（按筆數計費），但能加快查詢。</li>
<li><strong>Dataset 合併</strong>：多個小 dataset 合併成一個不影響成本，但能改善 BubbleUp 的統計品質。</li>
<li><strong>Team plan vs Enterprise</strong>：不同 plan 的 retention 跟 query 配額不同。</li>
</ul>
<p>跟 Prometheus 的成本比較：Prometheus 按 time series 數量計（self-host 的話是 infra 成本），Honeycomb 按 event 數量計。高 QPS + 低 cardinality 場景、Prometheus 成本優勢明顯。高 cardinality + 需要深度偵錯場景、Honeycomb 的 event cost 換到的是 BubbleUp 跟 arbitrary group by 的能力。</p>
<h3 id="不同規模的成本形態">不同規模的成本形態</h3>
<table>
  <thead>
      <tr>
          <th>規模</th>
          <th>月 event 量</th>
          <th>預估月成本範圍</th>
          <th>成本治理重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>小型（1-5 服務、&lt; 1K QPS）</td>
          <td>&lt; 50M events</td>
          <td>Free tier 或低帳單</td>
          <td>不需特別治理</td>
      </tr>
      <tr>
          <td>中型（10-30 服務、1-10K QPS）</td>
          <td>50M-500M events</td>
          <td>中等（依 plan）</td>
          <td>Refinery sampling 開始有 ROI</td>
      </tr>
      <tr>
          <td>大型（50+ 服務、10K+ QPS）</td>
          <td>1B+ events</td>
          <td>高（需要 Enterprise plan）</td>
          <td>Refinery + 動態 sampling 必備、跟 Prometheus 分工控制總成本</td>
      </tr>
  </tbody>
</table>
<p>大型場景的成本治理核心是 sampling 策略 — 全量 ingestion 的成本通常不可接受。Refinery 的 tail-based sampling 讓 error trace 跟 high-latency trace 全量保留、normal trace 做 1:10 到 1:100 sampling。Sampling rate 的選擇取決於「事故時需要多少正常 trace 做 baseline 比對」— BubbleUp 需要足夠的 baseline events 才能計算分布差異，sampling 太激進會讓 BubbleUp 的統計品質下降。</p>
<p>經驗值：保留至少 5-10% 的正常 trace、同時全量保留所有 error / slow trace。在 Gaming 案例的高峰期，正常 trace 的 sampling 可以暫時降到 1%（高峰流量 10 倍、1% sampling 仍有大量 baseline events），高峰結束後恢復到 10%。動態 sampling 根據當前 QPS 自動調整 — Refinery 的 <code>DynamicSampler</code> 會根據 key field（service.name + http.status_code）的分布自動決定 sample rate。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb 服務頁</a>：overview 與日常操作</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality 治理</a>：cardinality 在 metrics-first 跟 event-first 系統的不同治理策略</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO signal</a>：SLO / burn rate 的訊號設計</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>：OTLP ingestion 的上游標準</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a>：共存模式中的 metrics 面</li>
<li><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2 Gaming peak cardinality</a>：high-cardinality 場景的案例回寫</li>
</ul>
]]></content:encoded></item><item><title>Index Lifecycle Management 與 Log Pipeline</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/ilm-log-pipeline/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/ilm-log-pipeline/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &amp;#43; Beats / APM">Elastic Stack&lt;/a> 的 vendor deep article，深化 overview「Index Lifecycle Management」跟「採集 pipeline」段。初次接觸 Elastic 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &amp;#43; Beats / APM">Elastic Stack 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Elastic Stack 部署後，工程師通常能快速搜尋到 log。問題出在規模成長後：index 數量膨脹導致 cluster 效能退化、disk 滿了才發現沒有 lifecycle policy、shard 太小或太大造成查詢效能不均、採集 agent 的選擇在 Beats / Logstash / Elastic Agent / Fluent Bit 之間搖擺不定。ILM 跟 log pipeline 設計是 Elastic Stack 從「能用」到「可治理」的關鍵步驟。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="data-stream-vs-index-alias">Data Stream vs Index Alias&lt;/h3>
&lt;p>Elasticsearch 7.9+ 引入 data stream，取代傳統 index alias + rollover 模式。兩者的核心差異：&lt;/p>
&lt;p>&lt;strong>Data stream&lt;/strong> 是 append-only 的 time-series 資料結構。每個 data stream 下有多個 backing index，由 ILM 自動管理 rollover。寫入只能 append（沒有 update / delete single document），適合 log、metrics、traces。&lt;/p>
&lt;p>&lt;strong>Index alias&lt;/strong> 是傳統模式 — 手動建立 write alias 指向 current index，配合 ILM rollover action 觸發新 index 建立。支援 update / delete，適合需要修改文件的場景（例如 enrichment pipeline 的 lookup index）。&lt;/p>
&lt;p>選擇判讀：time-series 資料（log / metrics / APM trace）一律用 data stream。需要文件修改的 reference data、lookup table 用 index alias。新部署預設用 data stream，除非有明確理由。&lt;/p>
&lt;h3 id="ilm-policy-設計">ILM Policy 設計&lt;/h3>
&lt;p>ILM（Index Lifecycle Management）把 index 的生命週期分成五個 phase：&lt;/p>
&lt;p>&lt;strong>Hot phase&lt;/strong>：active write + 高頻查詢。Index 在 hot data node 上，用 SSD。Rollover 條件觸發後，current index 變 read-only，新 index 繼續寫入。&lt;/p>
&lt;p>&lt;strong>Warm phase&lt;/strong>：read-only + 中頻查詢。Index 搬到 warm data node（可以是 HDD 或較便宜的 SSD）。通常在 rollover 後 1-7 天觸發。可以執行 force merge（減少 segment 數量、提升查詢效能）跟 shrink（減少 shard 數量）。&lt;/p>
&lt;p>&lt;strong>Cold phase&lt;/strong>：searchable snapshot + 低頻查詢。Index 轉成 partial searchable snapshot，資料存在 object storage（S3 / GCS / Azure Blob），本地只保留 cache。查詢可用但較慢。適合 30 天到 1 年的保留。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a> 的 vendor deep article，深化 overview「Index Lifecycle Management」跟「採集 pipeline」段。初次接觸 Elastic 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Elastic Stack 部署後，工程師通常能快速搜尋到 log。問題出在規模成長後：index 數量膨脹導致 cluster 效能退化、disk 滿了才發現沒有 lifecycle policy、shard 太小或太大造成查詢效能不均、採集 agent 的選擇在 Beats / Logstash / Elastic Agent / Fluent Bit 之間搖擺不定。ILM 跟 log pipeline 設計是 Elastic Stack 從「能用」到「可治理」的關鍵步驟。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="data-stream-vs-index-alias">Data Stream vs Index Alias</h3>
<p>Elasticsearch 7.9+ 引入 data stream，取代傳統 index alias + rollover 模式。兩者的核心差異：</p>
<p><strong>Data stream</strong> 是 append-only 的 time-series 資料結構。每個 data stream 下有多個 backing index，由 ILM 自動管理 rollover。寫入只能 append（沒有 update / delete single document），適合 log、metrics、traces。</p>
<p><strong>Index alias</strong> 是傳統模式 — 手動建立 write alias 指向 current index，配合 ILM rollover action 觸發新 index 建立。支援 update / delete，適合需要修改文件的場景（例如 enrichment pipeline 的 lookup index）。</p>
<p>選擇判讀：time-series 資料（log / metrics / APM trace）一律用 data stream。需要文件修改的 reference data、lookup table 用 index alias。新部署預設用 data stream，除非有明確理由。</p>
<h3 id="ilm-policy-設計">ILM Policy 設計</h3>
<p>ILM（Index Lifecycle Management）把 index 的生命週期分成五個 phase：</p>
<p><strong>Hot phase</strong>：active write + 高頻查詢。Index 在 hot data node 上，用 SSD。Rollover 條件觸發後，current index 變 read-only，新 index 繼續寫入。</p>
<p><strong>Warm phase</strong>：read-only + 中頻查詢。Index 搬到 warm data node（可以是 HDD 或較便宜的 SSD）。通常在 rollover 後 1-7 天觸發。可以執行 force merge（減少 segment 數量、提升查詢效能）跟 shrink（減少 shard 數量）。</p>
<p><strong>Cold phase</strong>：searchable snapshot + 低頻查詢。Index 轉成 partial searchable snapshot，資料存在 object storage（S3 / GCS / Azure Blob），本地只保留 cache。查詢可用但較慢。適合 30 天到 1 年的保留。</p>
<p><strong>Frozen phase</strong>：fully mounted searchable snapshot + 極低頻查詢。資料完全在 object storage，本地無 cache。查詢最慢但成本最低。適合 1 年以上的合規保留。</p>
<p><strong>Delete phase</strong>：刪除 index。保留期到期後自動清理。</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">PUT _ilm/policy/application-log-policy
</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">  &#34;policy&#34;: {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    &#34;phases&#34;: {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">      &#34;hot&#34;: {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        &#34;actions&#34;: {
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">          &#34;rollover&#34;: {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            &#34;max_primary_shard_size&#34;: &#34;30gb&#34;,
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            &#34;max_age&#34;: &#34;1d&#34;
</span></span><span class="line"><span class="ln">10</span><span class="cl">          }
</span></span><span class="line"><span class="ln">11</span><span class="cl">        }
</span></span><span class="line"><span class="ln">12</span><span class="cl">      },
</span></span><span class="line"><span class="ln">13</span><span class="cl">      &#34;warm&#34;: {
</span></span><span class="line"><span class="ln">14</span><span class="cl">        &#34;min_age&#34;: &#34;3d&#34;,
</span></span><span class="line"><span class="ln">15</span><span class="cl">        &#34;actions&#34;: {
</span></span><span class="line"><span class="ln">16</span><span class="cl">          &#34;forcemerge&#34;: {&#34;max_num_segments&#34;: 1},
</span></span><span class="line"><span class="ln">17</span><span class="cl">          &#34;shrink&#34;: {&#34;number_of_shards&#34;: 1}
</span></span><span class="line"><span class="ln">18</span><span class="cl">        }
</span></span><span class="line"><span class="ln">19</span><span class="cl">      },
</span></span><span class="line"><span class="ln">20</span><span class="cl">      &#34;cold&#34;: {
</span></span><span class="line"><span class="ln">21</span><span class="cl">        &#34;min_age&#34;: &#34;30d&#34;,
</span></span><span class="line"><span class="ln">22</span><span class="cl">        &#34;actions&#34;: {
</span></span><span class="line"><span class="ln">23</span><span class="cl">          &#34;searchable_snapshot&#34;: {
</span></span><span class="line"><span class="ln">24</span><span class="cl">            &#34;snapshot_repository&#34;: &#34;s3-repo&#34;
</span></span><span class="line"><span class="ln">25</span><span class="cl">          }
</span></span><span class="line"><span class="ln">26</span><span class="cl">        }
</span></span><span class="line"><span class="ln">27</span><span class="cl">      },
</span></span><span class="line"><span class="ln">28</span><span class="cl">      &#34;delete&#34;: {
</span></span><span class="line"><span class="ln">29</span><span class="cl">        &#34;min_age&#34;: &#34;365d&#34;,
</span></span><span class="line"><span class="ln">30</span><span class="cl">        &#34;actions&#34;: {&#34;delete&#34;: {}}
</span></span><span class="line"><span class="ln">31</span><span class="cl">      }
</span></span><span class="line"><span class="ln">32</span><span class="cl">    }
</span></span><span class="line"><span class="ln">33</span><span class="cl">  }
</span></span><span class="line"><span class="ln">34</span><span class="cl">}</span></span></code></pre></div><p>Rollover 條件的選擇：<code>max_primary_shard_size</code> 比 <code>max_size</code> 更精確（直接控制單一 primary shard 大小）。目標是每個 primary shard 在 20-50 GB 之間。太小（&lt; 5 GB）造成 shard 過多、cluster state 膨脹；太大（&gt; 50 GB）造成 recovery 慢、query 效能下降。</p>
<h3 id="儲存成長回推-lifecycle-設計">儲存成長回推 lifecycle 設計</h3>
<p><a href="/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">Discord 儲存成長案例</a>揭露一個在快速成長服務反覆出現的模式：資料量倍增後才發現 ILM 的 hot → warm → cold 邊界不對、hot tier 佔比過高是最常見的成本問題。</p>
<p>問題的根源是 ILM policy 在服務初期設計、之後沒有隨資料量調整。一個服務從 10 GB/day 成長到 100 GB/day 時：</p>
<ul>
<li><strong>Hot tier 膨脹</strong>：原本 hot phase 設 7 天、10 GB/day × 7 天 = 70 GB。成長到 100 GB/day 後、hot tier 變成 700 GB、SSD 成本是原來的 10 倍</li>
<li><strong>Warm tier 延遲啟動</strong>：如果 warm phase 的 <code>min_age</code> 仍然是 7 天、資料在最貴的 tier 停留太久</li>
<li><strong>Cold/frozen phase 未啟用</strong>：初期資料量小時 cold phase 看不到成本效益、成長後才發現 30 天以上的資料全在 warm tier SSD 上</li>
</ul>
<p>修法是把 ILM review 放進服務的 capacity review cadence（季度或半年）。Review 時看三個指標：<code>hot_data_size / total_data_size</code>（hot tier 佔比超過 30% 就該重新評估）、<code>warm_tier_age_distribution</code>（warm tier 是否堆了太多舊資料）、<code>monthly_storage_cost_trend</code>（成本是否跟資料量同比例增長）。</p>
<p>Searchable snapshot（cold/frozen phase）是成本降幅最大的一步 — 資料從 local SSD 搬到 object storage，儲存成本降 70-90%。但搬遷後查詢延遲從 ms 退化到秒級。判讀「什麼資料該移」的訊號是該 index 在過去 30 天的查詢頻率 — 沒被查過的 index 留在 warm tier 是浪費。</p>
<h3 id="採集-pipelinebeats-vs-elastic-agent-vs-第三方">採集 Pipeline：Beats vs Elastic Agent vs 第三方</h3>
<table>
  <thead>
      <tr>
          <th>採集工具</th>
          <th>定位</th>
          <th>適用場景</th>
          <th>管理模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Filebeat</td>
          <td>單用途 log 採集</td>
          <td>成熟穩定、資源消耗低、K8s 環境輕量</td>
          <td>手動 config / ConfigMap</td>
      </tr>
      <tr>
          <td>Metricbeat</td>
          <td>單用途 metrics 採集</td>
          <td>host / container / service metrics</td>
          <td>手動 config</td>
      </tr>
      <tr>
          <td>Elastic Agent</td>
          <td>統一採集 agent</td>
          <td>logs + metrics + security + APM、Fleet 集中管理</td>
          <td>Fleet Server 集中</td>
      </tr>
      <tr>
          <td>Logstash</td>
          <td>重型 ETL pipeline</td>
          <td>複雜 parsing / enrichment / 多 output</td>
          <td>手動 config</td>
      </tr>
      <tr>
          <td>Fluent Bit / Vector</td>
          <td>第三方輕量 agent</td>
          <td>多 destination、低 resource、OTel 整合</td>
          <td>手動 config</td>
      </tr>
  </tbody>
</table>
<p>選擇判讀：</p>
<ul>
<li><strong>新部署、想要集中管理</strong>：Elastic Agent + Fleet。Fleet Server 提供 policy 集中推送、版本升級、health monitoring。代價是 Fleet Server 自身需要維運。</li>
<li><strong>既有 Beats 部署、穩定運行</strong>：不急著遷移。Elastic Agent 的 Beats integration 內部仍用 Beats 引擎。</li>
<li><strong>K8s 環境、resource 敏感</strong>：Filebeat DaemonSet。資源消耗 ~50-100 MB per node，比 Elastic Agent 低。</li>
<li><strong>多 destination（ES + S3 + Kafka）</strong>：Logstash 或 Vector。Beats 的 output 只能寫一個 destination（除非用 output plugin hack）。</li>
<li><strong>已有 OTel Collector</strong>：OTel Collector 可以直接把 log 送到 Elasticsearch（OTLP exporter 或 Elasticsearch exporter），不需要額外 Beats。</li>
</ul>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="ingest-pipeline-設計">Ingest Pipeline 設計</h3>
<p>Ingest pipeline 在 Elasticsearch 層做 log 的 parsing 跟 enrichment，在 index 前處理。</p>
<p>常用 processor：</p>
<ul>
<li><strong>grok</strong>：regex pattern 解析非結構化 log。適合 nginx access log、syslog 等固定格式。</li>
<li><strong>dissect</strong>：delimiter-based parsing。比 grok 快 5-10 倍，但只能處理固定 delimiter 格式。</li>
<li><strong>date</strong>：把 log 中的 timestamp string 解析成 <code>@timestamp</code>。</li>
<li><strong>geoip</strong>：IP 地址轉地理位置。</li>
<li><strong>script</strong>：Painless script 做自訂轉換。效能代價高，只在其他 processor 做不到時使用。</li>
<li><strong>set / rename / remove</strong>：field 操作。</li>
</ul>
<p>Pipeline 設計原則：先用 dissect（快）、dissect 做不到才用 grok（慢）。Pipeline 中的 processor 數量跟複雜度直接影響 ingest 吞吐。高 volume 場景（&gt; 10K events/sec per node）要做 ingest pipeline benchmark。</p>
<h3 id="mapping-template-與-dynamic-mapping-治理">Mapping Template 與 Dynamic Mapping 治理</h3>
<p>Mapping template 定義 index 的 field type。Dynamic mapping 對未知 field 自動建立 mapping — 這是 Elastic 的便利功能，也是最常見的治理問題。</p>
<p><strong>Dynamic mapping 風險</strong>：application log 帶 arbitrary JSON payload，dynamic mapping 對每個 key 建立 field mapping。一個 log 帶 100 個 unique key → 100 個 field mapping。大量 unique key 會導致 mapping explosion（field 數量爆、cluster state 膨脹、query routing 變慢）。</p>
<p><strong>治理策略</strong>：</p>
<ul>
<li>用 <code>dynamic: strict</code> 或 <code>dynamic: false</code>（strict = 拒絕未定義 field、false = 接受但不 index）</li>
<li>在 mapping template 明確定義已知 field，用 <code>dynamic_templates</code> 控制未知 field 的行為</li>
<li>對 arbitrary JSON payload 用 <code>flattened</code> field type（ES 7.3+）— 整個 JSON 存為 keyword，可查但不逐 key index</li>
</ul>





<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">PUT _index_template/app-logs
</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">  &#34;index_patterns&#34;: [&#34;app-logs-*&#34;],
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  &#34;template&#34;: {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    &#34;mappings&#34;: {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      &#34;dynamic&#34;: &#34;strict&#34;,
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      &#34;properties&#34;: {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        &#34;@timestamp&#34;: {&#34;type&#34;: &#34;date&#34;},
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        &#34;message&#34;: {&#34;type&#34;: &#34;text&#34;},
</span></span><span class="line"><span class="ln">10</span><span class="cl">        &#34;log.level&#34;: {&#34;type&#34;: &#34;keyword&#34;},
</span></span><span class="line"><span class="ln">11</span><span class="cl">        &#34;service.name&#34;: {&#34;type&#34;: &#34;keyword&#34;},
</span></span><span class="line"><span class="ln">12</span><span class="cl">        &#34;trace.id&#34;: {&#34;type&#34;: &#34;keyword&#34;},
</span></span><span class="line"><span class="ln">13</span><span class="cl">        &#34;metadata&#34;: {&#34;type&#34;: &#34;flattened&#34;}
</span></span><span class="line"><span class="ln">14</span><span class="cl">      }
</span></span><span class="line"><span class="ln">15</span><span class="cl">    }
</span></span><span class="line"><span class="ln">16</span><span class="cl">  }
</span></span><span class="line"><span class="ln">17</span><span class="cl">}</span></span></code></pre></div><h3 id="shard-sizing">Shard Sizing</h3>
<p>Shard sizing 是 Elastic Stack 效能的核心變數。</p>
<p><strong>目標</strong>：每個 primary shard 20-50 GB（Elastic 官方建議）。每個 data node 管理的 shard 數量上限約 20 per GB heap（預設 heap 一般設 30 GB → ~600 shard per node）。</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>日 ingest 量</th>
          <th>primary shard 數</th>
          <th>rollover 頻率</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>小型（&lt; 10 GB/day）</td>
          <td>5 GB</td>
          <td>1</td>
          <td>每天或 max_size 30 GB</td>
          <td>簡單 ILM 即可</td>
      </tr>
      <tr>
          <td>中型（10-100 GB/day）</td>
          <td>50 GB</td>
          <td>2-3</td>
          <td>每天</td>
          <td>warm + cold ILM</td>
      </tr>
      <tr>
          <td>大型（100+ GB/day）</td>
          <td>500 GB</td>
          <td>10-15</td>
          <td>每小時或 max_size 30 GB</td>
          <td>hot-warm-cold-frozen 全用</td>
      </tr>
  </tbody>
</table>
<p>Shard 過多的症狀：cluster state 過大（<code>_cluster/stats</code> 的 <code>indices.shards.total</code> 數千或數萬）、master node CPU 高（維護 cluster state）、recovery 慢。</p>
<p>Shard 過大的症狀：single shard query 慢（&gt; 500ms for simple filter）、segment merge 時間長、recovery 時單一 shard 復原需要數分鐘。</p>
<h3 id="shard-count-治理">Shard count 治理</h3>
<p>大量 index 場景（微服務架構下每個服務每天產生一個 data stream backing index）容易累積過多 shard。一個 50 服務的組織、每個服務每天 rollover 一次、primary + 1 replica = 100 shard/day。30 天後 hot + warm tier 有 3000 個 shard。</p>
<p>Elasticsearch 的經驗法則是每個 data node 管理的 shard 數量上限約 20 per GB heap。30 GB heap 的 node 約能管 600 個 shard。3000 個 shard 需要至少 5 個 data node 才不觸發效能退化。</p>
<p>降低 shard 數量的手段：</p>
<ul>
<li><strong>ILM shrink action</strong>：warm phase 把 primary shard 數量縮減（例如 3 → 1）。適合查詢頻率下降的舊 index</li>
<li><strong>延長 rollover 週期</strong>：如果單個服務的日資料量只有 1-2 GB，每天 rollover 產生的 shard 太小。調整 rollover 條件為 <code>max_primary_shard_size: 30gb</code>（讓系統自動決定 rollover 時機）而非固定 <code>max_age: 1d</code></li>
<li><strong>合併小服務</strong>：QPS 很低的服務共用同一個 data stream（用 <code>service.name</code> field 區分），減少 data stream 數量</li>
</ul>
<p>監控指標：<code>_cat/health</code> 的 <code>active_shards</code> 持續觀察趨勢。設 alert 在 shard count 超過 <code>data_node_count × 500</code> 時通知（留 buffer 給 recovery 跟 rebalance）。</p>
<h2 id="故障演練與邊界">故障演練與邊界</h2>
<h3 id="ilm-rollover-沒觸發">ILM rollover 沒觸發</h3>
<p><strong>觸發條件</strong>：ILM policy 已設定但 rollover action 沒有執行。常見原因：index 沒有正確關聯到 ILM policy、或 ILM 被暫停（<code>_ilm/stop</code>）。</p>
<p><strong>判讀</strong>：用 <code>GET &lt;index&gt;/_ilm/explain</code> 看 ILM 狀態。<code>managed: false</code> 代表 index 不受 ILM 管理。<code>step: ERROR</code> 代表 ILM 卡在某個 action。</p>
<p><strong>修復</strong>：確認 index template 的 <code>index.lifecycle.name</code> 指向正確的 ILM policy。如果 ILM step error，用 <code>POST &lt;index&gt;/_ilm/retry</code> 重試。</p>
<h3 id="searchable-snapshot-查詢延遲高">Searchable snapshot 查詢延遲高</h3>
<p><strong>觸發條件</strong>：cold / frozen phase 的 searchable snapshot index 被高頻查詢。</p>
<p><strong>表現</strong>：query latency 從 ms 級退化到秒級。原因是每次查詢需要從 object storage（S3 / GCS）拉資料。</p>
<p><strong>修復</strong>：cold phase 有 local cache、查重複 query 較快；frozen phase 無 cache、每次都拉。如果查詢頻率高到需要 sub-second 回應，這些 index 不應該在 cold/frozen phase — 調整 ILM policy 的 <code>min_age</code> 讓它們留在 warm phase 更久。</p>
<h3 id="cross-cluster-search-vs-replication">Cross-cluster search vs replication</h3>
<p><strong>Cross-cluster search（CCS）</strong>：查詢時 fan-out 到遠端 cluster。適合偶爾跨 cluster 查詢、不需要常駐複製。代價是查詢 latency 包含跨 cluster 的網路延遲。</p>
<p><strong>Cross-cluster replication（CCR）</strong>：把 index 從 leader cluster 持續複製到 follower cluster。適合 DR、地理就近讀取。代價是複製的 storage 跟網路頻寬成本。</p>
<p>選擇判讀：「偶爾查」→ CCS。「需要低延遲讀 + DR」→ CCR。兩者可以並存。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>Elastic Stack 的成本由三個維度決定：</p>
<p><strong>License tier</strong>：Basic（免費、含 ILM / data streams）→ Gold（ML / alerting）→ Platinum（SIEM / endpoint）→ Enterprise。Elastic Cloud 的計費另加 infrastructure cost。</p>
<p><strong>Data tier storage</strong>：hot tier 用 SSD（最貴）、warm tier 用 HDD 或便宜 SSD、cold/frozen tier 用 object storage（最便宜）。ILM 的 phase 設計直接影響 storage cost。</p>
<p><strong>Node 數量</strong>：每增加 data node 增加 compute 成本。Shard sizing 跟 ILM 設計決定需要多少 node。</p>
<p>成本最佳化優先序：</p>
<ol>
<li><strong>ILM + searchable snapshot</strong>：30 天後移到 cold/frozen，storage 成本降 70-90%</li>
<li><strong>Shard sizing</strong>：避免 shard 過多造成的 cluster overhead</li>
<li><strong>Ingest pipeline</strong>：在 ingest 層 drop 不需要的 field，減少 index size</li>
<li><strong>Mapping 治理</strong>：避免 mapping explosion 造成的 cluster state overhead</li>
<li><strong>Retention policy</strong>：明確設定 delete phase，不讓過期資料佔空間</li>
</ol>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack 服務頁</a>：overview 與日常操作</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a>：採集 pipeline 在觀測架構中的定位</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：mapping drift 跟 field missing 的資料品質面</li>
<li><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a>：ILM + searchable snapshot 在合規場景的應用</li>
<li><a href="../migrate-to-elastic-cloud/">Elastic Cloud migration</a>：從自管 Elastic 遷移到 Elastic Cloud</li>
</ul>
]]></content:encoded></item><item><title>LGTM Stack 組合運維：Loki + Grafana + Tempo + Mimir</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/lgtm-stack-operations/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/lgtm-stack-operations/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a> 的 vendor deep article，深化 overview 的元件組合段。初次接觸 Grafana Stack 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Grafana Stack（LGTM = Loki + Grafana + Tempo + Mimir）是自架觀測平台的完整選項，四個元件各自承擔一類訊號的儲存跟查詢。理解每個元件的責任邊界、部署模式跟故障特性，才能避免「裝了四個元件但不知道哪個壞了」的黑盒問題。&lt;/p>
&lt;h2 id="四元件的責任分工">四元件的責任分工&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>元件&lt;/th>
 &lt;th>訊號類型&lt;/th>
 &lt;th>查詢語言&lt;/th>
 &lt;th>儲存後端&lt;/th>
 &lt;th>角色&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Loki&lt;/td>
 &lt;td>Log&lt;/td>
 &lt;td>LogQL&lt;/td>
 &lt;td>Object storage + BoltDB&lt;/td>
 &lt;td>Log aggregation、grep 替代品&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Mimir&lt;/td>
 &lt;td>Metric&lt;/td>
 &lt;td>PromQL&lt;/td>
 &lt;td>Object storage&lt;/td>
 &lt;td>Prometheus 的可擴展長期儲存&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tempo&lt;/td>
 &lt;td>Trace&lt;/td>
 &lt;td>TraceQL&lt;/td>
 &lt;td>Object storage&lt;/td>
 &lt;td>Trace 儲存、span 搜尋&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Grafana&lt;/td>
 &lt;td>視覺化&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>—&lt;/td>
 &lt;td>Dashboard、alert、data source&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Grafana 是查詢 / 視覺化層，Loki / Mimir / Tempo 是儲存 / 查詢層。Grafana 本身不存觀測資料，它連接 data source（Loki / Mimir / Tempo / Prometheus / Elasticsearch）做查詢跟渲染。&lt;/p>
&lt;p>四個元件獨立部署、獨立擴展、各自有健康指標。一個元件故障不影響其他元件 — Loki 掛了時 Grafana 的 metric dashboard 跟 trace 查詢仍然正常，只有 log panel 會報錯。&lt;/p>
&lt;h2 id="部署模式">部署模式&lt;/h2>
&lt;h3 id="monolithic-mode">Monolithic mode&lt;/h3>
&lt;p>四個元件（或其中幾個）跑在同一個 process / container。適合小規模（每天數 GB log、數十萬 metric series、少量 trace）。部署最簡單 — 一個 docker-compose 或 Helm chart 起全套。&lt;/p>
&lt;p>限制是沒辦法獨立擴展 — log 量大但 metric 量小時，monolithic mode 不能只加 Loki 的資源。&lt;/p>
&lt;h3 id="microservices-mode">Microservices mode&lt;/h3>
&lt;p>每個元件拆成獨立的 deployment、各自 autoscaling。Loki 拆成 distributor / ingester / querier / compactor；Mimir 拆成類似的元件；Tempo 也有對應的分層。&lt;/p>
&lt;p>適合中到大規模。部署跟維運複雜度顯著上升 — 每個元件的每個子服務都需要獨立的 health check、autoscaling 設定、persistent volume。&lt;/p>
&lt;h3 id="選擇判準">選擇判準&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>條件&lt;/th>
 &lt;th>建議模式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>團隊 &amp;lt; 5 人、日 log &amp;lt; 10 GB&lt;/td>
 &lt;td>Monolithic&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要獨立擴展某一類訊號&lt;/td>
 &lt;td>Microservices&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不想自管、預算足夠&lt;/td>
 &lt;td>Grafana Cloud&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>已有 Prometheus、只需要加 log / trace&lt;/td>
 &lt;td>漸進式加 Loki + Tempo&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見故障模式">常見故障模式&lt;/h2>
&lt;h3 id="lokiingester-oom">Loki：ingester OOM&lt;/h3>
&lt;p>Loki ingester 把 log chunks 保存在記憶體，高流量時容易 OOM。觸發條件是突然的 log 量爆增（部署後 error storm、某服務開了 debug log level）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> 的 vendor deep article，深化 overview 的元件組合段。初次接觸 Grafana Stack 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁</a>。</p></blockquote>
<h2 id="定位">定位</h2>
<p>Grafana Stack（LGTM = Loki + Grafana + Tempo + Mimir）是自架觀測平台的完整選項，四個元件各自承擔一類訊號的儲存跟查詢。理解每個元件的責任邊界、部署模式跟故障特性，才能避免「裝了四個元件但不知道哪個壞了」的黑盒問題。</p>
<h2 id="四元件的責任分工">四元件的責任分工</h2>
<table>
  <thead>
      <tr>
          <th>元件</th>
          <th>訊號類型</th>
          <th>查詢語言</th>
          <th>儲存後端</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loki</td>
          <td>Log</td>
          <td>LogQL</td>
          <td>Object storage + BoltDB</td>
          <td>Log aggregation、grep 替代品</td>
      </tr>
      <tr>
          <td>Mimir</td>
          <td>Metric</td>
          <td>PromQL</td>
          <td>Object storage</td>
          <td>Prometheus 的可擴展長期儲存</td>
      </tr>
      <tr>
          <td>Tempo</td>
          <td>Trace</td>
          <td>TraceQL</td>
          <td>Object storage</td>
          <td>Trace 儲存、span 搜尋</td>
      </tr>
      <tr>
          <td>Grafana</td>
          <td>視覺化</td>
          <td>—</td>
          <td>—</td>
          <td>Dashboard、alert、data source</td>
      </tr>
  </tbody>
</table>
<p>Grafana 是查詢 / 視覺化層，Loki / Mimir / Tempo 是儲存 / 查詢層。Grafana 本身不存觀測資料，它連接 data source（Loki / Mimir / Tempo / Prometheus / Elasticsearch）做查詢跟渲染。</p>
<p>四個元件獨立部署、獨立擴展、各自有健康指標。一個元件故障不影響其他元件 — Loki 掛了時 Grafana 的 metric dashboard 跟 trace 查詢仍然正常，只有 log panel 會報錯。</p>
<h2 id="部署模式">部署模式</h2>
<h3 id="monolithic-mode">Monolithic mode</h3>
<p>四個元件（或其中幾個）跑在同一個 process / container。適合小規模（每天數 GB log、數十萬 metric series、少量 trace）。部署最簡單 — 一個 docker-compose 或 Helm chart 起全套。</p>
<p>限制是沒辦法獨立擴展 — log 量大但 metric 量小時，monolithic mode 不能只加 Loki 的資源。</p>
<h3 id="microservices-mode">Microservices mode</h3>
<p>每個元件拆成獨立的 deployment、各自 autoscaling。Loki 拆成 distributor / ingester / querier / compactor；Mimir 拆成類似的元件；Tempo 也有對應的分層。</p>
<p>適合中到大規模。部署跟維運複雜度顯著上升 — 每個元件的每個子服務都需要獨立的 health check、autoscaling 設定、persistent volume。</p>
<h3 id="選擇判準">選擇判準</h3>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>建議模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>團隊 &lt; 5 人、日 log &lt; 10 GB</td>
          <td>Monolithic</td>
      </tr>
      <tr>
          <td>需要獨立擴展某一類訊號</td>
          <td>Microservices</td>
      </tr>
      <tr>
          <td>不想自管、預算足夠</td>
          <td>Grafana Cloud</td>
      </tr>
      <tr>
          <td>已有 Prometheus、只需要加 log / trace</td>
          <td>漸進式加 Loki + Tempo</td>
      </tr>
  </tbody>
</table>
<h2 id="常見故障模式">常見故障模式</h2>
<h3 id="lokiingester-oom">Loki：ingester OOM</h3>
<p>Loki ingester 把 log chunks 保存在記憶體，高流量時容易 OOM。觸發條件是突然的 log 量爆增（部署後 error storm、某服務開了 debug log level）。</p>
<p>判讀指標：<code>loki_ingester_memory_chunks</code>、<code>process_resident_memory_bytes</code>。修復方向：調整 chunk flush interval（更頻繁寫入 object storage、降低記憶體壓力）、加 ingester replica、或在 pipeline 層（OTel Collector）做 log volume rate limit。</p>
<h3 id="mimircompactor-卡住">Mimir：compactor 卡住</h3>
<p>Mimir compactor 負責合併 ingester 寫入的 block。Compactor 卡住時，block 數量持續增長、query 需要掃描更多 block、延遲上升。</p>
<p>判讀指標：<code>cortex_compactor_runs_completed_total</code> 停滯、<code>cortex_bucket_blocks_count</code> 持續增長。修復方向：檢查 object storage 的寫入權限跟延遲、增加 compactor 資源（CPU / memory）、或暫時停止 ingestion 讓 compactor 追上。</p>
<h3 id="tempotrace-not-found">Tempo：trace not found</h3>
<p>使用者用 trace ID 查詢時回 &ldquo;trace not found&rdquo;，但 trace 確實存在。常見原因是 Tempo 的 bloom filter / compacted block index 還沒包含該 trace（ingestion 到可查詢有延遲），或 trace 被 retention policy 刪除。</p>
<p>判讀方式：查 trace 的 timestamp 是否在 retention 範圍內、查 <code>tempo_ingester_traces_created_total</code> 確認 ingestion 正常、查 compactor 是否正常運行。</p>
<h3 id="grafanadashboard-provisioning-漂移">Grafana：dashboard provisioning 漂移</h3>
<p>用 provisioning（YAML / JSON 檔案）管理 dashboard 時，手動在 UI 修改的 dashboard 會在下次 provisioning 同步時被覆蓋。團隊成員在 UI 調整了 panel、下次重啟 Grafana 後修改消失。</p>
<p>修復方向：dashboard 修改統一透過 git → provisioning pipeline（GitOps），UI 只用於臨時調整跟探索。把 provisioning 的 <code>allowUiUpdates</code> 設為 false、強制所有變更走 git。</p>
<h2 id="dashboard-provisioning">Dashboard Provisioning</h2>
<p>Dashboard 的管理方式影響長期維護成本。手動在 UI 建立 dashboard 的起步最快，但隨 dashboard 數量增長會出現版本不一致、無法 rollback、owner 不明的問題。</p>
<h3 id="infrastructure-as-code">Infrastructure as Code</h3>
<p>Dashboard JSON 存在 git repo、透過 provisioning 同步到 Grafana。變更走 PR review、有版本歷史、可以 rollback。</p>
<p>Grafana 的 provisioning 機制讀 YAML config，指定 dashboard JSON 的來源（local file / HTTP / API）。Helm chart 部署時把 dashboard JSON 放在 ConfigMap 或 persistent volume。</p>
<h3 id="grafonnet--jsonnet">Grafonnet / Jsonnet</h3>
<p>用 Jsonnet（Grafana 的 dashboard-as-code library）產生 dashboard JSON。適合大量相似 dashboard 的場景 — 每個服務一個 dashboard，結構相同但 data source 跟 label 不同。</p>
<p>Grafonnet 的學習曲線比直接寫 JSON 高，但在 dashboard 數量 &gt; 20 個時開始有維護效率的回報。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁</a>：overview 跟日常操作</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：Mimir 的上游 metric 來源</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式</a>：LGTM 的 ingestion 入口</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a>：pipeline 各層的治理</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：dashboard / alert 的 ownership</li>
</ul>
]]></content:encoded></item><item><title>Prometheus 容量規劃與故障模式</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「Cardinality 管理」跟「Memory pressure」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Prometheus 的容量模型跟傳統資料庫不同 — 它的容量邊界主要受 active series 數量（cardinality）跟 retention 期決定，而非資料筆數或 disk size。理解 Prometheus 的資源消耗模型，才能判斷什麼時候單機夠用、什麼時候需要 remote write 卸載或遷移到 Mimir / Thanos。&lt;/p>
&lt;h2 id="資源消耗模型">資源消耗模型&lt;/h2>
&lt;h3 id="memory由-active-series-決定">Memory：由 active series 決定&lt;/h3>
&lt;p>Prometheus 把近期的 time series 保存在記憶體（head block）。每個 active series 大約消耗 3-4 KB 記憶體（含 index、chunks、postings；Prometheus TSDB 的業界經驗值，實際依 label 長度與 chunk encoding 而定）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Active series&lt;/th>
 &lt;th>預估 memory（head block）&lt;/th>
 &lt;th>適合的機器規格&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>10 萬&lt;/td>
 &lt;td>~400 MB&lt;/td>
 &lt;td>任何 VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>100 萬&lt;/td>
 &lt;td>~4 GB&lt;/td>
 &lt;td>8 GB VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>500 萬&lt;/td>
 &lt;td>~20 GB&lt;/td>
 &lt;td>32 GB VM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1000 萬&lt;/td>
 &lt;td>~40 GB&lt;/td>
 &lt;td>64 GB VM&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這是 head block 的記憶體，不含 query execution 跟 WAL replay 的暫時開銷。Heavy PromQL query（大範圍 aggregation、多 series join）會額外消耗數 GB 的暫時記憶體。&lt;/p>
&lt;p>判讀指標：&lt;code>prometheus_tsdb_head_series&lt;/code> 代表當前 active series 數量，&lt;code>process_resident_memory_bytes&lt;/code> 代表實際記憶體使用。兩者的比值偏離預期時（例如 50 萬 series 但記憶體用了 10 GB），可能是 query 記憶體壓力或 WAL corruption。&lt;/p>
&lt;h3 id="disk由-retention-期與-ingestion-rate-決定">Disk：由 retention 期與 ingestion rate 決定&lt;/h3>
&lt;p>Prometheus 的 disk 消耗 = ingestion rate × retention 期 × 壓縮後每 sample 大小（約 1-2 bytes，Gorilla 壓縮算法下的業界經驗值）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Ingestion rate&lt;/th>
 &lt;th>Retention&lt;/th>
 &lt;th>預估 disk&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>10 萬 samples/sec&lt;/td>
 &lt;td>15 天&lt;/td>
 &lt;td>~130 GB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10 萬 samples/sec&lt;/td>
 &lt;td>30 天&lt;/td>
 &lt;td>~260 GB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>50 萬 samples/sec&lt;/td>
 &lt;td>15 天&lt;/td>
 &lt;td>~650 GB&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Disk I/O 的瓶頸通常在 compaction — Prometheus 定期把 head block 壓縮成 persistent block。Compaction 期間的 disk write 跟 CPU 使用會短暫上升。SSD 環境下 compaction 通常不是問題；HDD 環境下可能造成 scrape timeout。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「Cardinality 管理」跟「Memory pressure」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="定位">定位</h2>
<p>Prometheus 的容量模型跟傳統資料庫不同 — 它的容量邊界主要受 active series 數量（cardinality）跟 retention 期決定，而非資料筆數或 disk size。理解 Prometheus 的資源消耗模型，才能判斷什麼時候單機夠用、什麼時候需要 remote write 卸載或遷移到 Mimir / Thanos。</p>
<h2 id="資源消耗模型">資源消耗模型</h2>
<h3 id="memory由-active-series-決定">Memory：由 active series 決定</h3>
<p>Prometheus 把近期的 time series 保存在記憶體（head block）。每個 active series 大約消耗 3-4 KB 記憶體（含 index、chunks、postings；Prometheus TSDB 的業界經驗值，實際依 label 長度與 chunk encoding 而定）。</p>
<table>
  <thead>
      <tr>
          <th>Active series</th>
          <th>預估 memory（head block）</th>
          <th>適合的機器規格</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬</td>
          <td>~400 MB</td>
          <td>任何 VM</td>
      </tr>
      <tr>
          <td>100 萬</td>
          <td>~4 GB</td>
          <td>8 GB VM</td>
      </tr>
      <tr>
          <td>500 萬</td>
          <td>~20 GB</td>
          <td>32 GB VM</td>
      </tr>
      <tr>
          <td>1000 萬</td>
          <td>~40 GB</td>
          <td>64 GB VM</td>
      </tr>
  </tbody>
</table>
<p>這是 head block 的記憶體，不含 query execution 跟 WAL replay 的暫時開銷。Heavy PromQL query（大範圍 aggregation、多 series join）會額外消耗數 GB 的暫時記憶體。</p>
<p>判讀指標：<code>prometheus_tsdb_head_series</code> 代表當前 active series 數量，<code>process_resident_memory_bytes</code> 代表實際記憶體使用。兩者的比值偏離預期時（例如 50 萬 series 但記憶體用了 10 GB），可能是 query 記憶體壓力或 WAL corruption。</p>
<h3 id="disk由-retention-期與-ingestion-rate-決定">Disk：由 retention 期與 ingestion rate 決定</h3>
<p>Prometheus 的 disk 消耗 = ingestion rate × retention 期 × 壓縮後每 sample 大小（約 1-2 bytes，Gorilla 壓縮算法下的業界經驗值）。</p>
<table>
  <thead>
      <tr>
          <th>Ingestion rate</th>
          <th>Retention</th>
          <th>預估 disk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>15 天</td>
          <td>~130 GB</td>
      </tr>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>30 天</td>
          <td>~260 GB</td>
      </tr>
      <tr>
          <td>50 萬 samples/sec</td>
          <td>15 天</td>
          <td>~650 GB</td>
      </tr>
  </tbody>
</table>
<p>Disk I/O 的瓶頸通常在 compaction — Prometheus 定期把 head block 壓縮成 persistent block。Compaction 期間的 disk write 跟 CPU 使用會短暫上升。SSD 環境下 compaction 通常不是問題；HDD 環境下可能造成 scrape timeout。</p>
<h3 id="cpu由-scrape-數量與-query-負載決定">CPU：由 scrape 數量與 query 負載決定</h3>
<p>Scrape 本身的 CPU 消耗不高（HTTP GET + parse），但 scrape 數量 × scrape 間隔決定了基本的 CPU 基線。1000 個 target × 15 秒間隔 = 每秒 ~67 次 scrape，單核可以處理。</p>
<p>Query 是 CPU 的主要消耗者。Recording rule evaluation、alert rule evaluation、dashboard panel 查詢各自佔 CPU。Recording rule 數量增長到數百條時，evaluation 的 CPU 消耗可能成為瓶頸。</p>
<p>判讀指標：<code>prometheus_rule_evaluation_duration_seconds</code> 的 p99 超過 evaluation interval 時，rule 跑不完、alert 會延遲。</p>
<h2 id="cardinality-失控的判讀">Cardinality 失控的判讀</h2>
<p>Cardinality 是 Prometheus 最常見的容量問題。一個意外的高 cardinality label（user_id、request_id、完整 URL）可以在分鐘內把 series 數從 10 萬推到 100 萬、消耗數 GB 記憶體。</p>
<h3 id="判讀訊號">判讀訊號</h3>
<ul>
<li><code>prometheus_tsdb_head_series</code> 持續成長、斜率陡峭</li>
<li><code>prometheus_tsdb_head_active_appenders</code> 成長（新 series 的寫入速率）</li>
<li>Prometheus 的 memory 持續上升、最終 OOM kill</li>
<li>Query 延遲增加（更多 series 要掃描）</li>
<li>Compaction 時間變長</li>
</ul>
<h3 id="定位方式">定位方式</h3>





<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"># 找出哪個 metric name 的 series 最多
</span></span><span class="line"><span class="ln">2</span><span class="cl">topk(10, count by (__name__)({__name__=~&#34;.+&#34;}))
</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"># 找出哪個 job（scrape target）的 series 最多
</span></span><span class="line"><span class="ln">5</span><span class="cl">topk(10, count by (job)({__name__=~&#34;.+&#34;}))
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"># 找出某個 metric 的哪個 label 組合在爆
</span></span><span class="line"><span class="ln">8</span><span class="cl">count by (method, status) (http_requests_total)</span></span></code></pre></div><h3 id="修復方向">修復方向</h3>
<ul>
<li><strong>Label 白名單</strong>：在 scrape config 或 relabeling rule 中 drop 高 cardinality label</li>
<li><strong>Metric relabeling</strong>：<code>metric_relabel_configs</code> 在 scrape 後、寫入前移除特定 label</li>
<li><strong>Recording rule 替代</strong>：把高 cardinality metric 聚合成低 cardinality 的 recording rule，下游只讀 recording rule</li>
<li><strong>移到 traces</strong>：user_id / request_id 這類維度放在 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 的 span attribute 而非 metric label</li>
</ul>
<h2 id="常見故障模式">常見故障模式</h2>
<h3 id="oom-kill">OOM Kill</h3>
<p><strong>觸發條件</strong>：active series 超過記憶體容量、或 heavy query 消耗大量暫時記憶體。</p>
<p><strong>表現</strong>：Prometheus process 被 kernel OOM killer 終止。重啟後 WAL replay 可能需要分鐘到十分鐘（取決於 WAL 大小），期間 scrape 跟 query 都不可用。</p>
<p><strong>預防</strong>：設定 memory limit alert（process_resident_memory_bytes / machine memory &gt; 70%）、tracking cardinality growth slope、query timeout 限制。</p>
<h3 id="scrape-timeout-連鎖">Scrape timeout 連鎖</h3>
<p><strong>觸發條件</strong>：target 的 metrics endpoint 回應慢（&gt; scrape_timeout）、或 target 數量超過 Prometheus 的並行 scrape 能力。</p>
<p><strong>表現</strong>：<code>up</code> metric 為 0、<code>scrape_duration_seconds</code> 升高、dashboard 出現資料斷層（missing data points）。大量 target 同時 timeout 時，Prometheus 的 scrape goroutine pool 被佔滿，影響其他健康 target 的 scrape。</p>
<p><strong>修復</strong>：調整 <code>scrape_timeout</code>（預設 10s，太短會造成 false timeout）、把慢 target 移到獨立的 scrape pool、或把 metrics endpoint 的回應最佳化（減少 expose 的 metric 數量）。</p>
<h3 id="wal-corruption">WAL corruption</h3>
<p><strong>觸發條件</strong>：Prometheus process 非正常終止（OOM kill、機器斷電）時，WAL 可能損壞。</p>
<p><strong>表現</strong>：重啟後 WAL replay 失敗、Prometheus 無法啟動。Error log 顯示 <code>WAL corrupted</code> 或 <code>invalid segment</code>。</p>
<p><strong>修復</strong>：刪除損壞的 WAL segment（丟失對應時間段的資料），重啟 Prometheus。嚴重時刪除整個 data 目錄重新開始（丟失所有歷史資料）。WAL 的持久性保證不如資料庫 — Prometheus 設計上允許短暫資料丟失，長期儲存靠 remote write 到 Mimir / Thanos。</p>
<h3 id="recording-rule-evaluation-lag">Recording rule evaluation lag</h3>
<p><strong>觸發條件</strong>：recording rule 數量多且表達式複雜、evaluation 時間超過 evaluation interval。</p>
<p><strong>表現</strong>：<code>prometheus_rule_group_last_duration_seconds</code> 超過 <code>prometheus_rule_group_interval_seconds</code>。Dashboard 讀 recording rule 的 panel 看到的資料落後當前時間。Alert rule 也在同一個 evaluation pipeline 裡，evaluation lag 會讓 alert 延遲觸發。</p>
<p><strong>修復</strong>：把重的 recording rule 拆到獨立的 rule group（各自 evaluation interval）、最佳化 PromQL expression（減少 aggregation 層數、縮小 time range）、或把 recording rule 卸載到 Mimir（ruler component 獨立擴展）。</p>
<h2 id="何時該從單機-prometheus-遷出">何時該從單機 Prometheus 遷出</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Active series &gt; 500 萬、memory 吃緊（32 GB VM 上 head block ~20 GB + query overhead 接近上限）</td>
          <td>Remote write 到 Mimir / Thanos 做長期儲存</td>
      </tr>
      <tr>
          <td>需要跨 region / cluster 查詢</td>
          <td>Thanos query 或 Mimir multi-tenant</td>
      </tr>
      <tr>
          <td>Recording rule evaluation lag 持續</td>
          <td>把 rule evaluation 卸載到 Mimir ruler</td>
      </tr>
      <tr>
          <td>需要 HA（single Prometheus = SPOF）</td>
          <td>兩個 instance + Thanos dedup</td>
      </tr>
      <tr>
          <td>Retention 要 &gt; 90 天但 disk 不夠</td>
          <td>Remote write + 短 local retention</td>
      </tr>
  </tbody>
</table>
<p>遷出的第一步通常是加 remote write — Prometheus 繼續本地 scrape 跟短期查詢，長期資料寫到遠端。這是最低風險的演進路徑，不需要改 scrape config 或 PromQL。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：overview 跟日常操作</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a>：cardinality 治理的完整策略</li>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a>：recording rule 跟 rollup 的查詢面設計</li>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>：Mimir 作為 Prometheus 的長期儲存後端</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：recording rule 在查詢設計中的定位</li>
</ul>
]]></content:encoded></item><item><title>Sentry Error Grouping 與 Fingerprinting 策略</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/error-grouping-fingerprinting/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/error-grouping-fingerprinting/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry&lt;/a> 的 vendor deep article，深化 overview「Issue grouping / fingerprint」段。初次接觸 Sentry 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Error grouping 決定 Sentry 的使用體驗。Grouping 太粗（不同 bug 被合併成同一個 issue），團隊會漏掉新問題；grouping 太細（同一個 bug 被拆成數百個 issue），issue list 變成 noise。理解 Sentry 的 grouping 演算法跟自訂 fingerprint 機制，才能讓 issue list 反映真實的 bug 數量而非 error event 數量。&lt;/p>
&lt;h2 id="預設-grouping-演算法">預設 Grouping 演算法&lt;/h2>
&lt;h3 id="stack-trace-為主">Stack trace 為主&lt;/h3>
&lt;p>Sentry 的預設 grouping 策略以 exception type + stack trace 為核心。兩個 error event 會被歸到同一個 issue，如果它們的 exception type 相同、且 stack trace 的「相關 frame」相同。&lt;/p>
&lt;p>「相關 frame」是 Sentry 的判定結果 — 它會過濾掉標準函式庫、框架內部 frame 跟已知 noise frame，只留下 application code frame。這個過濾邏輯叫 stack trace rules，由 Sentry 的 grouping 引擎自動決定。&lt;/p>
&lt;h3 id="grouping-版本">Grouping 版本&lt;/h3>
&lt;p>Sentry 的 grouping 演算法有多個版本（稱為 grouping config）。新建的 project 自動用最新版（截至 2024 年是 &lt;code>newstyle:2023-01-11&lt;/code>），舊 project 可能還在用舊版。升級 grouping config 會改變 issue 的歸屬 — 之前合併的 event 可能被拆開，之前分開的可能合併。&lt;/p>
&lt;p>確認目前的 grouping config：Project Settings → General Settings → Event Grouping。升級前先用 Sentry 的 grouping preview 功能測試影響範圍。&lt;/p>
&lt;h3 id="非-exception-事件">非 exception 事件&lt;/h3>
&lt;p>沒有 stack trace 的事件（&lt;code>capture_message&lt;/code>、breadcrumb-only event、CSP violation）用 message 內容做 grouping。相同 message template 的事件歸到同一個 issue。&lt;/p>
&lt;p>message 中如果包含動態值（user ID、request ID、timestamp），Sentry 會嘗試辨識並忽略動態部分。但辨識不完美 — 如果 message 格式不一致，同一種錯誤可能被拆成多個 issue。&lt;/p>
&lt;h2 id="自訂-fingerprint">自訂 Fingerprint&lt;/h2>
&lt;h3 id="何時需要自訂">何時需要自訂&lt;/h3>
&lt;p>預設 grouping 不夠用的常見場景：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>問題&lt;/th>
 &lt;th>Fingerprint 解法&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>外部 API timeout&lt;/td>
 &lt;td>不同 caller 的 stack trace 不同，但根因相同&lt;/td>
 &lt;td>用 &lt;code>{{ default }}&lt;/code> + error type 做 fingerprint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database connection error&lt;/td>
 &lt;td>每個 query 的 stack trace 不同&lt;/td>
 &lt;td>用 error message pattern 做 fingerprint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>前端 minified code&lt;/td>
 &lt;td>source map 缺失導致 frame 不穩定&lt;/td>
 &lt;td>先修 source map 上傳，而非硬 fingerprint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rate limit / 429 error&lt;/td>
 &lt;td>大量 429 拆成數百個 issue&lt;/td>
 &lt;td>用 HTTP status code 做 fingerprint&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="server-side-fingerprint-rules">Server-side fingerprint rules&lt;/h3>
&lt;p>在 Project Settings → Issue Grouping → Fingerprint Rules 設定。語法：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a> 的 vendor deep article，深化 overview「Issue grouping / fingerprint」段。初次接觸 Sentry 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Error grouping 決定 Sentry 的使用體驗。Grouping 太粗（不同 bug 被合併成同一個 issue），團隊會漏掉新問題；grouping 太細（同一個 bug 被拆成數百個 issue），issue list 變成 noise。理解 Sentry 的 grouping 演算法跟自訂 fingerprint 機制，才能讓 issue list 反映真實的 bug 數量而非 error event 數量。</p>
<h2 id="預設-grouping-演算法">預設 Grouping 演算法</h2>
<h3 id="stack-trace-為主">Stack trace 為主</h3>
<p>Sentry 的預設 grouping 策略以 exception type + stack trace 為核心。兩個 error event 會被歸到同一個 issue，如果它們的 exception type 相同、且 stack trace 的「相關 frame」相同。</p>
<p>「相關 frame」是 Sentry 的判定結果 — 它會過濾掉標準函式庫、框架內部 frame 跟已知 noise frame，只留下 application code frame。這個過濾邏輯叫 stack trace rules，由 Sentry 的 grouping 引擎自動決定。</p>
<h3 id="grouping-版本">Grouping 版本</h3>
<p>Sentry 的 grouping 演算法有多個版本（稱為 grouping config）。新建的 project 自動用最新版（截至 2024 年是 <code>newstyle:2023-01-11</code>），舊 project 可能還在用舊版。升級 grouping config 會改變 issue 的歸屬 — 之前合併的 event 可能被拆開，之前分開的可能合併。</p>
<p>確認目前的 grouping config：Project Settings → General Settings → Event Grouping。升級前先用 Sentry 的 grouping preview 功能測試影響範圍。</p>
<h3 id="非-exception-事件">非 exception 事件</h3>
<p>沒有 stack trace 的事件（<code>capture_message</code>、breadcrumb-only event、CSP violation）用 message 內容做 grouping。相同 message template 的事件歸到同一個 issue。</p>
<p>message 中如果包含動態值（user ID、request ID、timestamp），Sentry 會嘗試辨識並忽略動態部分。但辨識不完美 — 如果 message 格式不一致，同一種錯誤可能被拆成多個 issue。</p>
<h2 id="自訂-fingerprint">自訂 Fingerprint</h2>
<h3 id="何時需要自訂">何時需要自訂</h3>
<p>預設 grouping 不夠用的常見場景：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>問題</th>
          <th>Fingerprint 解法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>外部 API timeout</td>
          <td>不同 caller 的 stack trace 不同，但根因相同</td>
          <td>用 <code>{{ default }}</code> + error type 做 fingerprint</td>
      </tr>
      <tr>
          <td>Database connection error</td>
          <td>每個 query 的 stack trace 不同</td>
          <td>用 error message pattern 做 fingerprint</td>
      </tr>
      <tr>
          <td>前端 minified code</td>
          <td>source map 缺失導致 frame 不穩定</td>
          <td>先修 source map 上傳，而非硬 fingerprint</td>
      </tr>
      <tr>
          <td>Rate limit / 429 error</td>
          <td>大量 429 拆成數百個 issue</td>
          <td>用 HTTP status code 做 fingerprint</td>
      </tr>
  </tbody>
</table>
<h3 id="server-side-fingerprint-rules">Server-side fingerprint rules</h3>
<p>在 Project Settings → Issue Grouping → Fingerprint Rules 設定。語法：</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"># 所有 ConnectionError 歸成一個 issue
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">error.type:ConnectionError -&gt; connection-error
</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"># 特定 message pattern 歸成一個 issue
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">message:&#34;Rate limit exceeded*&#34; -&gt; rate-limit
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"># 特定 module 的所有 error 歸成一組
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">module:payment.gateway.* -&gt; payment-gateway-error
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"># 組合條件
</span></span><span class="line"><span class="ln">11</span><span class="cl">error.type:TimeoutError module:external.api.* -&gt; external-api-timeout</span></span></code></pre></div><p>Server-side rules 的優先順序：越後面的 rule 優先順序越高。如果一個 event 匹配多條 rule，用最後一條。</p>
<h3 id="sdk-side-fingerprint">SDK-side fingerprint</h3>
<p>在 SDK 的 <code>before_send</code> callback 中設定 <code>event.fingerprint</code>：</p>





<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="k">def</span> <span class="nf">before_send</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">hint</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">if</span> <span class="s2">&#34;ConnectionError&#34;</span> <span class="ow">in</span> <span class="nb">str</span><span class="p">(</span><span class="n">hint</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;exc_info&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">event</span><span class="p">[</span><span class="s2">&#34;fingerprint&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;connection-error&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="n">event</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">sentry_sdk</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="n">dsn</span><span class="o">=</span><span class="s2">&#34;...&#34;</span><span class="p">,</span> <span class="n">before_send</span><span class="o">=</span><span class="n">before_send</span><span class="p">)</span></span></span></code></pre></div><p>SDK-side 跟 server-side 的差異：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Server-side rules</th>
          <th>SDK-side fingerprint</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>設定位置</td>
          <td>Sentry Web UI</td>
          <td>程式碼</td>
      </tr>
      <tr>
          <td>部署速度</td>
          <td>即時生效</td>
          <td>需要 deploy</td>
      </tr>
      <tr>
          <td>可見性</td>
          <td>團隊都能看到跟修改</td>
          <td>散在程式碼裡</td>
      </tr>
      <tr>
          <td>複雜邏輯</td>
          <td>只支援 pattern matching</td>
          <td>可用任意程式邏輯</td>
      </tr>
  </tbody>
</table>
<p>優先用 server-side rules — 集中管理、即時生效。SDK-side 用在 server-side rules 表達不了的複雜邏輯。</p>
<h3 id="-default--組合"><code>{{ default }}</code> 組合</h3>
<p>Fingerprint 中的 <code>{{ default }}</code> 代表 Sentry 預設的 grouping 結果。跟自訂值組合使用：</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"># 用預設 grouping + environment 維度拆分
</span></span><span class="line"><span class="ln">2</span><span class="cl">fingerprint: [&#34;{{ default }}&#34;, &#34;{{ environment }}&#34;]</span></span></code></pre></div><p>這樣同一個 bug 在 staging 跟 production 會分成兩個 issue，方便分別追蹤。</p>
<h2 id="merge-與-unmerge">Merge 與 Unmerge</h2>
<h3 id="事後修正">事後修正</h3>
<p>當 grouping 不準時，Sentry 提供事後修正：</p>
<p><strong>Merge</strong>：選擇多個 issue，合併成一個。合併後的 issue 保留所有 event，但只保留一個 issue ID。適合預設 grouping 太細（同一 bug 被拆成多個 issue）的情況。</p>
<p><strong>Unmerge</strong>（拆分）：從一個 issue 中選擇部分 event，拆出成新 issue。適合預設 grouping 太粗（不同 bug 被合在同一個 issue）的情況。</p>
<h3 id="mergeunmerge-的限制">Merge/Unmerge 的限制</h3>
<p>Merge 跟 Unmerge 都是「貼 OK 繃」— 只影響現有 event，新進的 event 仍然用原來的 grouping 邏輯。如果根因是 grouping 太粗或太細，應該修 fingerprint rule，而非持續 merge/unmerge。</p>
<p>判讀順序：</p>
<ol>
<li>發現 grouping 不準</li>
<li>先用 merge/unmerge 處理現有 issue（止血）</li>
<li>分析 root cause — 是 stack trace 不穩定、message 有動態值、還是缺 fingerprint rule</li>
<li>加 fingerprint rule 永久修正</li>
<li>驗證新進 event 的 grouping 是否正確</li>
</ol>
<h2 id="grouping-不準的判讀">Grouping 不準的判讀</h2>
<h3 id="太細的訊號">太細的訊號</h3>
<ul>
<li>Issue list 中出現大量「相似標題但不同 ID」的 issue</li>
<li>單一事件只有 1-2 個 occurrence 的 issue 大量出現</li>
<li>同一個使用者操作觸發的 error 被分散到多個 issue</li>
</ul>
<p>常見原因：message 中包含動態值（user ID、timestamp、request path）、source map 缺失（前端）、stack trace 包含 generated code frame。</p>
<h3 id="太粗的訊號">太粗的訊號</h3>
<ul>
<li>一個 issue 的 event 數量持續增長，但 event detail 看起來是不同問題</li>
<li>Issue 的 status 被 resolve 後馬上 regress，但新 event 跟原因不同</li>
<li>團隊 ignore 了一個「雜 issue」但裡面混著真正需要處理的 bug</li>
</ul>
<p>常見原因：exception type 太通用（<code>RuntimeError</code>、<code>Exception</code>）、fingerprint rule 太粗（把整個 module 的 error 合成一個 issue）。</p>
<h2 id="大量-unique-errors-的治理">大量 Unique Errors 的治理</h2>
<h3 id="問題issue-爆量">問題：Issue 爆量</h3>
<p>project 的 issue 數量超過數千時，issue list 失去可操作性。on-call 打開 Sentry 看到 2000 個 unresolved issue，等於沒有 triage。</p>
<h3 id="治理策略">治理策略</h3>
<p><strong>Inbound filter</strong>：在 Project Settings → Inbound Filters 設定，丟棄已知的 noise event（browser extension error、crawler error、legacy browser error）。丟棄在 ingestion 層，不消耗 quota。</p>
<p><strong>Rate limit</strong>：project 或 key 級別的 rate limit。超過限額的 event 被丟棄。適合防止單一 bug 的暴增 event 耗盡 quota，但不解決 issue 數量問題。</p>
<p><strong>Alert rule 搭配 ownership</strong>：用 Sentry alert rule 把特定 tag（service、team、module）的新 issue 通知對應 team。不是所有 issue 都要同一個人看。</p>
<p><strong>定期 triage cadence</strong>：每週或每兩週的 triage session，把 issue 分成 fix / ignore / merge 三類。Sentry 的 <code>For Review</code> tab 自動列出需要初次 triage 的 issue。</p>
<p><strong>Auto-resolve</strong>：設定 auto-resolve policy — 超過 N 天沒有新 event 的 issue 自動 resolve。避免舊 issue 永遠佔據 unresolved list。</p>
<h3 id="治理後的穩態">治理後的穩態</h3>
<p>合理的穩態是：unresolved issue 數量穩定在數十到數百，每週新增 issue 跟 resolve issue 數量大致平衡。如果 unresolved 持續增長，先檢查是否有 noise event 沒被 filter，或 fingerprint 太細。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>Error tracking 跟 observability 的邊界：Sentry 處理 error lifecycle、metrics/logs/traces 處理系統行為，見 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>OTel context 整合：Sentry SDK 接受 OTel trace_id / span_id，讓 error 跟 trace 關聯，見 <a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OpenTelemetry Collector 部署模式</a></li>
<li>Release tracking 跟 session replay：見 <a href="../release-tracking-session-replay/">Release Tracking 與 Session Replay</a></li>
<li>事故響應整合：嚴重 issue → alert → on-call，見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 Incident Response 模組</a></li>
</ul>
]]></content:encoded></item><item><title>OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry&lt;/a> 的 vendor deep article，深化 overview「Collector 部署模式」段。初次接觸 OpenTelemetry 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry 服務頁&lt;/a>，再回到本文。指令於 2026-06-16 用 &lt;code>otel/opentelemetry-collector-contrib:0.154.0&lt;/code> 在 docker 實機驗證。&lt;/p>&lt;/blockquote>
&lt;p>應用程式產生的 telemetry 跟最終存放的 backend 之間需要一個中介層 — OTel Collector 就是這個中介。應用只負責用 OTLP 把資料吐給 collector，collector 負責接收、處理、轉發，兩邊解耦。部署這個 collector 的第一個決策是它擺在哪裡（同 host、集中 gateway、還是 pod sidecar），而非配置細節。位置決定了 buffer 能力、enrichment 時機與失效影響面。&lt;/p>
&lt;h2 id="問題情境telemetry-直送-backend-的三個代價">問題情境：telemetry 直送 backend 的三個代價&lt;/h2>
&lt;p>應用程式直接用 vendor SDK 把 telemetry 送到後端，會在規模變大時撞到三個問題。第一是耦合：每個服務都寫死了某個 backend 的 endpoint 與認證，換 backend 要改所有服務重新部署。第二是缺乏 buffer：backend 短暫不可用時，telemetry 直接丟失，因為應用程式不會為了觀測資料保留重試佇列。第三是 enrichment 分散：每個服務各自加 resource attribute、各自做 sampling，標準難統一。&lt;/p>
&lt;p>Collector 把這三件事收斂到一個中介層。應用只認 collector 的 OTLP endpoint，換 backend 只改 collector 配置；collector 有 queue 與重試；enrichment 與 sampling 在 collector 統一做。但這個中介層擺在哪裡，決定了它各自解掉多少。&lt;/p>
&lt;p>服務數少、backend 單一且穩定時，應用直送 backend 是合理起點 — 上述三個代價在小規模下可控。Collector 是規模化後的升級：當 backend 要換、服務數成長到 enrichment 要統一、或 sampling 需求出現時，再引入 collector 補這一層。&lt;/p>
&lt;h2 id="核心概念三種部署位置的責任分工">核心概念：三種部署位置的責任分工&lt;/h2>
&lt;p>Collector 的部署位置分三種，差別在「離應用多近」與「聚合多少來源」。&lt;/p>
&lt;p>Agent 模式把 collector 跟應用程式放在同一個 host 或同一個 K8s node（DaemonSet）。它的責任是做 local buffer 與 host 層 enrichment：應用透過 localhost 把 telemetry 吐給同機的 collector，延遲極低、不跨網路；collector 補上 host name、container id 這類只有在本機才知道的 resource attribute。agent 的價值是「離應用最近」，應用送出 telemetry 後就不必管後續，buffer 與重試由同機 collector 承擔。&lt;/p>
&lt;p>Agent 解了「離應用近、不丟資料」的問題，但它只看得到本機 — 需要全域視野的處理放不進去。Gateway 模式補這一塊：把 collector 集中部署成一個獨立的服務叢集，跨多個 agent 或多個應用接收 telemetry，負責需要全域視野的處理：tail-based sampling（要看完整 trace 才決定採不採）、跨來源的 routing（不同 telemetry 送不同 backend）、集中的 rate limit 與成本控制。gateway 的價值是「集中決策」，把只有匯流後才做得到的處理放在這一層。&lt;/p>
&lt;p>Sidecar 模式在 K8s 把 collector 當成跟應用 pod 同生命週期的 sidecar container。它的責任跟 agent 相似（local buffer、pod 層 enrichment），差別在隔離粒度是 pod 而非 node：比 DaemonSet agent 更貼近單一 pod（共享 pod 網路、隨 pod 起停），適合需要 pod 級獨立配置或強隔離的場景，代價是每個 pod 都多一份 collector 的資源開銷。&lt;/p>
&lt;p>常見部署是兩層組合：agent（DaemonSet）做 local buffer + host enrichment，再把資料送到 gateway 叢集做 tail sampling 與 routing。agent 解掉「離應用近、不丟資料」，gateway 解掉「需要全域視野的處理」，兩層各司其職。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a> 的 vendor deep article，深化 overview「Collector 部署模式」段。初次接觸 OpenTelemetry 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry 服務頁</a>，再回到本文。指令於 2026-06-16 用 <code>otel/opentelemetry-collector-contrib:0.154.0</code> 在 docker 實機驗證。</p></blockquote>
<p>應用程式產生的 telemetry 跟最終存放的 backend 之間需要一個中介層 — OTel Collector 就是這個中介。應用只負責用 OTLP 把資料吐給 collector，collector 負責接收、處理、轉發，兩邊解耦。部署這個 collector 的第一個決策是它擺在哪裡（同 host、集中 gateway、還是 pod sidecar），而非配置細節。位置決定了 buffer 能力、enrichment 時機與失效影響面。</p>
<h2 id="問題情境telemetry-直送-backend-的三個代價">問題情境：telemetry 直送 backend 的三個代價</h2>
<p>應用程式直接用 vendor SDK 把 telemetry 送到後端，會在規模變大時撞到三個問題。第一是耦合：每個服務都寫死了某個 backend 的 endpoint 與認證，換 backend 要改所有服務重新部署。第二是缺乏 buffer：backend 短暫不可用時，telemetry 直接丟失，因為應用程式不會為了觀測資料保留重試佇列。第三是 enrichment 分散：每個服務各自加 resource attribute、各自做 sampling，標準難統一。</p>
<p>Collector 把這三件事收斂到一個中介層。應用只認 collector 的 OTLP endpoint，換 backend 只改 collector 配置；collector 有 queue 與重試；enrichment 與 sampling 在 collector 統一做。但這個中介層擺在哪裡，決定了它各自解掉多少。</p>
<p>服務數少、backend 單一且穩定時，應用直送 backend 是合理起點 — 上述三個代價在小規模下可控。Collector 是規模化後的升級：當 backend 要換、服務數成長到 enrichment 要統一、或 sampling 需求出現時，再引入 collector 補這一層。</p>
<h2 id="核心概念三種部署位置的責任分工">核心概念：三種部署位置的責任分工</h2>
<p>Collector 的部署位置分三種，差別在「離應用多近」與「聚合多少來源」。</p>
<p>Agent 模式把 collector 跟應用程式放在同一個 host 或同一個 K8s node（DaemonSet）。它的責任是做 local buffer 與 host 層 enrichment：應用透過 localhost 把 telemetry 吐給同機的 collector，延遲極低、不跨網路；collector 補上 host name、container id 這類只有在本機才知道的 resource attribute。agent 的價值是「離應用最近」，應用送出 telemetry 後就不必管後續，buffer 與重試由同機 collector 承擔。</p>
<p>Agent 解了「離應用近、不丟資料」的問題，但它只看得到本機 — 需要全域視野的處理放不進去。Gateway 模式補這一塊：把 collector 集中部署成一個獨立的服務叢集，跨多個 agent 或多個應用接收 telemetry，負責需要全域視野的處理：tail-based sampling（要看完整 trace 才決定採不採）、跨來源的 routing（不同 telemetry 送不同 backend）、集中的 rate limit 與成本控制。gateway 的價值是「集中決策」，把只有匯流後才做得到的處理放在這一層。</p>
<p>Sidecar 模式在 K8s 把 collector 當成跟應用 pod 同生命週期的 sidecar container。它的責任跟 agent 相似（local buffer、pod 層 enrichment），差別在隔離粒度是 pod 而非 node：比 DaemonSet agent 更貼近單一 pod（共享 pod 網路、隨 pod 起停），適合需要 pod 級獨立配置或強隔離的場景，代價是每個 pod 都多一份 collector 的資源開銷。</p>
<p>常見部署是兩層組合：agent（DaemonSet）做 local buffer + host enrichment，再把資料送到 gateway 叢集做 tail sampling 與 routing。agent 解掉「離應用近、不丟資料」，gateway 解掉「需要全域視野的處理」，兩層各司其職。</p>
<h2 id="pipeline-模型receivers--processors--exporters">pipeline 模型：receivers / processors / exporters</h2>
<p>不論擺在哪個位置，collector 的內部都是同一個 pipeline 模型：telemetry 從 receivers 進來、經過 processors 加工、由 exporters 送出。三者用 <code>service.pipelines</code> 依訊號類型（traces / metrics / logs）串接。以下是最小可驗證配置，三個區塊（receivers / processors / exporters）對應 pipeline 的三個階段，各自職責在後面逐段說明。這份配置在 docker 驗證過可正常啟動並端到端流通（<code>validate --config</code> 回傳 0、送 5 條 trace 後 debug exporter 完整輸出 spans）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">receivers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">otlp</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">protocols</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">      </span><span class="nt">grpc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">        </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="p">:</span><span class="m">4317</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">processors</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">memory_limiter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">check_interval</span><span class="p">:</span><span class="w"> </span><span class="l">1s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">limit_mib</span><span class="p">:</span><span class="w"> </span><span class="m">256</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">spike_limit_mib</span><span class="p">:</span><span class="w"> </span><span class="m">64</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">batch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">send_batch_size</span><span class="p">:</span><span class="w"> </span><span class="m">1024</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="nt">exporters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">debug</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">verbosity</span><span class="p">:</span><span class="w"> </span><span class="l">detailed</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">  </span><span class="nt">pipelines</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">traces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">receivers</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">otlp]</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span><span class="nt">processors</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">memory_limiter, batch]</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span><span class="nt">exporters</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">debug]</span></span></span></code></pre></div><p>receivers 定義「資料怎麼進來」，OTLP（gRPC 4317 / HTTP 4318）是標準入口。processors 定義「資料怎麼加工」，順序有意義：<code>memory_limiter</code> 放最前面，先擋住記憶體爆掉；<code>batch</code> 放後面，把零散 span 攢成批次再送，降低下游請求數。此處 256 / 64 MiB 是 demo 用量，production 應依 container memory limit 按比例設定（常見做法是 limit_mib 設為 container memory 的 80%、spike 設為 limit 的 20-25%）。exporters 定義「資料送到哪」，正式環境會是 OTLP 到 backend 或某 vendor exporter，這裡用 <code>debug</code> 驗證流通。service.pipelines 才是真正生效的接線：只有被掛進某個 pipeline 的元件才會運作，定義了卻沒掛進 pipeline 的元件不生效。</p>
<p>processor 順序是常見踩雷點。<code>memory_limiter</code> 要排在第一個，讓它在資料進入後續 processor 前就有機會審查與拒收；<code>batch</code> 排在它之後，因為如果 batch 先跑，telemetry 會先在 batch processor 累積成大批，等觸發記憶體限制時壓力已經更高、拒收效果下降。需要 sampling 時，head sampling 可以放 agent 層的 pipeline，tail sampling 必須放 gateway 層（它要匯流完整 trace），且同一 trace 的所有 span 要路由到同一個 gateway 實例（用 trace-id 維度的 load balancing exporter），否則各 gateway 節點各看片段、tail 決策仍不完整。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<p>Collector 失效的影響面取決於部署模式，這是選位置時要先想清楚的。agent 模式下，單一 node 的 collector 掛掉只影響該 node 的應用，且應用送往 localhost 失敗可以 fail-fast；gateway 模式下，gateway 叢集掛掉會影響所有上游 agent，因此 gateway 必須多副本 + 負載均衡，不能單點；sidecar 模式下，失效影響面比 agent 更窄（只影響同 pod 的應用），但每個 pod 各自是獨立失效點，pod 數多時同時出狀況的機率也高。演練時要分別注入「單 agent 掛」與「gateway 叢集不可用」，確認前者影響被局限、後者有 agent 層 buffer 兜著。</p>
<p>記憶體壓力是 collector 最常見的故障。telemetry 流入速度超過 exporter 送出速度時，資料在 collector 內累積、記憶體上升，沒有保護會 OOM 被 kill、整段 telemetry 全丟。<code>memory_limiter</code> processor 是這道防線，它定期（<code>check_interval</code>）檢查記憶體並用兩個閾值分級反應：記憶體超過軟上限（<code>limit_mib</code> 減去 <code>spike_limit_mib</code>）時強制觸發 GC 並開始拒收，給回收一個緩衝區間；超過硬上限（<code>limit_mib</code>）時全面拒收新資料。只設 <code>limit_mib</code>、不設 <code>spike_limit_mib</code> 是不完整的配置，等於沒有軟性緩衝、直接撞硬牆。演練時用高於 exporter 吞吐的速率灌資料，確認 memory_limiter 在軟上限就介入、collector 存活，而不是 OOM。</p>
<p>Backpressure 的傳遞要驗證到底。當 backend 變慢、exporter queue 滿，collector 的 OTLP receiver 會回壓給上游（gRPC 層用 resource-exhausted 拒收）。在 agent 模式這個回壓會傳到應用的 OTLP exporter，應用 SDK 的 queue 也會滿——此時 SDK 的反應取決於 exporter 配置，要確認 queue-full 策略設為 drop 而非 block，讓 telemetry 被丟棄而非阻塞業務執行緒（各語言 SDK 預設不同，不能假設一定是 drop）。演練要確認「backend 慢 → collector 回壓 → 應用丟 telemetry 但業務不受影響」這條鏈成立，避免觀測系統的壓力反噬主流程。</p>
<table>
  <thead>
      <tr>
          <th>觀察訊號</th>
          <th>判讀</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>collector 容器頻繁 OOM restart</td>
          <td>memory_limiter 閾值過高或未啟用</td>
          <td>調低 limit_mib、確認 spike_limit_mib 有設</td>
      </tr>
      <tr>
          <td>exporter queue depth 持續飽和</td>
          <td>下游 backend 回應慢或不可用</td>
          <td>查 backend 狀態、確認 exporter retry 與 timeout 設定</td>
      </tr>
      <tr>
          <td>receiver refused spans 計數上升</td>
          <td>memory_limiter 啟動拒收、collector 處於壓力狀態</td>
          <td>查上游流量是否異常、考慮擴容 gateway 或調降 sampling</td>
      </tr>
      <tr>
          <td>gateway 全部不可用、agent buffer 開始丟棄</td>
          <td>全域 telemetry 中斷</td>
          <td>確認 gateway 多副本與負載均衡、agent 的 queue 與 drop 策略</td>
      </tr>
      <tr>
          <td>telemetry 到 backend 有延遲但不丟失</td>
          <td>batch processor 正常攢批</td>
          <td>正常行為、確認 batch timeout 符合預期</td>
      </tr>
  </tbody>
</table>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>agent 與 gateway 的成本曲線不同，選型要對著規模看。agent（DaemonSet）的成本是「每個 node 一份 collector」的固定開銷：node 多時總開銷隨 node 數線性成長，但每份 collector 只處理本機流量、單份負載可控。gateway 的成本是「集中叢集」：份數少但每份要扛匯流後的總流量，要按總 telemetry 吞吐量做容量規劃與水平擴展。</p>
<p>兩層架構的成本判讀是：agent 層用最小配置（夠做 buffer + enrichment 即可，<code>limit_mib</code> 設小），把重處理（tail sampling、大量 routing）集中到 gateway，讓 gateway 的擴展跟總流量綁定、agent 的開銷跟 node 數綁定。把 tail sampling 誤放在 agent 層是常見的成本錯誤——agent 看不到完整 trace、做不了正確的 tail sampling，還白白吃掉每個 node 的記憶體。</p>
<p>gateway 層的 processor 是攔截高 cardinality attribute 的有效位置：在 telemetry 流入 backend 前用 <code>attributes</code> / <code>transform</code> processor 把高 cardinality label（user id、request id 當 metric label）移除或降維，比讓它流到 backend 後才治理便宜。高 cardinality 的 attribute 會在下游 backend 炸開成本，是另一條要在 collector 攔截的成本線。這條跟 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理與成本邊界</a> 對齊。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Collector 部署模式是 OTel 落地的第一個決策，它的下游是 sampling 策略與 backend 選型。決定了 agent + gateway 兩層後，tail sampling 的設計接到 gateway 層的 pipeline；exporter 指向哪個 backend 則回到 <a href="/blog/backend/04-observability/vendors/opentelemetry/#%e4%bd%95%e6%99%82%e6%94%b9%e8%b5%b0%e5%85%b6%e4%bb%96%e6%9c%8d%e5%8b%99" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">何時改走其他服務</a> 的 vendor portability 判讀。</p>
<p>pipeline 的訊號治理與資料品質回到 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline 架構</a> 與 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>；cardinality 攔截回到 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理與成本邊界</a>。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry 服務頁</a></li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline 架構</a></li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理與成本邊界</a></li>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing 與 context link</a></li>
</ul>
]]></content:encoded></item><item><title>4.C10 對照：規模差異下的觀測遷移</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/</guid><description>&lt;p>這篇對照的核心責任是提醒觀測遷移是治理能力轉換，工具替換只是表面動作。&lt;/p>
&lt;h2 id="小型團隊常見判讀">小型團隊常見判讀&lt;/h2>
&lt;p>小型團隊最怕雙軌過久。若同時維護兩套儀表，通常會先耗盡人力。小團隊更需要短期對照、快速收斂，而不是一次拉滿所有治理流程。&lt;/p>
&lt;h2 id="中型團隊常見判讀">中型團隊常見判讀&lt;/h2>
&lt;p>中型團隊會碰到 schema 漂移與標籤膨脹。這個階段的失敗常見於「看得到數據，但看不懂是否同一語意」，導致告警與容量判讀彼此矛盾。&lt;/p>
&lt;h2 id="大型團隊常見判讀">大型團隊常見判讀&lt;/h2>
&lt;p>大型團隊的觀測遷移會牽涉成本分攤、採樣策略、collector 拓撲。若只追求功能對齊，往往在遷移後才出現成本暴增與告警漂移。&lt;/p>
&lt;h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>新舊管線 &lt;code>error rate&lt;/code> 或 &lt;code>burn rate&lt;/code> 偏差長期超標&lt;/li>
&lt;li>missing signal 比例持續上升&lt;/li>
&lt;li>同一事件在兩套儀表板得到相反結論&lt;/li>
&lt;/ul>
&lt;p>觸發條件時應停止切換，先修資料語意與採樣策略，再決定是否繼續遷移。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;p>判讀重點是「兩套觀測是否仍在描述同一個系統狀態」。當 error rate、burn rate、trace coverage 三者任一長期偏離，就代表遷移證據不可信，應先停切換再修資料品質。&lt;/p>
&lt;h2 id="邊界判讀">邊界判讀&lt;/h2>
&lt;p>這篇對照只處理觀測遷移的判讀邊界，不處理各 vendor 的實作細節。主要風險是把資料語意不一致當成短暫噪音，導致團隊在錯誤證據上推進切換。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality&lt;/a> 修正語意與採樣，再到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline&lt;/a> 校正雙軌管線。若已影響事故判讀，交接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp;amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18 Incident Intake&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這篇對照的核心責任是提醒觀測遷移是治理能力轉換，工具替換只是表面動作。</p>
<h2 id="小型團隊常見判讀">小型團隊常見判讀</h2>
<p>小型團隊最怕雙軌過久。若同時維護兩套儀表，通常會先耗盡人力。小團隊更需要短期對照、快速收斂，而不是一次拉滿所有治理流程。</p>
<h2 id="中型團隊常見判讀">中型團隊常見判讀</h2>
<p>中型團隊會碰到 schema 漂移與標籤膨脹。這個階段的失敗常見於「看得到數據，但看不懂是否同一語意」，導致告警與容量判讀彼此矛盾。</p>
<h2 id="大型團隊常見判讀">大型團隊常見判讀</h2>
<p>大型團隊的觀測遷移會牽涉成本分攤、採樣策略、collector 拓撲。若只追求功能對齊，往往在遷移後才出現成本暴增與告警漂移。</p>
<h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件</h2>
<ul>
<li>新舊管線 <code>error rate</code> 或 <code>burn rate</code> 偏差長期超標</li>
<li>missing signal 比例持續上升</li>
<li>同一事件在兩套儀表板得到相反結論</li>
</ul>
<p>觸發條件時應停止切換，先修資料語意與採樣策略，再決定是否繼續遷移。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>判讀重點是「兩套觀測是否仍在描述同一個系統狀態」。當 error rate、burn rate、trace coverage 三者任一長期偏離，就代表遷移證據不可信，應先停切換再修資料品質。</p>
<h2 id="邊界判讀">邊界判讀</h2>
<p>這篇對照只處理觀測遷移的判讀邊界，不處理各 vendor 的實作細節。主要風險是把資料語意不一致當成短暫噪音，導致團隊在錯誤證據上推進切換。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>先回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a> 修正語意與採樣，再到 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a> 校正雙軌管線。若已影響事故判讀，交接到 <a href="/blog/backend/08-incident-response/incident-intake-evidence-triage/" data-link-title="8.18 Incident Intake &amp; Evidence Triage" data-link-desc="把告警、客訴、支援回報與第三方狀態轉成同一個 intake / evidence 判讀流程">8.18 Incident Intake</a>。</p>
]]></content:encoded></item><item><title>Datadog OTLP Ingestion 與 OTel 整合</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/otlp-ingestion-otel-integration/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/otlp-ingestion-otel-integration/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog&lt;/a> 的 vendor deep article，深化 overview「OTLP ingestion」段。初次接觸 Datadog 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>兩種觸發情境會讓團隊需要 Datadog 的 OTLP ingestion：&lt;/p>
&lt;p>團隊已經使用 Datadog APM，但新服務或新語言想用 OTel SDK 避免 vendor lock-in。Datadog SDK 覆蓋的語言有限（Go / Java / Python / Ruby / Node / .NET / PHP / C++），如果服務用 Rust / Elixir / Kotlin multiplatform，OTel SDK 的覆蓋更廣。&lt;/p>
&lt;p>另一種情境是團隊原本用 OTel + Jaeger 或 OTel + Grafana，現在想把 visualization 遷到 Datadog 但不想重新 instrument。OTLP ingestion 讓 OTel SDK 產出的 traces / metrics / logs 直接送進 Datadog，不改 application code。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="datadog-agent-的-otlp-receiver">Datadog Agent 的 OTLP receiver&lt;/h3>
&lt;p>Datadog Agent 6.32+ 內建 OTLP receiver，接受 gRPC（port 4317）和 HTTP（port 4318）兩種 protocol。Agent 收到 OTLP 資料後轉換成 Datadog 內部格式，走跟 Datadog SDK 相同的 pipeline（sampling、tagging、forwarding to Datadog backend）。&lt;/p>
&lt;p>這代表 OTLP path 的資料在 Datadog UI 裡跟 Datadog SDK path 的資料一樣被處理 — 相同的 APM trace waterfall、相同的 service map、相同的 error tracking。差異在 metadata 完整度（見下方 feature parity）。&lt;/p>
&lt;h3 id="三種-signal-的-otlp-支援度">三種 signal 的 OTLP 支援度&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&lt;/th>
 &lt;th>OTLP 支援&lt;/th>
 &lt;th>到 Datadog 的對應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Traces&lt;/td>
 &lt;td>完整（OTLP gRPC / HTTP）&lt;/td>
 &lt;td>APM traces、service map、error tracking&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Metrics&lt;/td>
 &lt;td>完整（OTLP gRPC / HTTP）&lt;/td>
 &lt;td>Custom metrics（按 metric 計費）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Logs&lt;/td>
 &lt;td>有限（Agent 7.54+ 支援 OTLP logs）&lt;/td>
 &lt;td>Datadog Logs（按 ingestion volume 計費）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Traces 的 OTLP 支援最成熟、metrics 次之、logs 最新。混合環境常見做法是 traces + metrics 走 OTLP、logs 走 Datadog Agent 的原生 log collection（file tailing / container stdout）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> 的 vendor deep article，深化 overview「OTLP ingestion」段。初次接觸 Datadog 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>兩種觸發情境會讓團隊需要 Datadog 的 OTLP ingestion：</p>
<p>團隊已經使用 Datadog APM，但新服務或新語言想用 OTel SDK 避免 vendor lock-in。Datadog SDK 覆蓋的語言有限（Go / Java / Python / Ruby / Node / .NET / PHP / C++），如果服務用 Rust / Elixir / Kotlin multiplatform，OTel SDK 的覆蓋更廣。</p>
<p>另一種情境是團隊原本用 OTel + Jaeger 或 OTel + Grafana，現在想把 visualization 遷到 Datadog 但不想重新 instrument。OTLP ingestion 讓 OTel SDK 產出的 traces / metrics / logs 直接送進 Datadog，不改 application code。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="datadog-agent-的-otlp-receiver">Datadog Agent 的 OTLP receiver</h3>
<p>Datadog Agent 6.32+ 內建 OTLP receiver，接受 gRPC（port 4317）和 HTTP（port 4318）兩種 protocol。Agent 收到 OTLP 資料後轉換成 Datadog 內部格式，走跟 Datadog SDK 相同的 pipeline（sampling、tagging、forwarding to Datadog backend）。</p>
<p>這代表 OTLP path 的資料在 Datadog UI 裡跟 Datadog SDK path 的資料一樣被處理 — 相同的 APM trace waterfall、相同的 service map、相同的 error tracking。差異在 metadata 完整度（見下方 feature parity）。</p>
<h3 id="三種-signal-的-otlp-支援度">三種 signal 的 OTLP 支援度</h3>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>OTLP 支援</th>
          <th>到 Datadog 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Traces</td>
          <td>完整（OTLP gRPC / HTTP）</td>
          <td>APM traces、service map、error tracking</td>
      </tr>
      <tr>
          <td>Metrics</td>
          <td>完整（OTLP gRPC / HTTP）</td>
          <td>Custom metrics（按 metric 計費）</td>
      </tr>
      <tr>
          <td>Logs</td>
          <td>有限（Agent 7.54+ 支援 OTLP logs）</td>
          <td>Datadog Logs（按 ingestion volume 計費）</td>
      </tr>
  </tbody>
</table>
<p>Traces 的 OTLP 支援最成熟、metrics 次之、logs 最新。混合環境常見做法是 traces + metrics 走 OTLP、logs 走 Datadog Agent 的原生 log collection（file tailing / container stdout）。</p>
<h3 id="datadog-sdk-vs-otel-sdk-feature-parity">Datadog SDK vs OTel SDK feature parity</h3>
<table>
  <thead>
      <tr>
          <th>功能</th>
          <th>Datadog SDK</th>
          <th>OTel SDK → Datadog</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Distributed tracing</td>
          <td>有</td>
          <td>有（完整）</td>
      </tr>
      <tr>
          <td>Continuous profiling</td>
          <td>有</td>
          <td>無（Datadog 專有）</td>
      </tr>
      <tr>
          <td>ASM（Application Security）</td>
          <td>有</td>
          <td>無（需要 Datadog library）</td>
      </tr>
      <tr>
          <td>CI Visibility</td>
          <td>有</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Dynamic instrumentation</td>
          <td>有</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Runtime metrics（GC、thread）</td>
          <td>自動</td>
          <td>需手動配置 OTel metric instrumentation</td>
      </tr>
      <tr>
          <td>Log correlation（trace_id 注入 log）</td>
          <td>自動</td>
          <td>需手動配置（MDC / context propagation）</td>
      </tr>
      <tr>
          <td>Unified service tagging</td>
          <td>自動（<code>DD_SERVICE</code> / <code>DD_ENV</code> / <code>DD_VERSION</code>）</td>
          <td>需 resource attribute mapping</td>
      </tr>
  </tbody>
</table>
<p>判讀：如果團隊需要 profiling / ASM / CI Visibility，對應服務仍需 Datadog SDK。其他服務可以用 OTel SDK + OTLP ingestion，兩者在同一個 Datadog org 共存。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="datadog-agent-otlp-設定">Datadog Agent OTLP 設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># datadog.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="nt">otlp_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="nt">receiver</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">protocols</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span><span class="nt">grpc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">        </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="p">:</span><span class="m">4317</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">        </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="m">0.0.0.0</span><span class="p">:</span><span class="m">4318</span></span></span></code></pre></div><p>Agent 重啟後用 <code>datadog-agent status</code> 確認 OTLP receiver 啟動。</p>
<h3 id="otel-sdk-endpoint-配置">OTel SDK endpoint 配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 環境變數（語言無關）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">export</span> <span class="nv">OTEL_EXPORTER_OTLP_ENDPOINT</span><span class="o">=</span><span class="s2">&#34;http://datadog-agent:4317&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">export</span> <span class="nv">OTEL_EXPORTER_OTLP_PROTOCOL</span><span class="o">=</span><span class="s2">&#34;grpc&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">export</span> <span class="nv">OTEL_SERVICE_NAME</span><span class="o">=</span><span class="s2">&#34;checkout-api&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nb">export</span> <span class="nv">OTEL_RESOURCE_ATTRIBUTES</span><span class="o">=</span><span class="s2">&#34;deployment.environment=production,service.version=1.2.3&#34;</span></span></span></code></pre></div><h3 id="resource-attribute--datadog-tag-mapping">Resource attribute → Datadog tag mapping</h3>
<p>Datadog Agent 自動把 OTel resource attributes 轉成 Datadog tags：</p>
<table>
  <thead>
      <tr>
          <th>OTel resource attribute</th>
          <th>Datadog tag</th>
          <th>備註</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>service.name</code></td>
          <td><code>service</code></td>
          <td>Datadog unified service tagging 的核心</td>
      </tr>
      <tr>
          <td><code>deployment.environment</code></td>
          <td><code>env</code></td>
          <td>必填、否則 Datadog UI 的環境篩選失效</td>
      </tr>
      <tr>
          <td><code>service.version</code></td>
          <td><code>version</code></td>
          <td>用於 deployment tracking</td>
      </tr>
      <tr>
          <td><code>host.name</code></td>
          <td><code>host</code></td>
          <td>Agent 通常自動帶、不需手動設</td>
      </tr>
      <tr>
          <td><code>container.name</code></td>
          <td><code>container_name</code></td>
          <td>K8s 環境自動帶</td>
      </tr>
  </tbody>
</table>
<p>如果 resource attribute 沒設 <code>deployment.environment</code>，Datadog 會把 trace 歸到 <code>env:none</code> — 在 APM 介面幾乎不可見。這是最常見的 OTLP onboarding 問題。</p>
<h3 id="otel-collector--datadogalternative-path">OTel Collector → Datadog（alternative path）</h3>
<p>如果不想讓 application 直連 Datadog Agent，可以在中間放 OTel Collector：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># otel-collector-config.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">exporters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">datadog</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">api</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">${DD_API_KEY}</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">site</span><span class="p">:</span><span class="w"> </span><span class="l">datadoghq.com</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">pipelines</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">traces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span><span class="nt">receivers</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">otlp]</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">processors</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">batch]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">exporters</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">datadog]</span></span></span></code></pre></div><p>OTel Collector 的 <code>datadog</code> exporter 直接把資料送到 Datadog backend（不經 Agent）。適合已有 OTel Collector 基礎設施、不想每個 node 都部署 Datadog Agent 的場景。</p>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="resource-attribute-mapping-不對齊">Resource attribute mapping 不對齊</h3>
<p>OTel 的 <code>service.name</code> 用 dot notation（如 <code>com.example.checkout</code>），Datadog 預設用 hyphen（如 <code>checkout-api</code>）。如果 mapping 不一致，同一個服務在 Datadog APM 的 service map 會出現多個節點（OTel path 一個、Datadog SDK path 一個）。</p>
<p>修法：統一 <code>service.name</code> 命名。如果兩種 SDK 並存，在 OTel SDK 的 resource attribute 設跟 Datadog SDK 的 <code>DD_SERVICE</code> 完全相同的值。</p>
<h3 id="metric-naming-convention-差異">Metric naming convention 差異</h3>
<p>OTel metric 用 dot notation（<code>http.server.request.duration</code>），Datadog 預設用 underscore（<code>http_server_request_duration</code>）。Agent 會自動轉換（dot → underscore），但如果團隊同時有 Datadog SDK 產出的 metric 跟 OTel SDK 產出的 metric，兩者可能在 Datadog 裡產生重複（語意相同但名稱不同）。</p>
<p>修法：用 OTel Collector 的 <code>metricstransform</code> processor 在 export 前統一命名，或在 Datadog 用 metric alias 合併。</p>
<h3 id="log-correlation-在-otlp-path-的限制">Log correlation 在 OTLP path 的限制</h3>
<p>Datadog SDK 自動把 <code>dd.trace_id</code> 和 <code>dd.span_id</code> 注入 application log（如 Python logging、Java MDC）。OTel SDK 不做這件事 — log correlation 需要手動設定（把 <code>trace_id</code> 從 OTel context 注入 logging framework）。</p>
<p>如果 log correlation 缺失，Datadog 的 trace → log 跳轉功能失效。修法依語言不同：Java 用 MDC + OTel Java agent 的 log context instrumentation；Python 用 <code>opentelemetry-instrumentation-logging</code>；Go 需要手動從 span context 取 trace ID 寫到 log field。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>OTLP path 的計費跟 Datadog SDK path 相同：</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>計費單位</th>
          <th>OTLP vs Datadog SDK</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>APM traces</td>
          <td>Per ingested span</td>
          <td>相同</td>
      </tr>
      <tr>
          <td>Metrics</td>
          <td>Per custom metric（unique metric name × tag combination）</td>
          <td>相同</td>
      </tr>
      <tr>
          <td>Logs</td>
          <td>Per ingested GB</td>
          <td>相同</td>
      </tr>
  </tbody>
</table>
<p>成本差異不在 ingestion pricing，在 <strong>feature access</strong>。用 OTel SDK 失去 Profiling / ASM / CI Visibility，這些功能需要 Datadog SDK。如果團隊需要這些功能，走 OTLP 反而要為核心服務額外部署 Datadog SDK — 雙 SDK 的 maintenance cost 可能超過直接全用 Datadog SDK。</p>
<p>判斷分水嶺：如果 &gt; 80% 的服務不需要 Profiling / ASM，走 OTLP + 少數服務用 Datadog SDK 是合理的混合模式。如果核心服務都需要 Profiling，全用 Datadog SDK 更簡單。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁</a>：overview 與日常操作</li>
<li><a href="../cost-governance-agent-config/">Datadog 成本治理</a>：Agent 配置與 cost control</li>
<li><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a>：從 Datadog SDK 轉向 OTel 相容模式的治理案例</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OpenTelemetry Collector 部署模式</a>：OTel Collector → Datadog 的 alternative path</li>
<li><a href="../migrate-from-new-relic/">← New Relic migration</a>：New Relic → Datadog 的遷移中 OTLP 扮演的橋接角色</li>
</ul>
]]></content:encoded></item><item><title>Grafana Loki 設計與操作限制</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/loki-design-operational-limits/</link><pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/loki-design-operational-limits/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a> 的 vendor deep article，深化 overview「Loki 設計與限制」段。初次接觸 Grafana Stack 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>團隊從 ELK stack 或 CloudWatch Logs 遷到 Grafana Stack 時，Loki 是 log backend 的預設選擇。遷移後最常遇到的衝擊是查詢模式的根本差異：Elasticsearch 做 full-text index（寫入時索引每個欄位、查詢時任意搜尋），Loki 只 index labels（寫入時只索引 stream labels、查詢時先篩 stream 再 grep content）。&lt;/p>
&lt;p>這個差異是刻意的設計選擇 — Loki 的目標是「Prometheus for logs」：用跟 Prometheus metrics 相同的 label 體系管理 logs，讓 log 查詢跟 metric 查詢使用同一組 label selector。代價是失去 full-text search 的即時性。理解這個設計哲學才能正確設計 label、寫出有效率的 LogQL、避免常見的效能陷阱。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="like-prometheus-but-for-logs">Like Prometheus, but for logs&lt;/h3>
&lt;p>Prometheus 用 label set 識別 time series — &lt;code>{job=&amp;quot;checkout&amp;quot;, instance=&amp;quot;10.0.1.5&amp;quot;}&lt;/code> 是一條 series。Loki 用相同概念識別 log stream — &lt;code>{job=&amp;quot;checkout&amp;quot;, namespace=&amp;quot;production&amp;quot;}&lt;/code> 是一條 stream。同一條 stream 的所有 log entries 存在同一組 chunks。&lt;/p>
&lt;p>Elasticsearch 的索引模式是「寫入時建 inverted index、查詢時走索引」。Loki 的索引模式是「寫入時只記錄 stream label → chunk 的 mapping、查詢時先用 label 選 stream、再在 chunk 內做 grep」。&lt;/p>
&lt;p>這代表：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>有 label filter 的查詢很快&lt;/strong> — Loki 只掃對應 stream 的 chunks&lt;/li>
&lt;li>&lt;strong>沒有 label filter 的查詢很慢&lt;/strong> — Loki 要掃所有 stream 的 chunks（相當於 full scan）&lt;/li>
&lt;li>&lt;strong>Label cardinality 跟 Prometheus 一樣敏感&lt;/strong> — 高 cardinality label 產生大量 stream、每個 stream 的 chunk 很小、index 膨脹&lt;/li>
&lt;/ul>
&lt;h3 id="stream-與-chunk">Stream 與 chunk&lt;/h3>
&lt;p>一條 stream = 一組唯一的 label set。每條 stream 的 log entries 依時間排序存在 chunks 裡。Chunk 是 Loki 的最小儲存單位。&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">Stream: {job=&amp;#34;checkout&amp;#34;, namespace=&amp;#34;production&amp;#34;}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> └─ Chunk 1: [2026-06-22T00:00 ~ 2026-06-22T01:00] (compressed)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └─ Chunk 2: [2026-06-22T01:00 ~ 2026-06-22T02:00] (compressed)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> └─ ...&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Chunk 存在 object storage（S3 / GCS / MinIO），index 存在 key-value store（BoltDB / TSDB，3.0 起預設 TSDB）。Object storage 便宜（相比 Elasticsearch 的 SSD），這是 Loki 成本優勢的來源。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> 的 vendor deep article，深化 overview「Loki 設計與限制」段。初次接觸 Grafana Stack 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>團隊從 ELK stack 或 CloudWatch Logs 遷到 Grafana Stack 時，Loki 是 log backend 的預設選擇。遷移後最常遇到的衝擊是查詢模式的根本差異：Elasticsearch 做 full-text index（寫入時索引每個欄位、查詢時任意搜尋），Loki 只 index labels（寫入時只索引 stream labels、查詢時先篩 stream 再 grep content）。</p>
<p>這個差異是刻意的設計選擇 — Loki 的目標是「Prometheus for logs」：用跟 Prometheus metrics 相同的 label 體系管理 logs，讓 log 查詢跟 metric 查詢使用同一組 label selector。代價是失去 full-text search 的即時性。理解這個設計哲學才能正確設計 label、寫出有效率的 LogQL、避免常見的效能陷阱。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="like-prometheus-but-for-logs">Like Prometheus, but for logs</h3>
<p>Prometheus 用 label set 識別 time series — <code>{job=&quot;checkout&quot;, instance=&quot;10.0.1.5&quot;}</code> 是一條 series。Loki 用相同概念識別 log stream — <code>{job=&quot;checkout&quot;, namespace=&quot;production&quot;}</code> 是一條 stream。同一條 stream 的所有 log entries 存在同一組 chunks。</p>
<p>Elasticsearch 的索引模式是「寫入時建 inverted index、查詢時走索引」。Loki 的索引模式是「寫入時只記錄 stream label → chunk 的 mapping、查詢時先用 label 選 stream、再在 chunk 內做 grep」。</p>
<p>這代表：</p>
<ul>
<li><strong>有 label filter 的查詢很快</strong> — Loki 只掃對應 stream 的 chunks</li>
<li><strong>沒有 label filter 的查詢很慢</strong> — Loki 要掃所有 stream 的 chunks（相當於 full scan）</li>
<li><strong>Label cardinality 跟 Prometheus 一樣敏感</strong> — 高 cardinality label 產生大量 stream、每個 stream 的 chunk 很小、index 膨脹</li>
</ul>
<h3 id="stream-與-chunk">Stream 與 chunk</h3>
<p>一條 stream = 一組唯一的 label set。每條 stream 的 log entries 依時間排序存在 chunks 裡。Chunk 是 Loki 的最小儲存單位。</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">Stream: {job=&#34;checkout&#34;, namespace=&#34;production&#34;}
</span></span><span class="line"><span class="ln">2</span><span class="cl">  └─ Chunk 1: [2026-06-22T00:00 ~ 2026-06-22T01:00] (compressed)
</span></span><span class="line"><span class="ln">3</span><span class="cl">  └─ Chunk 2: [2026-06-22T01:00 ~ 2026-06-22T02:00] (compressed)
</span></span><span class="line"><span class="ln">4</span><span class="cl">  └─ ...</span></span></code></pre></div><p>Chunk 存在 object storage（S3 / GCS / MinIO），index 存在 key-value store（BoltDB / TSDB，3.0 起預設 TSDB）。Object storage 便宜（相比 Elasticsearch 的 SSD），這是 Loki 成本優勢的來源。</p>
<h3 id="跟-elasticsearch-的根本差異">跟 Elasticsearch 的根本差異</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Loki</th>
          <th>Elasticsearch</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引對象</td>
          <td>只索引 labels（stream metadata）</td>
          <td>索引所有欄位（full-text + structured）</td>
      </tr>
      <tr>
          <td>查詢模式</td>
          <td>Label selector → stream → grep content</td>
          <td>Query DSL / KQL → inverted index lookup</td>
      </tr>
      <tr>
          <td>寫入成本</td>
          <td>低（不建 content index）</td>
          <td>高（建 inverted index + doc values）</td>
      </tr>
      <tr>
          <td>查詢成本</td>
          <td>取決於 stream 篩選效率（label 越精準越快）</td>
          <td>取決於 index 覆蓋度（indexed field 查詢快）</td>
      </tr>
      <tr>
          <td>儲存成本</td>
          <td>低（object storage）</td>
          <td>高（SSD / local disk）</td>
      </tr>
      <tr>
          <td>Full-text search</td>
          <td>不支援（只有 line filter grep）</td>
          <td>原生支援</td>
      </tr>
      <tr>
          <td>適用場景</td>
          <td>已有 Prometheus/Grafana 生態的 log aggregation</td>
          <td>需要 full-text search 的 log analytics / SIEM</td>
      </tr>
  </tbody>
</table>
<p>判讀：如果團隊的 log 查詢模式是「先選 service/namespace/pod、再看時間範圍內的 log entries」，Loki 足夠。如果查詢模式是「在所有 log 裡搜某個 error message 或 request ID」，Elasticsearch 的 full-text index 更適合。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="label-設計原則">Label 設計原則</h3>
<p>Label 設計是 Loki 最重要的操作決策。原則跟 Prometheus 相同：低 cardinality、穩定、有查詢意義。</p>
<table>
  <thead>
      <tr>
          <th>Label</th>
          <th>Cardinality</th>
          <th>適合當 label</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>job</code></td>
          <td>低（服務數量）</td>
          <td>適合</td>
          <td>篩選到特定服務</td>
      </tr>
      <tr>
          <td><code>namespace</code></td>
          <td>低</td>
          <td>適合</td>
          <td>篩選到特定環境</td>
      </tr>
      <tr>
          <td><code>pod_name</code></td>
          <td>中（pod 數量）</td>
          <td>視情境</td>
          <td>K8s 環境常用但 pod 頻繁重建會產生大量短命 stream</td>
      </tr>
      <tr>
          <td><code>level</code>（info/warn/error）</td>
          <td>低（3-5 值）</td>
          <td>適合</td>
          <td>快速篩選 error log</td>
      </tr>
      <tr>
          <td><code>request_id</code></td>
          <td>極高（per-request）</td>
          <td>不適合</td>
          <td>每個 request 一條 stream、chunk 極小、index 爆炸</td>
      </tr>
      <tr>
          <td><code>user_id</code></td>
          <td>高</td>
          <td>不適合</td>
          <td>同上</td>
      </tr>
      <tr>
          <td><code>trace_id</code></td>
          <td>極高</td>
          <td>不適合</td>
          <td>用 Tempo 查 trace、不用 Loki label</td>
      </tr>
  </tbody>
</table>
<p>request_id / user_id / trace_id 不應該是 label，它們應該在 log content 裡用 structured JSON 欄位表達，查詢時用 LogQL 的 line filter 或 parser 提取。</p>
<h3 id="logql-常見查詢模式">LogQL 常見查詢模式</h3>
<p><strong>Stream selector + line filter</strong>（最基本）：</p>





<pre tabindex="0"><code class="language-logql" data-lang="logql">{job=&#34;checkout&#34;, namespace=&#34;production&#34;} |= &#34;error&#34; |= &#34;timeout&#34;</code></pre><p>先選 stream、再 grep 包含 &ldquo;error&rdquo; 和 &ldquo;timeout&rdquo; 的 log lines。<code>|=</code> 是包含、<code>!=</code> 是不包含、<code>|~</code> 是 regex。</p>
<p><strong>Structured metadata parser</strong>（JSON log）：</p>





<pre tabindex="0"><code class="language-logql" data-lang="logql">{job=&#34;checkout&#34;} | json | status_code &gt;= 500 | line_format &#34;{{.method}} {{.path}} {{.status_code}}&#34;</code></pre><p><code>| json</code> 解析 JSON log entry 的欄位，後續可以用欄位做 filter 和格式化。</p>
<p><strong>Metric 聚合</strong>（log → metric）：</p>





<pre tabindex="0"><code class="language-logql" data-lang="logql">sum by (status_code) (rate({job=&#34;checkout&#34;} | json | __error__=&#34;&#34; [5m]))</code></pre><p>計算每 5 分鐘每個 status_code 的 log entry 速率。這是 Loki 的「metric from logs」能力 — 不需要額外的 metrics pipeline，直接從 log 產生 time series。</p>
<h3 id="loki-config-核心段">Loki config 核心段</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># loki-config.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">schema_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span>- <span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="ld">2024-01-01</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">store</span><span class="p">:</span><span class="w"> </span><span class="l">tsdb</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">object_store</span><span class="p">:</span><span class="w"> </span><span class="l">s3</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">schema</span><span class="p">:</span><span class="w"> </span><span class="l">v13</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">index</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="l">loki_index_</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">period</span><span class="p">:</span><span class="w"> </span><span class="l">24h</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="nt">storage_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">tsdb_shipper</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">active_index_directory</span><span class="p">:</span><span class="w"> </span><span class="l">/loki/index</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">cache_location</span><span class="p">:</span><span class="w"> </span><span class="l">/loki/cache</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">aws</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="nt">s3</span><span class="p">:</span><span class="w"> </span><span class="l">s3://loki-chunks-bucket</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">region</span><span class="p">:</span><span class="w"> </span><span class="l">us-east-1</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w"></span><span class="nt">limits_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">  </span><span class="nt">ingestion_rate_mb</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="nt">ingestion_burst_size_mb</span><span class="p">:</span><span class="w"> </span><span class="m">20</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="nt">max_streams_per_user</span><span class="p">:</span><span class="w"> </span><span class="m">10000</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">  </span><span class="nt">max_label_name_length</span><span class="p">:</span><span class="w"> </span><span class="m">1024</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">  </span><span class="nt">max_label_value_length</span><span class="p">:</span><span class="w"> </span><span class="m">2048</span></span></span></code></pre></div><p><code>limits_config</code> 是防護網。<code>max_streams_per_user</code> 限制每個 tenant 的 stream 數量，超過時新 stream 的 log 被拒（HTTP 429）。這是 label cardinality 爆炸的最後防線。</p>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="label-cardinality-爆炸">Label cardinality 爆炸</h3>
<p><strong>觸發條件</strong>：label 包含高 cardinality 值（pod UID、request ID、container ID）。每個唯一 label set 產生一條 stream，stream 數量快速增長。</p>
<p><strong>表現</strong>：<code>loki_ingester_memory_streams</code> 持續上升、ingester memory 增長、最終觸發 <code>max_streams_per_user</code> 限制（429 error）。跟 Prometheus series explosion 是同一個問題的 log 版本。</p>
<p><strong>修法</strong>：檢查產出大量 stream 的 label。Loki 的 <code>/loki/api/v1/labels</code> 和 <code>/loki/api/v1/label/{name}/values</code> API 可以列出所有 label 值。找到高 cardinality label 後，從 promtail / alloy 的 pipeline 中移除該 label、改放進 log content 的 structured field。</p>
<h3 id="stream-rate-limit">Stream rate limit</h3>
<p><strong>觸發條件</strong>：單一 stream 的 ingestion rate 超過 <code>per_stream_rate_limit</code>（預設 3 MB/s）。通常是某個 service 大量噴 debug log。</p>
<p><strong>表現</strong>：Loki 回傳 429 + <code>rate limit exceeded</code> error。部分 log entries 被丟棄。</p>
<p><strong>修法</strong>：先解決 log 噴量問題（降低 debug log level 或加 sampling）。如果噴量合理（高 QPS 服務），調高 <code>per_stream_rate_limit</code> 或拆分 stream（加一層 label 分散流量）。</p>
<h3 id="大時間範圍查詢-timeout">大時間範圍查詢 timeout</h3>
<p><strong>觸發條件</strong>：LogQL 查詢沒有精確的 label filter、時間範圍 &gt; 24 小時。Loki 要掃描大量 chunks、query timeout（預設 3 分鐘）觸發。</p>
<p><strong>表現</strong>：Grafana 顯示 query timeout error。</p>
<p><strong>修法</strong>：查詢時先用 label selector 縮小 stream 範圍（<code>{job=&quot;checkout&quot;, namespace=&quot;production&quot;}</code> 而非 <code>{namespace=&quot;production&quot;}</code>），再用 line filter 進一步篩。如果業務需要長時間範圍的 log analytics，考慮用 LogQL 的 metric aggregation（<code>rate(...)</code> / <code>count_over_time(...)</code>）替代原始 log 掃描。</p>
<h3 id="chunk-target-size-與-ingestion-rate-的關係">Chunk target size 與 ingestion rate 的關係</h3>
<p><code>chunk_target_size</code>（預設 1.5 MB）控制 chunk 的大小。ingestion rate 低的 stream 可能幾個小時才填滿一個 chunk — 這段期間 chunk 停在 ingester memory 裡。大量低 ingestion rate 的 stream（= 高 cardinality label）會讓 ingester 同時持有大量未 flush 的 chunks，佔用記憶體。</p>
<p>修法方向：降低 <code>chunk_idle_period</code>（預設 30 分鐘，時間到即使 chunk 未滿也 flush），或減少低 cardinality stream 的數量。</p>
<h2 id="容量與成本">容量與成本</h2>
<p>Loki 的成本結構跟 Elasticsearch 根本不同：</p>
<table>
  <thead>
      <tr>
          <th>成本項</th>
          <th>Loki</th>
          <th>Elasticsearch</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>儲存</td>
          <td>Object storage（S3/GCS）— 便宜</td>
          <td>SSD / local disk — 貴</td>
      </tr>
      <tr>
          <td>Index</td>
          <td>小（只索引 labels）</td>
          <td>大（inverted index + doc values）</td>
      </tr>
      <tr>
          <td>查詢 compute</td>
          <td>每次查詢 grep chunks — CPU 密集</td>
          <td>走 index — 相對輕</td>
      </tr>
      <tr>
          <td>適合的 workload</td>
          <td>高 volume、低 query frequency</td>
          <td>高 query frequency、需要 full-text</td>
      </tr>
  </tbody>
</table>
<p>Loki 在「每天寫 TB 級 log、偶爾查一下」的場景成本遠低於 Elasticsearch。但在「每天查數百次、需要快速 full-text search」的場景，Elasticsearch 的 pre-indexed 查詢效能更好，Loki 每次 grep 的 compute cost 反而更高。</p>
<p>成本治理的判讀：監控 <code>loki_ingester_bytes_received_total</code>（ingestion volume）和 <code>loki_querier_query_duration_seconds</code>（query cost）。如果 query duration 持續上升，先檢查是 label filter 不夠精確還是 query 時間範圍太大。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack 服務頁</a>：overview 與全棧操作</li>
<li><a href="../lgtm-stack-operations/">LGTM Stack Operations</a>：Loki 在 LGTM 全棧中的部署位置</li>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log Governance</a>：Loki 不適合 audit log 的 compliance 查詢（無 immutable storage 保證、無 fine-grained access control）— 合規需求用 BigQuery 或 dedicated audit backend</li>
<li><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">Healthcare 存取追溯案例</a>：分層 retention 在 Loki 用 tenant-level retention policy 實現</li>
<li><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 Log Schema</a>：log 欄位設計影響 Loki 的 label 設計與 parser 效率</li>
<li><a href="/blog/backend/04-observability/vendors/elastic-stack/ilm-log-pipeline/" data-link-title="Index Lifecycle Management 與 Log Pipeline" data-link-desc="說明 Elasticsearch ILM policy 設計、data stream / rollover、Beats vs Elastic Agent 採集選擇、ingest pipeline 與 shard sizing、cross-cluster 策略與 cost governance">Elasticsearch ILM 與 Log Pipeline</a>：需要 full-text search 時的替代方案</li>
</ul>
]]></content:encoded></item><item><title>4.C11 Uber：M3 大規模 Metrics 平台</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/</guid><description>&lt;p>Uber 的 M3 案例揭露了 metrics 系統從「每個團隊各跑一套 Prometheus」到「全公司共用的 metrics 平台」的轉折點。轉折的核心判斷是：當 active series 總量超過單機 Prometheus 的記憶體上限、且多個團隊需要跨叢集查詢時，自建平台層的成本低於持續橫向複製 Prometheus 實例的成本。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Uber 的服務觀測涵蓋行程追蹤、即時定價、ETA 計算、司機定位、支付結算與推播通知。每個微服務都暴露 Prometheus-compatible metrics，隨著服務數量成長到數千個，寫入速率達到每秒數十億 data points。&lt;/p>
&lt;p>早期每個團隊各自部署 Prometheus，各管自己的 retention、scrape config 與 alerting rules。規模小時這個模式運作良好 — 每個 Prometheus 實例只需要處理自己團隊的幾萬到幾十萬 series。但當組織成長到數百個團隊、數千個服務時，散落的 Prometheus 實例帶來三個問題。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="單機記憶體天花板">單機記憶體天花板&lt;/h3>
&lt;p>Prometheus 的 TSDB 把 active series 放在記憶體的 head block，每個 series 消耗約 3-4 KB（詳見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/" data-link-title="Prometheus 容量規劃與故障模式" data-link-desc="說明 Prometheus 單機容量邊界、cardinality 與 retention 的資源模型、常見故障模式與判讀方式">Prometheus 容量規劃&lt;/a>）。當單一 Prometheus 實例需要 scrape 的 series 超過 1000 萬時，head block 就需要 40+ GB 記憶體。加上 query execution 跟 WAL replay 的暫時開銷，單機很容易 OOM。&lt;/p>
&lt;p>團隊的第一反應是按服務拆分多個 Prometheus 實例，但這讓跨服務查詢變得困難 — 要看一條 request 從 gateway 到 payment 的 latency 分布，需要分別查三個 Prometheus 再手動關聯。&lt;/p>
&lt;h3 id="retention-與長期趨勢">Retention 與長期趨勢&lt;/h3>
&lt;p>Prometheus 預設 retention 15 天。容量規劃與季度趨勢分析需要 90 天甚至 1 年的歷史資料。把 Prometheus retention 拉長到 90 天，disk 跟 memory 需求同步上升，而且 compaction 效率在資料量大時會下降。&lt;/p>
&lt;p>團隊需要的是分層 retention — 近期資料保留全精度、歷史資料做 downsampling 後保留更久。Prometheus 原生不支援 downsampling。&lt;/p>
&lt;h3 id="高可用與跨叢集查詢">高可用與跨叢集查詢&lt;/h3>
&lt;p>Prometheus 沒有原生 HA — 標準做法是跑兩個 instance scrape 同一批 target，靠下游去重。但兩個 instance 各自獨立儲存，查詢只打一個；instance 故障切換時會有短暫資料缺口。&lt;/p>
&lt;p>跨叢集查詢更困難。Prometheus federation 可以做簡單的 metric 聚合，但 federation 本身是 pull-based scrape — federation target 太多或 series 太大時，federation Prometheus 自己也會 OOM。&lt;/p>
&lt;h2 id="解法m3-平台">解法：M3 平台&lt;/h2>
&lt;p>Uber 開發了 M3 — 一個 Prometheus-compatible 的分散式 metrics 平台，由三個核心元件組成。&lt;/p>
&lt;h3 id="m3db分散式-time-series-storage">M3DB：分散式 time series storage&lt;/h3>
&lt;p>M3DB 是分散式 TSDB，資料按 namespace 和 shard 分布在多個節點。每個 namespace 可以有不同的 retention 和 resolution — 例如 &lt;code>realtime&lt;/code> namespace 保留 2 天全精度，&lt;code>aggregated_1m&lt;/code> namespace 保留 90 天 1 分鐘精度。這解決了 retention tiering 的問題。&lt;/p></description><content:encoded><![CDATA[<p>Uber 的 M3 案例揭露了 metrics 系統從「每個團隊各跑一套 Prometheus」到「全公司共用的 metrics 平台」的轉折點。轉折的核心判斷是：當 active series 總量超過單機 Prometheus 的記憶體上限、且多個團隊需要跨叢集查詢時，自建平台層的成本低於持續橫向複製 Prometheus 實例的成本。</p>
<h2 id="業務背景">業務背景</h2>
<p>Uber 的服務觀測涵蓋行程追蹤、即時定價、ETA 計算、司機定位、支付結算與推播通知。每個微服務都暴露 Prometheus-compatible metrics，隨著服務數量成長到數千個，寫入速率達到每秒數十億 data points。</p>
<p>早期每個團隊各自部署 Prometheus，各管自己的 retention、scrape config 與 alerting rules。規模小時這個模式運作良好 — 每個 Prometheus 實例只需要處理自己團隊的幾萬到幾十萬 series。但當組織成長到數百個團隊、數千個服務時，散落的 Prometheus 實例帶來三個問題。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="單機記憶體天花板">單機記憶體天花板</h3>
<p>Prometheus 的 TSDB 把 active series 放在記憶體的 head block，每個 series 消耗約 3-4 KB（詳見 <a href="/blog/backend/04-observability/vendors/prometheus/capacity-failure-modes/" data-link-title="Prometheus 容量規劃與故障模式" data-link-desc="說明 Prometheus 單機容量邊界、cardinality 與 retention 的資源模型、常見故障模式與判讀方式">Prometheus 容量規劃</a>）。當單一 Prometheus 實例需要 scrape 的 series 超過 1000 萬時，head block 就需要 40+ GB 記憶體。加上 query execution 跟 WAL replay 的暫時開銷，單機很容易 OOM。</p>
<p>團隊的第一反應是按服務拆分多個 Prometheus 實例，但這讓跨服務查詢變得困難 — 要看一條 request 從 gateway 到 payment 的 latency 分布，需要分別查三個 Prometheus 再手動關聯。</p>
<h3 id="retention-與長期趨勢">Retention 與長期趨勢</h3>
<p>Prometheus 預設 retention 15 天。容量規劃與季度趨勢分析需要 90 天甚至 1 年的歷史資料。把 Prometheus retention 拉長到 90 天，disk 跟 memory 需求同步上升，而且 compaction 效率在資料量大時會下降。</p>
<p>團隊需要的是分層 retention — 近期資料保留全精度、歷史資料做 downsampling 後保留更久。Prometheus 原生不支援 downsampling。</p>
<h3 id="高可用與跨叢集查詢">高可用與跨叢集查詢</h3>
<p>Prometheus 沒有原生 HA — 標準做法是跑兩個 instance scrape 同一批 target，靠下游去重。但兩個 instance 各自獨立儲存，查詢只打一個；instance 故障切換時會有短暫資料缺口。</p>
<p>跨叢集查詢更困難。Prometheus federation 可以做簡單的 metric 聚合，但 federation 本身是 pull-based scrape — federation target 太多或 series 太大時，federation Prometheus 自己也會 OOM。</p>
<h2 id="解法m3-平台">解法：M3 平台</h2>
<p>Uber 開發了 M3 — 一個 Prometheus-compatible 的分散式 metrics 平台，由三個核心元件組成。</p>
<h3 id="m3db分散式-time-series-storage">M3DB：分散式 time series storage</h3>
<p>M3DB 是分散式 TSDB，資料按 namespace 和 shard 分布在多個節點。每個 namespace 可以有不同的 retention 和 resolution — 例如 <code>realtime</code> namespace 保留 2 天全精度，<code>aggregated_1m</code> namespace 保留 90 天 1 分鐘精度。這解決了 retention tiering 的問題。</p>
<p>M3DB 的記憶體模型跟 Prometheus 不同 — 近期資料在記憶體，冷資料在 disk，不像 Prometheus 把所有 active series 都放 head block。這讓它能處理遠超單機 Prometheus 的 series 數量。</p>
<h3 id="m3-coordinator統一查詢入口">M3 Coordinator：統一查詢入口</h3>
<p>M3 Coordinator 接收 PromQL 查詢，轉譯後分發到 M3DB 節點，聚合結果後返回。對 Grafana 和 alerting rules 來說，M3 Coordinator 的 API 跟 Prometheus 完全相容 — 不需要改 dashboard 或 alert config。</p>
<h3 id="m3-aggregator寫入路徑聚合">M3 Aggregator：寫入路徑聚合</h3>
<p>高 cardinality 的原始 series 在寫入 M3DB 前先經過 M3 Aggregator 做 pre-aggregation — 例如把每秒的 request count 聚合成每分鐘，再寫入長期 namespace。這控制了長期儲存的資料量跟成本。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Prometheus standalone</th>
          <th>M3 平台</th>
          <th>Mimir / Thanos（替代）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署複雜度</td>
          <td>低（單一 binary）</td>
          <td>高（M3DB + Coordinator + Aggregator）</td>
          <td>中到高</td>
      </tr>
      <tr>
          <td>單機 series 上限</td>
          <td>~500 萬-1000 萬</td>
          <td>不適用（分散式）</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td>Retention tiering</td>
          <td>無</td>
          <td>原生支援</td>
          <td>Thanos compactor / Mimir 支援</td>
      </tr>
      <tr>
          <td>PromQL 相容</td>
          <td>原生</td>
          <td>相容</td>
          <td>相容</td>
      </tr>
      <tr>
          <td>社群活躍度</td>
          <td>高（CNCF）</td>
          <td>低（Uber 主導、2023 後維護縮減）</td>
          <td>高（Grafana Labs / 社群）</td>
      </tr>
      <tr>
          <td>適用規模</td>
          <td>單團隊到中型組織</td>
          <td>大型組織（數十億 series）</td>
          <td>中型到大型</td>
      </tr>
  </tbody>
</table>
<p>M3 的最大風險是社群活躍度 — Uber 自 2023 年後縮減了 M3 的開發投入，Grafana Mimir 成為更活躍的替代。新專案選型時，Mimir 跟 Thanos 的社群支援度跟 Grafana 生態整合度都優於 M3。M3 的價值在於它驗證了「分散式 TSDB + 寫入路徑聚合 + retention tiering」這組設計模式，這組模式在 Mimir 跟 Thanos 裡以不同形式被採用。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 Metrics Basics</a>：active series、cardinality 與 recording rules 的基礎模型，M3 的 pre-aggregation 對應 recording rules 的平台化版本。</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a>：M3 的 Aggregator 是 pipeline 中 processing 層的實例。</li>
<li><a href="/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/" data-link-title="Remote Write 與長期儲存整合" data-link-desc="說明 Prometheus remote write 的配置、三家長期儲存後端比較（Mimir / Thanos / Cortex）、故障模式與容量規劃">Prometheus Remote Write 與長期儲存</a>：M3 是 remote write 目標之一，跟 Mimir / Thanos / Cortex 的比較在該文。</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理</a>：M3 的 per-namespace cardinality limit 是治理機制的生產實例。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>單一 Prometheus 實例 memory 接近機器上限，開始 OOM restart</li>
<li>多個 Prometheus 實例各自 scrape，跨服務查詢需要手動關聯</li>
<li>Retention 15 天不夠做季度趨勢分析，但拉長 retention 資源撐不住</li>
<li>團隊開始問「我們的 metrics 總共有多少 series、誰佔最多」但沒有統一的 cardinality 觀測</li>
<li>Grafana federation dashboard 查詢越來越慢或經常 timeout</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.uber.com/en-GB/blog/m3/">M3: Uber&rsquo;s Open Source, Large-scale Metrics Platform for Prometheus</a></li>
</ul>
]]></content:encoded></item><item><title>Cloud Logging 查詢、匯出與合規</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/cloud-logging-export-compliance/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/cloud-logging-export-compliance/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations&lt;/a> 的 vendor deep article，深化 overview「Cloud Logging 結構化 logs」跟「BigQuery 匯出長期儲存」段。初次接觸 GCP 觀測的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Cloud Logging 對 GCP 服務是預設開啟的 — GKE、Cloud Run、Cloud Functions 的 stdout/stderr 自動進 Cloud Logging，工程師不需要配置就能查。問題出在後續階段：log 量成長後的成本控制（GCP 的 ingestion 計費讓高 volume 服務成本快速累積）、合規需求要求特定 log 保留特定時間（healthcare / fintech 的 7 年留存）、organization-level 的 log 聚合與存取控制（多 project 集中 audit）、以及 PII 在 log 中的遮罩與加密。理解 Cloud Logging 的 router / sink 架構跟 retention bucket 才能從「預設全收」走向「可治理的 log pipeline」。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="log-router-與-sink">Log Router 與 Sink&lt;/h3>
&lt;p>Cloud Logging 的資料流是 &lt;strong>log entry → log router → sink → destination&lt;/strong>。每一筆 log 進入 Cloud Logging 後，log router 根據 inclusion filter 跟 exclusion filter 決定這筆 log 送到哪些 destination。&lt;/p>
&lt;p>&lt;strong>Sink&lt;/strong> 是 log router 的輸出端點。每個 GCP project 預設有兩個 sink：&lt;code>_Required&lt;/code>（admin activity audit log、system event，不可關閉）和 &lt;code>_Default&lt;/code>（其他所有 log、送到 &lt;code>_Default&lt;/code> log bucket、可修改 filter）。工程師可以建立自訂 sink，把符合條件的 log 送到 BigQuery、Cloud Storage、Pub/Sub 或 Splunk。&lt;/p>
&lt;p>&lt;strong>Exclusion filter&lt;/strong> 在 log router 層攔截 — 被排除的 log 不會寫入任何 sink destination，也不計入 ingestion 計費。這是成本控制的第一道防線。&lt;/p>
&lt;p>&lt;strong>Inclusion filter&lt;/strong> 在 sink 層生效 — 只有符合 filter 的 log 會送到該 sink 的 destination。&lt;/p>
&lt;p>路由順序很重要：exclusion filter 先執行（全域攔截），然後 &lt;code>_Required&lt;/code> sink 攔走必留 log，然後 &lt;code>_Default&lt;/code> sink 跟自訂 sink 各自的 inclusion filter 平行執行。一筆 log 可以同時送到多個 sink。&lt;/p>
&lt;h3 id="retention-與-log-bucket">Retention 與 Log Bucket&lt;/h3>
&lt;p>Cloud Logging 的儲存單位是 &lt;strong>log bucket&lt;/strong>。每個 project 預設有兩個 bucket：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations</a> 的 vendor deep article，深化 overview「Cloud Logging 結構化 logs」跟「BigQuery 匯出長期儲存」段。初次接觸 GCP 觀測的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Cloud Logging 對 GCP 服務是預設開啟的 — GKE、Cloud Run、Cloud Functions 的 stdout/stderr 自動進 Cloud Logging，工程師不需要配置就能查。問題出在後續階段：log 量成長後的成本控制（GCP 的 ingestion 計費讓高 volume 服務成本快速累積）、合規需求要求特定 log 保留特定時間（healthcare / fintech 的 7 年留存）、organization-level 的 log 聚合與存取控制（多 project 集中 audit）、以及 PII 在 log 中的遮罩與加密。理解 Cloud Logging 的 router / sink 架構跟 retention bucket 才能從「預設全收」走向「可治理的 log pipeline」。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="log-router-與-sink">Log Router 與 Sink</h3>
<p>Cloud Logging 的資料流是 <strong>log entry → log router → sink → destination</strong>。每一筆 log 進入 Cloud Logging 後，log router 根據 inclusion filter 跟 exclusion filter 決定這筆 log 送到哪些 destination。</p>
<p><strong>Sink</strong> 是 log router 的輸出端點。每個 GCP project 預設有兩個 sink：<code>_Required</code>（admin activity audit log、system event，不可關閉）和 <code>_Default</code>（其他所有 log、送到 <code>_Default</code> log bucket、可修改 filter）。工程師可以建立自訂 sink，把符合條件的 log 送到 BigQuery、Cloud Storage、Pub/Sub 或 Splunk。</p>
<p><strong>Exclusion filter</strong> 在 log router 層攔截 — 被排除的 log 不會寫入任何 sink destination，也不計入 ingestion 計費。這是成本控制的第一道防線。</p>
<p><strong>Inclusion filter</strong> 在 sink 層生效 — 只有符合 filter 的 log 會送到該 sink 的 destination。</p>
<p>路由順序很重要：exclusion filter 先執行（全域攔截），然後 <code>_Required</code> sink 攔走必留 log，然後 <code>_Default</code> sink 跟自訂 sink 各自的 inclusion filter 平行執行。一筆 log 可以同時送到多個 sink。</p>
<h3 id="retention-與-log-bucket">Retention 與 Log Bucket</h3>
<p>Cloud Logging 的儲存單位是 <strong>log bucket</strong>。每個 project 預設有兩個 bucket：</p>
<ul>
<li><code>_Required</code> bucket：admin activity audit log 跟 system event，保留 400 天，不可刪除或修改 retention</li>
<li><code>_Default</code> bucket：其他所有 log，預設保留 30 天，可調整為 1-3650 天</li>
</ul>
<p>自訂 log bucket 可以設定不同 retention 期。常見用法：把 application log 留 30 天、把 audit log 留 7 年（送到自訂 bucket 或 BigQuery）。</p>
<p>Cloud Logging 的 ingestion 計費跟 storage 計費是分開的。前 50 GiB/month per billing account 的 ingestion 免費；超過後按 ingestion volume 計費。<code>_Required</code> log 的 ingestion 免費。Storage 在 <code>_Default</code> bucket 的前 0.5 GiB 免費，自訂 bucket 按用量計費。</p>
<p>成本治理判讀：高 volume 服務（例如 GKE 的 container stdout）的成本主要來自 ingestion，而非 storage。Exclusion filter 攔掉不需要的 log 是最直接的降成本方式。</p>
<h3 id="查詢語言">查詢語言</h3>
<p>Cloud Logging 的查詢語言用在 Logs Explorer 跟 gcloud CLI：</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">resource.type=&#34;k8s_container&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">resource.labels.cluster_name=&#34;prod-us-central1&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">severity&gt;=ERROR
</span></span><span class="line"><span class="ln">4</span><span class="cl">jsonPayload.order_id=&#34;ord-12345&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">timestamp&gt;=&#34;2026-06-22T00:00:00Z&#34;</span></span></code></pre></div><p>語法特點：field path 用 <code>.</code> 分隔、支援 comparison operators（<code>=</code> / <code>!=</code> / <code>&gt;</code> / <code>&gt;=</code> / <code>&lt;</code> / <code>&lt;=</code>）、支援 boolean（<code>AND</code> / <code>OR</code> / <code>NOT</code>）、支援 regex（<code>=~</code> / <code>!~</code>）。</p>
<p>跟 KQL（Elastic）或 LogQL（Loki）相比，Cloud Logging 查詢語言更接近 structured filter 而非 full-text search。Full-text 搜尋要用 <code>textPayload:</code> 或 <code>jsonPayload:</code> prefix。進階分析（aggregation、time bucketing、join）需要匯出到 BigQuery 後用 SQL 做。</p>
<h2 id="配置-step-by-step">配置 step-by-step</h2>
<h3 id="organization-level-log-聚合">Organization-level log 聚合</h3>
<p>多 project 環境下，集中 log 的標準做法是在 organization 或 folder level 建立 aggregated sink：</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">gcloud logging sinks create org-audit-sink \
</span></span><span class="line"><span class="ln">2</span><span class="cl">  bigquery.googleapis.com/projects/central-audit/datasets/org_audit_logs \
</span></span><span class="line"><span class="ln">3</span><span class="cl">  --organization=123456789 \
</span></span><span class="line"><span class="ln">4</span><span class="cl">  --include-children \
</span></span><span class="line"><span class="ln">5</span><span class="cl">  --log-filter=&#39;logName:&#34;cloudaudit.googleapis.com&#34;&#39;</span></span></code></pre></div><p><code>--include-children</code> 讓 organization 下所有 project、folder 的符合 log 都送到同一個 BigQuery dataset。Sink 的 service account 需要 destination 的寫入權限（BigQuery Data Editor）。</p>
<p>適用場景：SOC 團隊需要跨 project 的 audit log 查詢、compliance team 需要集中的 data access log 存檔、security team 需要異常 IAM 變更的全域偵測。</p>
<h3 id="data-access-audit-logs-啟用">Data Access Audit Logs 啟用</h3>
<p>GCP 的 audit log 分三類：</p>
<ul>
<li><strong>Admin Activity</strong>：對資源的管理操作（建立 / 刪除 / 修改 IAM）。預設開啟、不可關閉、不計費。</li>
<li><strong>Data Access</strong>：對資源的讀取操作（BigQuery query、GCS read、Cloud SQL connect）。預設關閉（除 BigQuery）、需手動啟用、計費。</li>
<li><strong>System Event</strong>：GCP 系統自動操作。預設開啟、不可關閉、不計費。</li>
</ul>
<p>Data Access audit log 的啟用是 per-service、per-project（或 org level）。啟用後 log 量會大幅增加 — 一個高 QPS 的 Cloud SQL 服務可能每秒產生數百筆 data access log。成本跟 volume 判讀要先做。</p>
<p>建議做法：先對 security-sensitive 服務啟用（IAM / KMS / Cloud SQL / GCS），其他服務按需啟用。用 exclusion filter 精細控制 — 例如只保留 <code>ADMIN_READ</code> 跟 <code>DATA_WRITE</code>、排除 <code>DATA_READ</code>（read 量通常遠大於 write）。</p>
<h3 id="vpc-flow-logs-與-dns-logs-的觀測用途">VPC Flow Logs 與 DNS Logs 的觀測用途</h3>
<p>VPC Flow Logs 記錄每一筆通過 VPC 的網路流量元資料（src/dst IP、port、protocol、bytes、packets）。啟用方式是 per-subnet 設定、支援 sampling rate（100% / 50% / 10%）。</p>
<p>DNS Logs 記錄 VPC 內的 DNS 查詢（query name、response code、source VM）。啟用方式是 per-VPC 或 per-policy 設定。</p>
<p>觀測用途：</p>
<ul>
<li><strong>異常流量偵測</strong>：VPC Flow Logs 送到 BigQuery 後用 SQL 找出異常流量模式（大量對外連線、非預期 port、跨 region 資料傳輸）</li>
<li><strong>網路效能分析</strong>：量測 inter-service latency、跨 AZ 流量比例</li>
<li><strong>安全稽核</strong>：DNS Logs 偵測 DNS tunneling 或 C2 callback</li>
</ul>
<p>成本注意：VPC Flow Logs 在高流量服務上的 ingestion 量非常大。100% sampling + 高 QPS 服務可能每天產生 TB 級 log。建議用 sampling rate 控制、或只對 security-sensitive subnet 啟用 100%。</p>
<h3 id="自建-vs-managed-pipeline-的取捨">自建 vs managed pipeline 的取捨</h3>
<p><a href="/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/" data-link-title="4.C12 Cloudflare：內部觀測平台的三層能力" data-link-desc="全球 300&#43; edge 節點的觀測架構，把 monitoring、analytics 與 forensics 拆成三個獨立能力層。">Cloudflare 觀測案例</a>展示了自建觀測 pipeline 的理由 — 全球 300+ edge locations、每秒數十億 request 的規模下，SaaS 觀測平台的帳單不合理，自建 pipeline 的 compute 成本反而更低。</p>
<p>但多數團隊的結論是反過來的。GCP 環境下，Cloud Logging 的 managed pipeline（log entry → router → sink → BigQuery / Cloud Storage）幾乎不需要維運人力。自建等價的 pipeline（Fluent Bit → Kafka → Elasticsearch / BigQuery）需要維運 Kafka cluster、Elasticsearch cluster、Fluent Bit DaemonSet 的升級與監控。</p>
<p>判斷分水嶺的兩個維度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>偏向 managed（Cloud Logging）</th>
          <th>偏向自建</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Log volume</td>
          <td>&lt; 1 TB/day</td>
          <td>&gt; 10 TB/day（SaaS ingestion 成本超過自建 compute）</td>
      </tr>
      <tr>
          <td>查詢需求</td>
          <td>Logs Insights + 偶爾 BigQuery</td>
          <td>需要 Elasticsearch 的全文搜尋 + aggregation + visualization</td>
      </tr>
  </tbody>
</table>
<p>1-10 TB/day 的灰色地帶取決於查詢模式 — 如果 Logs Insights 能滿足 90% 的查詢、BigQuery 能處理剩下 10% 的分析，不需要自建。如果團隊需要 Kibana dashboard、Elasticsearch alerting、或跨 cloud 的統一 log backend，自建可能更合理。</p>
<h3 id="healthcare-分層-retention-在-gcp-的實現">Healthcare 分層 retention 在 GCP 的實現</h3>
<p><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">Healthcare 案例</a>的核心需求是分層 retention — 不同 log 類型有不同的法規留存要求（data access audit log 要 6 年+、application operational log 要 90 天、debug log 要 7 天）。</p>
<p>在 GCP 上用三層架構實現：</p>
<p><strong>Hot 層（Cloud Logging custom bucket）</strong>：application log 保留 90 天、audit log 保留 1 年。設定 custom log bucket + retention。優點是 Logs Explorer 直接可查、延遲低。</p>
<p><strong>Warm 層（BigQuery）</strong>：audit log sink 到 BigQuery dataset，BigQuery 的 partition expiration 設 2 年。需要分析跟 correlation 時用 SQL 查。成本低於 Cloud Logging storage。</p>
<p><strong>Cold 層（Cloud Storage + Object Lifecycle）</strong>：BigQuery 的 scheduled export 或直接 Cloud Logging sink 到 GCS bucket。Object lifecycle rule 把 90 天以上的 object 轉 Nearline / Coldline / Archive class。最終刪除設定在 7 年。</p>
<p>三層各自的 access control 要獨立設定 — cold 層的 GCS bucket 只有 compliance team 有讀取權限，application team 看不到。CMEK 在三層都啟用（Cloud Logging custom bucket 的 CMEK + BigQuery dataset 的 CMEK + GCS bucket 的 CMEK），金鑰由安全團隊集中管理。</p>
<h3 id="pii-治理與-cmek">PII 治理與 CMEK</h3>
<p>Cloud Logging 中的 PII 治理有三層：</p>
<p><strong>第一層：不寫入</strong>。Application 端在 log 之前就遮罩 PII（email → <code>***@***.com</code>、credit card → last 4 digits）。這是最有效的方式，因為一旦寫入 Cloud Logging，即使後續刪除 log entry，在 deletion 前可能已經被 sink 匯出到 BigQuery / GCS。</p>
<p><strong>第二層：log 層過濾</strong>。用 exclusion filter 把含 PII 的 log field 排除（例如排除特定 jsonPayload field）。限制是 Cloud Logging 的 exclusion filter 只能排除整筆 log entry，不能 redact 單一 field。需要 field-level redaction 的話，在 OTel Collector 或 Fluentd 層做 processor 處理、再送到 Cloud Logging。</p>
<p><strong>第三層：加密</strong>。Cloud Logging 預設用 Google-managed encryption。需要自管金鑰的場景（HIPAA / PCI-DSS / 金融監管）用 CMEK（Customer-Managed Encryption Keys）。CMEK 設定在 log bucket 層 — 自訂 log bucket 可以指定 Cloud KMS key。<code>_Default</code> bucket 也可以啟用 CMEK（需要把 <code>_Default</code> bucket 的 region 從 <code>global</code> 改成特定 region）。</p>
<p>存取控制：Cloud Logging 的 IAM role 分 <code>roles/logging.viewer</code>（讀 log）、<code>roles/logging.privateLogViewer</code>（讀含 data access 的 log）、<code>roles/logging.admin</code>（管理 sink / bucket / filter）。Audit log 的存取用 <code>roles/logging.privateLogViewer</code>、不是一般的 <code>roles/logging.viewer</code>。對應 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">稽核追蹤與責任邊界</a> 的 GCP 實作。</p>
<h2 id="故障演練與邊界">故障演練與邊界</h2>
<h3 id="exclusion-filter-設太寬重要-log-被丟掉">Exclusion filter 設太寬，重要 log 被丟掉</h3>
<p><strong>觸發條件</strong>：為了降成本建立 exclusion filter，但 filter expression 太寬泛（例如排除整個 severity=INFO），連帶排除了 business-critical 的 info-level log。</p>
<p><strong>表現</strong>：事故時查不到關鍵 log、audit 證據鏈斷裂。因為 exclusion filter 在 ingestion 前執行，被排除的 log 無法回補。</p>
<p><strong>預防</strong>：exclusion filter 建立後先用 <code>gcloud logging read</code> 驗證哪些 log 會被排除。用 Logs Explorer 的 preview 功能確認 filter 不會命中關鍵 log。對 audit log 和 security log 不設 exclusion filter。</p>
<h3 id="bigquery-sink-匯出成本失控">BigQuery sink 匯出成本失控</h3>
<p><strong>觸發條件</strong>：org-level aggregated sink 把所有 log 送到 BigQuery，沒有 inclusion filter 限制。</p>
<p><strong>表現</strong>：BigQuery storage 跟 streaming insert 成本暴增。一個中型 GKE cluster 每天可能產生 100+ GB 的 container log，全部送 BigQuery 的月成本可能超過 Cloud Logging 本身。</p>
<p><strong>修復</strong>：在 sink 加 inclusion filter（只送 audit log 或 error-level log 到 BigQuery）。高 volume 的 application log 送 Cloud Storage（成本更低），需要查詢時用 BigQuery external table 做 federated query。</p>
<h3 id="log-entry-size-超過限制">Log entry size 超過限制</h3>
<p><strong>觸發條件</strong>：application log 寫入超過 256 KB 的單筆 log entry（Cloud Logging 的 per-entry 上限）。</p>
<p><strong>表現</strong>：超過限制的 log entry 被截斷或拒絕寫入。</p>
<p><strong>修復</strong>：application 端控制 log entry size — 大型 payload（request body / response body / stack trace）做 truncation 後再 log。需要完整內容的場景，把 payload 寫到 GCS、log 中只留 GCS URI。</p>
<h2 id="容量與成本">容量與成本</h2>
<table>
  <thead>
      <tr>
          <th>計費項目</th>
          <th>免費額度</th>
          <th>超出後計費</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Ingestion（非 <code>_Required</code>）</td>
          <td>50 GiB/month per billing account</td>
          <td>per GiB ingested</td>
      </tr>
      <tr>
          <td>Storage（<code>_Default</code> bucket）</td>
          <td>0.5 GiB</td>
          <td>per GiB-month</td>
      </tr>
      <tr>
          <td>Storage（custom bucket）</td>
          <td>無免費額度</td>
          <td>per GiB-month</td>
      </tr>
      <tr>
          <td><code>_Required</code> log ingestion</td>
          <td>不計費</td>
          <td>不計費</td>
      </tr>
      <tr>
          <td>BigQuery sink streaming insert</td>
          <td>依 BigQuery 計費</td>
          <td>per GB inserted</td>
      </tr>
  </tbody>
</table>
<p>成本最佳化優先序：</p>
<ol>
<li><strong>Exclusion filter</strong>：攔掉不需要的 log、最直接</li>
<li><strong>降 log level</strong>：application 端把 verbose debug log 關掉</li>
<li><strong>Sampling</strong>：高 QPS 服務的 request log 做 sampling（在 application 端或 OTel Collector 層）</li>
<li><strong>BigQuery sink filter</strong>：只送需要長期分析的 log 到 BigQuery</li>
<li><strong>Cloud Storage sink</strong>：高 volume + 低查詢頻率的 log 送 GCS、按需用 BigQuery external table 查</li>
</ol>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations 服務頁</a>：overview 與日常操作</li>
<li><a href="../cloud-monitoring-mql/">Cloud Monitoring Metrics Model 與 MQL</a>：同 vendor 的 metrics 面</li>
<li><a href="/blog/backend/04-observability/audit-log-governance/" data-link-title="4.12 Audit Log 邊界與 PII 治理" data-link-desc="把稽核訊號從 operational log 拆出、按法規與不變性治理">4.12 Audit Log 邊界與 PII 治理</a>：跨 vendor 的 audit log 治理策略</li>
<li><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 Fintech audit evidence</a>：審計證據鏈的案例回寫</li>
<li><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention</a>：長期保留的合規設計</li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 security 模組</a>：data access audit log 的安全面</li>
</ul>
]]></content:encoded></item><item><title>CloudWatch Alarms 與 Composite Alarms 操作實務</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/alarms-composite-operations/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/alarms-composite-operations/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch&lt;/a> 的 vendor deep article，深化 overview「Alarm + Composite alarm + EventBridge rule」段。初次接觸 CloudWatch 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>CloudWatch Alarm 是 AWS 原生的告警機制，跟 Prometheus Alertmanager 或 Datadog Monitor 的定位相同 — 把 metric 異常轉成可操作通知。CloudWatch Alarm 的特性是跟 AWS 服務深度整合（Auto Scaling、SNS、Lambda、Systems Manager），但告警邏輯表達力比 PromQL alerting rule 弱。Composite Alarm 是 CloudWatch 用來降低 alert noise 的方式，把多個 alarm 的布林組合當成觸發條件。&lt;/p>
&lt;h2 id="metric-alarm-基礎">Metric Alarm 基礎&lt;/h2>
&lt;h3 id="alarm-參數">Alarm 參數&lt;/h3>
&lt;p>每個 metric alarm 由五個參數決定行為：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>參數&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;th>常見設定&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Metric&lt;/td>
 &lt;td>要監控的 metric（namespace + metric name + dimension）&lt;/td>
 &lt;td>&lt;code>AWS/EC2 CPUUtilization InstanceId=i-xxx&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Statistic&lt;/td>
 &lt;td>聚合方式（Average / Sum / Maximum / Minimum / p99）&lt;/td>
 &lt;td>根據 metric 性質選擇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Period&lt;/td>
 &lt;td>每個 data point 的時間窗&lt;/td>
 &lt;td>60s（standard）/ 10s（high-resolution）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evaluation periods&lt;/td>
 &lt;td>連續幾個 period 超過閾值才觸發&lt;/td>
 &lt;td>3-5 個 period 減少 flapping&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Threshold&lt;/td>
 &lt;td>觸發閾值&lt;/td>
 &lt;td>跟 SLO 對齊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Evaluation periods 的意義是「連續 N 個 period 都違反閾值才進入 ALARM 狀態」。設太低（1 個 period）容易 flapping，設太高（10 個 period）會延遲告警。多數場景 3 個 period × 60 秒 = 3 分鐘是合理起點。&lt;/p>
&lt;h3 id="datapoints-to-alarm">Datapoints to Alarm&lt;/h3>
&lt;p>除了 evaluation periods，CloudWatch 還有 &lt;code>Datapoints to Alarm&lt;/code> 參數 — 在 evaluation periods 的窗口中，至少幾個 datapoint 超過閾值就觸發。例如 &lt;code>3 of 5&lt;/code> 代表最近 5 個 period 中有 3 個超過閾值就觸發。&lt;/p>
&lt;p>這個設計讓告警在有缺失 datapoint 的環境下更穩健。容器重啟、Lambda cold start 或 scrape timeout 都可能造成某些 period 沒有 datapoint，&lt;code>M of N&lt;/code> 模式避免因為缺失資料而延遲告警。&lt;/p>
&lt;h2 id="anomaly-detection-alarm">Anomaly Detection Alarm&lt;/h2>
&lt;h3 id="用途">用途&lt;/h3>
&lt;p>Anomaly Detection alarm 用機器學習模型建立 metric 的 baseline band，metric 偏離 band 就觸發。適合沒有固定閾值的 metric — 例如 request count 在白天高、晚上低，用固定閾值會在晚上誤報或白天漏報。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch</a> 的 vendor deep article，深化 overview「Alarm + Composite alarm + EventBridge rule」段。初次接觸 CloudWatch 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>CloudWatch Alarm 是 AWS 原生的告警機制，跟 Prometheus Alertmanager 或 Datadog Monitor 的定位相同 — 把 metric 異常轉成可操作通知。CloudWatch Alarm 的特性是跟 AWS 服務深度整合（Auto Scaling、SNS、Lambda、Systems Manager），但告警邏輯表達力比 PromQL alerting rule 弱。Composite Alarm 是 CloudWatch 用來降低 alert noise 的方式，把多個 alarm 的布林組合當成觸發條件。</p>
<h2 id="metric-alarm-基礎">Metric Alarm 基礎</h2>
<h3 id="alarm-參數">Alarm 參數</h3>
<p>每個 metric alarm 由五個參數決定行為：</p>
<table>
  <thead>
      <tr>
          <th>參數</th>
          <th>說明</th>
          <th>常見設定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Metric</td>
          <td>要監控的 metric（namespace + metric name + dimension）</td>
          <td><code>AWS/EC2 CPUUtilization InstanceId=i-xxx</code></td>
      </tr>
      <tr>
          <td>Statistic</td>
          <td>聚合方式（Average / Sum / Maximum / Minimum / p99）</td>
          <td>根據 metric 性質選擇</td>
      </tr>
      <tr>
          <td>Period</td>
          <td>每個 data point 的時間窗</td>
          <td>60s（standard）/ 10s（high-resolution）</td>
      </tr>
      <tr>
          <td>Evaluation periods</td>
          <td>連續幾個 period 超過閾值才觸發</td>
          <td>3-5 個 period 減少 flapping</td>
      </tr>
      <tr>
          <td>Threshold</td>
          <td>觸發閾值</td>
          <td>跟 SLO 對齊</td>
      </tr>
  </tbody>
</table>
<p>Evaluation periods 的意義是「連續 N 個 period 都違反閾值才進入 ALARM 狀態」。設太低（1 個 period）容易 flapping，設太高（10 個 period）會延遲告警。多數場景 3 個 period × 60 秒 = 3 分鐘是合理起點。</p>
<h3 id="datapoints-to-alarm">Datapoints to Alarm</h3>
<p>除了 evaluation periods，CloudWatch 還有 <code>Datapoints to Alarm</code> 參數 — 在 evaluation periods 的窗口中，至少幾個 datapoint 超過閾值就觸發。例如 <code>3 of 5</code> 代表最近 5 個 period 中有 3 個超過閾值就觸發。</p>
<p>這個設計讓告警在有缺失 datapoint 的環境下更穩健。容器重啟、Lambda cold start 或 scrape timeout 都可能造成某些 period 沒有 datapoint，<code>M of N</code> 模式避免因為缺失資料而延遲告警。</p>
<h2 id="anomaly-detection-alarm">Anomaly Detection Alarm</h2>
<h3 id="用途">用途</h3>
<p>Anomaly Detection alarm 用機器學習模型建立 metric 的 baseline band，metric 偏離 band 就觸發。適合沒有固定閾值的 metric — 例如 request count 在白天高、晚上低，用固定閾值會在晚上誤報或白天漏報。</p>
<h3 id="設定">設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws cloudwatch put-anomaly-detector <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --namespace AWS/ApplicationELB <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --metric-name RequestCount <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --dimensions <span class="nv">Name</span><span class="o">=</span>LoadBalancer,Value<span class="o">=</span>app/my-alb/xxx <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --stat Sum</span></span></code></pre></div><p>Anomaly Detection 需要至少兩週的歷史資料才能建立可靠 baseline。新服務上線初期先用固定閾值 alarm，等累積足夠資料後再切換。</p>
<h3 id="band-width-控制">Band width 控制</h3>
<p>Anomaly Detection band 的寬度用標準差倍數控制（預設 2）。band 太窄（1x）容易誤報，太寬（3x）漏報。生產經驗是 API latency 用 2x、batch job duration 用 3x（batch 的自然波動較大）。</p>
<h2 id="composite-alarm">Composite Alarm</h2>
<h3 id="問題alert-noise">問題：Alert noise</h3>
<p>單一 metric alarm 太多時，on-call 會收到大量相關但重複的通知。一個下游服務故障可能同時觸發 latency alarm、error rate alarm、timeout alarm、queue lag alarm — 都指向同一個根因，但各自通知。</p>
<h3 id="解法布林組合">解法：布林組合</h3>
<p>Composite Alarm 用布林表達式組合多個 alarm，只在組合條件成立時觸發。</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">ALARM(&#34;checkout-latency-high&#34;)
</span></span><span class="line"><span class="ln">2</span><span class="cl">AND ALARM(&#34;payment-error-rate-high&#34;)
</span></span><span class="line"><span class="ln">3</span><span class="cl">AND NOT ALARM(&#34;scheduled-maintenance-window&#34;)</span></span></code></pre></div><p>這個組合代表：checkout latency 高且 payment error rate 也高，但排除了計畫維護視窗 — 才通知 on-call。</p>
<h3 id="設計原則">設計原則</h3>
<p>Composite Alarm 的設計應該反映事故判讀邏輯，而非機械式組合。三個常見模式：</p>
<p><strong>Symptom + cause 組合</strong>：外部症狀（latency 高）加上內部原因（DB connection pool 飽和）同時成立才通知。避免 latency 短暫抖動就告警。</p>
<p><strong>Cross-service correlation</strong>：多個服務同時出現異常時觸發「可能是 shared dependency 問題」的 composite alarm。一個服務異常可能是部署問題，多個同時異常更可能是共用依賴（load balancer、DNS、shared database）。</p>
<p><strong>Suppression window</strong>：用 maintenance window alarm 做 NOT 條件，在計畫維護期間抑制告警。</p>
<h3 id="限制">限制</h3>
<ul>
<li>Composite Alarm 最多引用 5 個 child alarm</li>
<li>巢狀深度最多 1 層（composite 不能引用另一個 composite）</li>
<li>Composite Alarm 本身不產生 metric，只做觸發邏輯</li>
</ul>
<p>超過 5 個 child alarm 時，需要把相關 alarm 先組成一個 composite，再讓上層 composite 引用。但因為不支援巢狀，實際能組合的 alarm 數量有限。複雜告警邏輯需要用 EventBridge rule 搭配 Lambda 處理。</p>
<h2 id="alarm-actions">Alarm actions</h2>
<h3 id="常見-action-類型">常見 action 類型</h3>
<p>Alarm 進入 ALARM 狀態時可以觸發多種 action：</p>
<table>
  <thead>
      <tr>
          <th>Action 類型</th>
          <th>用途</th>
          <th>設定方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SNS Topic</td>
          <td>通知 on-call（email、SMS、PagerDuty integration）</td>
          <td>alarm action → SNS ARN</td>
      </tr>
      <tr>
          <td>Auto Scaling policy</td>
          <td>自動擴容</td>
          <td>alarm action → scaling policy ARN</td>
      </tr>
      <tr>
          <td>Lambda function</td>
          <td>自訂邏輯（建 ticket、關閉服務、修改 config）</td>
          <td>alarm action → Lambda ARN（透過 SNS）</td>
      </tr>
      <tr>
          <td>Systems Manager runbook</td>
          <td>自動執行 remediation runbook</td>
          <td>alarm action → SSM automation ARN</td>
      </tr>
      <tr>
          <td>EC2 action</td>
          <td>停止 / 重啟 / 終止 instance</td>
          <td>alarm action → EC2 action（僅限 EC2 metric）</td>
      </tr>
  </tbody>
</table>
<p>生產環境通常同時設定 ALARM 跟 OK action — ALARM 時通知 on-call，回到 OK 時自動 resolve incident。忘記設 OK action 會造成 on-call 收到告警但不知道何時恢復。</p>
<h3 id="跟-eventbridge-整合">跟 EventBridge 整合</h3>
<p>CloudWatch Alarm 狀態變更會自動送到 EventBridge（事件類型 <code>CloudWatch Alarm State Change</code>）。EventBridge rule 可以做更靈活的路由：</p>
<ul>
<li>根據 alarm name pattern 路由到不同 SNS topic</li>
<li>根據 alarm description 中的 severity tag 決定通知管道</li>
<li>多個 alarm 同時進入 ALARM 時觸發 incident 建立</li>
</ul>
<p>EventBridge 的路由能力彌補了 CloudWatch Alarm 本身路由邏輯簡單的限制。</p>
<h2 id="missing-data-處理">Missing data 處理</h2>
<h3 id="四種策略">四種策略</h3>
<p>Alarm evaluation 遇到缺失 datapoint 時，有四種處理方式：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>行為</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>missing</code></td>
          <td>維持上一個狀態</td>
          <td>多數場景的預設選擇</td>
      </tr>
      <tr>
          <td><code>breaching</code></td>
          <td>視為超過閾值</td>
          <td>metric 消失本身就是問題（heartbeat metric）</td>
      </tr>
      <tr>
          <td><code>notBreaching</code></td>
          <td>視為正常</td>
          <td>metric 在低流量時段自然消失</td>
      </tr>
      <tr>
          <td><code>ignore</code></td>
          <td>跳過該 period</td>
          <td>不影響 evaluation window</td>
      </tr>
  </tbody>
</table>
<p><code>breaching</code> 適合 heartbeat 類型的 metric — 服務應該持續回報 metric，停止回報代表服務掛了。<code>notBreaching</code> 適合流量驅動的 metric — 凌晨沒有 request 時自然沒有 latency datapoint，不應該觸發告警。</p>
<p>選錯 missing data 策略是 alarm flapping 的常見原因。Lambda function 的 metric 在沒有 invocation 時沒有 datapoint，用預設的 <code>missing</code> 或 <code>breaching</code> 都會造成問題。Lambda metric alarm 應該用 <code>notBreaching</code>。</p>
<h2 id="cross-region-限制">Cross-region 限制</h2>
<p>CloudWatch Alarm 跟 metric 綁定在同一個 region。跨 region 告警的兩種方式：</p>
<p><strong>Cross-account observability</strong>：monitoring account 可以看到 source account 的 CloudWatch 資料，但 alarm 仍然必須建在 metric 所在的 region。</p>
<p><strong>Custom metric replication</strong>：用 Lambda 或 Kinesis 把 metric 從 source region publish 到 central region，在 central region 建立統一 alarm。增加複雜度跟延遲，但能集中管理告警。</p>
<p>多數團隊選擇在每個 region 建各自的 alarm，用統一的 SNS topic（跨 region publish 到 central topic）收斂通知。告警邏輯去中心化，通知管道集中化。</p>
<h2 id="cost-考量">Cost 考量</h2>
<p>CloudWatch Alarm 的主要成本來自：</p>
<table>
  <thead>
      <tr>
          <th>計費項目</th>
          <th>計費方式</th>
          <th>常見數量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Standard resolution alarm</td>
          <td>每 alarm / month</td>
          <td>多數服務 10-50 個 alarm</td>
      </tr>
      <tr>
          <td>High-resolution alarm（10s）</td>
          <td>每 alarm / month（3 倍 standard）</td>
          <td>只用在關鍵 SLI</td>
      </tr>
      <tr>
          <td>Anomaly Detection alarm</td>
          <td>每 alarm / month（含 ML 模型）</td>
          <td>比 standard 貴約 2-3 倍</td>
      </tr>
      <tr>
          <td>Composite Alarm</td>
          <td>免費</td>
          <td>只算 child alarm</td>
      </tr>
  </tbody>
</table>
<p>數量控制的判準：每個服務 10-30 個 metric alarm 加 2-5 個 composite alarm 是合理範圍。超過 100 個 alarm 時先檢查是否有冗餘（同一 metric 不同 period 的重複 alarm）。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>告警設計原則：alarm 跟 dashboard 的搭配，見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 Dashboard 與 Alert 設計</a></li>
<li>SLI/SLO 對齊：把 alarm 閾值跟 SLO 對齊，見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI 量測與 SLO 訊號設計</a></li>
<li>Log-based alerting：從 log 產生 metric 再建 alarm，見 <a href="../logs-insights-governance/">CloudWatch Logs Insights 查詢與日誌治理</a></li>
<li>事故響應整合：alarm → EventBridge → PagerDuty / incident tool，見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 Incident Response 模組</a></li>
</ul>
]]></content:encoded></item><item><title>PromQL 與 Recording Rules 實務</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/promql-recording-rules/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/promql-recording-rules/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「PromQL 查詢」跟「Recording rules / Alerting rules」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Recording rules 把昂貴的即時聚合預先計算成低延遲 series，降低 dashboard 查詢成本並穩定 alerting 表達式。三個觸發點會讓團隊需要認真處理 PromQL 與 recording rules：&lt;/p>
&lt;p>Grafana dashboard 的某些 panel 載入超過 10 秒。原因通常是 panel 直接查詢高 cardinality 的原始 metric，每次載入都做一次完整的 range query aggregation。Recording rules 預先計算聚合結果，dashboard 只讀計算好的 series，查詢時間從秒級降到毫秒級。&lt;/p>
&lt;p>Alert 表達式想表達「最近 5 分鐘的 error rate 超過 1% 且持續 2 分鐘」，但寫出來的 PromQL 要麼漏抓（counter reset 時 rate 歸零）、要麼誤報（absent series 觸發 NaN 比較）。這類問題的根源是對 counter vs gauge 的語意差異理解不夠精確。&lt;/p>
&lt;p>Recording rules 堆了上百條但沒有命名慣例，新加的 rule 不確定是否跟既有 rule 重疊、也不確定 evaluation 順序是否正確。缺乏結構化的 rule 管理會讓 rule group 的 evaluation 時間逐漸超過 interval。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="counter-與-gauge-的查詢差異">Counter 與 gauge 的查詢差異&lt;/h3>
&lt;p>Counter 是單調遞增的累計值（total requests、total bytes sent），只在 process 重啟時 reset。Gauge 是瞬時值（temperature、goroutine count、queue depth），隨時上下波動。&lt;/p>
&lt;p>查詢 counter 必須用 &lt;code>rate()&lt;/code> 或 &lt;code>increase()&lt;/code> — 直接讀 counter 的原始值沒有業務意義（「從啟動到現在共 5 百萬個 request」不是有用訊號）。&lt;code>rate()&lt;/code> 回傳每秒平均增量，&lt;code>increase()&lt;/code> 回傳區間內的總增量。兩者都自動處理 counter reset — 當值突然下降時（process restart），rate 不會回傳負值。&lt;/p>
&lt;p>查詢 gauge 直接讀原始值即可，用 &lt;code>avg_over_time()&lt;/code>、&lt;code>max_over_time()&lt;/code> 等做區間統計。&lt;/p>
&lt;p>常見錯誤是對 gauge 用 rate（結果無意義 — 溫度的「每秒變化率」不是有用訊號）、或對 counter 直接取 max_over_time（只拿到 counter 的最大累計值、不是最大 QPS）。&lt;/p>
&lt;h3 id="rate-與-increase-的差異">rate 與 increase 的差異&lt;/h3>
&lt;p>&lt;code>rate(http_requests_total[5m])&lt;/code> 回傳 5 分鐘內的平均每秒 request 數。&lt;code>increase(http_requests_total[5m])&lt;/code> 回傳 5 分鐘內的總增量，等於 &lt;code>rate() * 300&lt;/code>。&lt;/p>
&lt;p>選擇取決於讀者的心智模型：SLI dashboard 用 rate（「每秒多少」直觀）；報表用 increase（「過去一小時多少筆」直觀）。&lt;/p>
&lt;p>Range 的選擇有一個實務邊界：range 至少要涵蓋 2 個 scrape interval。15 秒 scrape interval 搭配 &lt;code>rate(...[30s])&lt;/code> 是最小可用 range；&lt;code>rate(...[15s])&lt;/code> 可能只抓到一個 sample，回傳 NaN。production 常用 &lt;code>[5m]&lt;/code> 作為預設 range — 足夠平滑短暫抖動、又不會過度延遲異常偵測。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「PromQL 查詢」跟「Recording rules / Alerting rules」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Recording rules 把昂貴的即時聚合預先計算成低延遲 series，降低 dashboard 查詢成本並穩定 alerting 表達式。三個觸發點會讓團隊需要認真處理 PromQL 與 recording rules：</p>
<p>Grafana dashboard 的某些 panel 載入超過 10 秒。原因通常是 panel 直接查詢高 cardinality 的原始 metric，每次載入都做一次完整的 range query aggregation。Recording rules 預先計算聚合結果，dashboard 只讀計算好的 series，查詢時間從秒級降到毫秒級。</p>
<p>Alert 表達式想表達「最近 5 分鐘的 error rate 超過 1% 且持續 2 分鐘」，但寫出來的 PromQL 要麼漏抓（counter reset 時 rate 歸零）、要麼誤報（absent series 觸發 NaN 比較）。這類問題的根源是對 counter vs gauge 的語意差異理解不夠精確。</p>
<p>Recording rules 堆了上百條但沒有命名慣例，新加的 rule 不確定是否跟既有 rule 重疊、也不確定 evaluation 順序是否正確。缺乏結構化的 rule 管理會讓 rule group 的 evaluation 時間逐漸超過 interval。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="counter-與-gauge-的查詢差異">Counter 與 gauge 的查詢差異</h3>
<p>Counter 是單調遞增的累計值（total requests、total bytes sent），只在 process 重啟時 reset。Gauge 是瞬時值（temperature、goroutine count、queue depth），隨時上下波動。</p>
<p>查詢 counter 必須用 <code>rate()</code> 或 <code>increase()</code> — 直接讀 counter 的原始值沒有業務意義（「從啟動到現在共 5 百萬個 request」不是有用訊號）。<code>rate()</code> 回傳每秒平均增量，<code>increase()</code> 回傳區間內的總增量。兩者都自動處理 counter reset — 當值突然下降時（process restart），rate 不會回傳負值。</p>
<p>查詢 gauge 直接讀原始值即可，用 <code>avg_over_time()</code>、<code>max_over_time()</code> 等做區間統計。</p>
<p>常見錯誤是對 gauge 用 rate（結果無意義 — 溫度的「每秒變化率」不是有用訊號）、或對 counter 直接取 max_over_time（只拿到 counter 的最大累計值、不是最大 QPS）。</p>
<h3 id="rate-與-increase-的差異">rate 與 increase 的差異</h3>
<p><code>rate(http_requests_total[5m])</code> 回傳 5 分鐘內的平均每秒 request 數。<code>increase(http_requests_total[5m])</code> 回傳 5 分鐘內的總增量，等於 <code>rate() * 300</code>。</p>
<p>選擇取決於讀者的心智模型：SLI dashboard 用 rate（「每秒多少」直觀）；報表用 increase（「過去一小時多少筆」直觀）。</p>
<p>Range 的選擇有一個實務邊界：range 至少要涵蓋 2 個 scrape interval。15 秒 scrape interval 搭配 <code>rate(...[30s])</code> 是最小可用 range；<code>rate(...[15s])</code> 可能只抓到一個 sample，回傳 NaN。production 常用 <code>[5m]</code> 作為預設 range — 足夠平滑短暫抖動、又不會過度延遲異常偵測。</p>
<h3 id="histogram_quantile-的-bucket-設計">histogram_quantile 的 bucket 設計</h3>
<p>Prometheus histogram 使用預定義 bucket 邊界收集觀測值分布。<code>histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))</code> 計算 p95 延遲。</p>
<p>Bucket 邊界的設計直接影響精確度。預設 bucket（0.005, 0.01, 0.025, &hellip; 10）適合 HTTP request 延遲場景。如果服務的 p50 在 200ms 而 bucket 只有 0.1 跟 0.25 兩個相鄰邊界，p50 的計算會在 100ms-250ms 之間做線性內插，精確度受限。</p>
<p>設計 bucket 的判準：p50 和 p99 附近各要有 2-3 個相鄰 bucket，讓內插結果接近真實值。SLO 的 latency threshold 也應該落在某個 bucket 邊界上 — 例如 SLO 是 p95 &lt; 500ms，那 500ms 應該是一個 bucket 邊界。</p>
<p>每個 bucket 是一個 time series。10 個 bucket 的 histogram + 4 個 label 組合 = 40 個 series。Bucket 數量增加到 30 個時，同一個 metric 的 series 數量膨脹 3 倍。Bucket 設計要在精確度與 cardinality 之間取捨。</p>
<h3 id="label-matching-規則">Label matching 規則</h3>
<p>PromQL 的 binary operation（<code>/</code>、<code>+</code>、comparison）預設要求兩邊的 label set 完全一致才做 matching。這會在 error rate 計算時造成問題：<code>rate(http_requests_total{status=~&quot;5..&quot;}[5m])</code> 的 label set 含 status、但 <code>rate(http_requests_total[5m])</code> 的 total 不含 status。</p>
<p>解法是在分子做 aggregation 時 drop 掉 status label：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-promql" data-lang="promql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">sum</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="o">(</span><span class="nv">job</span><span class="p">,</span><span class="w"> </span><span class="nv">method</span><span class="o">)</span><span class="w"> </span><span class="o">(</span><span class="kr">rate</span><span class="o">(</span><span class="nv">http_requests_total</span><span class="p">{</span><span class="nl">status</span><span class="o">=~</span><span class="p">&#34;</span><span class="s">5..</span><span class="p">&#34;}[</span><span class="s">5m</span><span class="p">]</span><span class="o">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="o">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">sum</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="o">(</span><span class="nv">job</span><span class="p">,</span><span class="w"> </span><span class="nv">method</span><span class="o">)</span><span class="w"> </span><span class="o">(</span><span class="kr">rate</span><span class="o">(</span><span class="nv">http_requests_total</span><span class="p">[</span><span class="s">5m</span><span class="p">]</span><span class="o">))</span></span></span></code></pre></div><p><code>on()</code> 和 <code>ignoring()</code> 修飾符可以在不做 aggregation 的前提下控制 matching，但可讀性較差。production 推薦的做法是先用 <code>sum by()</code> 控制輸出的 label set，讓兩邊的 label 對齊。</p>
<h2 id="配置常見-sli-pattern">配置：常見 SLI Pattern</h2>
<h3 id="error-rate">Error rate</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># recording rule: 每 5 分鐘計算一次 error rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">groups</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sli_error_rate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_error_rate:ratio_rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="sd">          sum by (job) (rate(http_requests_total{status=~&#34;5..&#34;}[5m]))
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="sd">          /
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="sd">          sum by (job) (rate(http_requests_total[5m]))</span></span></span></code></pre></div><p>命名慣例 <code>level:metric:operations</code> 來自 Prometheus 官方建議：<code>job</code> 是聚合的 level、<code>http_request_error_rate</code> 是語意、<code>ratio_rate5m</code> 是操作。遵循慣例讓團隊成員看到 rule 名稱就知道它的聚合粒度與計算方式。</p>
<h3 id="latency-percentile">Latency percentile</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_duration_seconds:p95_rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="sd">          histogram_quantile(0.95,
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="sd">            sum by (job, le) (rate(http_request_duration_seconds_bucket[5m]))
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="sd">          )</span></span></span></code></pre></div><p><code>le</code> label 是 histogram bucket 邊界，<code>sum by (job, le)</code> 把 instance 維度聚合掉、保留 bucket 結構。如果漏掉 <code>le</code>，<code>histogram_quantile</code> 會回傳錯誤結果。</p>
<h3 id="throughput">Throughput</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_requests:rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">sum by (job) (rate(http_requests_total[5m]))</span></span></span></code></pre></div><p>三個 SLI — error rate、latency、throughput — 組成服務的 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">RED metrics</a>（Rate、Errors、Duration）。Recording rules 預先計算後，dashboard 只需讀三個 series。</p>
<h3 id="alerting-rule-搭配-recording-rule">Alerting rule 搭配 recording rule</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sli_alerts</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">      </span>- <span class="nt">alert</span><span class="p">:</span><span class="w"> </span><span class="l">HighErrorRate</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">job:http_request_error_rate:ratio_rate5m &gt; 0.01</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">        </span><span class="nt">for</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">        </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">          </span><span class="nt">severity</span><span class="p">:</span><span class="w"> </span><span class="l">page</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">        </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">          </span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{ $labels.job }} error rate above 1% for 5 minutes&#34;</span></span></span></code></pre></div><p>Alert 表達式讀 recording rule 而非原始 metric。好處有二：alert evaluation 更快（讀預先計算的 series）、alert 表達式與 dashboard panel 使用同一組 recording rule（確保看到的數字一致）。</p>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="series-churn-導致-absent-判斷失準">Series churn 導致 absent() 判斷失準</h3>
<p><code>absent(up{job=&quot;myapp&quot;})</code> 用來偵測 target 完全消失（沒在 scrape）。但在 K8s 環境，pod 頻繁 rolling update 會造成 series churn — 舊 pod 的 series 消失、新 pod 的 series 出現。短暫的時間窗內 <code>absent()</code> 可能誤觸。</p>
<p>修法：用 <code>absent_over_time(up{job=&quot;myapp&quot;}[5m])</code> 替代，要求整個 5 分鐘區間都沒有 series 才觸發。或用 <code>count(up{job=&quot;myapp&quot;}) == 0</code> 明確檢查 series 數量。</p>
<h3 id="recording-rules-circular-dependency">Recording rules circular dependency</h3>
<p>Rule group A 的 rule 讀 rule group B 的 recording rule、group B 又讀 group A 的結果。Prometheus 按 group name 字母序 evaluate，circular dependency 會讓一方讀到上一輪的 stale 結果。</p>
<p>預防方式：recording rules 形成 DAG（有向無環圖）。Prometheus 文件建議把 rule 分成 aggregation 層級 — 底層 group 算 raw metric 的 aggregation、上層 group 算 recording rule 的 aggregation。同一個 group 內的 rule 按宣告順序同步 evaluate。</p>
<h3 id="大-range-query-oom">大 range query OOM</h3>
<p>Dashboard panel 用 <code>rate(metric[30d])</code> 查詢 30 天 range — Prometheus 要載入 30 天的 samples 到記憶體做計算。100 萬 series × 30 天 × 15 秒 interval ≈ 1.7 億 samples per series 是不可能完成的查詢。</p>
<p>修法：長時間 range 必須用 recording rules 做 step-down aggregation。先用 <code>rate(...[5m])</code> recording rule 每 30 秒算一次、再用 <code>avg_over_time(recording_rule[30d])</code> 查詢。Recording rule 的 series 數量通常比原始 metric 少一到兩個數量級。</p>
<p>Prometheus 2.x 支援 <code>--query.max-samples</code> flag 限制單一 query 能處理的 sample 數量（預設 5000 萬），超過就回傳 error。這是 OOM 的最後防線、不是常態。</p>
<h3 id="counter-reset-導致-rate-異常">Counter reset 導致 rate 異常</h3>
<p>Process 重啟時 counter 歸零。<code>rate()</code> 和 <code>increase()</code> 自動偵測 counter reset 並補償，但有邊界條件：如果 scrape interval 內發生多次 restart（例如 crash loop），<code>rate()</code> 可能低估真實值（只能偵測到一次 reset）。</p>
<p>這種情境下的判讀：如果 <code>rate()</code> 的結果明顯低於預期、且同時段有 pod restart 紀錄，rate 低估是正常的。修法是解決 crash loop 本身、而非調整 PromQL。</p>
<h2 id="容量與-cost">容量與 Cost</h2>
<p>Recording rules 的 CPU 成本 = rule 數量 × 每條 rule 的 evaluation 時間 × (1 / evaluation interval)。</p>
<table>
  <thead>
      <tr>
          <th>Rule 數量</th>
          <th>平均 evaluation 時間</th>
          <th>Interval</th>
          <th>每秒 evaluation 消耗</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>50</td>
          <td>10ms</td>
          <td>30s</td>
          <td>50 × 0.01 / 30 = 0.017 core</td>
      </tr>
      <tr>
          <td>200</td>
          <td>50ms</td>
          <td>30s</td>
          <td>200 × 0.05 / 30 = 0.33 core</td>
      </tr>
      <tr>
          <td>500</td>
          <td>100ms</td>
          <td>15s</td>
          <td>500 × 0.1 / 15 = 3.33 core</td>
      </tr>
  </tbody>
</table>
<p>表中的 evaluation 時間是 10 萬到 50 萬 active series 規模下的經驗值。Series 數量影響 evaluation 時間 — 100 萬 series 的 complex aggregation 可能 500ms+，跟表中假設偏差很大。用 <code>prometheus_rule_group_last_duration_seconds</code> 量測自己環境的實際值。</p>
<p>500 條 complex rule 搭配 15 秒 interval 會消耗超過 3 個 CPU core 在 rule evaluation 上。這時候的修法方向有三：</p>
<ul>
<li>把 evaluation interval 放寬到 30s 或 60s（犧牲即時性）</li>
<li>把 rule 表達式最佳化（減少 aggregation 層數）</li>
<li>把 rule evaluation 卸載到 Mimir ruler（水平擴展）</li>
</ul>
<p>Recording rules 產生的新 series 也會增加 cardinality。200 條 recording rule × 平均 5 個 label 組合 = 1000 個新 series，通常可接受。但如果 recording rule 沒做 aggregation 而是直接 alias（<code>record: new_name expr: old_metric</code>），cardinality 不會減少，只增加了寫入成本。</p>
<p>判讀指標：<code>prometheus_rule_group_last_duration_seconds</code> 跟 <code>prometheus_rule_group_interval_seconds</code> 的比值。前者超過後者時，evaluation 跑不完、dashboard 跟 alert 都會延遲。見 <a href="../capacity-failure-modes/">容量規劃與故障模式</a> 的 Recording rule evaluation lag 段。</p>
<h3 id="recording-rules-作為成本控制工具">Recording rules 作為成本控制工具</h3>
<p><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">觀測成本治理案例</a>提出一個被低估的用法：recording rules 不只是加速查詢、也是控制 remote write 成本的手段。</p>
<p>模式是這樣的：application 暴露 200 個 label 組合的原始 metric（per-endpoint × per-status × per-region），recording rule 聚合成 5 個 label 組合（per-service × per-region）。如果 remote write 設定了 <code>write_relabel_configs</code> drop 掉原始 series、只 forward recording rule 產生的 aggregated series，remote write bandwidth 跟長期儲存的 cardinality 都大幅降低。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Step 1: recording rule 做 aggregation</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">groups</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cost_optimized</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span>- <span class="nt">record</span><span class="p">:</span><span class="w"> </span><span class="l">service_region:http_requests:rate5m</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">        </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="l">sum by (service, region) (rate(http_requests_total[5m]))</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="c"># Step 2: remote write 只送 aggregated series</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="nt">remote_write</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;http://mimir:9009/api/v1/push&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">__name__]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;service_region:.*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">keep</span></span></span></code></pre></div><p>這個模式的取捨：長期儲存只有 aggregated 資料、無法回溯到原始 per-endpoint 維度。如果事故時需要 per-endpoint 的歷史資料，要麼保留原始 series 在本地 Prometheus（短期 retention）、要麼接受長期儲存只有 aggregated 粒度。</p>
<p>適用場景判斷：如果 dashboard 跟 alert 都只看 service-level 聚合、per-endpoint 維度只在即時除錯時才需要（Prometheus 本地 15 天 retention 夠用），這個模式的成本節省值得。如果有合規需求要 per-endpoint 歷史資料（例如 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">FinTech 案例</a> 的 evidence chain），就不能 drop 原始 series。</p>
<h3 id="evaluation-interval-對-cpu-的影響">Evaluation interval 對 CPU 的影響</h3>
<p>Rule group 的 <code>interval</code> 決定 evaluation 頻率。同一組 rules 從 30s interval 改成 15s interval，CPU 消耗翻倍。從 30s 改成 60s，CPU 減半但 alert 跟 dashboard 的即時性下降。</p>
<p>經驗值：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議 interval</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SLI / SLO recording rules</td>
          <td>30s</td>
          <td>平衡即時性跟成本、多數 burn rate alert 的最小 window 是 5 分鐘</td>
      </tr>
      <tr>
          <td>Capacity trending rules</td>
          <td>60s-120s</td>
          <td>趨勢不需要秒級即時性</td>
      </tr>
      <tr>
          <td>High-frequency operational rules</td>
          <td>15s</td>
          <td>需要跟 scrape interval 對齊的場景（例如 real-time anomaly detection）</td>
      </tr>
  </tbody>
</table>
<p>15 秒 interval 的 rule group 要特別注意 evaluation 時間 — 如果 evaluation 本身花 12 秒，只剩 3 秒 buffer。<code>prometheus_rule_group_last_duration_seconds</code> 持續接近 <code>prometheus_rule_group_interval_seconds</code> 時，要麼拆 rule group 到不同 Prometheus instance、要麼放寬 interval。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="alertmanager">Alertmanager</h3>
<p>Alert rule 寫在 Prometheus 的 <code>rule_files</code> 內、觸發後送到 Alertmanager。Alertmanager 負責去重、分組、抑制與路由（route to PagerDuty / Slack / email）。Alert rule 的表達式跟 recording rule 共用同一組語意 — 讀 recording rule 而非原始 metric。</p>
<h3 id="grafana-dashboard">Grafana dashboard</h3>
<p>Grafana 的 Prometheus datasource 直接查 PromQL。Dashboard panel 推薦讀 recording rule series 而非寫 raw PromQL — 減少 dashboard 載入時間、確保 dashboard 跟 alert 看到的數字一致。</p>
<h3 id="對齊-slislo">對齊 SLI/SLO</h3>
<p>Recording rules 產生的 SLI metrics 是 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a> 的資料來源。SLO burn rate alert 也讀同一組 recording rule。確保 SLI recording rule 的 time window 跟 SLO window 對齊（例如 SLO 用 30 天 rolling window，recording rule 至少提供 5m 和 1h 兩個 aggregation 粒度給 burn rate 計算）。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：overview 跟日常操作入口</li>
<li><a href="../capacity-failure-modes/">容量規劃與故障模式</a>：recording rules 成長後的資源衝擊</li>
<li><a href="../remote-write-long-term-storage/">Remote Write 與長期儲存整合</a>：recording rule 在 remote write 架構下的部署選擇</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>：recording rules 如何餵給 SLO burn rate</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理</a>：recording rules 作為 cardinality 減量手段</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：recording rules 在 pre-aggregation 與 query tiering 中的定位</li>
</ul>
]]></content:encoded></item><item><title>Sentry Release Tracking 與 Session Replay</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/release-tracking-session-replay/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/release-tracking-session-replay/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry&lt;/a> 的 vendor deep article，深化 overview「Release / source map」跟「Session Replay」段。初次接觸 Sentry 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Release tracking 讓 Sentry 從「error 收集器」升級成「部署品質追蹤器」。每次部署標記一個 release，Sentry 自動計算 crash-free sessions、regressed errors 跟 release health。Session Replay 進一步把 error 的觸發脈絡從 stack trace 擴展到使用者操作錄影。兩者搭配使用時，團隊能看到「這個版本部署後、哪些使用者遇到什麼操作導致什麼錯誤」的完整鏈路。&lt;/p>
&lt;h2 id="release-health">Release Health&lt;/h2>
&lt;h3 id="核心概念">核心概念&lt;/h3>
&lt;p>Release health 追蹤每個版本的使用者體驗品質。核心指標：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>定義&lt;/th>
 &lt;th>健康閾值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Crash-free sessions&lt;/td>
 &lt;td>沒有 unhandled error 的 session 百分比&lt;/td>
 &lt;td>99.5% 以上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Crash-free users&lt;/td>
 &lt;td>沒有遇到 unhandled error 的使用者百分比&lt;/td>
 &lt;td>99.5% 以上&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Adoption rate&lt;/td>
 &lt;td>使用此版本的 session 佔比&lt;/td>
 &lt;td>依 rollout 策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Error count&lt;/td>
 &lt;td>此版本的 error event 數量&lt;/td>
 &lt;td>不應比前一版高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Crash-free sessions 跟 crash-free users 的差異：sessions 是頻率加權（一個使用者一天開 10 次 app，10 次都算），users 是去重的。Mobile app 通常看 crash-free users（使用者感知），web 通常看 crash-free sessions（頻率反映服務品質）。&lt;/p>
&lt;h3 id="release-標記">Release 標記&lt;/h3>
&lt;p>在 SDK 初始化時傳入 release 標記：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">sentry_sdk&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">init&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="n">dsn&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;...&amp;#34;&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="n">release&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;checkout-api@1.2.3&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="n">environment&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;production&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="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Release 命名慣例：&lt;code>&amp;lt;service&amp;gt;@&amp;lt;version&amp;gt;&lt;/code> 或 git SHA。用語意版本方便比較，用 git SHA 方便對應 commit。CI/CD pipeline 在 deploy step 自動設定。&lt;/p>
&lt;h3 id="deploy-標記">Deploy 標記&lt;/h3>
&lt;p>Release 建立後，用 Sentry CLI 或 API 標記 deploy：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sentry-cli releases deploys checkout-api@1.2.3 new &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --env production &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --started &lt;span class="k">$(&lt;/span>date -u +%s&lt;span class="k">)&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --finished &lt;span class="k">$(&lt;/span>date -u +%s&lt;span class="k">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Deploy 標記讓 Sentry 知道某個 release 何時部署到哪個環境。issue list 的 &amp;ldquo;First seen in release&amp;rdquo; 跟 &amp;ldquo;Regressed in release&amp;rdquo; 依賴這個資訊。&lt;/p>
&lt;h3 id="regressed-error-偵測">Regressed Error 偵測&lt;/h3>
&lt;p>Sentry 會追蹤已 resolve 的 issue。如果新 release 重新觸發了已 resolve 的 issue，Sentry 標記為 regression。這比人工追蹤有效 — 團隊不需要記住哪些 bug 修過，Sentry 自動偵測回歸。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a> 的 vendor deep article，深化 overview「Release / source map」跟「Session Replay」段。初次接觸 Sentry 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Release tracking 讓 Sentry 從「error 收集器」升級成「部署品質追蹤器」。每次部署標記一個 release，Sentry 自動計算 crash-free sessions、regressed errors 跟 release health。Session Replay 進一步把 error 的觸發脈絡從 stack trace 擴展到使用者操作錄影。兩者搭配使用時，團隊能看到「這個版本部署後、哪些使用者遇到什麼操作導致什麼錯誤」的完整鏈路。</p>
<h2 id="release-health">Release Health</h2>
<h3 id="核心概念">核心概念</h3>
<p>Release health 追蹤每個版本的使用者體驗品質。核心指標：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>定義</th>
          <th>健康閾值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Crash-free sessions</td>
          <td>沒有 unhandled error 的 session 百分比</td>
          <td>99.5% 以上</td>
      </tr>
      <tr>
          <td>Crash-free users</td>
          <td>沒有遇到 unhandled error 的使用者百分比</td>
          <td>99.5% 以上</td>
      </tr>
      <tr>
          <td>Adoption rate</td>
          <td>使用此版本的 session 佔比</td>
          <td>依 rollout 策略</td>
      </tr>
      <tr>
          <td>Error count</td>
          <td>此版本的 error event 數量</td>
          <td>不應比前一版高</td>
      </tr>
  </tbody>
</table>
<p>Crash-free sessions 跟 crash-free users 的差異：sessions 是頻率加權（一個使用者一天開 10 次 app，10 次都算），users 是去重的。Mobile app 通常看 crash-free users（使用者感知），web 通常看 crash-free sessions（頻率反映服務品質）。</p>
<h3 id="release-標記">Release 標記</h3>
<p>在 SDK 初始化時傳入 release 標記：</p>





<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="n">sentry_sdk</span><span class="o">.</span><span class="n">init</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">dsn</span><span class="o">=</span><span class="s2">&#34;...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">release</span><span class="o">=</span><span class="s2">&#34;checkout-api@1.2.3&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">environment</span><span class="o">=</span><span class="s2">&#34;production&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>Release 命名慣例：<code>&lt;service&gt;@&lt;version&gt;</code> 或 git SHA。用語意版本方便比較，用 git SHA 方便對應 commit。CI/CD pipeline 在 deploy step 自動設定。</p>
<h3 id="deploy-標記">Deploy 標記</h3>
<p>Release 建立後，用 Sentry CLI 或 API 標記 deploy：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sentry-cli releases deploys checkout-api@1.2.3 new <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --env production <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --started <span class="k">$(</span>date -u +%s<span class="k">)</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --finished <span class="k">$(</span>date -u +%s<span class="k">)</span></span></span></code></pre></div><p>Deploy 標記讓 Sentry 知道某個 release 何時部署到哪個環境。issue list 的 &ldquo;First seen in release&rdquo; 跟 &ldquo;Regressed in release&rdquo; 依賴這個資訊。</p>
<h3 id="regressed-error-偵測">Regressed Error 偵測</h3>
<p>Sentry 會追蹤已 resolve 的 issue。如果新 release 重新觸發了已 resolve 的 issue，Sentry 標記為 regression。這比人工追蹤有效 — 團隊不需要記住哪些 bug 修過，Sentry 自動偵測回歸。</p>
<p>Regression 通知的準確度取決於 grouping 品質。如果 grouping 不準（見 <a href="../error-grouping-fingerprinting/">Error Grouping 與 Fingerprinting</a>），regression 偵測也會不準 — 不同 bug 被合成同一 issue 時，resolve 一個 bug 後另一個觸發會被誤判為 regression。</p>
<h3 id="source-map-上傳">Source map 上傳</h3>
<p>前端 minified code 的 stack trace 不可讀。上傳 source map 讓 Sentry 還原原始 source code 位置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sentry-cli releases files checkout-api@1.2.3 upload-sourcemaps <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --url-prefix <span class="s1">&#39;~/static/js&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  ./build/static/js</span></span></code></pre></div><p>Source map 上傳必須在 deploy 前完成，且 release 版本跟前端 build 版本一致。版本不一致時，Sentry 找不到對應的 source map，stack trace 仍然是 minified。</p>
<p>CI/CD 整合：在 build step 之後、deploy step 之前上傳 source map。多數框架（Next.js、Vite、Webpack）有 Sentry plugin 自動處理。</p>
<h2 id="session-replay">Session Replay</h2>
<h3 id="核心能力">核心能力</h3>
<p>Session Replay 錄製使用者在網頁上的操作。Sentry 記錄的是 DOM mutation 跟使用者事件的結構化資料，播放時 replay DOM 變化，效果類似影片但資料量遠小於螢幕錄影。</p>
<p>replay 跟 error 關聯：Sentry 在 error event 中附帶 replay ID，讓工程師從 issue detail 直接跳到 error 發生前後的使用者操作。</p>
<h3 id="隱私設定">隱私設定</h3>
<p>Session Replay 預設會遮罩敏感資訊：</p>
<table>
  <thead>
      <tr>
          <th>遮罩類型</th>
          <th>預設行為</th>
          <th>自訂方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>文字內容</td>
          <td>所有文字替換成 <code>*</code></td>
          <td><code>maskAllText: false</code> 關閉、或用 CSS class <code>sentry-mask</code> 指定</td>
      </tr>
      <tr>
          <td>輸入框</td>
          <td>所有 input value 遮罩</td>
          <td><code>maskAllInputs: false</code> 關閉（注意 PII 風險）</td>
      </tr>
      <tr>
          <td>圖片</td>
          <td>不遮罩（但 <code>&lt;img&gt;</code> 從原始 URL 載入）</td>
          <td><code>blockAllMedia: true</code> 遮蔽所有媒體</td>
      </tr>
      <tr>
          <td>特定元素</td>
          <td>不遮罩</td>
          <td>加 <code>data-sentry-block</code> attribute 完全隱藏</td>
      </tr>
  </tbody>
</table>
<p>PII 合規考量：</p>
<ul>
<li>預設 <code>maskAllText: true</code> + <code>maskAllInputs: true</code> 是安全起點</li>
<li>GDPR / CCPA 場景需要額外確認：replay 資料存在 Sentry SaaS（美國資料中心），跨境傳輸需要評估</li>
<li>Self-hosted Sentry 可以把 replay 資料留在自己的基礎設施</li>
</ul>
<h3 id="sampling-策略">Sampling 策略</h3>
<p>Session Replay 會增加前端 SDK 的 payload 大小跟 Sentry 的 event quota。用 sampling rate 控制：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="nx">Sentry</span><span class="p">.</span><span class="nx">init</span><span class="p">({</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nx">dsn</span><span class="o">:</span> <span class="s2">&#34;...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">replaysSessionSampleRate</span><span class="o">:</span> <span class="mf">0.1</span><span class="p">,</span>  <span class="c1">// 10% 的 session 錄影
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"></span>  <span class="nx">replaysOnErrorSampleRate</span><span class="o">:</span> <span class="mf">1.0</span><span class="p">,</span>  <span class="c1">// error 發生時 100% 錄影
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>推薦策略：<code>replaysSessionSampleRate</code> 用低值（1-10%），<code>replaysOnErrorSampleRate</code> 用 100%。目的是確保每個 error 都有 replay 可看，但不錄所有正常 session。</p>
<p>高流量網站（每日百萬 session 以上）可能需要把 <code>replaysSessionSampleRate</code> 設到 0，只在 error 時才錄。session replay 的 quota 消耗速度可以在 Sentry Usage Stats 頁面監控。</p>
<h2 id="performance-monitoring">Performance Monitoring</h2>
<h3 id="transaction-based-tracing">Transaction-based tracing</h3>
<p>Sentry 的 performance monitoring 用 transaction / span 結構（跟 OpenTelemetry 的 trace / span 概念對齊）。每個 HTTP request、page load 或自訂操作是一個 transaction，transaction 內的子操作是 span。</p>





<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="k">with</span> <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">start_transaction</span><span class="p">(</span><span class="n">op</span><span class="o">=</span><span class="s2">&#34;checkout&#34;</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="s2">&#34;POST /api/checkout&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">with</span> <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">start_span</span><span class="p">(</span><span class="n">op</span><span class="o">=</span><span class="s2">&#34;db&#34;</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;insert order&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="c1"># DB operation</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="k">pass</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">with</span> <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">start_span</span><span class="p">(</span><span class="n">op</span><span class="o">=</span><span class="s2">&#34;http&#34;</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&#34;payment gateway&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="c1"># External API call</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="k">pass</span></span></span></code></pre></div><p>自動 instrumentation 會自動建立 transaction 跟 span（HTTP framework、DB driver、HTTP client）。手動 span 用在自訂業務邏輯或自動 instrumentation 沒覆蓋的路徑。</p>
<h3 id="otel-context-整合">OTel context 整合</h3>
<p>Sentry SDK 支援 OTel context propagation — 如果 upstream service 用 OTel SDK 產生 trace，Sentry SDK 會接受 <code>traceparent</code> header 中的 trace_id 跟 parent_span_id，把自己的 transaction 接到同一條 trace。</p>
<p>整合方式：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>設定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sentry SDK 接收 OTel context</td>
          <td>預設支援 W3C Trace Context、不需額外設定</td>
      </tr>
      <tr>
          <td>Sentry 資料送到 OTel backend</td>
          <td>用 Sentry 的 OTel exporter（experimental）</td>
      </tr>
      <tr>
          <td>OTel SDK 送資料到 Sentry</td>
          <td>OTel SDK → OTLP exporter → Sentry（Sentry 支援 OTLP ingestion）</td>
      </tr>
  </tbody>
</table>
<p>常見架構：backend service 用 OTel SDK + Collector，frontend 用 Sentry SDK（前端 error tracking 跟 session replay 是 Sentry 的強項）。兩者透過 trace_id 關聯，在 Sentry 看 frontend error + replay，在 OTel backend 看 backend trace。</p>
<h3 id="web-vitals">Web Vitals</h3>
<p>前端 SDK 自動收集 Core Web Vitals（LCP、FID / INP、CLS）跟 TTFB。這些指標跟 error 在同一個 dashboard，讓團隊在 release 後同時看 error regression 跟效能 regression。</p>
<p>Web Vitals 的觀測不需要額外設定 — 前端 SDK 自動收集。但 sampling rate 會影響資料量 — <code>tracesSampleRate</code> 設太低時，Web Vitals 的 sample 數量可能不夠做統計比較。</p>
<h2 id="self-hosted-vs-saas">Self-hosted vs SaaS</h2>
<h3 id="決策維度">決策維度</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>SaaS（sentry.io）</th>
          <th>Self-hosted</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>維運</td>
          <td>Sentry 負責</td>
          <td>自己維運（docker-compose、20+ 容器）</td>
      </tr>
      <tr>
          <td>資料位置</td>
          <td>Sentry 資料中心（美國為主）</td>
          <td>自己的基礎設施</td>
      </tr>
      <tr>
          <td>功能完整度</td>
          <td>全功能</td>
          <td>社群版功能略少（部分企業功能不含）</td>
      </tr>
      <tr>
          <td>升級</td>
          <td>自動</td>
          <td>手動（每月有新版、升級需要停機）</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>Event-based pricing</td>
          <td>基礎設施 + 人力成本</td>
      </tr>
      <tr>
          <td>Replay / Profiling</td>
          <td>含</td>
          <td>含（但 storage 自負）</td>
      </tr>
  </tbody>
</table>
<h3 id="何時選-self-hosted">何時選 self-hosted</h3>
<p>資料必須留在特定地理區域（GDPR / 特定產業法規）、或企業 security policy 不允許 error data 送到第三方 — 這是 self-hosted 的核心理由。</p>
<p>Self-hosted Sentry 的維運成本常被低估：20+ 個容器（Kafka、ClickHouse、PostgreSQL、Redis、Snuba、Relay 等）、升級可能需要資料庫 migration、troubleshooting 時沒有 vendor 支援。中小團隊通常 SaaS 的 event pricing 比 self-hosted 的人力成本低。</p>
<h3 id="混合模式">混合模式</h3>
<p>部分團隊用混合模式：production error 送 Sentry SaaS（低維運），但 audit-sensitive 的資料（PII-heavy environment）走 self-hosted。兩套 Sentry instance 各自獨立，不共享 issue。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>Error grouping 策略：在 issue 數量失控前建立 fingerprint rule，見 <a href="../error-grouping-fingerprinting/">Error Grouping 與 Fingerprinting</a></li>
<li>觀測證據整合：把 Sentry issue link 放進 evidence package，見 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>Client-side monitoring：Sentry 的前端 SDK 跟 RUM 的定位互補，見 <a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side Monitoring</a></li>
<li>事故響應整合：Sentry alert → PagerDuty / incident.io，見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 Incident Response 模組</a></li>
</ul>
]]></content:encoded></item><item><title>New Relic → Datadog：APM schema 對位 + agent 替換 + dashboard 重建</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-from-new-relic/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-from-new-relic/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://newrelic.com/">New Relic&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Schema = High（NRQL ↔ Datadog query、APM agent 不同）→ Type A phased translation&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>中型 SaaS 跑 New Relic 3-5 年、production observability 飽和、團隊發現幾個問題：cost 暴漲（per-host APM + custom event + synthetic）、APM trace 對 Kubernetes-native workload 不夠細、跟 PagerDuty / Slack integration 雖然有但 latency 偏高。同期 Datadog 在 K8s monitoring + APM 端深度整合、cost model 在 100-500 host 規模更可預測。&lt;/p>
&lt;p>評估遷移時、發現 New Relic → Datadog 不是「換個 agent 就好」 — APM schema、NRQL 查詢語言、custom dashboard、synthetic monitoring rule 全部要 &lt;em>重新對位&lt;/em>；application code 端的 agent 也要 &lt;em>完全換 binary&lt;/em>。是 Type A 高 schema 差 migration、不是 drop-in。&lt;/p>
&lt;h2 id="為什麼遷cost--k8s-native--vendor-consolidation-三條-driver">為什麼遷：cost / k8s-native / vendor consolidation 三條 driver&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>觸發場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Cost&lt;/strong>&lt;/td>
 &lt;td>New Relic per-host pricing + custom event + synthetic 加總爆、Datadog 在 K8s 場景單 host 多 container 更划算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>K8s-native&lt;/strong>&lt;/td>
 &lt;td>Datadog agent 對 K8s sidecar / DaemonSet / autodiscovery 更深&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Vendor consolidation&lt;/strong>&lt;/td>
 &lt;td>已用 Datadog log / metric、APM 統一 vendor 降工具切換 cost&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向 driver（Datadog → New Relic）：&lt;/p>
&lt;ul>
&lt;li>New Relic 對 &lt;em>full-stack observability&lt;/em>（APM + browser + mobile + synthetic）的整合包仍領先&lt;/li>
&lt;li>已深用 New Relic NRQL 跟 New Relic University 培訓的 organization、不切&lt;/li>
&lt;/ul>
&lt;h2 id="schema-對位">Schema 對位&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>New Relic concept&lt;/th>
 &lt;th>Datadog 對應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>APM agent (NR Java / Python / Node)&lt;/td>
 &lt;td>Datadog agent + APM tracer library&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>NRQL query&lt;/td>
 &lt;td>Datadog query (Metric / Log / Trace)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Synthetic monitor&lt;/td>
 &lt;td>Datadog Synthetic Tests&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Custom event&lt;/td>
 &lt;td>Datadog custom metric / log event&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>NRQL alert condition&lt;/td>
 &lt;td>Datadog monitor&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>New Relic dashboard&lt;/td>
 &lt;td>Datadog dashboard (need rebuild)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Apdex score&lt;/td>
 &lt;td>Datadog APM &lt;code>apm.service.errors&lt;/code> + &lt;code>apm.service.latency&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Distributed trace&lt;/td>
 &lt;td>Datadog APM trace（OpenTelemetry-compatible）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="phase-0audit--classify">Phase 0：Audit + classify&lt;/h2>
&lt;ul>
&lt;li>列所有 application 跟對應 NR agent version&lt;/li>
&lt;li>列所有 NRQL alert / dashboard / synthetic monitor&lt;/li>
&lt;li>估每月 cost 跟 Datadog 對比&lt;/li>
&lt;/ul>
&lt;h2 id="phase-1schema-對位--datadog-cluster-建置">Phase 1：Schema 對位 + Datadog cluster 建置&lt;/h2>
&lt;ul>
&lt;li>Datadog organization 申請 / IAM integration&lt;/li>
&lt;li>VPC peering / private link (如果用 self-hosted agent)&lt;/li>
&lt;/ul>
&lt;h2 id="phase-2translation-pipeline-3-tier">Phase 2：Translation pipeline (3-tier)&lt;/h2>
&lt;ul>
&lt;li>Tier 1: Datadog 端 import tool（API-based NRQL → Datadog query 轉換、cover ~40-60%）&lt;/li>
&lt;li>Tier 2: LLM-assisted（剩餘 query / dashboard）&lt;/li>
&lt;li>Tier 3: manual (synthetic / complex correlation)&lt;/li>
&lt;/ul>
&lt;h2 id="phase-3parallel-run-dual-agent-4-8-週">Phase 3：Parallel run (dual-agent 4-8 週)&lt;/h2>
&lt;p>兩個 agent 跑同 application、metric / trace / log 雙端輸出、SOC 比對 detection coverage / alert / dashboard 一致性。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="https://newrelic.com/">New Relic</a> 跟 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Schema = High（NRQL ↔ Datadog query、APM agent 不同）→ Type A phased translation</em>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>中型 SaaS 跑 New Relic 3-5 年、production observability 飽和、團隊發現幾個問題：cost 暴漲（per-host APM + custom event + synthetic）、APM trace 對 Kubernetes-native workload 不夠細、跟 PagerDuty / Slack integration 雖然有但 latency 偏高。同期 Datadog 在 K8s monitoring + APM 端深度整合、cost model 在 100-500 host 規模更可預測。</p>
<p>評估遷移時、發現 New Relic → Datadog 不是「換個 agent 就好」 — APM schema、NRQL 查詢語言、custom dashboard、synthetic monitoring rule 全部要 <em>重新對位</em>；application code 端的 agent 也要 <em>完全換 binary</em>。是 Type A 高 schema 差 migration、不是 drop-in。</p>
<h2 id="為什麼遷cost--k8s-native--vendor-consolidation-三條-driver">為什麼遷：cost / k8s-native / vendor consolidation 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Cost</strong></td>
          <td>New Relic per-host pricing + custom event + synthetic 加總爆、Datadog 在 K8s 場景單 host 多 container 更划算</td>
      </tr>
      <tr>
          <td><strong>K8s-native</strong></td>
          <td>Datadog agent 對 K8s sidecar / DaemonSet / autodiscovery 更深</td>
      </tr>
      <tr>
          <td><strong>Vendor consolidation</strong></td>
          <td>已用 Datadog log / metric、APM 統一 vendor 降工具切換 cost</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（Datadog → New Relic）：</p>
<ul>
<li>New Relic 對 <em>full-stack observability</em>（APM + browser + mobile + synthetic）的整合包仍領先</li>
<li>已深用 New Relic NRQL 跟 New Relic University 培訓的 organization、不切</li>
</ul>
<h2 id="schema-對位">Schema 對位</h2>
<table>
  <thead>
      <tr>
          <th>New Relic concept</th>
          <th>Datadog 對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>APM agent (NR Java / Python / Node)</td>
          <td>Datadog agent + APM tracer library</td>
      </tr>
      <tr>
          <td>NRQL query</td>
          <td>Datadog query (Metric / Log / Trace)</td>
      </tr>
      <tr>
          <td>Synthetic monitor</td>
          <td>Datadog Synthetic Tests</td>
      </tr>
      <tr>
          <td>Custom event</td>
          <td>Datadog custom metric / log event</td>
      </tr>
      <tr>
          <td>NRQL alert condition</td>
          <td>Datadog monitor</td>
      </tr>
      <tr>
          <td>New Relic dashboard</td>
          <td>Datadog dashboard (need rebuild)</td>
      </tr>
      <tr>
          <td>Apdex score</td>
          <td>Datadog APM <code>apm.service.errors</code> + <code>apm.service.latency</code></td>
      </tr>
      <tr>
          <td>Distributed trace</td>
          <td>Datadog APM trace（OpenTelemetry-compatible）</td>
      </tr>
  </tbody>
</table>
<h2 id="phase-0audit--classify">Phase 0：Audit + classify</h2>
<ul>
<li>列所有 application 跟對應 NR agent version</li>
<li>列所有 NRQL alert / dashboard / synthetic monitor</li>
<li>估每月 cost 跟 Datadog 對比</li>
</ul>
<h2 id="phase-1schema-對位--datadog-cluster-建置">Phase 1：Schema 對位 + Datadog cluster 建置</h2>
<ul>
<li>Datadog organization 申請 / IAM integration</li>
<li>VPC peering / private link (如果用 self-hosted agent)</li>
</ul>
<h2 id="phase-2translation-pipeline-3-tier">Phase 2：Translation pipeline (3-tier)</h2>
<ul>
<li>Tier 1: Datadog 端 import tool（API-based NRQL → Datadog query 轉換、cover ~40-60%）</li>
<li>Tier 2: LLM-assisted（剩餘 query / dashboard）</li>
<li>Tier 3: manual (synthetic / complex correlation)</li>
</ul>
<h2 id="phase-3parallel-run-dual-agent-4-8-週">Phase 3：Parallel run (dual-agent 4-8 週)</h2>
<p>兩個 agent 跑同 application、metric / trace / log 雙端輸出、SOC 比對 detection coverage / alert / dashboard 一致性。</p>
<h2 id="phase-4cutover--cleanup">Phase 4：Cutover + cleanup</h2>
<ul>
<li>Application 端切 agent</li>
<li>New Relic license downgrade / cancel</li>
<li>Decommission timeline 3-6 個月（保留歷史查詢能力）</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1nrql-不直接對位-datadog-query">Case 1：NRQL 不直接對位 Datadog query</h3>
<p><strong>徵兆</strong>：NRQL <code>SELECT count(*) FROM Transaction FACET name WHERE duration &gt; 5 SINCE 1 hour ago</code> 在 Datadog 端需要拆 metric query + filter + group by；翻譯後語意對等但 syntax 完全不同、SOC analyst 學習曲線陡。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>翻譯腳本 + LLM-assisted、保留 NRQL 字面 + Datadog query 對照表（runbook）</li>
<li>SOC training，1-2 週 hands-on</li>
<li>部分 query 改 <em>Datadog dashboard widget</em>、不用直接 query</li>
</ol>
<h3 id="case-2synthetic-monitor-對位失敗">Case 2：Synthetic monitor 對位失敗</h3>
<p><strong>徵兆</strong>：NR Synthetic 跑 100+ ping / browser / API test、切 Datadog Synthetic 後發現 <em>step-based</em> monitor 對應的「Browser Test」配置複雜、setup 工作量 2-3 倍預估。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 跑 sample synthetic、估真實 setup cost</li>
<li>優先遷 critical synthetic、其他評估退役</li>
<li>用 Datadog API + Terraform 自動化、避免 UI 手動建</li>
</ol>
<h3 id="case-3cost-模型反轉">Case 3：Cost 模型反轉</h3>
<p><strong>徵兆</strong>：cutover 後第一個月 Datadog 帳單比 NR 高 30%；breakdown 後發現 <em>log retention + custom metric series + log indexing</em> 三個項目超預估。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-migration 估 Datadog cost 必須含 <em>log indexing pricing</em>（按 indexed event 計）、不是純 ingest</li>
<li>Application 端 log scrub PII + sample debug log、降 ingest GB</li>
<li>Custom metric cardinality control（tag combination 爆 series count）</li>
</ol>
<h3 id="case-4dashboard-自動轉失敗人工-rebuild-80">Case 4：Dashboard 自動轉失敗、人工 rebuild 80%</h3>
<p><strong>徵兆</strong>：用 Datadog import tool 跑 NR dashboard、80% widget 缺 / 對應錯；team 估 2 週 dashboard rebuild、實際跑 6-8 週。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>接受重建</strong>：production dashboard 必須人工重建、不要期待自動轉</li>
<li><strong>Prioritize</strong>：先重建 SOC critical 30%、其他 deprecate</li>
<li><strong>Migration window 增 4-6 週</strong>：dashboard rebuild 是 underestimated effort</li>
</ol>
<h3 id="case-5cross-platform-metric-命名差">Case 5：Cross-platform metric 命名差</h3>
<p><strong>徵兆</strong>：NR 端 metric <code>Apdex/Apdex</code> 在 Datadog 沒對應、application code 寫死 metric name 失效；alert query 對 NR-specific metric 全失效。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 列所有 NR-specific metric、application code 改用 OpenTelemetry-style metric 命名</li>
<li>Datadog query 端 rebuild、用 application-level metric name 而非 vendor-specific</li>
<li>長期：metric naming 用 OpenTelemetry semantic conventions、避免 vendor lock</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>New Relic</th>
          <th>Datadog</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pricing model</td>
          <td>per-host + custom event / synthetic</td>
          <td>per-host APM + log indexing + custom metric</td>
      </tr>
      <tr>
          <td>K8s-friendly</td>
          <td>中、autodiscovery 有但配置複雜</td>
          <td>高、K8s-native autodiscovery first-class</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>2-4 FTE × 2-3 個月</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.6</td>
          <td>0.3-0.6（相當）</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-datadog--grafana-stack-migration-對位">跟 <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack migration</a> 對位</h3>
<p>兩種 Datadog 端的後續路線：</p>
<ul>
<li>切到 Datadog 後 <em>繼續用</em>（穩定 multi-year）</li>
<li>切到 Datadog 後 <em>再切 Grafana Stack</em> 省 cost（multi-tool 拆分、Type D）</li>
</ul>
<p>多數 organization 第一輪 NR → Datadog 已花 2-3 個月、不會立刻再切；至少穩定 1-2 年。</p>
<h3 id="跟-opentelemetry-對齊">跟 OpenTelemetry 對齊</h3>
<p>Migration 順便升 OTel 化 application、避免下次 vendor 切換重複工作量。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Target vendor：<a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></li>
<li>平行 migration playbook (Type A)：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> / <a href="/blog/backend/01-database/vendors/mysql/migrate-to-postgresql/" data-link-title="MySQL → PostgreSQL：從 SQL dialect diff 跑出來的 Type A 6-phase migration" data-link-desc="MySQL → PostgreSQL 是 Type A 高 schema 差 migration 的標準形態 — SQL dialect / collation / case sensitivity / replication 模型差異主導；用 pgloader / AWS DMS / 自管 dual-write 三條 path、5 個 production 踩雷（auto_increment vs SERIAL / charset 跟 collation / case sensitivity / index syntax / triggers）">MySQL → PostgreSQL</a></li>
<li>平行 migration playbook (D-type 對位)：<a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Self-managed Prometheus → Grafana Cloud Metrics：feature × ops × cost 對照</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/migrate-prometheus-to-cloud-metrics/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/migrate-prometheus-to-cloud-metrics/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a>（Grafana Cloud Metrics、Mimir-backed）。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Operational = High → Type C operational redesign hybrid&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="feature--ops--cost-三維對照">Feature / ops / cost 三維對照&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Self-managed Prometheus&lt;/th>
 &lt;th>Grafana Cloud Metrics&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Storage backend&lt;/td>
 &lt;td>Local disk + remote_write (optional)&lt;/td>
 &lt;td>Mimir + S3 (auto cold tier)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention&lt;/td>
 &lt;td>TSDB local 15 天 default&lt;/td>
 &lt;td>13 個月 default、可延長&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HA&lt;/td>
 &lt;td>Two Prometheus + sidecar&lt;/td>
 &lt;td>Built-in multi-AZ&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cardinality limit&lt;/td>
 &lt;td>自管 limit + recording rule&lt;/td>
 &lt;td>1.5M active series / tier、scale-up 配額&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query API&lt;/td>
 &lt;td>PromQL + Prometheus HTTP API&lt;/td>
 &lt;td>完全相容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert&lt;/td>
 &lt;td>Alertmanager self-managed&lt;/td>
 &lt;td>Grafana Cloud Alerting&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dashboard&lt;/td>
 &lt;td>Grafana self-managed&lt;/td>
 &lt;td>Grafana Cloud (included)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Long-term storage&lt;/td>
 &lt;td>Thanos / Cortex / Mimir 自管&lt;/td>
 &lt;td>Mimir 內建&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost (mid-tier)&lt;/td>
 &lt;td>$500-2000 / mo + ops FTE&lt;/td>
 &lt;td>$300-1500 / mo (按 series)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational FTE&lt;/td>
 &lt;td>0.3-0.8&lt;/td>
 &lt;td>0.05-0.15&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit&lt;/a>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>Low（PromQL + API 完全相容）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>（HA / retention / scaling 全託管）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Paradigm&lt;/td>
 &lt;td>Low（同 Prometheus metric paradigm）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Components&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>Low（remote_write endpoint 改）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Operational = High → Type C standard。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 跟 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（Grafana Cloud Metrics、Mimir-backed）。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Operational = High → Type C operational redesign hybrid</em>。</p></blockquote>
<h2 id="feature--ops--cost-三維對照">Feature / ops / cost 三維對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed Prometheus</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Storage backend</td>
          <td>Local disk + remote_write (optional)</td>
          <td>Mimir + S3 (auto cold tier)</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>TSDB local 15 天 default</td>
          <td>13 個月 default、可延長</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Two Prometheus + sidecar</td>
          <td>Built-in multi-AZ</td>
      </tr>
      <tr>
          <td>Cardinality limit</td>
          <td>自管 limit + recording rule</td>
          <td>1.5M active series / tier、scale-up 配額</td>
      </tr>
      <tr>
          <td>Query API</td>
          <td>PromQL + Prometheus HTTP API</td>
          <td>完全相容</td>
      </tr>
      <tr>
          <td>Alert</td>
          <td>Alertmanager self-managed</td>
          <td>Grafana Cloud Alerting</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Grafana self-managed</td>
          <td>Grafana Cloud (included)</td>
      </tr>
      <tr>
          <td>Long-term storage</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Mimir 內建</td>
      </tr>
      <tr>
          <td>Cost (mid-tier)</td>
          <td>$500-2000 / mo + ops FTE</td>
          <td>$300-1500 / mo (按 series)</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8</td>
          <td>0.05-0.15</td>
      </tr>
  </tbody>
</table>
<p>跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6 維 diff dimension audit</a>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Low（PromQL + API 完全相容）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td><strong>High</strong>（HA / retention / scaling 全託管）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low（同 Prometheus metric paradigm）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Low（remote_write endpoint 改）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Operational = High → Type C standard。</p>
<h2 id="為什麼遷retention--ops--vendor-consolidation-三條-driver">為什麼遷：retention / ops / vendor consolidation 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Retention</td>
          <td>Prometheus TSDB local 預設 15 天、長期 retention 需要 Thanos / Cortex / Mimir 自管</td>
      </tr>
      <tr>
          <td>Ops FTE</td>
          <td>Self-managed Prometheus + Alertmanager + Grafana 自管全部加起來 0.5-1 FTE</td>
      </tr>
      <tr>
          <td>Vendor consolidation</td>
          <td>已用 Grafana Cloud（logs / traces）、metric 加進 stack 統一</td>
      </tr>
  </tbody>
</table>
<h2 id="operational-redesign">Operational redesign</h2>
<table>
  <thead>
      <tr>
          <th>Concept</th>
          <th>Self-managed</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>Helm chart + manual config</td>
          <td>UI 一鍵建</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>Two Prometheus 配置</td>
          <td>內建 multi-AZ Mimir</td>
      </tr>
      <tr>
          <td>Long-term retention</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Built-in (S3-backed)</td>
      </tr>
      <tr>
          <td>Cardinality control</td>
          <td>Manual recording rule + relabel</td>
          <td>Adaptive sampling + cardinality limit</td>
      </tr>
      <tr>
          <td>Alerting</td>
          <td>Alertmanager 自管</td>
          <td>Grafana Cloud Alerting (integrated)</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Grafana self-host</td>
          <td>Grafana Cloud (free tier 包含)</td>
      </tr>
  </tbody>
</table>
<h2 id="migration-4-phase">Migration 4-phase</h2>
<h3 id="phase-0audit">Phase 0：Audit</h3>
<ul>
<li>列所有 Prometheus job / scrape config</li>
<li>統計 active series 數（Mimir tier 計費基準）</li>
<li>估 retention 需求</li>
</ul>
<h3 id="phase-1grafana-cloud-setup">Phase 1：Grafana Cloud setup</h3>
<ul>
<li>Account + organization 設定</li>
<li>API key for <code>remote_write</code></li>
<li>Grafana Cloud Mimir endpoint 啟用</li>
</ul>
<h3 id="phase-2dual-write">Phase 2：Dual-write</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">remote_write</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://prometheus-prod-XX-prod-us-central-0.grafana.net/api/prom/push</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">basic_auth</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">username</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;INSTANCE_ID&gt;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;API_KEY&gt;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="c"># Optional: drop high-cardinality before sending</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">__name__]</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;high_card_metric_.*&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">drop</span></span></span></code></pre></div><p>跑 4-8 週、確認 query 結果一致 + cost 在預期。</p>
<h3 id="phase-3cutover">Phase 3：Cutover</h3>
<ul>
<li>Dashboard / alert 切到 Grafana Cloud endpoint</li>
<li>應用層 / Grafana 自管 instance 關閉 query 對 self-managed Prometheus</li>
</ul>
<h3 id="phase-4cleanup">Phase 4：Cleanup</h3>
<ul>
<li>Self-managed Prometheus stop scrape</li>
<li>留 1-2 月歷史查詢能力（用 archive snapshot）</li>
<li>Decommission</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1cardinality-爆cost-暴漲">Case 1：Cardinality 爆、cost 暴漲</h3>
<p><strong>徵兆</strong>：dual-write 第 2 週 Grafana Cloud series 從預估 100K 漲到 800K、cost 翻 8 倍。</p>
<p><strong>根因</strong>：application-level high-cardinality label（user_id / request_id）沒被 drop、scraped 進來。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>write_relabel_configs</code> drop unbounded label</li>
<li>Application metric 設計改 fixed-bucket histogram、不用 unbounded label</li>
<li>Mimir cardinality limit 設保護 + alert</li>
</ol>
<h3 id="case-2recording-rule-對應失效">Case 2：Recording rule 對應失效</h3>
<p><strong>徵兆</strong>：cutover 後 Grafana dashboard 某些 panel 顯示空；發現用了 Prometheus 端 recording rule (<code>job:request_count:rate5m</code>)、Grafana Cloud 端沒對應 rule。</p>
<p><strong>根因</strong>：Prometheus 端 recording rule 是 <em>server-side</em>、不會跟著 remote_write 帶過去；Grafana Cloud 需要自己 setup recording rule。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Export 所有 recording rule、import 到 Grafana Cloud Mimir</li>
<li>或改用 <em>raw query</em> + Grafana query template、不依賴 recording rule</li>
</ol>
<h3 id="case-3promql-微差行為">Case 3：PromQL 微差行為</h3>
<p><strong>徵兆</strong>：某些 query 在 self-managed Prometheus 跑得好好的、切 Grafana Cloud Mimir 後 returns slightly different results。</p>
<p><strong>根因</strong>：Mimir 對某些 edge case（empty result handling / staleness marker timing）行為跟 Prometheus 略不同；多數 query 一致、&lt; 1% query 受影響。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover dual-query 驗證、用 critical dashboard 比對</li>
<li>Affected query 重寫、用更 robust PromQL pattern</li>
<li>文件 known incompatibility list</li>
</ol>
<h3 id="case-4alert-routing-改變">Case 4：Alert routing 改變</h3>
<p><strong>徵兆</strong>：Cutover 後 PagerDuty / Slack 收不到 alert；發現 Alertmanager 端 webhook 沒切。</p>
<p><strong>根因</strong>：alert 邏輯從 self-managed Alertmanager 搬到 Grafana Cloud Alerting、routing / contact 配置完全重做。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 在 Grafana Cloud 端 rebuild alert + routing</li>
<li>雙 alert pipeline 跑 1-2 週、確認 Grafana Cloud 收到</li>
<li>Cutover 切 routing、SOC drill 一次</li>
</ol>
<h3 id="case-5歷史資料查不到">Case 5：歷史資料查不到</h3>
<p><strong>徵兆</strong>：Cutover 後 SOC 想 query 6 個月前事件、Grafana Cloud 只有 2 個月（dual-write 後的）資料。</p>
<p><strong>根因</strong>：Grafana Cloud 從 dual-write 開始才有資料、之前的 self-managed Prometheus historical data 沒 backfill。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Phase 2 期間用 <code>promtool tsdb dump</code> + <code>mimirtool</code> 把 self-managed historical 灌進 Mimir</li>
<li>或保留 self-managed Prometheus read-only 6 個月（給 historical query）</li>
<li>Long-term：retention 從 cutover 開始算、historical 是 <em>one-time backfill</em></li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed</th>
          <th>Grafana Cloud Metrics</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute (100 host, 100K series)</td>
          <td>$500-1000 / mo + ops</td>
          <td>$300-800 / mo</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8 = $3K-8K</td>
          <td>0.05-0.15 = $500-1500</td>
      </tr>
      <tr>
          <td>Long-term retention</td>
          <td>Thanos / Cortex / Mimir 自管</td>
          <td>Built-in 13 個月</td>
      </tr>
      <tr>
          <td>Total (mid-tier)</td>
          <td>$4K-9K / mo (含 FTE)</td>
          <td>$1K-2.5K / mo</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-2 FTE × 1-2 個月</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-datadog--grafana-stack-migration-對位">跟 <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack migration</a> 對位</h3>
<p>兩條 Grafana Stack 路線：</p>
<ul>
<li>Self-host (Mimir + Loki + Tempo) on K8s：開源、自管</li>
<li>Grafana Cloud：SaaS、operational simplification</li>
</ul>
<p>本篇是「self-managed Prometheus → Grafana Cloud」、互補；如果跑兩階段（self-host → Cloud）跟「Datadog → Grafana Cloud」差不多。</p>
<h3 id="跟-opentelemetry-整合">跟 OpenTelemetry 整合</h3>
<p>OTel Collector 可同時 ship 到 Mimir (metric) + Loki (log) + Tempo (trace)；Migration 順便升 OTel 化避免下次 vendor 切換重複。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></li>
<li>Target vendor：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-to-msk/" data-link-title="Self-managed Kafka → AWS MSK：把 $15K/month operational cost 拆解到 managed" data-link-desc="Kafka self-managed → MSK 是 Type C operational redesign — protocol 完全相容、operational stack（ZooKeeper / brokers / monitoring / patching）全託管；本文用 cost 拆解開頭、5 個 production 踩雷（client connection pattern / version pinning / metric pipeline / IAM auth / cross-cluster mirror）">Kafka → MSK</a> / <a href="/blog/backend/04-observability/vendors/elastic-stack/migrate-to-elastic-cloud/" data-link-title="Self-managed ELK → Elastic Cloud：5 年 ELK 集群的 lifecycle 收尾" data-link-desc="Self-managed ELK Stack → Elastic Cloud 是 Type C operational redesign — protocol drop-in、operational stack（cluster sizing / shard 治理 / upgrade / backup）全託管；本文按 5 年 ELK lifecycle (build → scale → degrade → save → migrate) 組織、5 個 production 踩雷">ELK → Elastic Cloud</a></li>
<li>平行 D-type 對位：<a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Sentry → Honeycomb：trace 不是 error、是不同 observability paradigm</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/migrate-from-sentry/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/migrate-from-sentry/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Paradigm = High（error tracking ↔ wide-event observability）→ Type E paradigm shift&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="trace-不是-error是不同-paradigm">Trace 不是 error、是不同 paradigm&lt;/h2>
&lt;p>把 Sentry → Honeycomb 當「trace tool 替換」是最常見的誤判 — Sentry trace 是 &lt;em>error 上下文&lt;/em>、Honeycomb trace 是 &lt;em>observability 第一性&lt;/em>：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>Sentry&lt;/th>
 &lt;th>Honeycomb&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>核心 paradigm&lt;/td>
 &lt;td>Error tracking + transaction trace&lt;/td>
 &lt;td>High-cardinality wide-event observability&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>第一性 unit&lt;/td>
 &lt;td>Error event&lt;/td>
 &lt;td>Wide event (span with N fields)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Trace 角色&lt;/td>
 &lt;td>Error 的「附帶 context」&lt;/td>
 &lt;td>Observability 主軸、每 event 是 trace span&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sampling&lt;/td>
 &lt;td>Error 全收 + transaction sample&lt;/td>
 &lt;td>Adaptive sampling、保留 &lt;em>anomaly&lt;/em>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query model&lt;/td>
 &lt;td>Filter + group by + aggregation&lt;/td>
 &lt;td>High-cardinality 多維 query (BubbleUp / heatmap)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>User base&lt;/td>
 &lt;td>Developer (debug error)&lt;/td>
 &lt;td>SRE + Platform (debug system behavior)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost model&lt;/td>
 &lt;td>Per-error event + transaction&lt;/td>
 &lt;td>Per-event (wide event volume)&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心差異不在「Honeycomb 是 better Sentry」、在「兩者是不同 observability paradigm」&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Sentry 適合 &lt;em>application-level error debug&lt;/em> — 拿到 error stack trace + minimal context、快速 fix&lt;/li>
&lt;li>Honeycomb 適合 &lt;em>system-level behavior debug&lt;/em> — 看流量分佈 / 多維 correlation / 異常 outlier、找 &lt;em>為什麼這個 user 在這個時段在這個 endpoint 慢&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Migration scope 包含 &lt;em>paradigm reset&lt;/em> — 不是 SDK 換、是 SRE / Dev team 對 observability 的心智模型重設&lt;/strong>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a> 跟 <a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Paradigm = High（error tracking ↔ wide-event observability）→ Type E paradigm shift</em>。</p></blockquote>
<h2 id="trace-不是-error是不同-paradigm">Trace 不是 error、是不同 paradigm</h2>
<p>把 Sentry → Honeycomb 當「trace tool 替換」是最常見的誤判 — Sentry trace 是 <em>error 上下文</em>、Honeycomb trace 是 <em>observability 第一性</em>：</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>Sentry</th>
          <th>Honeycomb</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心 paradigm</td>
          <td>Error tracking + transaction trace</td>
          <td>High-cardinality wide-event observability</td>
      </tr>
      <tr>
          <td>第一性 unit</td>
          <td>Error event</td>
          <td>Wide event (span with N fields)</td>
      </tr>
      <tr>
          <td>Trace 角色</td>
          <td>Error 的「附帶 context」</td>
          <td>Observability 主軸、每 event 是 trace span</td>
      </tr>
      <tr>
          <td>Sampling</td>
          <td>Error 全收 + transaction sample</td>
          <td>Adaptive sampling、保留 <em>anomaly</em></td>
      </tr>
      <tr>
          <td>Query model</td>
          <td>Filter + group by + aggregation</td>
          <td>High-cardinality 多維 query (BubbleUp / heatmap)</td>
      </tr>
      <tr>
          <td>User base</td>
          <td>Developer (debug error)</td>
          <td>SRE + Platform (debug system behavior)</td>
      </tr>
      <tr>
          <td>Cost model</td>
          <td>Per-error event + transaction</td>
          <td>Per-event (wide event volume)</td>
      </tr>
  </tbody>
</table>
<p><strong>核心差異不在「Honeycomb 是 better Sentry」、在「兩者是不同 observability paradigm」</strong>：</p>
<ul>
<li>Sentry 適合 <em>application-level error debug</em> — 拿到 error stack trace + minimal context、快速 fix</li>
<li>Honeycomb 適合 <em>system-level behavior debug</em> — 看流量分佈 / 多維 correlation / 異常 outlier、找 <em>為什麼這個 user 在這個時段在這個 endpoint 慢</em></li>
</ul>
<p><strong>Migration scope 包含 <em>paradigm reset</em> — 不是 SDK 換、是 SRE / Dev team 對 observability 的心智模型重設</strong>。</p>
<h2 id="為什麼遷observability-成熟度--cardinality--cost-三條-driver">為什麼遷：observability 成熟度 / cardinality / cost 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Observability 成熟度</td>
          <td>Application 規模到 <em>跨多 service / multi-tenant</em>、Sentry error tracking 不夠細、SRE 要看 <em>high-cardinality</em> 多維 query</td>
      </tr>
      <tr>
          <td>High-cardinality</td>
          <td>Sentry tag system 限制 cardinality（~1000 unique value）、Honeycomb native 支援 millions cardinality</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>Per-error pricing 對 high-error volume 場景爆、Honeycomb per-event 在 <em>wide event</em> 場景更可預測</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（Honeycomb → Sentry）：</p>
<ul>
<li>Pure error tracking 場景、Honeycomb wide-event 過度設計</li>
<li>Frontend / mobile 客戶端 error tracking、Sentry 對 web/mobile/desktop SDK 成熟度高</li>
</ul>
<h2 id="6-維-audit">6 維 audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Medium（event schema 概念不同、SDK 完全換）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td>Low（兩者都 SaaS、operational 對等）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td><strong>High</strong>（error tracking ↔ wide-event observability）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low（同 1 個 observability vendor）</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td><strong>High</strong>（SDK 換 + instrumentation 重設計）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Paradigm = High（其他 Low-Medium）→ Type E paradigm shift；application change 雖 High 但是 paradigm 的 downstream。</p>
<h2 id="結構partial-migration--混合架構是-long-term-default">結構：partial migration + 混合架構是 long-term default</h2>
<p>跟 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a> 同 Type E pattern：</p>
<ul>
<li><strong>不存在 complete migration</strong>：Sentry 對 <em>frontend error tracking</em> 強項、Honeycomb 對 <em>backend system observability</em> 強項</li>
<li><strong>長期混合架構</strong>：frontend / mobile 保留 Sentry、backend / SRE 走 Honeycomb</li>
<li><strong>Application 重設計</strong>：instrumentation 用 OpenTelemetry、避免 vendor SDK lock-in</li>
</ul>
<h2 id="application-重設計範例">Application 重設計範例</h2>





<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="c1"># Before: Sentry SDK</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">sentry_sdk</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">sentry_sdk</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="n">dsn</span><span class="o">=</span><span class="s1">&#39;https://x@sentry.io/y&#39;</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="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">process_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">sentry_sdk</span><span class="o">.</span><span class="n">capture_exception</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">raise</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># After: OpenTelemetry + Honeycomb</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry</span> <span class="kn">import</span> <span class="n">trace</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.sdk.trace</span> <span class="kn">import</span> <span class="n">TracerProvider</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.sdk.trace.export</span> <span class="kn">import</span> <span class="n">BatchSpanProcessor</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.exporter.otlp.proto.grpc.trace_exporter</span> <span class="kn">import</span> <span class="n">OTLPSpanExporter</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">trace</span><span class="o">.</span><span class="n">set_tracer_provider</span><span class="p">(</span><span class="n">TracerProvider</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="n">trace</span><span class="o">.</span><span class="n">get_tracer_provider</span><span class="p">()</span><span class="o">.</span><span class="n">add_span_processor</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">BatchSpanProcessor</span><span class="p">(</span><span class="n">OTLPSpanExporter</span><span class="p">(</span><span class="n">endpoint</span><span class="o">=</span><span class="s1">&#39;https://api.honeycomb.io&#39;</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s1">&#39;x-honeycomb-team&#39;</span><span class="p">:</span> <span class="s1">&#39;YOUR_API_KEY&#39;</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">tracer</span> <span class="o">=</span> <span class="n">trace</span><span class="o">.</span><span class="n">get_tracer</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s1">&#39;process_order&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s1">&#39;order.id&#39;</span><span class="p">,</span> <span class="n">order_id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s1">&#39;user.id&#39;</span><span class="p">,</span> <span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s1">&#39;order.amount&#39;</span><span class="p">,</span> <span class="n">order</span><span class="o">.</span><span class="n">amount</span><span class="p">)</span>  <span class="c1"># high-cardinality 自然</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s1">&#39;order.region&#39;</span><span class="p">,</span> <span class="n">region</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="n">process_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="n">span</span><span class="o">.</span><span class="n">set_status</span><span class="p">(</span><span class="n">trace</span><span class="o">.</span><span class="n">Status</span><span class="p">(</span><span class="n">trace</span><span class="o">.</span><span class="n">StatusCode</span><span class="o">.</span><span class="n">OK</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="n">span</span><span class="o">.</span><span class="n">set_status</span><span class="p">(</span><span class="n">trace</span><span class="o">.</span><span class="n">Status</span><span class="p">(</span><span class="n">trace</span><span class="o">.</span><span class="n">StatusCode</span><span class="o">.</span><span class="n">ERROR</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)))</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="n">span</span><span class="o">.</span><span class="n">record_exception</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">        <span class="k">raise</span></span></span></code></pre></div><p>差異：</p>
<ul>
<li>Sentry 只 capture exception + 簡 context</li>
<li>Honeycomb 對每 operation 寫 <em>wide event</em> 含 high-cardinality field（user.id / order.amount / order.region）</li>
<li>SRE 端能跑 <code>WHERE order.region = &quot;us-west-2&quot; AND duration &gt; 5000</code> 的 multi-dim query</li>
</ul>
<h2 id="migration-流程">Migration 流程</h2>





<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">1. Audit application：列所有 Sentry SDK 使用 + capture pattern
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 分類處理 plan:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - Pure error tracking (frontend): 保留 Sentry
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - Backend system trace: 切 Honeycomb / OTel
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   - Error + context (混合): 雙寫期 evaluate
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">3. OpenTelemetry instrumentation 化:
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - 用 OTel SDK 取代 vendor SDK
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   - Honeycomb 是 OTLP target、跟 vendor lock 解耦
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">4. Backend application 切 Honeycomb (3-6 個月)
</span></span><span class="line"><span class="ln">10</span><span class="cl">5. Frontend / mobile 保留 Sentry
</span></span><span class="line"><span class="ln">11</span><span class="cl">6. SRE training: Honeycomb BubbleUp / heatmap / multi-dim query</span></span></code></pre></div><h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1event-schema-對位失敗sre-不會用-bubbleup">Case 1：Event schema 對位失敗、SRE 不會用 BubbleUp</h3>
<p><strong>徵兆</strong>：切 Honeycomb 後 SRE 用 Sentry 思維 — 找 error → fix；Honeycomb BubbleUp / heatmap 沒人會用、observability 退化到 <em>只看 error count</em>。</p>
<p><strong>根因</strong>：Sentry → Honeycomb migration 不只是 tool 換、是 <em>observability mindset 換</em>；SRE 沒培訓 wide-event query / BubbleUp anomaly detection。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>SRE training</strong>：1-2 週 hands-on Honeycomb BubbleUp + heatmap + multi-dim query</li>
<li><strong>Migration scope 含 sample query playbook</strong>：每個 incident type 對應 Honeycomb query 寫成 runbook</li>
<li><strong>保留 Sentry frontend / mobile</strong>：不要逼 SRE 全切、保留 <em>paradigm fit</em> 的部分</li>
</ol>
<h3 id="case-2sampling-行為差production-cost-飛">Case 2：Sampling 行為差、production cost 飛</h3>
<p><strong>徵兆</strong>：切 Honeycomb 後第 1 個月 event volume 比 Sentry 高 100x；帳單暴漲。</p>
<p><strong>根因</strong>：Sentry 對 transaction 端 sample（10% 預設）、error 全收；Honeycomb 端 <em>每 span 都 wide event</em>、application 端沒設 sampling 全送、event volume 爆。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Honeycomb Refinery (sampling proxy)</strong>：deploy refinery 在 application 端跟 Honeycomb 之間、tail-based sampling</li>
<li><strong>Sample rule</strong>：保留 <em>anomaly</em> (error / slow / outlier)、drop <em>boring success</em> 90%+</li>
<li><strong>Cost monitoring 第一週密集</strong>：cardinality + event volume + cost dashboard、catch 預期外 spike</li>
</ol>
<h3 id="case-3error-grouping-失效">Case 3：Error grouping 失效</h3>
<p><strong>徵兆</strong>：切 Honeycomb 後 <em>相似 error</em> 沒被 group 成「同類 issue」、SRE 看每 event 獨立、failure 模式淹沒在 noise。</p>
<p><strong>根因</strong>：Sentry 自動 error grouping (by stack trace fingerprint)、Honeycomb 沒對等 — wide event 是 first-class、event grouping 需要 application 端 explicit 設 <code>error.type</code> field。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application 端設 error type field</strong>：<code>span.set_attribute('error.type', exception_class)</code></li>
<li><strong>Honeycomb derived column</strong>：用 derived column 算 error fingerprint</li>
<li><strong>保留 Sentry error tracking</strong>：純 error grouping 場景 Sentry 強項、別硬切</li>
</ol>
<h3 id="case-4cost-模型差預估錯">Case 4：Cost 模型差、預估錯</h3>
<p><strong>徵兆</strong>：切 Honeycomb 後預估 50% cost saving、實際只省 10-15%。</p>
<p><strong>根因</strong>：Sentry per-error pricing 對 error-heavy application 貴；Honeycomb per-event pricing 對 <em>wide event volume</em> application 貴；如果 application 是 <em>event volume 高 但 error 少</em>、Honeycomb 反而貴。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration 估</strong>：用 OTel pilot 跑 1-2 週、估真實 event volume</li>
<li><strong>Sample rule 設計</strong>：retention 7 天 hot + 30 天 cold + 1 年 archive、降 cost</li>
<li><strong>混合架構保留</strong>：frontend / mobile 走 Sentry、backend 走 Honeycomb、避免一邊 cost 爆</li>
</ol>
<h3 id="case-5alert-paradigm-不對等">Case 5：Alert paradigm 不對等</h3>
<p><strong>徵兆</strong>：Sentry alert 簡單（error rate / latency p99 threshold）、Honeycomb trigger 配置複雜（SLO + burn rate + BubbleUp）；SOC 學習曲線 1-2 個月。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Migration 含 alert rebuild scope</strong>：Honeycomb trigger 不直接對位 Sentry alert、要重寫</li>
<li><strong>SLO-driven alert</strong>：用 Honeycomb SLO 取代 Sentry threshold alert、降 alert fatigue</li>
<li><strong>PagerDuty integration</strong>：兩家都支援、routing rule 跟 dedup 要 review</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Sentry</th>
          <th>Honeycomb</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pricing model</td>
          <td>Per-error + transaction</td>
          <td>Per-event (wide event)</td>
      </tr>
      <tr>
          <td>Cost (mid-tier)</td>
          <td>$500-2000 / mo</td>
          <td>$400-3000 / mo (依 event volume)</td>
      </tr>
      <tr>
          <td>Sampling</td>
          <td>Built-in transaction sampling</td>
          <td>Refinery (additional component)</td>
      </tr>
      <tr>
          <td>Cardinality</td>
          <td>~1000 unique value / tag</td>
          <td>Millions / field</td>
      </tr>
      <tr>
          <td>Application complexity</td>
          <td>Low (SDK + capture exception)</td>
          <td>Medium (OTel + wide event instrument)</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>2-4 FTE × 2-3 個月</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-opentelemetry-整合">跟 OpenTelemetry 整合</h3>
<p>OTel 是 vendor-neutral instrumentation、Honeycomb 是 OTLP backend；application 端 OTel 化後可以同時 ship 到多個 backend（dev 端 Jaeger / production 端 Honeycomb / fallback 端 Tempo）。</p>
<h3 id="跟-datadog--grafana-stack-對位">跟 <a href="/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/" data-link-title="Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability" data-link-desc="Datadog 五層計費（host APM / metric / log ingest / log retention / RUM）拆解、對位 Grafana Stack（Mimir / Loki / Tempo / Grafana / Alloy）的 5 層責任；OTel-based agent migration、5 個 production 踩雷（cardinality 爆 / log volume cost / dashboard 不直接轉 / alert routing 換邏輯 / SLO definition 差異）、cost reality check">Datadog → Grafana Stack</a> 對位</h3>
<p>兩條 observability 路線：</p>
<ul>
<li>Grafana Stack (Mimir / Loki / Tempo)：self-host or Grafana Cloud、open source baseline</li>
<li>Honeycomb：SaaS-only、focus wide-event observability</li>
</ul>
<p>選擇取決於 <em>observability paradigm</em>：trace-heavy 走 Tempo / Honeycomb、metric-heavy 走 Mimir / Datadog。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></li>
<li>Target vendor：<a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></li>
<li>平行 migration playbook (Type E)：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a> / <a href="/blog/backend/05-deployment-platform/vendors/consul/migrate-from-etcd/" data-link-title="etcd → Consul：KV &#43; N 個 extras feature matrix" data-link-desc="etcd → Consul 是 Type E paradigm shift expansion — 從 pure KV store 升到 service mesh / discovery / health check / multi-DC；本文用對照表 &#43; paradigm expansion 路線、5 個 production 踩雷（API 對位 / lock semantics / watch event model / multi-DC topology / ACL system）">etcd → Consul</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>4.11 Telemetry Pipeline 架構</title><link>https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何要把 telemetry 當 pipeline 看：每層有獨立失敗模式與成本邊界&lt;/li>
&lt;li>分層責任：agent（採集）、collector（聚合 / 轉換）、ingest（寫入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a>）、storage（保留 / 查詢）、query（dashboard / alert）&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure&lt;/a>：collector 端緩衝、ingest 滿時的降級策略&lt;/li>
&lt;li>OpenTelemetry Collector 的角色：vendor-neutral 中介層&lt;/li>
&lt;li>pipeline 失敗時的 graceful &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation&lt;/a>：訊號斷一層、其他層仍可用&lt;/li>
&lt;li>multi-tenant 環境的 quota / 隔離&lt;/li>
&lt;li>觀測遷移流程：先換 collector 再換 instrumentation、雙軌期保留對照&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality&lt;/a> 的分工：4.7 是治理輸入、4.11 是 pipeline 執行&lt;/li>
&lt;li>反模式：pipeline 是黑盒、無 self-monitoring；agent 直連 vendor 無 collector 中介；ingest 滿時直接 drop 無告警&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Telemetry pipeline 是把訊號從 service process 帶到查詢與告警面的資料路徑，責任是讓採集、轉換、寫入、儲存與查詢各層都有可觀測的邊界。&lt;/p>
&lt;p>這一頁處理的是觀測系統本身的可靠性。當 pipeline 是黑盒，訊號消失時團隊需要額外排查服務是否真的沒事件，或 agent、collector、ingest、query 哪一層失效。&lt;/p>
&lt;p>Pipeline 視角的另一個價值是把採集策略跟儲存後端解耦。應用層只需要產生標準訊號，pipeline 處理 schema 轉換、sampling、enrichment、routing 與 vendor 對接；當儲存後端或 vendor 改變時，應用層不必重新 instrument。&lt;/p>
&lt;h2 id="分層責任與失敗模式">分層責任與失敗模式&lt;/h2>
&lt;p>Pipeline 各層責任不同，失敗模式也不同。把 pipeline 視為單一黑盒會讓事故定位停在「訊號不見了」這層觀察，無法回答是哪一層的問題。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分層&lt;/th>
 &lt;th>主要責任&lt;/th>
 &lt;th>典型失敗模式&lt;/th>
 &lt;th>健康訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Agent&lt;/td>
 &lt;td>從 process / host 抓取原始訊號&lt;/td>
 &lt;td>升版需重啟、container restart 造成短期缺洞&lt;/td>
 &lt;td>export queue depth、dropped batches&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Collector&lt;/td>
 &lt;td>聚合、轉換、enrichment、routing&lt;/td>
 &lt;td>OOM、配置漂移、規則衝突&lt;/td>
 &lt;td>receiver / processor / exporter 指標&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ingest&lt;/td>
 &lt;td>接收並寫入 buffer 或排隊&lt;/td>
 &lt;td>滿載拒收（429）、區域故障&lt;/td>
 &lt;td>ingestion success rate、queue depth&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Storage&lt;/td>
 &lt;td>保留資料、支援查詢索引&lt;/td>
 &lt;td>索引膨脹、保留策略誤刪、查詢退化&lt;/td>
 &lt;td>storage size、query latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query&lt;/td>
 &lt;td>dashboard / alert / 即席查詢&lt;/td>
 &lt;td>查詢逾時、aggregate 失真、permission 漂移&lt;/td>
 &lt;td>query QPS、p95 latency、permission 拒絕&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Agent 層的關鍵風險是部署綁定。若 agent 跟應用同進程，升版需要重啟服務；若 agent 是獨立 DaemonSet 或 sidecar，升版可以獨立進行，但要承擔網路與資源額外開銷。Agent 自身故障時，service 看起來健康，dashboard 看起來空，事故指揮會把這個空白誤讀成系統靜默。&lt;/p>
&lt;p>Collector 層是 pipeline 最有彈性的地方，也是最容易漏掉自我觀測的地方。OpenTelemetry Collector 的 receiver / processor / exporter 各自有 metrics，部署時要把這些 metrics 自身送回觀測平台。配置漂移是長期維護的主要失敗：sampling 規則改了沒紀錄、attribute 重命名沒同步、tail sampling decision window 縮短，都會讓下游看到的訊號跟以前不同。Collector 的三種部署位置（agent / gateway / sidecar）與 pipeline 設計細節見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何要把 telemetry 當 pipeline 看：每層有獨立失敗模式與成本邊界</li>
<li>分層責任：agent（採集）、collector（聚合 / 轉換）、ingest（寫入 <a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a>）、storage（保留 / 查詢）、query（dashboard / alert）</li>
<li><a href="/blog/backend/knowledge-cards/buffer/" data-link-title="Buffer" data-link-desc="說明系統如何用暫存空間吸收短暫速度差與尖峰流量">buffer</a> 與 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a>：collector 端緩衝、ingest 滿時的降級策略</li>
<li>OpenTelemetry Collector 的角色：vendor-neutral 中介層</li>
<li>pipeline 失敗時的 graceful <a href="/blog/backend/knowledge-cards/degradation/" data-link-title="Degradation" data-link-desc="說明服務部分能力失效時如何保留核心功能與控制風險">degradation</a>：訊號斷一層、其他層仍可用</li>
<li>multi-tenant 環境的 quota / 隔離</li>
<li>觀測遷移流程：先換 collector 再換 instrumentation、雙軌期保留對照</li>
<li>跟 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a> 的分工：4.7 是治理輸入、4.11 是 pipeline 執行</li>
<li>反模式：pipeline 是黑盒、無 self-monitoring；agent 直連 vendor 無 collector 中介；ingest 滿時直接 drop 無告警</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Telemetry pipeline 是把訊號從 service process 帶到查詢與告警面的資料路徑，責任是讓採集、轉換、寫入、儲存與查詢各層都有可觀測的邊界。</p>
<p>這一頁處理的是觀測系統本身的可靠性。當 pipeline 是黑盒，訊號消失時團隊需要額外排查服務是否真的沒事件，或 agent、collector、ingest、query 哪一層失效。</p>
<p>Pipeline 視角的另一個價值是把採集策略跟儲存後端解耦。應用層只需要產生標準訊號，pipeline 處理 schema 轉換、sampling、enrichment、routing 與 vendor 對接；當儲存後端或 vendor 改變時，應用層不必重新 instrument。</p>
<h2 id="分層責任與失敗模式">分層責任與失敗模式</h2>
<p>Pipeline 各層責任不同，失敗模式也不同。把 pipeline 視為單一黑盒會讓事故定位停在「訊號不見了」這層觀察，無法回答是哪一層的問題。</p>
<table>
  <thead>
      <tr>
          <th>分層</th>
          <th>主要責任</th>
          <th>典型失敗模式</th>
          <th>健康訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Agent</td>
          <td>從 process / host 抓取原始訊號</td>
          <td>升版需重啟、container restart 造成短期缺洞</td>
          <td>export queue depth、dropped batches</td>
      </tr>
      <tr>
          <td>Collector</td>
          <td>聚合、轉換、enrichment、routing</td>
          <td>OOM、配置漂移、規則衝突</td>
          <td>receiver / processor / exporter 指標</td>
      </tr>
      <tr>
          <td>Ingest</td>
          <td>接收並寫入 buffer 或排隊</td>
          <td>滿載拒收（429）、區域故障</td>
          <td>ingestion success rate、queue depth</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>保留資料、支援查詢索引</td>
          <td>索引膨脹、保留策略誤刪、查詢退化</td>
          <td>storage size、query latency</td>
      </tr>
      <tr>
          <td>Query</td>
          <td>dashboard / alert / 即席查詢</td>
          <td>查詢逾時、aggregate 失真、permission 漂移</td>
          <td>query QPS、p95 latency、permission 拒絕</td>
      </tr>
  </tbody>
</table>
<p>Agent 層的關鍵風險是部署綁定。若 agent 跟應用同進程，升版需要重啟服務；若 agent 是獨立 DaemonSet 或 sidecar，升版可以獨立進行，但要承擔網路與資源額外開銷。Agent 自身故障時，service 看起來健康，dashboard 看起來空，事故指揮會把這個空白誤讀成系統靜默。</p>
<p>Collector 層是 pipeline 最有彈性的地方，也是最容易漏掉自我觀測的地方。OpenTelemetry Collector 的 receiver / processor / exporter 各自有 metrics，部署時要把這些 metrics 自身送回觀測平台。配置漂移是長期維護的主要失敗：sampling 規則改了沒紀錄、attribute 重命名沒同步、tail sampling decision window 縮短，都會讓下游看到的訊號跟以前不同。Collector 的三種部署位置（agent / gateway / sidecar）與 pipeline 設計細節見 <a href="/blog/backend/04-observability/vendors/opentelemetry/collector-deployment-patterns/" data-link-title="OTel Collector 部署模式：agent / gateway / sidecar 與 pipeline 設計" data-link-desc="說明 OpenTelemetry Collector 三種部署位置的責任分工、receivers/processors/exporters pipeline 設計，以及 collector 失效、記憶體壓力與 backpressure 的故障演練與容量邊界">OTel Collector 部署模式</a>。</p>
<p>Ingest 層的失敗模式集中在容量邊界。當 vendor 端 quota 觸發或內部 queue 滿，ingest 會回 429 或直接丟棄；應用層通常無感、dashboard 顯示流量下降。這層需要把拒收事件本身變成告警訊號、讓事故定位即時看到拒收量、避免靠事後對賬發現。</p>
<p>Storage 跟 query 層的失敗多半是漸進式：保留策略誤刪、查詢隨時間退化、索引隨流量膨脹。這類失敗不會在當下觸發告警，要靠週期性審視 storage size、query latency 與 retention compliance 才能發現。</p>
<h2 id="buffer-與-backpressure">Buffer 與 Backpressure</h2>
<p>Buffer 是 pipeline 吸收瞬時尖峰的緩衝，責任是讓 collector 跟 ingest 在後端短暫故障或速率不足時仍保住高價值訊號。</p>
<ul>
<li><strong>In-memory queue</strong>：吸收秒級尖峰、容量小、process 重啟會丟。</li>
<li><strong>Persistent queue</strong>（local disk、Kafka）：吸收分鐘到小時級積壓、有持久性、需要額外運維成本。</li>
<li><strong>Spillover storage</strong>（S3 等冷儲存）：當 hot path 滿載時，把低優先訊號暫存到便宜後端、之後 replay。</li>
</ul>
<p>Backpressure 策略決定 buffer 滿時的行為。<code>block</code> 策略會讓上游採集慢下來、可能影響應用；<code>drop oldest</code> 跟 <code>drop newest</code> 各自影響 timeline 的開始或結束；<code>sample-by-priority</code> 則保留錯誤、長尾與低流量樣本、丟棄一般成功 request。Buffer 跟 backpressure 策略要在容量規劃階段顯式設定、進 release flow、避免事故時臨時拍定。</p>
<p>Buffer 對事故判讀的影響是 freshness。當 buffer 累積分鐘級資料時，dashboard 看到的指標其實落後當前狀態；incident commander 看到 error rate 下降時，需要知道是真的恢復還是 buffer 尚未排空。把 buffer depth 跟 ingest delay 暴露成 dashboard 指標，能避免事中決策建立在過期資料上。</p>
<p>Buffer 跟 backpressure 怎麼選：低延遲容忍 + 容量充足的場景用 in-memory queue + <code>drop oldest</code>（保留最新狀態）；高訊號完整性需求（例：audit log、事故證據）用 persistent queue + <code>block</code> 或 <code>sample-by-priority</code>；高流量爆量但允許部分遺失（例：debug log）用 spillover storage + <code>drop newest</code>。事故時的回退路徑是「在 backpressure 政策中先標明哪類訊號絕對保留、哪類訊號可丟」、避免事故當下臨時決定。</p>
<h2 id="opentelemetry-collector-的中介定位">OpenTelemetry Collector 的中介定位</h2>
<p>OpenTelemetry Collector 把採集、轉換與 routing 從應用程式抽離，責任是讓觀測 vendor 跟採集 SDK 各自演進。</p>
<p>Collector 在 pipeline 中扮演三個角色：</p>
<ol>
<li><strong>Vendor-neutral 中介</strong>：應用層只需 export OTLP，collector 端決定要不要把資料同時送到多個後端（Datadog、Honeycomb、self-hosted Prometheus）。切換 vendor 時不需要改應用層。</li>
<li><strong>Schema / sampling 集中治理</strong>：attribute 重命名、敏感欄位 redaction、tail sampling decision、cardinality 限制都集中在 collector，不分散在每個服務。</li>
<li><strong>Topology 適配層</strong>：collector 可以部署為 sidecar（與應用同 Pod）、DaemonSet（每個 node 一份）或 gateway（集中接收）。不同部署形態適合不同規模與隔離需求，並不互斥；大型部署常見「應用 → sidecar → cluster gateway → 後端」的多級拓樸。</li>
</ol>
<p>對應 <a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5 Cloud Trace OTLP 導入</a>：標準化傳輸協定降低跨環境的 instrumentation 重複，揭露「資料通道標準化」是觀測平台轉換的常見起點。對應 <a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6 ADOT on EKS 管線遷移</a>：多代理混用在規模化時放大配置漂移，揭露 collector 集中治理的營運價值。兩個案例的具體實作差異留給原案例，本章關注的是 collector 在 pipeline 中的責任邊界。</p>
<h2 id="觀測遷移的執行順序">觀測遷移的執行順序</h2>
<p>觀測遷移的執行順序決定短期雙軌成本能否轉化為長期語意一致性。把替換風險限制在採集中介層、是先換 collector / agent、再換應用層 instrumentation 的設計理由。</p>
<p>可重複套用的順序是先換採集中介、再換採集點：</p>
<ol>
<li><strong>先換 collector / agent</strong>：把 collector 從 vendor-specific 換成 vendor-neutral（如 OTel Collector），同時保留舊 vendor 的 exporter，讓資料同時送到新舊後端。這層替換對應用層無感，可以快速完成。</li>
<li><strong>建立雙軌對照</strong>：以新舊後端對照 SLI 是否一致（query 設計、偏差閾值、退出條件等對照細節由 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a> 處理）、差異超過閾值時停止下一步。</li>
<li><strong>逐步改應用端 instrumentation</strong>：把應用層的 vendor-specific SDK 換成 OTel SDK，分服務分批進行。每批切換後重跑對照驗證。</li>
<li><strong>以對照驗證進入 release gate</strong>：在 release pipeline 加上「新舊管線 SLI 偏差」檢查，作為遷移階段的閘門。對照穩定後才能關閉舊管線。</li>
</ol>
<p>執行順序的設計理由：collector 是 vendor-neutral 抽象、可以雙軌並存承受對照成本；應用層 instrumentation 改動會跨眾多 service team、變更面廣、要在 collector 對照穩定後才大規模推進。把次序反過來容易在 instrumentation 全面改完才發現 collector 抽象有缺失、被迫重做。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4 X-Ray 到 OpenTelemetry 轉換</a>：揭露「先 collector 後 instrumentation」的階段切換方向。對應 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel 相容遷移實務</a>：揭露「雙軌期成本跟語意漂移是遷移期主要風險」（單一 agent 安裝是次要議題）。本章關注的是執行順序，schema drift 跟資料品質的對照驗證細節由 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a> 處理。</p>
<h2 id="規模差異下的遷移節奏">規模差異下的遷移節奏</h2>
<p>遷移節奏由團隊規模、可承受雙軌成本、配置漂移風險與治理成熟度共同決定。本段聚焦遷移期的節奏取捨；常態 ownership 配置由 <a href="/blog/backend/04-observability/observability-operating-model/#%e8%a6%8f%e6%a8%a1%e5%b7%ae%e7%95%b0%e4%b8%8b%e7%9a%84%e8%a7%92%e8%89%b2%e9%85%8d%e7%bd%ae" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 規模差異下的角色配置</a> 處理，兩者 lens 不同。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模差異下觀測遷移</a>：揭露三種規模團隊的失敗模式骨架；以下三段的具體操作做法均屬通用工程知識展開、case 本身只列方向。</p>
<p>小團隊的核心風險是雙軌維護消耗人力。同時看兩套 dashboard、雙倍 alert noise、雙倍 on-call 負擔，很容易讓遷移本身拖累業務維運。小團隊適合用「短期對照、快速收斂」策略：把對照期壓到一個迭代週期內，固定一個服務作為先導，把問題在小範圍內收斂，再快速複製到其他服務。</p>
<p>中型團隊的失敗模式集中在 schema 漂移。服務數量增加後，attribute 命名一致性、service name 規約、label cardinality 邊界容易在雙軌期擴散。中型團隊要在遷移開始前先固化 semantic convention，並在 collector 層自動校驗；不固化會在遷移後拼湊出多套互相矛盾的 dashboard。</p>
<p>大型團隊的主要失敗集中在治理面：collector 拓樸（sidecar / DaemonSet / gateway 的選擇）、sampling 政策、成本分攤、tenant 隔離都會在遷移後顯著影響成本與告警品質。大型團隊用「pilot region 先行、其他 region 批次跟進」策略、把 collector 配置版本化、變更接到 release gate。大型團隊的回退單位通常是 region 或 tenant 群、不是整體切回。</p>
<p>三類團隊的共同教訓是：先決定「何時可以關閉舊管線」的退出條件，再開始遷移。沒有退出條件的雙軌會無限期延長，最後在成本壓力下被動關閉，反而失去對照驗證的能力。</p>
<h2 id="遷移漂移的回退判讀">遷移漂移的回退判讀</h2>
<p>漂移回退的責任是把降級決策權跟資料採集分離、讓回退保留可分析的對照證據。直接關閉新管線會失去漂移原因的線索、後續再遷移容易出同樣的事故。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel 遷移訊號漂移反例</a>：揭露遷移失敗的主要型態是語意漂移、回退要保留對照證據。</p>
<p>漂移發生時，主要訊號是「兩套儀表板看似都有資料、但對同一事故的判讀不同」。新舊管線對同一服務的 error rate 長期偏離、missing span 或 missing metric 比例上升、alert 噪音增加但事故量沒對應增加，都是漂移在 pipeline 層的表現。</p>
<p>回退判讀的核心是分辨「遷移問題」跟「服務問題」。比較穩定的回退節奏：</p>
<ol>
<li>先停止讓新管線主導告警跟 SLO 判定，把告警入口切回舊管線。</li>
<li>保留新管線採集、但只作為對照證據，不參與決策。</li>
<li>用對照資料找出語意漂移點（attribute 名稱、sampling 規則、aggregation 視窗），分項修正。</li>
<li>修正後重新進入雙軌對照、確認偏差收斂、再讓新管線恢復主導。</li>
</ol>
<p>這個流程把回退視為降級決策權的釋放、而非整體關閉訊號採集。把回退做成可重播流程，下次遷移才能避免在錯誤訊號上做服務回退。</p>
<h2 id="multi-tenant-與-quota">Multi-tenant 與 Quota</h2>
<p>Pipeline 的多租戶治理責任是讓單一服務或團隊的爆量不會拖累其他租戶。沒有租戶隔離時，單一服務的 cardinality 爆炸或 sampling 失控會直接耗盡 pipeline 容量。</p>
<p>可操作的隔離手段：</p>
<ul>
<li><strong>Ingestion quota per tenant</strong>：限制單一服務的 ingest rate，超過時觸發降級或退單。</li>
<li><strong>Buffer 與 storage 分區</strong>：高優先 tenant 使用獨立 buffer 或 storage shard，避免 noisy neighbor。</li>
<li><strong>Sampling 政策 per tenant</strong>：成本敏感 tenant 走較高採樣比例，關鍵 tenant 走 minimum sample floor。</li>
<li><strong>Cost attribution</strong>：把 ingestion、storage、query 成本拆到 tenant，回到 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>。</li>
</ul>
<p>Quota 觸發時的告警設計比 quota 本身更重要。沒有告警的 quota 等於沒有 quota，因為觸發後訊號靜默，事故定位會把靜默誤讀為系統穩定。</p>
<h2 id="讀取路徑作為-pipeline-的延伸">讀取路徑作為 pipeline 的延伸</h2>
<p>Pipeline 的分層敘事（agent → collector → ingest → storage → query）在 query 這層停得太早。寫入路徑的資料從 agent 流到 storage 是單向的；讀取路徑從 query engine 向 storage 發起請求，方向相反、效能瓶頸不同、治理責任也不同。把 query 視為 pipeline 的終端消費者而非獨立系統，才能完整理解觀測資料的生命週期。</p>
<h3 id="query-engine-的責任邊界">Query engine 的責任邊界</h3>
<p>Query engine 在 pipeline 中的責任是把儲存層的資料轉換成使用者可操作的回應。這包括 query planning（決定掃描哪些 shard、哪些 tier）、聚合計算（rate / sum / quantile）、結果快取與 query 排程。</p>
<p>Query engine 的設計取捨跟儲存層不同。儲存層追求寫入吞吐與持久性；query engine 追求查詢延遲與併發能力。兩者獨立擴展 — 寫入量大但查詢量小的場景，storage 需要更多容量但 query engine 不需要；反過來，dashboard 多但寫入量穩定的場景，query engine 需要更多 CPU 但 storage 不需要。</p>
<h3 id="query-time-的資源隔離">Query-time 的資源隔離</h3>
<p>Query engine 服務三種查詢模式：alert rule evaluation（系統關鍵、定期、不可延遲）、dashboard 刷新（高頻、穩定、可容忍短暫延遲）、即席診斷（偶發、突增、事故中最需要低延遲）。三者搶同一個 query engine 時，穩定的背景負載會擠壓突發的即席查詢。</p>
<p>資源隔離的可操作方式：</p>
<ul>
<li><strong>Query priority</strong>：alert evaluation 最高、即席查詢次之、dashboard 最低。Alert 不能因為 dashboard 重查詢排隊而漏發。</li>
<li><strong>Query queue 分離</strong>：不同類型的查詢進不同的 queue，各自有併發上限。Thanos / Mimir 的 query-frontend 支援 query 分類與排程。</li>
<li><strong>Query timeout 差異化</strong>：alert evaluation 設短 timeout（跑不完就是問題）、即席查詢設中等 timeout、dashboard 的大範圍查詢允許較長 timeout。</li>
<li><strong>Query cost estimation</strong>：在查詢執行前估算掃描量，超過閾值的查詢降級或拒絕，避免單一 heavy query 拖垮整個 query engine。</li>
</ul>
<h3 id="buffer-lag-對查詢-freshness-的影響">Buffer lag 對查詢 freshness 的影響</h3>
<p>寫入面的 buffer lag 會直接影響讀取面的 freshness。當 collector 或 ingest 端有分鐘級的 buffer 累積，query engine 讀到的是延遲過的資料。Dashboard 顯示的 error rate 可能反映的是兩分鐘前的狀態；incident commander 看到 error rate 下降，可能是 buffer 開始排空而非服務真的恢復。</p>
<p>把 buffer lag 轉成查詢面的可見指標是基本的設計要求。在 dashboard 上顯示「資料延遲：目前最新資料點是 N 秒前」，讓讀取者知道自己看到的資料有多新。當 lag 超過告警閾值，除了觸發 pipeline 健康告警外，dashboard 本身也應該標示警告狀態。</p>
<p>跨訊號類型的查詢設計見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 telemetry pipeline 時，先看每一層是否有健康訊號，再看滿載時是否能降級。</p>
<p>重點訊號包括：</p>
<ul>
<li>agent、collector、ingest、storage、query 是否各自有 SLI</li>
<li>buffer 與 backpressure 是否能保住高價值訊號</li>
<li>multi-tenant quota 是否能隔離單一服務爆量</li>
<li>collector 是否保留 vendor-neutral 的轉換空間</li>
<li>遷移期是否有雙軌對照、是否有退出條件</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>訊號間歇性消失、需要人工判斷是 pipeline 還是 service 問題</li>
<li>agent 升版需要 service 重啟、運維成本高</li>
<li>ingest 拒收（429）發生時、應用層無感</li>
<li>切換 vendor 需要改所有 service 的 instrumentation</li>
<li>pipeline 自身無 SLI、健康度靠經驗判斷</li>
<li>遷移期雙軌維護過久、退出條件不明</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pipeline 是黑盒</td>
          <td>訊號消失時靠經驗判斷層級</td>
          <td>每層暴露 SLI、量化 self-monitoring</td>
      </tr>
      <tr>
          <td>Agent 直連 vendor 無中介層</td>
          <td>切換 vendor 要改所有應用層</td>
          <td>加 collector 作為 vendor-neutral 中介</td>
      </tr>
      <tr>
          <td>Ingest 拒收靜默</td>
          <td>429 觸發但應用層 / 告警都無感</td>
          <td>把拒收事件變成告警與 dashboard 指標</td>
      </tr>
      <tr>
          <td>雙軌無退出條件</td>
          <td>遷移期無限延長、成本不斷雙倍</td>
          <td>預設退出 SLI 偏差閾值、加入 release gate</td>
      </tr>
      <tr>
          <td>配置漂移無版本控制</td>
          <td>collector 規則改了沒紀錄</td>
          <td>collector 配置進 git、變更走 release flow</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：pipeline 各層的 quota</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：雙軌對照的資料品質判讀</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：collector / pipeline 的 ownership 邊界</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：讀取路徑的系統設計與資源治理</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a>：collector 部署形態（DaemonSet / sidecar / gateway）</li>
<li><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">6.4 chaos</a>：pipeline 故障模擬作為 chaos 場景</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>：pipeline 各層的成本歸屬</li>
<li><a href="/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/" data-link-title="4.C12 Cloudflare：內部觀測平台的三層能力" data-link-desc="全球 300&#43; edge 節點的觀測架構，把 monitoring、analytics 與 forensics 拆成三個獨立能力層。">4.C12 Cloudflare 內部觀測</a>：大規模自建 pipeline 的三層能力設計</li>
</ul>
]]></content:encoded></item><item><title>4.C12 Cloudflare：內部觀測平台的三層能力</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/</guid><description>&lt;p>Cloudflare 的觀測架構把 monitoring、analytics 和 forensics 拆成三層 pipeline，三層各自承擔不同的 resolution、retention 和查詢模式。規模到達每秒數十億 request、300+ edge location 時，用同一套 pipeline 處理三種能力會同時在成本跟查詢延遲上碰壁。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Cloudflare 的服務涵蓋 CDN、DNS、DDoS 防護、Workers 邊緣運算與 Zero Trust 安全。每秒處理數十億 HTTP request，分布在全球 300+ 資料中心。觀測資料量極大 — 僅 HTTP request log 每秒就產生數百 GB 未壓縮的結構化日誌。&lt;/p>
&lt;p>早期觀測用單一 pipeline 處理所有資料，隨著資料量成長，pipeline 面臨三個壓力：monitoring 需要秒級即時性但不需要全量資料；analytics 需要完整資料但可以延遲分鐘級；forensics（鑑識）需要保留原始事件但查詢頻率極低。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="資料量與成本">資料量與成本&lt;/h3>
&lt;p>每秒數十億 request 的全量日誌，即使壓縮後仍是 PB 級月儲存量。把全量資料送到集中式 log backend（無論是自建 Elasticsearch 或 SaaS Datadog）的 ingestion 成本本身就是天文數字。&lt;/p>
&lt;p>Cloudflare 公開表示過去曾用過 Kafka + Elasticsearch + Grafana 的組合，但隨著 edge 節點增加，centralized ingestion 的頻寬跟儲存成本持續超線性成長。&lt;/p>
&lt;h3 id="edge-到-core-的延遲">Edge 到 Core 的延遲&lt;/h3>
&lt;p>觀測資料從 300+ edge 節點匯聚到中心叢集，網路延遲跟 bandwidth 是物理限制。monitoring 需要秒級判斷（alert 要快觸發），但全量日誌的傳輸延遲可能是分鐘級。&lt;/p>
&lt;h3 id="查詢模式衝突">查詢模式衝突&lt;/h3>
&lt;p>on-call 值班需要的是 dashboard 上的 aggregated metrics（error rate、latency percentile、traffic volume），查詢要快、資料要即時。analytics 團隊需要的是全量日誌做 ad-hoc 查詢（某個 IP 在過去 24 小時的 request pattern），查詢可以慢、但資料要完整。forensics 需要的是單一事件的原始內容（某筆 request 的完整 header 跟 body），查詢極少但需要保留數月。&lt;/p>
&lt;p>三種查詢模式在 resolution、freshness 跟 retention 上的需求完全不同，用同一套 backend 處理會讓所有人的體驗都變差。&lt;/p>
&lt;h2 id="解法三層觀測能力">解法：三層觀測能力&lt;/h2>
&lt;h3 id="monitoringpre-aggregated-metrics--alerting">Monitoring：pre-aggregated metrics + alerting&lt;/h3>
&lt;p>edge 節點在本地做 pre-aggregation — 把每秒的 request count、error count、latency histogram 聚合成每 10 秒的 metric batch，push 到中心的 metrics backend。資料量從 PB/月壓縮到 TB/月。&lt;/p>
&lt;p>Alerting 跟 dashboard 只看聚合後的 metrics，查詢延遲在毫秒級。metrics backend 用 Prometheus-compatible 儲存，Grafana 作為查詢入口。&lt;/p>
&lt;h3 id="analyticssampled--full-fidelity-log-pipeline">Analytics：sampled + full-fidelity log pipeline&lt;/h3>
&lt;p>analytics 層接收全量日誌但做分層處理：高流量 endpoint 的日誌做 adaptive sampling（保留 1%-10%），低流量跟異常 request 保留全量。日誌送到自建的 columnar store（Cloudflare 用 ClickHouse 類的 OLAP 引擎），支援 ad-hoc 查詢。&lt;/p>
&lt;p>Retention 30-90 天，查詢延遲在秒到分鐘級。成本比 monitoring 層高但仍可控 — sampling 是關鍵的成本旋鈕。&lt;/p>
&lt;h3 id="forensics原始事件歸檔">Forensics：原始事件歸檔&lt;/h3>
&lt;p>需要完整保留的事件（安全事件、DDoS 攻擊、客戶投訴關聯的 request）寫入冷儲存（object storage）。查詢走 batch 模式（scan-based），延遲在分鐘到小時級。&lt;/p>
&lt;p>Retention 按合規需求保留 6 個月到數年。成本主要是儲存（object storage 便宜），ingestion 跟 query 成本極低。&lt;/p>
&lt;h2 id="取捨">取捨&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>單一 pipeline&lt;/th>
 &lt;th>三層拆分&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>架構複雜度&lt;/td>
 &lt;td>低（一條路走完）&lt;/td>
 &lt;td>高（三條路各自維護）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本可控度&lt;/td>
 &lt;td>差（全量資料走同一條路，成本隨 traffic 線性成長）&lt;/td>
 &lt;td>好（每層各自有成本旋鈕）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢一致性&lt;/td>
 &lt;td>高（同一個 backend 查）&lt;/td>
 &lt;td>低（三個 backend，查詢語言可能不同）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Freshness&lt;/td>
 &lt;td>被最慢的一段拖住&lt;/td>
 &lt;td>每層獨立（monitoring 秒級、analytics 分鐘級、forensics 小時級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Debugging 路徑&lt;/td>
 &lt;td>短（一個入口）&lt;/td>
 &lt;td>長（先看 monitoring 判斷層級、再決定進 analytics 或 forensics）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三層拆分的最大風險是 debugging 路徑變長 — on-call 先看 dashboard 發現異常，再到 analytics 查 sampled log 找 pattern，最後到 forensics 查原始事件確認細節。如果三層之間的 correlation ID（trace ID、request ID）沒有對齊，跨層查詢會斷掉。&lt;/p></description><content:encoded><![CDATA[<p>Cloudflare 的觀測架構把 monitoring、analytics 和 forensics 拆成三層 pipeline，三層各自承擔不同的 resolution、retention 和查詢模式。規模到達每秒數十億 request、300+ edge location 時，用同一套 pipeline 處理三種能力會同時在成本跟查詢延遲上碰壁。</p>
<h2 id="業務背景">業務背景</h2>
<p>Cloudflare 的服務涵蓋 CDN、DNS、DDoS 防護、Workers 邊緣運算與 Zero Trust 安全。每秒處理數十億 HTTP request，分布在全球 300+ 資料中心。觀測資料量極大 — 僅 HTTP request log 每秒就產生數百 GB 未壓縮的結構化日誌。</p>
<p>早期觀測用單一 pipeline 處理所有資料，隨著資料量成長，pipeline 面臨三個壓力：monitoring 需要秒級即時性但不需要全量資料；analytics 需要完整資料但可以延遲分鐘級；forensics（鑑識）需要保留原始事件但查詢頻率極低。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="資料量與成本">資料量與成本</h3>
<p>每秒數十億 request 的全量日誌，即使壓縮後仍是 PB 級月儲存量。把全量資料送到集中式 log backend（無論是自建 Elasticsearch 或 SaaS Datadog）的 ingestion 成本本身就是天文數字。</p>
<p>Cloudflare 公開表示過去曾用過 Kafka + Elasticsearch + Grafana 的組合，但隨著 edge 節點增加，centralized ingestion 的頻寬跟儲存成本持續超線性成長。</p>
<h3 id="edge-到-core-的延遲">Edge 到 Core 的延遲</h3>
<p>觀測資料從 300+ edge 節點匯聚到中心叢集，網路延遲跟 bandwidth 是物理限制。monitoring 需要秒級判斷（alert 要快觸發），但全量日誌的傳輸延遲可能是分鐘級。</p>
<h3 id="查詢模式衝突">查詢模式衝突</h3>
<p>on-call 值班需要的是 dashboard 上的 aggregated metrics（error rate、latency percentile、traffic volume），查詢要快、資料要即時。analytics 團隊需要的是全量日誌做 ad-hoc 查詢（某個 IP 在過去 24 小時的 request pattern），查詢可以慢、但資料要完整。forensics 需要的是單一事件的原始內容（某筆 request 的完整 header 跟 body），查詢極少但需要保留數月。</p>
<p>三種查詢模式在 resolution、freshness 跟 retention 上的需求完全不同，用同一套 backend 處理會讓所有人的體驗都變差。</p>
<h2 id="解法三層觀測能力">解法：三層觀測能力</h2>
<h3 id="monitoringpre-aggregated-metrics--alerting">Monitoring：pre-aggregated metrics + alerting</h3>
<p>edge 節點在本地做 pre-aggregation — 把每秒的 request count、error count、latency histogram 聚合成每 10 秒的 metric batch，push 到中心的 metrics backend。資料量從 PB/月壓縮到 TB/月。</p>
<p>Alerting 跟 dashboard 只看聚合後的 metrics，查詢延遲在毫秒級。metrics backend 用 Prometheus-compatible 儲存，Grafana 作為查詢入口。</p>
<h3 id="analyticssampled--full-fidelity-log-pipeline">Analytics：sampled + full-fidelity log pipeline</h3>
<p>analytics 層接收全量日誌但做分層處理：高流量 endpoint 的日誌做 adaptive sampling（保留 1%-10%），低流量跟異常 request 保留全量。日誌送到自建的 columnar store（Cloudflare 用 ClickHouse 類的 OLAP 引擎），支援 ad-hoc 查詢。</p>
<p>Retention 30-90 天，查詢延遲在秒到分鐘級。成本比 monitoring 層高但仍可控 — sampling 是關鍵的成本旋鈕。</p>
<h3 id="forensics原始事件歸檔">Forensics：原始事件歸檔</h3>
<p>需要完整保留的事件（安全事件、DDoS 攻擊、客戶投訴關聯的 request）寫入冷儲存（object storage）。查詢走 batch 模式（scan-based），延遲在分鐘到小時級。</p>
<p>Retention 按合規需求保留 6 個月到數年。成本主要是儲存（object storage 便宜），ingestion 跟 query 成本極低。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>單一 pipeline</th>
          <th>三層拆分</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>架構複雜度</td>
          <td>低（一條路走完）</td>
          <td>高（三條路各自維護）</td>
      </tr>
      <tr>
          <td>成本可控度</td>
          <td>差（全量資料走同一條路，成本隨 traffic 線性成長）</td>
          <td>好（每層各自有成本旋鈕）</td>
      </tr>
      <tr>
          <td>查詢一致性</td>
          <td>高（同一個 backend 查）</td>
          <td>低（三個 backend，查詢語言可能不同）</td>
      </tr>
      <tr>
          <td>Freshness</td>
          <td>被最慢的一段拖住</td>
          <td>每層獨立（monitoring 秒級、analytics 分鐘級、forensics 小時級）</td>
      </tr>
      <tr>
          <td>Debugging 路徑</td>
          <td>短（一個入口）</td>
          <td>長（先看 monitoring 判斷層級、再決定進 analytics 或 forensics）</td>
      </tr>
  </tbody>
</table>
<p>三層拆分的最大風險是 debugging 路徑變長 — on-call 先看 dashboard 發現異常，再到 analytics 查 sampled log 找 pattern，最後到 forensics 查原始事件確認細節。如果三層之間的 correlation ID（trace ID、request ID）沒有對齊，跨層查詢會斷掉。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 Log Schema</a>：三層共用的欄位設計（correlation ID、timestamp、service tag）是 log schema 的規模化實例。</li>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 Tracing Context</a>：跨層 correlation 依賴 trace context propagation，edge → core 的 context 傳遞是挑戰。</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a>：三層拆分就是 pipeline 的 routing 跟 processing 層設計。</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a>：三層各自的成本旋鈕（sampling rate、retention、storage tier）是成本歸因的實作入口。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>觀測平台帳單主要被全量日誌 ingestion 佔據，但 90% 的日誌沒人查過</li>
<li>Dashboard 查詢越來越慢，因為查詢打的是存了全量資料的同一個 backend</li>
<li>on-call 跟 analytics 團隊對觀測 backend 的需求衝突（一個要快、一個要全）</li>
<li>edge / CDN / 多 region 架構下，central pipeline 的 ingestion bandwidth 成為瓶頸</li>
<li>安全團隊要求保留原始事件 6 個月以上，但 hot tier 儲存成本撐不住</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/vision-for-observability/">Our Vision for Observability at Cloudflare</a></li>
<li><a href="https://blog.cloudflare.com/building-cloudflare-on-cloudflare/">Building Cloudflare on Cloudflare</a></li>
</ul>
]]></content:encoded></item><item><title>Remote Write 與長期儲存整合</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/remote-write-long-term-storage/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a> 的 vendor deep article，深化 overview「Remote write / read」段。初次接觸 Prometheus 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>Remote write 把 Prometheus 的 metrics 即時推送到外部長期儲存，解決單機 retention 上限與跨實例統一查詢的限制。三個觸發點會讓團隊需要 remote write 與長期儲存：&lt;/p>
&lt;p>Prometheus 預設 retention 是 15 天。業務需要回顧 90 天的趨勢（容量規劃、季度 SLO 報告、成本歸因），本地 disk 不夠放。加大 disk 可以延長 retention，但 Prometheus 的查詢效能會隨資料量下降 — 本地 TSDB 不做 downsampling，查 90 天 range 的 query 要掃描全量 sample。&lt;/p>
&lt;p>多個 Prometheus 實例分散在不同叢集（prod-us、prod-eu、staging），團隊需要一個統一查詢入口看跨叢集 metrics。每個 Prometheus 各自保存自己的資料，沒有跨實例查詢能力。手動切換 Grafana datasource 容易遺漏某個叢集的異常。&lt;/p>
&lt;p>單機 Prometheus 是 SPOF — process crash 或 VM 故障時 metrics 完全不可用。跑兩個 Prometheus 各自 scrape 同一組 target 可以達到 HA，但兩份資料有微小差異（scrape 時間偏移），下游查詢需要 dedup。&lt;/p>
&lt;p>Remote write 解決這三個問題：Prometheus 保持短期本地儲存（scrape + 即時查詢），同時把 metrics 串流到長期儲存後端。長期後端負責壓縮、downsampling、跨實例查詢與 HA dedup。&lt;/p>
&lt;h2 id="核心概念">核心概念&lt;/h2>
&lt;h3 id="remote-write-protocol">Remote write protocol&lt;/h3>
&lt;p>Prometheus 透過 HTTP POST 把 time series 送到 remote write endpoint。每次 POST 包含一批 samples（protobuf 編碼、snappy 壓縮），由 Prometheus 的 WAL（write-ahead log）驅動 — WAL 記錄所有 scrape 到的 samples，remote write 從 WAL 讀取並串流到遠端。&lt;/p>
&lt;p>這個設計意味著 remote write 是 best-effort 但有 buffer：如果遠端暫時不可達，samples 會堆在 WAL 裡等重試。WAL 的大小有上限（&lt;code>--storage.tsdb.wal-segment-size&lt;/code>，預設 128 MB per segment），堆積太多會導致 WAL 佔用大量 disk。&lt;/p>
&lt;h3 id="exemplar-forwarding">Exemplar forwarding&lt;/h3>
&lt;p>Prometheus 2.26 開始支援 exemplar — 在 histogram 或 counter sample 上附加 trace_id / span_id。Remote write 也能把 exemplar 送到支援的後端（Mimir、Grafana Cloud、Tempo）。Exemplar 讓讀者從 metric anomaly 一鍵跳到對應的 trace，是 metrics-to-traces 橋接的關鍵能力。&lt;/p>
&lt;p>啟用方式：scrape config 加 &lt;code>enable_features: [exemplar-storage]&lt;/code>，remote write endpoint 支援 exemplar 即可自動 forward。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a> 的 vendor deep article，深化 overview「Remote write / read」段。初次接觸 Prometheus 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>Remote write 把 Prometheus 的 metrics 即時推送到外部長期儲存，解決單機 retention 上限與跨實例統一查詢的限制。三個觸發點會讓團隊需要 remote write 與長期儲存：</p>
<p>Prometheus 預設 retention 是 15 天。業務需要回顧 90 天的趨勢（容量規劃、季度 SLO 報告、成本歸因），本地 disk 不夠放。加大 disk 可以延長 retention，但 Prometheus 的查詢效能會隨資料量下降 — 本地 TSDB 不做 downsampling，查 90 天 range 的 query 要掃描全量 sample。</p>
<p>多個 Prometheus 實例分散在不同叢集（prod-us、prod-eu、staging），團隊需要一個統一查詢入口看跨叢集 metrics。每個 Prometheus 各自保存自己的資料，沒有跨實例查詢能力。手動切換 Grafana datasource 容易遺漏某個叢集的異常。</p>
<p>單機 Prometheus 是 SPOF — process crash 或 VM 故障時 metrics 完全不可用。跑兩個 Prometheus 各自 scrape 同一組 target 可以達到 HA，但兩份資料有微小差異（scrape 時間偏移），下游查詢需要 dedup。</p>
<p>Remote write 解決這三個問題：Prometheus 保持短期本地儲存（scrape + 即時查詢），同時把 metrics 串流到長期儲存後端。長期後端負責壓縮、downsampling、跨實例查詢與 HA dedup。</p>
<h2 id="核心概念">核心概念</h2>
<h3 id="remote-write-protocol">Remote write protocol</h3>
<p>Prometheus 透過 HTTP POST 把 time series 送到 remote write endpoint。每次 POST 包含一批 samples（protobuf 編碼、snappy 壓縮），由 Prometheus 的 WAL（write-ahead log）驅動 — WAL 記錄所有 scrape 到的 samples，remote write 從 WAL 讀取並串流到遠端。</p>
<p>這個設計意味著 remote write 是 best-effort 但有 buffer：如果遠端暫時不可達，samples 會堆在 WAL 裡等重試。WAL 的大小有上限（<code>--storage.tsdb.wal-segment-size</code>，預設 128 MB per segment），堆積太多會導致 WAL 佔用大量 disk。</p>
<h3 id="exemplar-forwarding">Exemplar forwarding</h3>
<p>Prometheus 2.26 開始支援 exemplar — 在 histogram 或 counter sample 上附加 trace_id / span_id。Remote write 也能把 exemplar 送到支援的後端（Mimir、Grafana Cloud、Tempo）。Exemplar 讓讀者從 metric anomaly 一鍵跳到對應的 trace，是 metrics-to-traces 橋接的關鍵能力。</p>
<p>啟用方式：scrape config 加 <code>enable_features: [exemplar-storage]</code>，remote write endpoint 支援 exemplar 即可自動 forward。</p>
<h3 id="dedup-策略">Dedup 策略</h3>
<p>跑兩個 Prometheus HA pair 時，兩個實例都 scrape 同一組 target、都 remote write 到同一個後端。後端會收到兩份幾乎相同但不完全一致的 samples（scrape 時間差 ±1-2 秒）。</p>
<p>Thanos 和 Mimir 都有 dedup 機制：Thanos 在 query 層根據 <code>external_labels</code>（replica label）做 dedup，每個 time window 只取一個 replica 的值。Mimir 在 ingester 層做 dedup，同一個 series 的重複 sample 在寫入時合併。</p>
<p>Dedup 的前提是兩個 Prometheus 實例設定不同的 <code>external_labels</code>（例如 <code>replica: a</code> / <code>replica: b</code>），讓後端能辨別哪些 series 是同一組的不同副本。</p>
<h2 id="配置">配置</h2>
<h3 id="remote-write-基本設定">Remote write 基本設定</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># prometheus.yml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">remote_write</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;http://mimir-distributor:9009/api/v1/push&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">queue_config</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span><span class="nt">capacity</span><span class="p">:</span><span class="w"> </span><span class="m">10000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">      </span><span class="nt">max_shards</span><span class="p">:</span><span class="w"> </span><span class="m">30</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">max_samples_per_send</span><span class="p">:</span><span class="w"> </span><span class="m">5000</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span><span class="nt">batch_send_deadline</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="nt">write_relabel_configs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="nt">source_labels</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">__name__]</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">regex</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;go_.*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">        </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">drop</span></span></span></code></pre></div><p><code>queue_config</code> 控制 remote write 的並行度與批次大小：</p>
<ul>
<li><code>capacity</code>：內存中暫存的 sample 數量。太小會頻繁 flush、太大會佔記憶體</li>
<li><code>max_shards</code>：並行的 write goroutine 數量。Shard 太少會造成 backlog、太多會壓垮遠端</li>
<li><code>max_samples_per_send</code>：每次 POST 的 sample 數量。5000 是常用值</li>
<li><code>batch_send_deadline</code>：即使 batch 沒滿也在這個時間內 flush，避免低流量時 sample 延遲太久</li>
</ul>
<p><code>write_relabel_configs</code> 在 remote write 前過濾 series — 不需要長期保存的 internal metrics（go runtime、scrape metadata）可以在這裡 drop，減少長期儲存的 cardinality 與成本。</p>
<h3 id="external-labelsha-與多叢集">External labels（HA 與多叢集）</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">global</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">external_labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">cluster</span><span class="p">:</span><span class="w"> </span><span class="l">prod-us</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">replica</span><span class="p">:</span><span class="w"> </span><span class="l">a</span></span></span></code></pre></div><p><code>cluster</code> label 區分來源叢集，<code>replica</code> label 讓長期儲存做 dedup。每個 Prometheus 實例的 external_labels 必須唯一。</p>
<h3 id="三家長期儲存比較">三家長期儲存比較</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Mimir</th>
          <th>Thanos</th>
          <th>Cortex</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>架構模式</td>
          <td>Microservice（distributor / ingester / compactor / querier）</td>
          <td>Sidecar + Store Gateway + Compactor + Query</td>
          <td>Microservice（跟 Mimir 同源、Mimir 是 Cortex fork）</td>
      </tr>
      <tr>
          <td>部署複雜度</td>
          <td>中（Helm chart，最少 4 個元件）</td>
          <td>中高（sidecar 綁 Prometheus pod，元件分散）</td>
          <td>高（元件多、已進入維護模式）</td>
      </tr>
      <tr>
          <td>Query layer</td>
          <td>原生 PromQL + split/merge</td>
          <td>Thanos Query 做 fan-out + dedup</td>
          <td>原生 PromQL（跟 Mimir 共用）</td>
      </tr>
      <tr>
          <td>多租戶</td>
          <td>原生（X-Scope-OrgID header）</td>
          <td>有限（靠 label 或獨立部署）</td>
          <td>原生（Mimir 繼承）</td>
      </tr>
      <tr>
          <td>Downsampling</td>
          <td>支援（compactor 做 1h/5m 降取樣）</td>
          <td>支援（compactor）</td>
          <td>支援</td>
      </tr>
      <tr>
          <td>開發狀態</td>
          <td>活躍（Grafana Labs 主推）</td>
          <td>活躍（CNCF incubating）</td>
          <td>維護模式（Grafana Labs 把精力轉到 Mimir）</td>
      </tr>
      <tr>
          <td>對象儲存</td>
          <td>S3 / GCS / Azure Blob</td>
          <td>S3 / GCS / Azure Blob / 本地</td>
          <td>S3 / GCS</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>自管 compute + storage；Grafana Cloud 按 active series 計費</td>
          <td>自管 compute + storage</td>
          <td>自管（不推薦新部署）</td>
      </tr>
  </tbody>
</table>
<p>選擇判準依三個維度排序：</p>
<p><strong>已經在用 Grafana 生態</strong>（Grafana dashboard、Loki、Tempo）：Mimir 是最自然的選擇，跟 Grafana Stack 的整合最深，Grafana Cloud 可以免管 Mimir。</p>
<p><strong>需要最小化對 Prometheus 的改動</strong>：Thanos sidecar 模式不改 Prometheus 配置（sidecar 讀本地 TSDB block），適合「先加長期儲存、Prometheus 維持現狀」的漸進路徑。但 sidecar 綁 Prometheus pod，K8s 環境外的部署更複雜。</p>
<p><strong>多租戶需求</strong>：Mimir 原生支援多租戶隔離（每個 tenant 獨立 TSDB、query isolation），Thanos 的多租戶靠 label 或獨立部署。</p>
<p>Cortex 是 Mimir 的前身，新部署不推薦。既有 Cortex 部署可參考 Grafana Labs 的 Mimir migration guide。</p>
<h3 id="uber-m3-的第四條路">Uber M3 的第四條路</h3>
<p><a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">Uber M3 案例</a>選擇了自建 M3DB 而非 Mimir / Thanos / Cortex — 原因是 M3DB 在 2018 年啟動時、Mimir 尚未存在、Cortex 還在早期階段、Thanos 也剛開源。M3DB 的設計核心是 namespace-level retention（不同 namespace 不同 retention 跟 resolution）、跟 Uber 的 etcd service discovery 深度整合。</p>
<p>M3 的經驗對後來的三家有直接影響：Mimir 的 per-tenant retention、Thanos 的 downsampling compactor、都能追溯到 M3 先踩過的問題。今天做新部署不需要重走 M3 的路 — Mimir 跟 Thanos 已經成熟。但 M3 案例揭露的設計判準仍然有效：</p>
<ul>
<li><strong>跨 cluster 查詢需要 fan-out + dedup</strong>：三家都實作了這個能力，但部署配置跟 dedup 策略各有差異</li>
<li><strong>Downsampling 是長期成本控制的必要手段</strong>：不做 downsampling、90 天 range query 的效能跟成本都不可接受</li>
<li><strong>多租戶隔離不只是 query 層面</strong>：ingestion rate limit 跟 storage quota per tenant 才能防止「一個團隊的 cardinality 爆炸拖垮整個平台」</li>
</ul>
<h2 id="故障與邊界">故障與邊界</h2>
<h3 id="remote-write-backlog-佔滿-wal">Remote write backlog 佔滿 WAL</h3>
<p><strong>觸發條件</strong>：遠端不可達（network 問題、後端過載）持續超過數分鐘，WAL segment 堆積。</p>
<p><strong>表現</strong>：<code>prometheus_remote_storage_bytes_total</code> 停止增長（寫不出去）、<code>prometheus_wal_storage_size_bytes</code> 持續增長、disk 使用率上升。嚴重時 WAL 佔滿 disk，Prometheus 無法寫入新 sample、連 local scrape 也受影響。</p>
<p><strong>修復</strong>：先恢復遠端連線。WAL backlog 會在連線恢復後自動 catch up — Prometheus 按 WAL 順序重送積壓的 samples。如果 catch up 時間太長（例如堆了數小時），remote write 的 max_shards 可以暫時調高加速回補，但要注意不要壓垮剛恢復的遠端。</p>
<p><strong>預防</strong>：監控 <code>prometheus_remote_storage_queue_highest_sent_timestamp_seconds</code> 跟 current time 的差距 — 差距代表 remote write 延遲。差距超過 5 分鐘時告警。設定 WAL 的 disk 空間上限（<code>--storage.tsdb.max-block-duration</code> 搭配 retention 控制 total disk）。</p>
<h3 id="target-不可達時的-retry-storm">Target 不可達時的 retry storm</h3>
<p><strong>觸發條件</strong>：remote write endpoint 回傳 5xx 或 429（rate limit），Prometheus 進入指數退避重試。大量 shard 同時 retry，CPU 跟 network 消耗上升。</p>
<p><strong>表現</strong>：<code>prometheus_remote_storage_retried_samples_total</code> 增長、CPU 使用上升、remote write 延遲拉大。如果後端本來就過載，retry storm 會讓情況惡化。</p>
<p><strong>修復</strong>：remote write 配置中的 <code>min_backoff</code> / <code>max_backoff</code> 控制 retry 間隔（預設 30ms / 5s）。可以調高 <code>min_backoff</code> 減緩 retry 頻率。長期修法是讓後端回傳 429 搭配 <code>Retry-After</code> header，Prometheus 會遵守。</p>
<h3 id="metrics-語意-drift">Metrics 語意 drift</h3>
<p><strong>觸發條件</strong>：多個 Prometheus 實例的 <code>write_relabel_configs</code> 不一致、或 external_labels 設定有誤。</p>
<p><strong>表現</strong>：同一個 metric 在長期儲存中出現語意不同的 series — 有些 instance 保留了某個 label、有些 drop 掉了。Dashboard 查詢結果不一致（取決於查到哪個實例的 series）。</p>
<p><strong>修復</strong>：remote write 的 <code>write_relabel_configs</code> 集中管理（配置模板或 Prometheus Operator 的 PrometheusSpec.remoteWrite）。每次修改 relabel 規則後，驗證所有實例的 series label set 一致。Mimir 的 <code>active_series</code> API 可以列出目前所有 active series 的 label set。</p>
<h3 id="remote-write-protocol-版本不匹配">Remote write protocol 版本不匹配</h3>
<p><strong>觸發條件</strong>：Prometheus 版本跟長期儲存後端期望的 remote write protocol 版本不一致。Prometheus 2.x 使用 remote write v1（protobuf + snappy），部分較新後端開始支援 v2（native histogram 支援、metadata 改進）。</p>
<p><strong>表現</strong>：後端回傳 400 Bad Request。Prometheus 對 4xx 的預設行為是不 retry（視為 client error、retry 無意義），samples 被 drop。<code>prometheus_remote_storage_samples_failed_total</code> 增長但不像 5xx 那樣有明顯的 retry storm — 靜默丟失更難察覺。</p>
<p><strong>修復</strong>：確認 Prometheus 版本跟後端的 protocol 相容性。Mimir / Thanos 的文件通常標明支援的 remote write protocol 版本。版本不匹配時升級 Prometheus 或降級後端配置。</p>
<h3 id="何時單機-prometheus-不夠">何時單機 Prometheus 不夠</h3>
<p>三個訊號同時出現時，remote write + 長期儲存從「可選」變成「必要」：</p>
<p><strong>Active series 超過 500 萬</strong>。單機 Prometheus 在 500 萬 series 左右開始出現記憶體壓力（head block ~20 GB）、WAL replay 時間拉長（重啟要數分鐘）、compaction 佔用 CPU。<a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">Uber 在 M3 專案</a>遇到的正是這個天花板 — 數十個叢集各自 scrape 的 metrics 匯總後 series 數遠超單機能力，但「用更大的 VM 跑 Prometheus」不是解法，因為 Prometheus 的 TSDB 是單線程 compaction、垂直擴展的效益有上限。</p>
<p><strong>Retention 需求超過 30 天</strong>。本地 TSDB 的 retention 拉長時，range query 的效能線性退化 — 查 90 天 range 要掃描的 block 數量是 15 天的 6 倍。Downsampling 是長期儲存後端的標準能力（Mimir / Thanos compactor 把 5 分鐘 resolution 降到 1 小時），但 Prometheus 本地 TSDB 不做 downsampling。Uber 的 M3DB 設計了 namespace-level retention（short-term 48h full resolution、long-term 1y downsampled），讓查詢成本不隨 retention 線性成長。</p>
<p><strong>跨叢集統一查詢</strong>。多個 Prometheus 各自 scrape 不同 cluster 時，工程師需要一個入口看「所有 cluster 的 checkout error rate」。手動切 Grafana datasource 容易遺漏。Remote write 把所有 Prometheus 的 metrics 匯入同一個長期儲存、用單一查詢入口（Mimir querier / Thanos Query）做 fan-out。</p>
<p>這三個需求在中型公司（50-200 服務、3+ K8s cluster）通常在 1-2 年內同時浮現。規劃 remote write 時不用等三個都出現 — 任一個出現就是啟動的合理時機。</p>
<h2 id="容量與-cost">容量與 Cost</h2>
<h3 id="remote-write-bandwidth">Remote write bandwidth</h3>
<p>Remote write 的 bandwidth ≈ ingestion rate × 每 sample 壓縮後大小（約 1-2 bytes with snappy）。</p>
<table>
  <thead>
      <tr>
          <th>Ingestion rate</th>
          <th>估算 bandwidth</th>
          <th>對應規模參考</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>10 萬 samples/sec</td>
          <td>~100-200 KB/s</td>
          <td>小型：5-10 服務、1 cluster</td>
      </tr>
      <tr>
          <td>50 萬 samples/sec</td>
          <td>~500 KB/s-1 MB/s</td>
          <td>中型：50 服務、2-3 cluster</td>
      </tr>
      <tr>
          <td>200 萬 samples/sec</td>
          <td>~2-4 MB/s</td>
          <td>大型：200 服務、5+ cluster</td>
      </tr>
      <tr>
          <td>1000 萬 samples/sec</td>
          <td>~10-20 MB/s</td>
          <td>平台級：Uber M3 等級</td>
      </tr>
  </tbody>
</table>
<p>每個 active series 在 15 秒 scrape interval 下每秒產生 ~0.067 個 sample。100 萬 active series 的 ingestion rate ≈ 6.7 萬 samples/sec，對應 ~70-140 KB/s remote write bandwidth。這個數字在內網環境下通常不是瓶頸。</p>
<p>真正的瓶頸在兩個地方：<strong>roundtrip latency</strong> 決定單 shard 吞吐上限（每次 POST 等回應才發下一批）、<strong>後端 ingestion capacity</strong> 決定能消化多少 samples/sec。Mimir 的 distributor 跟 ingester 可以水平擴展，但每加一個 ingester 增加 compute 成本。bandwidth 只是 capacity planning 的第一步，實際規模要用 Mimir 的 <code>cortex_distributor_received_samples_total</code> 跟 <code>cortex_ingester_memory_series</code> 做持續觀測。</p>
<h3 id="長期儲存的-compaction-與-downsampling-cost">長期儲存的 compaction 與 downsampling cost</h3>
<p>Mimir 和 Thanos 的 compactor 定期合併 block 並做 downsampling（5m → 1h 粒度）。Compaction 消耗 CPU 和 disk I/O，但跑在長期儲存自己的 compute 上，不影響 Prometheus。</p>
<p>成本結構：</p>
<ul>
<li><strong>Compute</strong>：distributor + ingester + querier + compactor 的 CPU / memory。Mimir 官方建議 ingester 是最吃資源的元件（記憶體中保存 active series）</li>
<li><strong>Object storage</strong>：S3 / GCS 的儲存量 ≈ ingestion rate × retention × 壓縮率。Compaction 跟 downsampling 會降低儲存量（通常 2-5x 壓縮）</li>
<li><strong>Query cost</strong>：長 range query 需要讀大量 block — 在 cloud object storage 上是 GET request 成本。Mimir 用 index cache（memcached）降低重複查詢的 GET request</li>
</ul>
<p>跟 Prometheus 本地 TSDB 比，長期儲存把 disk cost 換成 object storage cost（通常更便宜），但增加了 compute cost（長期儲存的 ingester / querier / compactor）。判斷轉折點的方式是比較本地 SSD cost × retention 跟 object storage cost + compute cost。retention 超過 30 天時，object storage 的成本優勢通常明顯。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="接-grafana-stack-lgtm">接 Grafana Stack LGTM</h3>
<p>Mimir 是 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a> LGTM（Loki + Grafana + Tempo + Mimir）的 metrics 後端。Prometheus remote write 到 Mimir 後，Grafana 用 Mimir 作為 Prometheus-compatible datasource，查詢語言仍是 PromQL。Exemplar forwarding 讓 Mimir metrics 可以連結到 Tempo traces。</p>
<h3 id="接-telemetry-pipeline">接 Telemetry Pipeline</h3>
<p>Remote write 在 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 中扮演 metrics ingestion 段。如果同時使用 OpenTelemetry Collector，Collector 可以作為 remote write 的中繼（接收 Prometheus scrape → OTLP export → Mimir OTLP endpoint），但多一層中繼增加了 failure point。直接 Prometheus → Mimir remote write 是最簡路徑。</p>
<h3 id="接-cost-attribution">接 Cost Attribution</h3>
<p>長期儲存的多租戶能力讓 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a> 可以按 tenant / team / service 拆分 metrics 成本。Mimir 的 per-tenant active series quota 同時控制 cardinality 與成本。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus 服務頁</a>：overview 跟日常操作入口</li>
<li><a href="../promql-recording-rules/">PromQL 與 Recording Rules 實務</a>：remote write 架構下 recording rules 的部署位置選擇</li>
<li><a href="../capacity-failure-modes/">容量規劃與故障模式</a>：remote write 作為容量超限時的卸載路徑</li>
<li><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>：Mimir 作為長期儲存的完整操作指南</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a>：remote write 在 pipeline 架構中的定位</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a>：多租戶 metrics 的成本拆分</li>
</ul>
]]></content:encoded></item><item><title>Datadog → Grafana Stack：把 $50K/month bill 拆解到 self-hosted observability</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/migrate-to-grafana-stack/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a>（target）。跟前三篇 migration（&lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic&lt;/a> phased / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> drop-in / &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &amp;#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora&lt;/a> hybrid）對照、本篇是 &lt;em>cost-driven multi-tool migration&lt;/em> — 不是換一個產品、是把 &lt;em>一站式 SaaS&lt;/em> 拆成 &lt;em>五個專責 OSS / cloud component&lt;/em>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a>（source）跟 <a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a>（target）。跟前三篇 migration（<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic</a> phased / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> drop-in / <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> hybrid）對照、本篇是 <em>cost-driven multi-tool migration</em> — 不是換一個產品、是把 <em>一站式 SaaS</em> 拆成 <em>五個專責 OSS / cloud component</em>。</p></blockquote>
<h2 id="50kmonth-bill-拆解先看錢花在哪再決定怎麼遷">$50K/month bill 拆解：先看錢花在哪、再決定怎麼遷</h2>
<p>中型 SaaS（100-500 host、5K-50K metric series、TB-level log/day）的 Datadog 月帳單長這樣：</p>
<table>
  <thead>
      <tr>
          <th>計費項</th>
          <th>平均單價</th>
          <th>中型 SaaS 估算 / month</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infrastructure host</td>
          <td>$15-23 / host</td>
          <td>200 host × $20 = $4,000</td>
      </tr>
      <tr>
          <td>APM host</td>
          <td>$31 / host</td>
          <td>100 host × $31 = $3,100</td>
      </tr>
      <tr>
          <td>Custom metrics</td>
          <td>$0.05 / 100 series</td>
          <td>30K series × $0.05 = $1,500</td>
      </tr>
      <tr>
          <td>Log ingest</td>
          <td>$0.10 / GB ingested</td>
          <td>50TB × $0.10 = $5,000</td>
      </tr>
      <tr>
          <td>Log retention（15-day）</td>
          <td>$1.27 / million events</td>
          <td>50G event × $1.27 = $6,350</td>
      </tr>
      <tr>
          <td>Log indexing</td>
          <td>$1.70 / million events</td>
          <td>50G × $1.70 = $8,500</td>
      </tr>
      <tr>
          <td>Network</td>
          <td>$5 / host</td>
          <td>200 × $5 = $1,000</td>
      </tr>
      <tr>
          <td>RUM / Session</td>
          <td>$1.50 / 1000 session</td>
          <td>30M session × $1.5 = $4,500</td>
      </tr>
      <tr>
          <td>Synthetics</td>
          <td>$5 / 10K test runs</td>
          <td>50K test = $25</td>
      </tr>
      <tr>
          <td>Total</td>
          <td>-</td>
          <td><strong>$34,000 / month</strong>（保守估）</td>
      </tr>
  </tbody>
</table>
<p>擴張到 500 host / 100TB log 的 production：$80K-150K / month 範圍。Grafana stack（self-hosted on K8s + Grafana Cloud 部分服務）對等 capacity 通常 $8K-30K / month — <em>2.5-5x cost reduction</em>。</p>
<p>但 cost 不是唯一 driver。其他 driver：</p>
<ul>
<li><strong>Multi-cloud / hybrid</strong>：Datadog 集中、Grafana 可分散部署符合資料 residency</li>
<li><strong>OpenTelemetry-first</strong>：Grafana stack 對 OTel 是 native、Datadog 仍 vendor-specific agent</li>
<li><strong>Long-term retention</strong>：Loki 用 S3 cold tier 跑 1 年 retention 比 Datadog 便宜 10-50x</li>
</ul>
<h2 id="五個責任五個-component不是替換一個產品">五個責任、五個 component：不是替換一個產品</h2>
<p>Datadog 是 <em>一站式 SaaS</em>、單一 agent + 單一 UI 包 5 個責任。Grafana stack 把責任拆給 5 個專責 component：</p>
<table>
  <thead>
      <tr>
          <th>責任</th>
          <th>Datadog 處理</th>
          <th>Grafana Stack 對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Metric</td>
          <td>Datadog metric</td>
          <td>Mimir（Prometheus-compatible long-term）</td>
      </tr>
      <tr>
          <td>Log</td>
          <td>Datadog Logs</td>
          <td>Loki（label-indexed log）</td>
      </tr>
      <tr>
          <td>Trace</td>
          <td>Datadog APM</td>
          <td>Tempo（trace-only object storage）</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>Datadog dashboard</td>
          <td>Grafana</td>
      </tr>
      <tr>
          <td>Agent / shipper</td>
          <td>Datadog Agent</td>
          <td>Alloy（OTel-based collector）+ Grafana Agent / Promtail</td>
      </tr>
  </tbody>
</table>
<p>Migration 是 <em>五個獨立 stream</em>、不是單一 cutover。SRE 對「一個 agent 包所有」的心智模型要拆。</p>
<h2 id="migration-結構每個-component-各自-phased整體-staggered">Migration 結構：每個 component 各自 phased、整體 staggered</h2>
<p>不像前三篇 migration 是線性流程、本篇是 <em>5 個 parallel migration stream</em> + 跨 stream coordination：</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">           Phase 0           Phase 1            Phase 2          Phase 3
</span></span><span class="line"><span class="ln">2</span><span class="cl">           Audit             Deploy             Dual-ship        Cutover
</span></span><span class="line"><span class="ln">3</span><span class="cl">Metric    [audit]──→        [deploy Mimir]──→ [dual-ship]──→  [cutover]
</span></span><span class="line"><span class="ln">4</span><span class="cl">APM       [audit]──→        [deploy Tempo]──→ [dual-ship]──→  [cutover]
</span></span><span class="line"><span class="ln">5</span><span class="cl">Log       [audit]──→        [deploy Loki]──→  [dual-ship]──→  [cutover]
</span></span><span class="line"><span class="ln">6</span><span class="cl">Dashboard [audit]──→        [deploy Grafana]──→ [rebuild]──→   [cutover]
</span></span><span class="line"><span class="ln">7</span><span class="cl">Alert     [audit]──→        [deploy Alertmgr]──→ [parallel]──→ [cutover]</span></span></code></pre></div><p>每個 stream 獨立做 dual-ship + cutover、不必同步；通常 <em>Metric 先遷</em>（cardinality 議題暴露最快）、然後 Log、最後 APM（trace correlation 最依賴 dashboard / alert）。</p>
<h2 id="agent-migrationdatadog-agent--otel-collector--alloy">Agent migration：Datadog Agent → OTel Collector / Alloy</h2>
<p>Datadog Agent 是 vendor-specific binary、抽出來換成 OpenTelemetry Collector / Grafana Alloy：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># alloy config (HCL-like)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="l">prometheus.scrape &#34;k8s_pods&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="l">targets = discovery.kubernetes.pods.targets</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="l">forward_to = [prometheus.remote_write.mimir.receiver]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span>}<span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="l">prometheus.remote_write &#34;mimir&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="l">endpoint {</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="l">url = &#34;https://mimir.internal/api/v1/push&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span>}<span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="l">loki.source.kubernetes &#34;pods&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="l">targets = discovery.kubernetes.pods.targets</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="l">forward_to = [loki.write.production.receiver]</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span>}<span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w"></span><span class="l">otelcol.receiver.otlp &#34;default&#34; {</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="l">grpc {}</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span><span class="l">output {</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="l">traces = [otelcol.exporter.otlp.tempo.input]</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span>}<span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w"></span>}</span></span></code></pre></div><p>Migration 期間 <em>dual-shipper</em> 是標準作法：</p>
<ul>
<li>Datadog Agent 跟 Alloy 並存（短期 capacity 兩倍）</li>
<li>同 host 同時 ship 兩端、觀察一致性</li>
<li>漸進 disable Datadog Agent 的 metric / log / APM 子模組</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1cardinality-爆mimir-端-series-暴增">Case 1：Cardinality 爆，Mimir 端 series 暴增</h3>
<p><strong>徵兆</strong>：Datadog 端 30K series、ship 到 Mimir 後 series 變 500K、Mimir indexer OOM。</p>
<p><strong>根因</strong>：Datadog 內部對 tag 做 <em>自動 aggregation</em> 跟 <em>low-cardinality enforcement</em>；Prometheus / Mimir 對 <em>每個 unique label set</em> 算一個 series、application code 的 high-cardinality label（user_id / request_id）直接爆。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Audit 階段</strong> 跑 <code>topk(100, count by (__name__) ({__name__=~&quot;.+&quot;}))</code> 找 high-cardinality metric</li>
<li><strong>drop high-cardinality label</strong>：Alloy / OTel collector 端 <code>relabel</code> 規則 drop user_id 等 unbounded label</li>
<li><strong>改 histogram bucket</strong>：高 cardinality 通常來自 label combination、改用 fixed-bucket histogram</li>
<li><strong>適當改 metric 為 log</strong>：請求 ID 是 trace context、不該是 metric label</li>
</ol>
<h3 id="case-2log-volume-cost-預估失準">Case 2：Log volume cost 預估失準</h3>
<p><strong>徵兆</strong>：Loki 部署 1 個月後 S3 帳單比預估高 2x；object storage 跟 query GB-scan 都超預期。</p>
<p><strong>根因</strong>：Datadog 對 log 做自動 sampling / aggregation、bill 是 indexed event；Loki 是 <em>全量 raw ingest</em> + S3 cold storage、按實際 byte 計費。raw log volume 比 indexed event 高 3-10x。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Ingest-side sampling</strong>：Alloy / Promtail 端 sample debug / info log、只 ingest warn / error 全量</li>
<li><strong>Log structure</strong>：JSON log 比 text log 壓縮率高、Loki S3 size 少 50%</li>
<li><strong>Retention tier</strong>：hot 7 天 S3 standard / cold 1 年 S3 Glacier、retention budget 控制</li>
</ol>
<h3 id="case-3datadog-dashboard-不能直接轉-grafana">Case 3：Datadog dashboard 不能直接轉 Grafana</h3>
<p><strong>徵兆</strong>：Migration 計畫設「dashboard 自動轉換」、實際跑 Datadog API export → Grafana import、80% dashboard 缺 widget / metric 對不上。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>Datadog query syntax 跟 Grafana / Mimir 的 PromQL 不直接相容</li>
<li>Datadog widget type（top-list / hostmap）Grafana 沒對應</li>
<li>Tag-based aggregation 對應 Prometheus label 但語法不同</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>接受重建</strong>：production-grade dashboard 必須人工重建、不要期待自動轉</li>
<li><strong>Prioritize</strong>：先重建 <em>SOC 用 / production-critical</em> 30%、其他 deprecate</li>
<li><strong>migration window 增 4-6 週</strong>：dashboard rebuild 是 underestimated effort</li>
</ol>
<h3 id="case-4alert-routing-換邏輯pagerduty-integration-不通">Case 4：Alert routing 換邏輯，PagerDuty integration 不通</h3>
<p><strong>徵兆</strong>：Cutover 後 alert 不送 PagerDuty、SOC 半小時才發現；alert 端 webhook 配置正確、但 payload format 跟 Datadog 不同、PagerDuty 端 rule 過濾掉。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>Datadog alert payload 含 <code>event_type=alert</code>、PagerDuty integration 用這個 routing</li>
<li>Alertmanager 預設 payload 結構不同</li>
<li>PagerDuty rule 端針對 Datadog event 寫 schema、Alertmanager event 不 match</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-cutover test</strong>：Alertmanager → PagerDuty 跑 dry-run、send test alert 驗證</li>
<li><strong>PagerDuty Service</strong>：建獨立 Grafana-source Service、不共用 Datadog Service</li>
<li><strong>Alertmanager template</strong>：用 webhook 自定 JSON template、payload 接近 Datadog 結構</li>
</ol>
<h3 id="case-5slo-definition-跟-monitor-type-對不上">Case 5：SLO definition 跟 monitor type 對不上</h3>
<p><strong>徵兆</strong>：Datadog SLO 跑 99.9% availability、轉到 Grafana SLO + Mimir 後實際 9X% 數字不一致；SOC 跑 dashboard 比對 5 個 SLO、4 個誤差 0.1-0.3%。</p>
<p><strong>根因</strong>：</p>
<ul>
<li>Datadog SLO 計算 over time window 用內部 query；Grafana SLO 用 PromQL 寫公式</li>
<li>Datadog 對 <code>success_rate</code> 處理 missing data 跟 PromQL 預設不同</li>
<li>Time bucket boundary 處理差異</li>
</ul>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>重定義 SLO 在 PromQL</strong>：不嘗試「複製」、是「重定義」、認真寫 PromQL 表達式</li>
<li><strong>接受 ±0.1% drift</strong>：production-critical SLO 跑 dual-track 1-2 個月、tune PromQL 到 acceptable drift</li>
<li><strong>SLO migration 不是 dashboard migration 子集</strong>：獨立 stream、留更多時間</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Datadog</th>
          <th>Grafana Stack（self-hosted on K8s）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Setup cost</td>
          <td>低（SaaS）</td>
          <td>中高（K8s deploy + storage backend）</td>
      </tr>
      <tr>
          <td>Operational cost (200 host)</td>
          <td>$34K / month</td>
          <td>$8-12K / month（含 S3 + K8s）</td>
      </tr>
      <tr>
          <td>Operational cost (500 host)</td>
          <td>$80-150K / month</td>
          <td>$15-30K / month</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.1-0.3</td>
          <td>1-2 FTE（K8s + storage + Grafana operator）</td>
      </tr>
      <tr>
          <td>Long-term retention</td>
          <td>$1.27 / million event for 15+ day</td>
          <td>S3 + Loki：~$0.02 / GB / month</td>
      </tr>
      <tr>
          <td>Multi-cloud / hybrid</td>
          <td>受 Datadog region 限</td>
          <td>自由部署</td>
      </tr>
      <tr>
          <td>Vendor lock-in</td>
          <td>高</td>
          <td>低（OSS + OTel）</td>
      </tr>
      <tr>
          <td>Time to value</td>
          <td>1-2 週</td>
          <td>4-8 週</td>
      </tr>
      <tr>
          <td>Migration cost (one-time)</td>
          <td>-</td>
          <td>1-3 FTE × 3 個月</td>
      </tr>
  </tbody>
</table>
<p><strong>Break-even point</strong>：~150 host 規模、3 年 amortized 後 self-hosted cheaper；&lt; 100 host 規模 SaaS 較 ROI 高。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-opentelemetry-對齊">跟 OpenTelemetry 對齊</h3>
<p>Migration 是 <em>OTel-first 轉型</em> 的機會：</p>
<ul>
<li>Application code 用 OTel SDK、避免 Datadog SDK lock-in</li>
<li>Trace context propagation 走 W3C Trace Context</li>
<li>未來換 backend 不用再改 application</li>
</ul>
<h3 id="跟-splunk--elastic-對照">跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic</a> 對照</h3>
<p>兩篇都是 <em>cost-driven SaaS migration</em>、但細節差：</p>
<ul>
<li>Splunk → Elastic 是 SIEM 領域、schema translation 是核心議題</li>
<li>Datadog → Grafana 是 multi-tool 拆分、agent + dashboard 重建是核心</li>
<li>共同 pattern：dual-ship → parallel run → cutover</li>
</ul>
<h3 id="反向遷移grafana-stack--datadog">反向遷移（Grafana Stack → Datadog）</h3>
<p>存在但少數 — 主要是 <em>operational complexity reduction</em>（不想自管 Mimir / Loki）；schema 對位方向相反、agent 換回 Datadog Agent。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Grafana Cloud 混合</strong>：部分 component（Tempo）用 Grafana Cloud SaaS、其他 self-host、混合架構</li>
<li><strong>OpenTelemetry Collector 跟 Alloy 取捨</strong>：兩者都是 OTel-based、Alloy 是 Grafana 自家 fork</li>
<li><strong>Vector vs Alloy vs Fluentd</strong>：log shipper 戰場、cost / 功能 / OTel 整合度比較</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></li>
<li>Target vendor：<a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></li>
<li>平行 vendor：<a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a> / <a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> / <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> / <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Self-managed ELK → Elastic Cloud：5 年 ELK 集群的 lifecycle 收尾</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/migrate-to-elastic-cloud/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/migrate-to-elastic-cloud/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &amp;#43; Beats / APM">Elastic Stack&lt;/a> 跟 Elastic Cloud。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Operational = High（self-managed → Elastic managed）→ Type C operational redesign hybrid&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;h2 id="5-年-elk-集群的-lifecycle-收尾">5 年 ELK 集群的 lifecycle 收尾&lt;/h2>
&lt;p>跟前批 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &amp;#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora&lt;/a> 同 Type C、本文用 &lt;em>lifecycle-driven&lt;/em> entry — 看 5 年 ELK 集群典型壽命曲線：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>年份&lt;/th>
 &lt;th>Phase&lt;/th>
 &lt;th>集群狀態&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>0-1&lt;/td>
 &lt;td>Build&lt;/td>
 &lt;td>3 node、簡單部署、SOC 學 Lucene query / dashboard / alert&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1-2&lt;/td>
 &lt;td>Scale-out&lt;/td>
 &lt;td>5-7 node、shard 計畫、hot/warm/cold tier、index lifecycle management&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2-3&lt;/td>
 &lt;td>Degrade&lt;/td>
 &lt;td>10+ node、shard 過多、query latency 升、upgrade window 開始痛&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3-4&lt;/td>
 &lt;td>Save&lt;/td>
 &lt;td>加 dedicated master / cross-cluster replication、ops cost 飛漲&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4-5&lt;/td>
 &lt;td>Migrate decision&lt;/td>
 &lt;td>評估走 Elastic Cloud（managed）或下一個 SIEM vendor&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多數中型 organization 在 lifecycle 第 4-5 年遇到 &lt;em>operational ceiling&lt;/em> — SRE team 0.5-1.5 FTE 跑 ELK ops、新 feature 開發停滯、cost 跟 alternative observability vendor 比較。Elastic Cloud 把 operational stack 全託管、SOC 留在 &lt;em>Lucene query + dashboard + alert&lt;/em> 層、不再管 cluster sizing。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a> 跟 Elastic Cloud。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Operational = High（self-managed → Elastic managed）→ Type C operational redesign hybrid</em>。</p></blockquote>
<h2 id="5-年-elk-集群的-lifecycle-收尾">5 年 ELK 集群的 lifecycle 收尾</h2>
<p>跟前批 <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> 同 Type C、本文用 <em>lifecycle-driven</em> entry — 看 5 年 ELK 集群典型壽命曲線：</p>
<table>
  <thead>
      <tr>
          <th>年份</th>
          <th>Phase</th>
          <th>集群狀態</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0-1</td>
          <td>Build</td>
          <td>3 node、簡單部署、SOC 學 Lucene query / dashboard / alert</td>
      </tr>
      <tr>
          <td>1-2</td>
          <td>Scale-out</td>
          <td>5-7 node、shard 計畫、hot/warm/cold tier、index lifecycle management</td>
      </tr>
      <tr>
          <td>2-3</td>
          <td>Degrade</td>
          <td>10+ node、shard 過多、query latency 升、upgrade window 開始痛</td>
      </tr>
      <tr>
          <td>3-4</td>
          <td>Save</td>
          <td>加 dedicated master / cross-cluster replication、ops cost 飛漲</td>
      </tr>
      <tr>
          <td>4-5</td>
          <td>Migrate decision</td>
          <td>評估走 Elastic Cloud（managed）或下一個 SIEM vendor</td>
      </tr>
  </tbody>
</table>
<p>多數中型 organization 在 lifecycle 第 4-5 年遇到 <em>operational ceiling</em> — SRE team 0.5-1.5 FTE 跑 ELK ops、新 feature 開發停滯、cost 跟 alternative observability vendor 比較。Elastic Cloud 把 operational stack 全託管、SOC 留在 <em>Lucene query + dashboard + alert</em> 層、不再管 cluster sizing。</p>
<h2 id="為什麼遷fte--availability--version-cadence-三條-driver">為什麼遷：FTE / availability / version cadence 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FTE</td>
          <td>Self-managed ELK 0.5-1.5 FTE 跑 ops、Elastic Cloud 降到 0.1-0.3 FTE</td>
      </tr>
      <tr>
          <td>Availability</td>
          <td>Cross-AZ failover 自管太複雜、Cloud 內建</td>
      </tr>
      <tr>
          <td>Version cadence</td>
          <td>Elasticsearch 8.x quarterly release、self-managed upgrade window 是痛點、Cloud 自動</td>
      </tr>
  </tbody>
</table>
<h2 id="6-維-audit">6 維 audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Low（Elasticsearch API 完全相容）</td>
      </tr>
      <tr>
          <td>Operational</td>
          <td><strong>High</strong>（cluster mgmt 全託管）</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Low（同 Elasticsearch + Kibana + Beats / Logstash）</td>
      </tr>
      <tr>
          <td>Components</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>Low-Medium（連線 endpoint + auth 改）</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Operational = High → Type C standard。</p>
<h2 id="operational-redesign-對位">Operational redesign 對位</h2>
<table>
  <thead>
      <tr>
          <th>Concept</th>
          <th>Self-managed ELK</th>
          <th>Elastic Cloud</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cluster bootstrap</td>
          <td>手動 install + config</td>
          <td>UI / API 一鍵建 deployment</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>自管 master / dedicated voting / cross-AZ</td>
          <td>內建 multi-AZ</td>
      </tr>
      <tr>
          <td>Upgrade</td>
          <td>手動 rolling restart 6-12 小時</td>
          <td>自動 patch + minor version</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>自管 snapshot to S3</td>
          <td>內建 snapshot lifecycle</td>
      </tr>
      <tr>
          <td>Shard management</td>
          <td>手動 ILM policy</td>
          <td>UI-driven ILM</td>
      </tr>
      <tr>
          <td>Security</td>
          <td>自管 X-Pack / SSL cert</td>
          <td>內建 + 自動 cert rotation</td>
      </tr>
      <tr>
          <td>Monitoring</td>
          <td>自管 Metricbeat → 自己集群</td>
          <td>內建 deployment monitoring</td>
      </tr>
  </tbody>
</table>
<h2 id="migration-4-phase">Migration 4-phase</h2>
<h3 id="phase-0pre-migration-audit">Phase 0：Pre-migration audit</h3>
<ul>
<li>列 application 連線 endpoint (Logstash / Beats / SDK direct)</li>
<li>列 ILM policy + retention setting</li>
<li>估 deployment size（hot tier RAM / cold tier storage）</li>
</ul>
<h3 id="phase-1elastic-cloud-deployment-建置">Phase 1：Elastic Cloud deployment 建置</h3>
<ul>
<li>選 region + provider（AWS / GCP / Azure）</li>
<li>Hot tier RAM × N + cold tier S3-backed × N</li>
<li>Snapshot lifecycle 配置</li>
</ul>
<h3 id="phase-2data-migration">Phase 2：Data migration</h3>
<ul>
<li><strong>Cross-cluster replication (CCR)</strong> 從 self-managed → Cloud（推薦、incremental）</li>
<li>或 <strong>snapshot + restore</strong>（簡單但需要 maintenance window）</li>
</ul>
<h3 id="phase-3cutover--cleanup">Phase 3：Cutover + cleanup</h3>
<ul>
<li>Application 端切 endpoint</li>
<li>Self-managed 端 read-only 1-2 月</li>
<li>Decommission</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1application-endpoint-hardcodecutover-失敗">Case 1：Application endpoint hardcode、cutover 失敗</h3>
<p><strong>徵兆</strong>：cutover 後 N 個 application 仍連舊 endpoint、log / metric 斷流。</p>
<p><strong>根因</strong>：endpoint 寫死在 config file、deploy 時沒一起改。</p>
<p><strong>修法</strong>：endpoint 用 ENV variable + service discovery、cutover 是 single deploy。</p>
<h3 id="case-2ccr-replication-lagcutover-時資料-gap">Case 2：CCR replication lag、cutover 時資料 gap</h3>
<p><strong>徵兆</strong>：CCR 跑 1 週、cutover 前 lag 200ms 看似 OK；application 切到 Cloud 後 search 顯示 <em>缺最近 5 分鐘 data</em>。</p>
<p><strong>根因</strong>：CCR replication 不保證即時 catch up、cutover 期間仍可能 lag；且 follower index 對 <em>write</em> 不接受。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Cutover 流程加 <em>drain window</em> — 停 application write 5-10 分鐘、等 CCR catch up</li>
<li>確認 follower index 已 <em>promote</em> 成 write-capable</li>
<li>監控 CCR lag、&lt; 100ms 才 cutover</li>
</ol>
<h3 id="case-3auth-改變soc-alert-失效">Case 3：Auth 改變、SOC alert 失效</h3>
<p><strong>徵兆</strong>：cutover 後 SOC dashboard 顯示「authentication failed」、SIEM rule 全失效。</p>
<p><strong>根因</strong>：self-managed 用 X-Pack basic auth、Cloud 用 API key + SSO；SOC tooling 沒改 auth。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover 列所有 tool 連線 ELK 的 auth</li>
<li>改 API key、用 IAM-friendly token rotation</li>
<li>Cloud 端 enable SSO + 設 service account</li>
</ol>
<h3 id="case-4cost-暴漲cold-tier-設定錯">Case 4：Cost 暴漲、cold tier 設定錯</h3>
<p><strong>徵兆</strong>：第一個月 Cloud 帳單比預估高 50%；cold tier 用 <em>fast storage</em>（hot-tier-level）而非 S3-backed。</p>
<p><strong>根因</strong>：Cloud deployment template 預設 hot 是 fast、cold 也是 fast（slow 需要明示）；team 沒 review template。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Pre-cutover review deployment template、確認 cold tier = searchable snapshot to S3</li>
<li>Cost monitor 第一週密集 check</li>
<li>Hot tier RAM 估算 conservative</li>
</ol>
<h3 id="case-5snapshot-跨-region-失效">Case 5：Snapshot 跨 region 失效</h3>
<p><strong>徵兆</strong>：DR drill 切 region 失敗；Cloud 內建 snapshot 是 same-region、不跨 region。</p>
<p><strong>根因</strong>：multi-region DR 需要 <em>cross-region snapshot</em> 或 <em>multi-deployment</em>、不是預設。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>評估 DR 需求、是否需要 cross-region</li>
<li>配 <em>additional deployment in DR region</em> + CCR</li>
<li>Cost 增 50-100%、是 DR 投資不是 cost optimization</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Self-managed ELK</th>
          <th>Elastic Cloud</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute cost (5 node)</td>
          <td>$1,000-2,000 / mo</td>
          <td>$1,500-3,000 / mo</td>
      </tr>
      <tr>
          <td>Storage cost</td>
          <td>EBS</td>
          <td>included + 加 S3 cold tier</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.5-1.5 = $5K-15K</td>
          <td>0.1-0.3 = $1K-3K</td>
      </tr>
      <tr>
          <td>Total (5 node, mid-tier)</td>
          <td>$6K-17K / mo</td>
          <td>$2.5K-6K / mo</td>
      </tr>
      <tr>
          <td>Migration cost</td>
          <td>-</td>
          <td>1-2 FTE × 1-2 個月</td>
      </tr>
  </tbody>
</table>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-splunk--elastic-security-migration-對位">跟 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security migration</a> 對位</h3>
<p>兩篇都到 Elastic 生態、但 Splunk → Elastic Security 是 Schema 高差 Type A、本篇是 Operational 高差 Type C；如果同時跑兩個 migration、Splunk → Elastic Security 先、ELK Cloud 後（避免雙重變動）。</p>
<h3 id="跟-application-observability-stack-整合">跟 Application observability stack 整合</h3>
<p>Elastic Cloud + APM + OpenTelemetry：cutover 後可以 <em>順便升 OTel 化 application</em>、避免下次 vendor 切換重複工作。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a></li>
<li>平行 migration playbook (Type C)：<a href="/blog/backend/01-database/vendors/postgresql/migrate-to-aurora/" data-link-title="PostgreSQL → Aurora Migration：protocol 相容、operational 重設計" data-link-desc="Aurora 號稱 PostgreSQL-compatible 但 operational model 不同（storage decouple / cluster endpoint / instance class / 自家備份）；遷移流程是混合（protocol drop-in &#43; operational phased）、5 個 production 踩雷（extension 不支援 / replication slot 不直通 / autovacuum 行為差 / IAM 認證強制 / cost model 換算）、跟 Patroni / read replica / DR 對位">PostgreSQL → Aurora</a> / <a href="/blog/backend/01-database/vendors/mongodb/migrate-to-atlas/" data-link-title="MongoDB → Atlas：Atlas 不是 MongoDB &#43; managed、是另一個 product" data-link-desc="Atlas 號稱「MongoDB managed」但 operational model 完全不同（auto-scaling / VPC peering / IAM-driven access / 內建 backup / billing 模型）；本文採用 Type C operational redesign hybrid 結構、4-phase operational migration &#43; drop-in cutover、5 個 production 踩雷（連線數限制 / IP whitelist / backup retention / IAM token 過期 / billing 暴漲）">MongoDB → Atlas</a> / <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-to-msk/" data-link-title="Self-managed Kafka → AWS MSK：把 $15K/month operational cost 拆解到 managed" data-link-desc="Kafka self-managed → MSK 是 Type C operational redesign — protocol 完全相容、operational stack（ZooKeeper / brokers / monitoring / patching）全託管；本文用 cost 拆解開頭、5 個 production 踩雷（client connection pattern / version pinning / metric pipeline / IAM auth / cross-cluster mirror）">Kafka → MSK</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>0.12 觀測、可靠性與事故服務選型</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-service-selection/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-service-selection/</guid><description>&lt;p>觀測、可靠性與事故服務選型的核心責任是把操作風險拆成「看得見、驗得過、接得住」三層能力。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>處理訊號是否足以支援判讀，&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程&lt;/a>處理失敗是否能被安全預演，&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤&lt;/a>處理事故是否能被接住、分工與回寫。&lt;/p>
&lt;p>這三類服務常被一起採購或一起導入，但它們回答不同問題。觀測平台回答「現在發生什麼」，可靠性工具回答「失敗前能否先驗證」，事故平台回答「事情發生後誰做什麼」。選型時先分清能力層，再比較 vendor、SaaS、OSS 或自建方案，能降低工具堆疊與流程空轉的風險。&lt;/p>
&lt;h2 id="選型錨點">選型錨點&lt;/h2>
&lt;p>選型錨點是先問服務要降低哪一種操作不確定性。當團隊只知道系統「好像怪怪的」，優先補訊號；當團隊知道風險但缺少安全驗證路徑，優先補可靠性驗證；當團隊知道事故已發生但協作混亂，優先補事故流程。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>能力層&lt;/th>
 &lt;th>核心問題&lt;/th>
 &lt;th>對應模組&lt;/th>
 &lt;th>常見服務類型&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>訊號層&lt;/td>
 &lt;td>發生什麼、影響哪裡&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>&lt;/td>
 &lt;td>telemetry、APM、log、dashboard&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>驗證層&lt;/td>
 &lt;td>風險能否提前預演&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程&lt;/a>&lt;/td>
 &lt;td>CI、load test、chaos、SLO&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>響應層&lt;/td>
 &lt;td>誰接手、如何收斂&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤&lt;/a>&lt;/td>
 &lt;td>on-call、IR、status、postmortem&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>閉環層&lt;/td>
 &lt;td>教訓如何回寫&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">觀測、驗證與事故閉環&lt;/a>&lt;/td>
 &lt;td>workflow、action tracking&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>訊號層的責任是讓系統行為可被查詢與判讀。這一層的選型重點是資料模型、查詢能力、關聯能力、保留成本與告警品質；產品名稱排在後面，因為 log、metric、trace 與 error event 是否能互相串接，才是事故時真正影響判讀速度的條件。&lt;/p>
&lt;p>驗證層的責任是讓風險在事故前被安全暴露。這一層的選型重點是測試是否接近真實 workload、故障注入是否有停止條件、SLO 是否能被量測、release gate 是否能阻止高風險變更；工具越強，越需要 blast radius 與權限邊界。&lt;/p>
&lt;p>響應層的責任是讓事故進入可交接流程。這一層的選型重點是 paging、升級、角色分工、狀態更新、decision log、stakeholder mapping 與 post-incident action tracking；工具的價值來自流程一致性，通知訊息數量只是輔助訊號。&lt;/p>
&lt;p>閉環層的責任是把事故與演練教訓回寫到系統設計。這一層可能由 incident platform、ticket system、runbook repository 或內部 workflow 承擔；判準是 action item 是否能被排序、驗證、關閉，並回到訊號治理、可靠性演練或事故流程。&lt;/p>
&lt;h2 id="判讀順序">判讀順序&lt;/h2>
&lt;p>操作服務選型的穩定順序是「症狀 → 缺口 → 能力 → 工具」。症狀描述使用者痛點或工程痛點，缺口描述目前缺少的判讀或流程，能力描述需要補的系統責任，工具才是最後的落地選項。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>症狀&lt;/th>
 &lt;th>主要缺口&lt;/th>
 &lt;th>優先能力&lt;/th>
 &lt;th>下一步路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>客訴比告警早&lt;/td>
 &lt;td>訊號覆蓋不足&lt;/td>
 &lt;td>symptom-based alert&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">dashboard 與 alert&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故時 trace 接不上 queue&lt;/td>
 &lt;td>關聯線索斷裂&lt;/td>
 &lt;td>context propagation&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">tracing 與 context link&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>發版後才發現容量曲線崩壞&lt;/td>
 &lt;td>失敗前驗證不足&lt;/td>
 &lt;td>load / perf gate&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">load test&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>chaos 實驗影響超出預期&lt;/td>
 &lt;td>實驗安全邊界不足&lt;/td>
 &lt;td>experiment guardrail&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">experiment safety boundary&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多人同時修事故但決策互相覆蓋&lt;/td>
 &lt;td>指揮與紀錄不足&lt;/td>
 &lt;td>command / decision log&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對外狀態更新慢於內部復原&lt;/td>
 &lt;td>stakeholder 節奏不足&lt;/td>
 &lt;td>status / comms&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">stakeholder comms&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>客訴比告警早代表系統的外部痛點先於內部訊號出現。這種情境應先補服務健康指標、使用者可感知訊號與 alert runbook，再討論要用哪個監控平台；否則平台上線後仍可能只收集到工程師方便看的資料。&lt;/p></description><content:encoded><![CDATA[<p>觀測、可靠性與事故服務選型的核心責任是把操作風險拆成「看得見、驗得過、接得住」三層能力。<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>處理訊號是否足以支援判讀，<a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a>處理失敗是否能被安全預演，<a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a>處理事故是否能被接住、分工與回寫。</p>
<p>這三類服務常被一起採購或一起導入，但它們回答不同問題。觀測平台回答「現在發生什麼」，可靠性工具回答「失敗前能否先驗證」，事故平台回答「事情發生後誰做什麼」。選型時先分清能力層，再比較 vendor、SaaS、OSS 或自建方案，能降低工具堆疊與流程空轉的風險。</p>
<h2 id="選型錨點">選型錨點</h2>
<p>選型錨點是先問服務要降低哪一種操作不確定性。當團隊只知道系統「好像怪怪的」，優先補訊號；當團隊知道風險但缺少安全驗證路徑，優先補可靠性驗證；當團隊知道事故已發生但協作混亂，優先補事故流程。</p>
<table>
  <thead>
      <tr>
          <th>能力層</th>
          <th>核心問題</th>
          <th>對應模組</th>
          <th>常見服務類型</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊號層</td>
          <td>發生什麼、影響哪裡</td>
          <td><a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a></td>
          <td>telemetry、APM、log、dashboard</td>
      </tr>
      <tr>
          <td>驗證層</td>
          <td>風險能否提前預演</td>
          <td><a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a></td>
          <td>CI、load test、chaos、SLO</td>
      </tr>
      <tr>
          <td>響應層</td>
          <td>誰接手、如何收斂</td>
          <td><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a></td>
          <td>on-call、IR、status、postmortem</td>
      </tr>
      <tr>
          <td>閉環層</td>
          <td>教訓如何回寫</td>
          <td><a href="/blog/backend/08-incident-response/observability-reliability-incident-loop/" data-link-title="8.11 Observability / Reliability / Incident Response 閉環" data-link-desc="把 04 / 06 / 08 三個模組的雙向反饋串成可判讀循環，定義閉環健康度判讀訊號">觀測、驗證與事故閉環</a></td>
          <td>workflow、action tracking</td>
      </tr>
  </tbody>
</table>
<p>訊號層的責任是讓系統行為可被查詢與判讀。這一層的選型重點是資料模型、查詢能力、關聯能力、保留成本與告警品質；產品名稱排在後面，因為 log、metric、trace 與 error event 是否能互相串接，才是事故時真正影響判讀速度的條件。</p>
<p>驗證層的責任是讓風險在事故前被安全暴露。這一層的選型重點是測試是否接近真實 workload、故障注入是否有停止條件、SLO 是否能被量測、release gate 是否能阻止高風險變更；工具越強，越需要 blast radius 與權限邊界。</p>
<p>響應層的責任是讓事故進入可交接流程。這一層的選型重點是 paging、升級、角色分工、狀態更新、decision log、stakeholder mapping 與 post-incident action tracking；工具的價值來自流程一致性，通知訊息數量只是輔助訊號。</p>
<p>閉環層的責任是把事故與演練教訓回寫到系統設計。這一層可能由 incident platform、ticket system、runbook repository 或內部 workflow 承擔；判準是 action item 是否能被排序、驗證、關閉，並回到訊號治理、可靠性演練或事故流程。</p>
<h2 id="判讀順序">判讀順序</h2>
<p>操作服務選型的穩定順序是「症狀 → 缺口 → 能力 → 工具」。症狀描述使用者痛點或工程痛點，缺口描述目前缺少的判讀或流程，能力描述需要補的系統責任，工具才是最後的落地選項。</p>
<table>
  <thead>
      <tr>
          <th>症狀</th>
          <th>主要缺口</th>
          <th>優先能力</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>客訴比告警早</td>
          <td>訊號覆蓋不足</td>
          <td>symptom-based alert</td>
          <td><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">dashboard 與 alert</a></td>
      </tr>
      <tr>
          <td>事故時 trace 接不上 queue</td>
          <td>關聯線索斷裂</td>
          <td>context propagation</td>
          <td><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">tracing 與 context link</a></td>
      </tr>
      <tr>
          <td>發版後才發現容量曲線崩壞</td>
          <td>失敗前驗證不足</td>
          <td>load / perf gate</td>
          <td><a href="/blog/backend/06-reliability/load-testing/" data-link-title="6.2 load test" data-link-desc="把 production 流量結構轉成可重播壓力情境，定位 saturation 轉折與容量邊界">load test</a></td>
      </tr>
      <tr>
          <td>chaos 實驗影響超出預期</td>
          <td>實驗安全邊界不足</td>
          <td>experiment guardrail</td>
          <td><a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">experiment safety boundary</a></td>
      </tr>
      <tr>
          <td>多人同時修事故但決策互相覆蓋</td>
          <td>指揮與紀錄不足</td>
          <td>command / decision log</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log</a></td>
      </tr>
      <tr>
          <td>對外狀態更新慢於內部復原</td>
          <td>stakeholder 節奏不足</td>
          <td>status / comms</td>
          <td><a href="/blog/backend/08-incident-response/stakeholder-communication/" data-link-title="8.10 Stakeholder 通訊與外部狀態頁" data-link-desc="把 impact scope、status page、補償政策串成節奏">stakeholder comms</a></td>
      </tr>
  </tbody>
</table>
<p>客訴比告警早代表系統的外部痛點先於內部訊號出現。這種情境應先補服務健康指標、使用者可感知訊號與 alert runbook，再討論要用哪個監控平台；否則平台上線後仍可能只收集到工程師方便看的資料。</p>
<p>trace 接不上 queue 代表跨邊界關聯失效。這種情境應先檢查 trace context、correlation id、message metadata 與 sampling 策略，再選擇 OpenTelemetry backend、APM SaaS 或 log search 方案。</p>
<p>發版後才發現容量曲線崩壞代表驗證層缺少 gate。這種情境應先建立 workload model、baseline、回歸門檻與 release gate，再選 load test 工具或 performance dashboard。</p>
<p>chaos 實驗影響超出預期代表驗證工具先於安全邊界。這種情境應先定義 steady state、blast radius、停止條件與授權範圍，再決定使用 chaos mesh、fault proxy 或商業 chaos 平台。</p>
<p>多人同時修事故但決策互相覆蓋代表響應層缺少 command model。這種情境應先定義 incident commander、scribe、owner、decision log 與 handoff，再導入 IR 平台或 chat workflow。</p>
<p>對外狀態更新慢於內部復原代表 stakeholder 節奏不足。這種情境應先定義影響評估、更新頻率、外部狀態頁與客戶溝通責任，再選 status page 或 customer comms 工具。</p>
<h2 id="服務組合策略">服務組合策略</h2>
<p>服務組合策略的核心原則是先選最小閉環，再擴展平台覆蓋。完整閉環至少包含一個可判讀訊號、一個可驗證門檻、一個可接手流程與一個可回寫的 action tracking；缺任一層時，工具組合就會變成單點能力。</p>
<table>
  <thead>
      <tr>
          <th>組合型態</th>
          <th>適合情境</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>雲端原生整合</td>
          <td>團隊集中在單一 cloud provider</td>
          <td>跨雲、跨 SaaS 與高階查詢受限</td>
      </tr>
      <tr>
          <td>OSS 可組裝平台</td>
          <td>團隊有平台工程能力</td>
          <td>維護、升級、容量與成本治理重</td>
      </tr>
      <tr>
          <td>All-in-one SaaS</td>
          <td>團隊需要快速覆蓋與低維運</td>
          <td>成本、資料鎖定與自訂邊界受限</td>
      </tr>
      <tr>
          <td>混合式最小閉環</td>
          <td>既有工具已分散</td>
          <td>整合責任與 ownership 容易模糊</td>
      </tr>
  </tbody>
</table>
<p>雲端原生整合適合雲端邊界清楚的團隊。它能快速取得 infrastructure 訊號、IAM 整合與預設 dashboard，但跨外部 SaaS、跨語言 trace 或高基數探索時，需要提前確認資料出口與查詢能力。</p>
<p>OSS 可組裝平台適合有平台團隊維護 ingestion、storage、query 與 dashboard 的組織。它能降低 vendor lock-in 並保留彈性，但容量規劃、升級、安全修補、保留策略與 on-call 都會變成內部成本。</p>
<p>All-in-one SaaS 適合需要快速建立可觀測、告警與事故協作的團隊。它能把 log、metric、trace、APM、paging 或 workflow 整合在單一產品，但成本模型、資料保留、客製化限制與資料治理要在導入前確認。</p>
<p>混合式最小閉環適合已經有多套工具的團隊。它的重點是定義哪個系統是 alert source、哪個系統是 incident source of truth、哪個系統負責 action item closure；整合邊界比新增工具更重要。</p>
<h2 id="導入順序">導入順序</h2>
<p>導入順序的責任是降低一次導入多套工具的失敗風險。觀測、驗證與事故服務應依照事故風險與團隊成熟度逐層補齊，功能清單只適合放在能力判準之後。</p>
<ol>
<li>先補最小訊號：定義 SLI、error rate、latency、dependency failure、queue lag 與 customer-facing symptom。</li>
<li>再補最小告警與 runbook：讓 alert 指向可執行動作，避免只把噪音送到 on-call。</li>
<li>接著補驗證門檻：把 load、contract、migration、chaos 或 SLO 變成 release 前後的 gate。</li>
<li>然後補事故協作：定義 paging、severity、角色、decision log、status update 與 post-incident review。</li>
<li>最後補閉環治理：把偵測缺口、演練缺口與 action item 回寫到觀測、驗證與事故流程。</li>
</ol>
<p>這個順序讓工具投資跟風險暴露同步。若團隊在沒有基本訊號時先導入 incident workflow，事故流程會缺少證據；若在沒有實驗安全邊界時先導入 chaos 工具，驗證本身會變成風險來源；若在沒有 action tracking 時只做 postmortem，復盤會停在文字紀錄。</p>
<h2 id="交接路由">交接路由</h2>
<p>交接路由的責任是把服務選型判斷送到正確模組。選型章只決定「需要哪一類能力」，後續模組負責欄位、流程、工具與實作細節。</p>
<ul>
<li>需要判斷訊號是否足以支援診斷時，進入 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>。</li>
<li>需要判斷失敗是否能被安全驗證時，進入 <a href="/blog/backend/06-reliability/" data-link-title="模組六：可靠性驗證流程" data-link-desc="用 SRE 領域詞彙建問題節點、以服務級案例庫累積驗證脈絡，先建概念與案例庫再進實作交接">可靠性驗證流程</a>。</li>
<li>需要判斷事故是否能被接住與回寫時，進入 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a>。</li>
<li>需要比較具體 vendor 時，先讀各模組的 vendors index，再回到本章確認工具是否補到正確能力層。</li>
</ul>
<h2 id="完成判準">完成判準</h2>
<p>本章完成的判準是能把工具需求翻成能力需求。當團隊能說清楚「我們缺的是訊號、驗證、響應還是閉環」，選型討論才適合進入 vendor 比較。</p>
<p>檢查時可以問四個問題：</p>
<ol>
<li>現在的痛點是看不見、驗不過、接不住，還是回寫斷掉？</li>
<li>這個工具補的是哪一層能力，會產生哪些新操作成本？</li>
<li>導入後誰負責維護資料品質、流程品質與 action closure？</li>
<li>如果三個月後事故型態改變，哪個 tripwire 會提醒團隊重新評估？</li>
</ol>
]]></content:encoded></item><item><title>4.12 Audit Log 邊界與 PII 治理</title><link>https://tarrragon.github.io/blog/backend/04-observability/audit-log-governance/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/audit-log-governance/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a> 跟 operational log 的本質差異：對象、不變性、保留、法規&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log&lt;/a> 該記什麼：who / what / when / where / outcome、不可被應用層改寫&lt;/li>
&lt;li>不變性保證：append-only storage、tamper-evident hash chain、independent retention&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII&lt;/a> 治理：log 中的 PII 偵測、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking&lt;/a>、tokenization、最小揭露原則&lt;/li>
&lt;li>法規維度：GDPR / HIPAA / SOC2 / 個資法 對保留期與存取的要求&lt;/li>
&lt;li>跨團隊存取證據連續性：避免責任鏈斷在團隊邊界&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a> 的分工：4.1 是欄位設計、4.12 是治理邊界&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安&lt;/a> 的交接：稽核責任邊界&lt;/li>
&lt;li>反模式：audit 跟 operational 混在同 stream；PII 直接打進 log；audit log 跟 application DB 同保留期&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit log&lt;/a> 是把責任、授權與敏感操作留下可稽核證據的訊號，責任是支援合規、責任追蹤與安全事件調查。&lt;/p>
&lt;p>這一頁處理的是 governance 邊界。Operational log 服務於除錯，audit log 服務於證據；兩者可以共享部分欄位，但保留、不變性、存取權限與 PII 規則不同。&lt;/p>
&lt;p>Audit log 的治理優先序跟 operational log 相反。Operational log 優先服務 &lt;em>當下&lt;/em> 的事故定位、追求即時性與覆蓋廣度；audit log 優先服務 &lt;em>未來&lt;/em> 的責任追蹤、追求完整性、不變性與長期可查詢。當這兩種優先序衝突時，audit 治理要勝過 operational 便利性。&lt;/p>
&lt;h2 id="兩種-log-的責任分工">兩種 log 的責任分工&lt;/h2>
&lt;p>Audit log 跟 operational log 承擔兩條獨立治理鏈：前者服務證據與責任追蹤、後者服務除錯與事故定位。兩者在對象、保留、不變性、權限與粒度上的差異決定它們需要走分開的 pipeline、storage 與保留策略。把 audit log 視為 operational log 的子集、混在同一 stream 治理、會在第一次合規稽核或法規請求時讓證據鏈被打斷（典型徵兆是「靠 grep operational log 拼湊稽核需求」）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Operational log&lt;/th>
 &lt;th>Audit log&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>主要對象&lt;/td>
 &lt;td>工程師、SRE、IC&lt;/td>
 &lt;td>合規、法務、安全事件調查、外部稽核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>主要目的&lt;/td>
 &lt;td>還原事件、定位 root cause&lt;/td>
 &lt;td>證明授權、責任追蹤、事件不可否認&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>保留期&lt;/td>
 &lt;td>7-30 天為典型、依除錯需求&lt;/td>
 &lt;td>數月到數年、依法規與合約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>不變性&lt;/td>
 &lt;td>通常可被 rotate、aggregate、re-index&lt;/td>
 &lt;td>append-only、tamper-evident&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>存取權限&lt;/td>
 &lt;td>工程團隊廣泛存取&lt;/td>
 &lt;td>最小授權、存取本身也要被稽核&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>內容粒度&lt;/td>
 &lt;td>高頻、雜訊容忍&lt;/td>
 &lt;td>低頻、語意精準、欄位穩定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢期望&lt;/td>
 &lt;td>秒級、即席&lt;/td>
 &lt;td>分鐘到小時級、結構化、可重現&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Operational log 在 incident timeline 還原時是主力證據。它的失分容忍度高：丟掉 1% 的 log 通常不影響 root cause 分析。&lt;/p>
&lt;p>Audit log 的失分容忍度極低。一次授權記錄遺失、一個欄位漂移、一段時區錯位，都可能讓事後責任追蹤失效。這個差異決定 audit log 必須走獨立 pipeline、獨立 storage、獨立保留策略。&lt;/p>
&lt;h2 id="核心欄位與不變性">核心欄位與不變性&lt;/h2>
&lt;p>Audit event 的核心責任是回答五個問題：誰（who）、做了什麼（what）、何時（when）、在哪（where）、結果如何（outcome）。任一欄位缺失，責任追蹤鏈就有缺口。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>欄位&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;th>失分風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>who&lt;/td>
 &lt;td>認證主體（user id、service account）&lt;/td>
 &lt;td>用 IP 代替主體 → 多人共用無法區分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>what&lt;/td>
 &lt;td>操作類型 + 對象 ID&lt;/td>
 &lt;td>只記操作不記對象 → 無法重現範圍&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>when&lt;/td>
 &lt;td>事件時間（含時區）+ ingest 時間&lt;/td>
 &lt;td>單一 timestamp → 無法判斷漂移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>where&lt;/td>
 &lt;td>來源 IP、region、tenant、session&lt;/td>
 &lt;td>缺 tenant → 跨租戶事件無法區分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>outcome&lt;/td>
 &lt;td>成功 / 失敗 / 拒絕 + 拒絕原因&lt;/td>
 &lt;td>只記成功 → 失敗操作無痕跡&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>不變性保證有三層遞進：&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a> 跟 operational log 的本質差異：對象、不變性、保留、法規</li>
<li><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">audit log</a> 該記什麼：who / what / when / where / outcome、不可被應用層改寫</li>
<li>不變性保證：append-only storage、tamper-evident hash chain、independent retention</li>
<li><a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> 治理：log 中的 PII 偵測、<a href="/blog/backend/knowledge-cards/data-masking/" data-link-title="Data Masking" data-link-desc="說明敏感資料如何在顯示、匯出、log 與測試資料中降低暴露">data masking</a>、tokenization、最小揭露原則</li>
<li>法規維度：GDPR / HIPAA / SOC2 / 個資法 對保留期與存取的要求</li>
<li>跨團隊存取證據連續性：避免責任鏈斷在團隊邊界</li>
<li>跟 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a> 的分工：4.1 是欄位設計、4.12 是治理邊界</li>
<li>跟 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資安</a> 的交接：稽核責任邊界</li>
<li>反模式：audit 跟 operational 混在同 stream；PII 直接打進 log；audit log 跟 application DB 同保留期</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p><a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit log</a> 是把責任、授權與敏感操作留下可稽核證據的訊號，責任是支援合規、責任追蹤與安全事件調查。</p>
<p>這一頁處理的是 governance 邊界。Operational log 服務於除錯，audit log 服務於證據；兩者可以共享部分欄位，但保留、不變性、存取權限與 PII 規則不同。</p>
<p>Audit log 的治理優先序跟 operational log 相反。Operational log 優先服務 <em>當下</em> 的事故定位、追求即時性與覆蓋廣度；audit log 優先服務 <em>未來</em> 的責任追蹤、追求完整性、不變性與長期可查詢。當這兩種優先序衝突時，audit 治理要勝過 operational 便利性。</p>
<h2 id="兩種-log-的責任分工">兩種 log 的責任分工</h2>
<p>Audit log 跟 operational log 承擔兩條獨立治理鏈：前者服務證據與責任追蹤、後者服務除錯與事故定位。兩者在對象、保留、不變性、權限與粒度上的差異決定它們需要走分開的 pipeline、storage 與保留策略。把 audit log 視為 operational log 的子集、混在同一 stream 治理、會在第一次合規稽核或法規請求時讓證據鏈被打斷（典型徵兆是「靠 grep operational log 拼湊稽核需求」）。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Operational log</th>
          <th>Audit log</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要對象</td>
          <td>工程師、SRE、IC</td>
          <td>合規、法務、安全事件調查、外部稽核</td>
      </tr>
      <tr>
          <td>主要目的</td>
          <td>還原事件、定位 root cause</td>
          <td>證明授權、責任追蹤、事件不可否認</td>
      </tr>
      <tr>
          <td>保留期</td>
          <td>7-30 天為典型、依除錯需求</td>
          <td>數月到數年、依法規與合約</td>
      </tr>
      <tr>
          <td>不變性</td>
          <td>通常可被 rotate、aggregate、re-index</td>
          <td>append-only、tamper-evident</td>
      </tr>
      <tr>
          <td>存取權限</td>
          <td>工程團隊廣泛存取</td>
          <td>最小授權、存取本身也要被稽核</td>
      </tr>
      <tr>
          <td>內容粒度</td>
          <td>高頻、雜訊容忍</td>
          <td>低頻、語意精準、欄位穩定</td>
      </tr>
      <tr>
          <td>查詢期望</td>
          <td>秒級、即席</td>
          <td>分鐘到小時級、結構化、可重現</td>
      </tr>
  </tbody>
</table>
<p>Operational log 在 incident timeline 還原時是主力證據。它的失分容忍度高：丟掉 1% 的 log 通常不影響 root cause 分析。</p>
<p>Audit log 的失分容忍度極低。一次授權記錄遺失、一個欄位漂移、一段時區錯位，都可能讓事後責任追蹤失效。這個差異決定 audit log 必須走獨立 pipeline、獨立 storage、獨立保留策略。</p>
<h2 id="核心欄位與不變性">核心欄位與不變性</h2>
<p>Audit event 的核心責任是回答五個問題：誰（who）、做了什麼（what）、何時（when）、在哪（where）、結果如何（outcome）。任一欄位缺失，責任追蹤鏈就有缺口。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
          <th>失分風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>who</td>
          <td>認證主體（user id、service account）</td>
          <td>用 IP 代替主體 → 多人共用無法區分</td>
      </tr>
      <tr>
          <td>what</td>
          <td>操作類型 + 對象 ID</td>
          <td>只記操作不記對象 → 無法重現範圍</td>
      </tr>
      <tr>
          <td>when</td>
          <td>事件時間（含時區）+ ingest 時間</td>
          <td>單一 timestamp → 無法判斷漂移</td>
      </tr>
      <tr>
          <td>where</td>
          <td>來源 IP、region、tenant、session</td>
          <td>缺 tenant → 跨租戶事件無法區分</td>
      </tr>
      <tr>
          <td>outcome</td>
          <td>成功 / 失敗 / 拒絕 + 拒絕原因</td>
          <td>只記成功 → 失敗操作無痕跡</td>
      </tr>
  </tbody>
</table>
<p>不變性保證有三層遞進：</p>
<ol>
<li><strong>Append-only storage</strong>：寫入後不可修改、不可刪除。一般 object storage（S3 Object Lock、GCS Bucket Lock）或 immutable database table 可實作。</li>
<li><strong>Tamper-evident hash chain</strong>：每個 audit event 含前一個 event 的 hash，篡改任一筆會破壞整條 chain。需要週期性 anchor 到外部時間戳服務或第三方公證。</li>
<li><strong>Independent retention</strong>：audit log 的保留期跟 application DB 解耦，application 刪資料不影響 audit。retention 由合規團隊定義、不由應用團隊調整。</li>
</ol>
<p>對應 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 FinTech 審計證據鏈</a>：揭露「audit log completeness、event correlation integrity、retention policy drift」是合規場景的核心治理項目，本章關注的是治理邊界跟欄位設計，事件相關的 evidence 包裝由 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 處理。</p>
<h2 id="跨團隊存取證據連續性">跨團隊存取證據連續性</h2>
<p>跨團隊 audit 治理的核心責任是維持責任鏈在團隊邊界上的連續性。應用團隊記應用層事件、基礎設施團隊記 infra 層存取、IAM 團隊記授權變更，三段證據各自必要、但只有拼接後才能還原一次跨團隊敏感操作。常見失敗來自團隊邊界上的責任鏈斷裂 — 而非單一團隊技術不到位 — 任一段缺失都會讓事後復盤無法閉合。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare 存取可追溯性與保留邊界</a>：揭露「access evidence continuity、retention boundary violations、timestamp integrity」三個方向。Healthcare 場景把這個問題放大，但跨團隊存取連續性是所有合規場景的共同議題。</p>
<p>讓存取證據跨團隊連續的可操作做法：</p>
<ol>
<li><strong>共用 correlation field</strong>：把 request id、trace id、session id 拉到應用層、infra 層、IAM 層共用，讓三段 log 可以拼起來。</li>
<li><strong>明確團隊 ownership 邊界</strong>：每類 audit event 指定唯一 owner team，避免「應該是另一隊負責」的責任轉嫁。</li>
<li><strong>跨團隊 retention 對齊</strong>：應用 audit、infra audit、IAM audit 的保留期要對齊或互為超集，避免一段過期一段還在的拼接斷裂。</li>
<li><strong>跨團隊查詢入口</strong>：合規團隊有單一查詢介面能跨三段 log 拉同一 correlation id 的完整證據鏈。</li>
</ol>
<p>把這些做法寫進 <a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a> 的 ownership 矩陣，能避免單次合規請求引發跨團隊的拼接工作。</p>
<h2 id="retention-與保留策略漂移">Retention 與保留策略漂移</h2>
<p>Retention 是 audit log 跟 operational log 最大的治理差異。Operational log 通常用 30-90 天 rotation；audit log 依資料類型跟法規可能要 1-10 年。</p>
<p>把 audit log 跟 operational log 用同一條 retention 策略治理，會在合規稽核時被抓出來。常見的失敗：</p>
<ul>
<li>audit log 跟 application DB 同保留 90 天、不符 GDPR / HIPAA / 金融法規。</li>
<li>audit log 經過 aggregation 處理、原始事件丟失、但 aggregated view 無法滿足法規要求。</li>
<li>retention 策略由應用團隊調整、不經合規團隊審批、容易在成本壓力下被縮短。</li>
</ul>
<p>Retention 漂移的偵測手段：把 retention compliance 變成可查詢的訊號。週期性對照各類 audit log 的實際留存時間跟政策要求、偏差超過閾值時觸發告警、讓漂移在治理週期內就被處理、避免等到稽核時才發現。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1 FinTech retention policy drift</a> 跟 <a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3 Healthcare retention boundary violations</a>：兩個案例的判讀訊號都把 retention 偏離列為一級訊號（兩 case 的表格行明示這點）；本章在此基礎上補上「偏離視為治理事件、retention compliance 變成可查詢訊號」的展開、屬章節推論。</p>
<p>保留階梯（hot / warm / cold tier）與成本歸屬的詳細設計見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#%e6%8e%a7%e5%88%b6%e9%9d%a2%e8%88%87%e4%bf%9d%e7%95%99%e9%9a%8e%e6%a2%af" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 控制面與保留階梯</a>。</p>
<h2 id="pii-治理與最小揭露">PII 治理與最小揭露</h2>
<p><a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> 在 log 治理裡是雙重風險：寫入時的合規風險、長期保留時的外洩風險。Audit log 的長保留期讓 PII 風險被放大。</p>
<p>可操作的 PII 治理層次：</p>
<ol>
<li><strong>寫入前 redaction</strong>：應用層在輸出 log 時用結構化欄位 + 顯式 marking，避免把整個 request body 序列化進 log。</li>
<li><strong>Pipeline 層 PII 偵測</strong>：collector 加上 PII pattern 偵測（信用卡號、身分證、token），預設遮罩、例外要顯式授權。</li>
<li><strong>Tokenization / pseudonymization</strong>：把直接識別碼換成 token，token 跟原值的映射存在獨立、受嚴格授權的 vault 中。</li>
<li><strong>存取本身的稽核</strong>：誰存取了哪段 audit log、何時存取、為什麼存取，本身也是 audit event。</li>
</ol>
<p>最小揭露原則的實作關鍵是「預設遮罩、需要時申請」。把預設值設成揭露，會在某次事故除錯為了方便而打開、之後忘記關閉。預設遮罩讓每次解碼都是可追蹤的事件。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 audit log 時，先看事件是否能回答 who / what / when / where / outcome，再看資料是否受到獨立保護。</p>
<p>重點訊號包括：</p>
<ul>
<li>audit event 是否不可由一般應用流程修改</li>
<li><a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> 是否經過 redaction、tokenization 或最小揭露</li>
<li><a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 是否符合法規與客戶合約要求</li>
<li>security incident 與 operational incident 是否能引用同一條證據鏈</li>
<li>跨團隊存取的 correlation field 是否連續</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>稽核需求出現時、靠 grep operational log 拼湊</li>
<li>log 中發現 credit card / 身分證 / token 等 PII</li>
<li>audit log 跟 application 同 retention（30 / 90 天）、不符法規</li>
<li>應用層帳號可寫入 / 修改 audit log</li>
<li>法規稽核請求耗時數週、事件鏈定位需要人工補洞</li>
<li>跨團隊查詢同一 correlation id 拼不出完整鏈</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Audit 跟 operational 同 stream</td>
          <td>用一條 pipeline 處理所有 log</td>
          <td>拆獨立 pipeline、獨立 storage</td>
      </tr>
      <tr>
          <td>PII 直接進 log</td>
          <td>信用卡、身分證在 raw log 中可見</td>
          <td>Pipeline 層偵測 + 預設 redaction</td>
      </tr>
      <tr>
          <td>同保留期治理</td>
          <td>audit log 跟 application DB 同 90 天</td>
          <td>依法規重訂保留期、retention compliance 變成告警</td>
      </tr>
      <tr>
          <td>應用層可改寫 audit</td>
          <td>service account 對 audit storage 有 write/delete 權限</td>
          <td>append-only + tamper-evident hash chain</td>
      </tr>
      <tr>
          <td>跨團隊責任鏈斷裂</td>
          <td>同一事件三段 log 互不關聯</td>
          <td>共用 correlation field、跨團隊 retention 對齊</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>：欄位設計</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：audit 的長期保留成本</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：跨團隊 audit ownership 矩陣</li>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 evidence package</a>：audit log 進入 evidence 交接</li>
<li><a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">07 資料保護</a>：<a href="/blog/backend/knowledge-cards/pii/" data-link-title="PII" data-link-desc="說明可識別個人的資料如何影響權限、遮罩、保留與稽核">PII</a> redaction 與責任邊界</li>
<li><a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">8.5 post-incident review</a>：事故證據鏈引用 audit log</li>
<li><a href="/blog/backend/08-incident-response/security-vs-operational-incident/" data-link-title="8.17 Security Incident vs Operational Incident 分流" data-link-desc="把資安事故跟可用性事故的 IR 流程分支點明確化">8.17 security vs operational IR</a>：證據鏈來源</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：鑑識回溯查詢模式跟 audit log 的長期查詢設計</li>
</ul>
]]></content:encoded></item><item><title>4.C13 Discord：從儲存問題回推觀測缺口</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/</guid><description>&lt;p>Discord 的儲存演進案例從觀測角度回推一個教訓：儲存成長問題通常先表現為觀測缺口。不是資料庫變慢了才去看 metric，是該有的 metric 從一開始就沒設計。每一次儲存遷移（MongoDB → Cassandra → ScyllaDB）都揭露了上一階段缺少的訊號。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Discord 處理 trillions of messages。訊息是核心 user journey — 文字、圖片、附件、thread、搜尋全部依賴訊息儲存層。從 2015 年到 2023 年，Discord 的訊息儲存經歷三代架構。&lt;/p>
&lt;p>每一代遷移都由 production 問題觸發 — 追查後發現儲存層已經撐不住，才啟動下一代架構。追查過程中反覆出現的盲區是：觀測訊號不夠早、不夠細或不夠可信。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="mongodb-階段latency-tail-不可見">MongoDB 階段：latency tail 不可見&lt;/h3>
&lt;p>早期用 MongoDB 儲存訊息。隨著使用者成長，部分大型 server（Discord 的群組概念）的訊息量遠超平均值。這些 server 的查詢 latency 偶爾飆升到秒級，但 aggregated latency metric（p50、p95）看起來正常 — 因為大型 server 的 request 數量在整體中佔比極低。&lt;/p>
&lt;p>缺少的訊號：per-server latency breakdown。aggregated metric 遮蔽了局部惡化。&lt;/p>
&lt;h3 id="cassandra-階段hot-partition-沒有早期訊號">Cassandra 階段：hot partition 沒有早期訊號&lt;/h3>
&lt;p>遷移到 Cassandra 後，partition key 設計（channel ID）讓某些高流量 channel 成為 hot partition。Cassandra 的 compaction 在 hot partition 上延遲，讀取 latency 上升。&lt;/p>
&lt;p>問題由使用者回報「訊息載入很慢」才被發現，alert 沒有提前攔截。事後回看，Cassandra 的 read latency per partition 跟 compaction pending bytes per table 這兩個 metric 都有異常，但沒有人在 dashboard 上設 alert — 因為這兩個 metric 在 Cassandra 的預設 monitoring 裡不是 first-class 告警對象。&lt;/p>
&lt;p>缺少的訊號：hot partition 識別跟 compaction health 的主動告警。&lt;/p>
&lt;h3 id="scylladb-遷移階段dual-read-沒有比對-metric">ScyllaDB 遷移階段：dual-read 沒有比對 metric&lt;/h3>
&lt;p>從 Cassandra 遷移到 ScyllaDB 的過程中，Discord 做了 dual-read（同時讀舊資料庫跟新資料庫、比對結果）。dual-read 的正確性比對有做，但 latency 跟 error rate 的比對 metric 設計不完整 — 知道結果一致，但不知道 ScyllaDB 在特定 query pattern 下是否比 Cassandra 慢。&lt;/p>
&lt;p>遷移後才發現某些 query pattern 在 ScyllaDB 上的 tail latency 比 Cassandra 高，需要額外的 schema 調整。如果 dual-read 階段就有 per-query-pattern latency comparison metric，這個問題可以在 cutover 前發現。&lt;/p>
&lt;p>缺少的訊號：migration 期間的 per-pattern latency comparison。&lt;/p>
&lt;h2 id="教訓">教訓&lt;/h2>
&lt;p>三次遷移暴露的觀測缺口有共同結構：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>缺口類型&lt;/th>
 &lt;th>MongoDB 階段&lt;/th>
 &lt;th>Cassandra 階段&lt;/th>
 &lt;th>ScyllaDB 遷移&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>維度不夠細&lt;/td>
 &lt;td>aggregated latency 遮蔽局部惡化&lt;/td>
 &lt;td>table-level metric 遮蔽 partition-level 問題&lt;/td>
 &lt;td>整體 dual-read match rate 遮蔽 per-pattern 差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>告警設計缺失&lt;/td>
 &lt;td>沒有 per-entity latency alert&lt;/td>
 &lt;td>沒有 hot partition alert&lt;/td>
 &lt;td>沒有 latency comparison alert&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>發現方式&lt;/td>
 &lt;td>使用者回報&lt;/td>
 &lt;td>使用者回報&lt;/td>
 &lt;td>遷移後才發現&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>共同模式：觀測訊號的粒度不夠、或告警只設在 aggregated 層 — 局部惡化被平均值淹沒，直到使用者感受到影響才被發現。&lt;/p></description><content:encoded><![CDATA[<p>Discord 的儲存演進案例從觀測角度回推一個教訓：儲存成長問題通常先表現為觀測缺口。不是資料庫變慢了才去看 metric，是該有的 metric 從一開始就沒設計。每一次儲存遷移（MongoDB → Cassandra → ScyllaDB）都揭露了上一階段缺少的訊號。</p>
<h2 id="業務背景">業務背景</h2>
<p>Discord 處理 trillions of messages。訊息是核心 user journey — 文字、圖片、附件、thread、搜尋全部依賴訊息儲存層。從 2015 年到 2023 年，Discord 的訊息儲存經歷三代架構。</p>
<p>每一代遷移都由 production 問題觸發 — 追查後發現儲存層已經撐不住，才啟動下一代架構。追查過程中反覆出現的盲區是：觀測訊號不夠早、不夠細或不夠可信。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="mongodb-階段latency-tail-不可見">MongoDB 階段：latency tail 不可見</h3>
<p>早期用 MongoDB 儲存訊息。隨著使用者成長，部分大型 server（Discord 的群組概念）的訊息量遠超平均值。這些 server 的查詢 latency 偶爾飆升到秒級，但 aggregated latency metric（p50、p95）看起來正常 — 因為大型 server 的 request 數量在整體中佔比極低。</p>
<p>缺少的訊號：per-server latency breakdown。aggregated metric 遮蔽了局部惡化。</p>
<h3 id="cassandra-階段hot-partition-沒有早期訊號">Cassandra 階段：hot partition 沒有早期訊號</h3>
<p>遷移到 Cassandra 後，partition key 設計（channel ID）讓某些高流量 channel 成為 hot partition。Cassandra 的 compaction 在 hot partition 上延遲，讀取 latency 上升。</p>
<p>問題由使用者回報「訊息載入很慢」才被發現，alert 沒有提前攔截。事後回看，Cassandra 的 read latency per partition 跟 compaction pending bytes per table 這兩個 metric 都有異常，但沒有人在 dashboard 上設 alert — 因為這兩個 metric 在 Cassandra 的預設 monitoring 裡不是 first-class 告警對象。</p>
<p>缺少的訊號：hot partition 識別跟 compaction health 的主動告警。</p>
<h3 id="scylladb-遷移階段dual-read-沒有比對-metric">ScyllaDB 遷移階段：dual-read 沒有比對 metric</h3>
<p>從 Cassandra 遷移到 ScyllaDB 的過程中，Discord 做了 dual-read（同時讀舊資料庫跟新資料庫、比對結果）。dual-read 的正確性比對有做，但 latency 跟 error rate 的比對 metric 設計不完整 — 知道結果一致，但不知道 ScyllaDB 在特定 query pattern 下是否比 Cassandra 慢。</p>
<p>遷移後才發現某些 query pattern 在 ScyllaDB 上的 tail latency 比 Cassandra 高，需要額外的 schema 調整。如果 dual-read 階段就有 per-query-pattern latency comparison metric，這個問題可以在 cutover 前發現。</p>
<p>缺少的訊號：migration 期間的 per-pattern latency comparison。</p>
<h2 id="教訓">教訓</h2>
<p>三次遷移暴露的觀測缺口有共同結構：</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>MongoDB 階段</th>
          <th>Cassandra 階段</th>
          <th>ScyllaDB 遷移</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>維度不夠細</td>
          <td>aggregated latency 遮蔽局部惡化</td>
          <td>table-level metric 遮蔽 partition-level 問題</td>
          <td>整體 dual-read match rate 遮蔽 per-pattern 差異</td>
      </tr>
      <tr>
          <td>告警設計缺失</td>
          <td>沒有 per-entity latency alert</td>
          <td>沒有 hot partition alert</td>
          <td>沒有 latency comparison alert</td>
      </tr>
      <tr>
          <td>發現方式</td>
          <td>使用者回報</td>
          <td>使用者回報</td>
          <td>遷移後才發現</td>
      </tr>
  </tbody>
</table>
<p>共同模式：觀測訊號的粒度不夠、或告警只設在 aggregated 層 — 局部惡化被平均值淹沒，直到使用者感受到影響才被發現。</p>
<p>三個缺口的修正方向也一致：</p>
<ol>
<li>把 entity-level metric（per-server、per-partition、per-query-pattern）從 debug-only 提升為 first-class 觀測訊號</li>
<li>在 aggregated alert 之外加 percentile 跟 tail latency alert（p99.9 而非只看 p95）</li>
<li>Migration 期間把 latency comparison 做成 per-pattern 的 real-time dashboard，不只看 overall match rate</li>
</ol>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>：aggregated metric 遮蔽局部惡化是 data quality 問題 — 訊號存在但粒度不足以判讀。</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 Observability Operating Model</a>：觀測缺口反覆出現代表 operating model 缺少「新服務上線 / 遷移時強制檢查觀測覆蓋」的 gate。</li>
<li><a href="/blog/backend/04-observability/debuggability-by-design/" data-link-title="4.19 Debuggability by Design" data-link-desc="把可診斷性前移到 API、async workflow、dependency call 與錯誤模型設計">4.19 Debuggability by Design</a>：per-entity latency breakdown 跟 migration comparison metric 應該在系統設計時就規劃，不是事故後補。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>使用者回報問題但 dashboard 看起來正常 — aggregated metric 可能遮蔽局部惡化</li>
<li>資料庫或儲存層偶爾變慢但找不到原因 — 可能缺少 per-entity 或 per-partition metric</li>
<li>Migration 做了 dual-read 但只比對正確性、沒比對 latency — 遷移後才發現效能回歸</li>
<li>告警設計只有 error rate 跟 aggregated latency — 缺少 tail latency 跟 entity-level alert</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://discord.com/blog/how-discord-stores-billions-of-messages">How Discord Stores Billions of Messages</a>（MongoDB → Cassandra 階段）</li>
<li><a href="https://discord.com/blog/how-discord-stores-trillions-of-messages">How Discord Stores Trillions of Messages</a>（Cassandra → ScyllaDB 階段）</li>
</ul>
]]></content:encoded></item><item><title>0.13 操作控制 vertical slice 實作入口</title><link>https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-vertical-slice/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/00-service-selection/operations-control-vertical-slice/</guid><description>&lt;p>操作控制 vertical slice 的核心責任是把「看得見、驗得過、接得住、回寫得動」落到同一個服務流程。這一章把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 與 action item closure 串成第一個可實作切片。&lt;/p>
&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>實作目標：選一個核心 user journey，建立最小操作控制閉環&lt;/li>
&lt;li>輸入：服務入口、核心依賴、SLO / SLI、告警、驗證場景、事故流程&lt;/li>
&lt;li>產出：evidence package、verification evidence handoff、incident decision log、write-back item&lt;/li>
&lt;li>邊界：先做 artifact 與路由，工具與語言實作留給 04 / 06 / 08 與語言教材&lt;/li>
&lt;li>驗收：能從一次異常走完 triage、verification、decision、write-back&lt;/li>
&lt;/ul>
&lt;h2 id="實作目標">實作目標&lt;/h2>
&lt;p>Vertical slice 的目標是先做一條可回放的操作控制路徑。選一個核心 user journey，例如 checkout、message delivery、document publish、login 或 invoice generation，讓這條路徑同時具備觀測證據、驗證門檻、事故決策與回寫機制。&lt;/p>
&lt;p>這一輪的交付是 artifact 與流程責任。工具可以是現有 log search、dashboard、ticket、runbook repository 與 chat；重點是資料欄位與流程責任先成立，後續才判斷是否需要 Prometheus、OpenTelemetry backend、PagerDuty、incident.io 或 chaos tooling。&lt;/p>
&lt;h2 id="選擇服務切片">選擇服務切片&lt;/h2>
&lt;p>服務切片的選擇責任是找到最能暴露 04 / 06 / 08 交接問題的路徑。第一條 slice 應該具備使用者影響、依賴邊界、可量測訊號與可驗證失敗模式。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>候選切片&lt;/th>
 &lt;th>適合原因&lt;/th>
 &lt;th>常見失敗模式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Checkout&lt;/td>
 &lt;td>直接連到收入與客戶痛點&lt;/td>
 &lt;td>payment timeout、inventory lag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Message delivery&lt;/td>
 &lt;td>同時包含同步入口與非同步處理&lt;/td>
 &lt;td>queue lag、redelivery loop&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Login&lt;/td>
 &lt;td>影響所有後續功能&lt;/td>
 &lt;td>identity provider outage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Document publish&lt;/td>
 &lt;td>涵蓋寫入、背景工作與通知&lt;/td>
 &lt;td>stale read、worker backlog&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Invoice&lt;/td>
 &lt;td>牽涉正確性與客戶信任&lt;/td>
 &lt;td>duplicate charge、missing file&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Checkout 適合第一輪，因為它同時暴露 latency、dependency failure、customer impact 與 rollback decision。若團隊沒有交易路徑，可以選 message delivery 或 login；判準是這條路徑一旦失效，on-call 需要在 15 分鐘內做出明確決策。&lt;/p>
&lt;p>Message delivery 適合用來驗證 async observability。它能暴露 request id、correlation id、queue lag、DLQ、retry policy 與 replay runbook 的交接品質。&lt;/p>
&lt;p>Login 適合用來驗證外部依賴事故。它能暴露 identity provider、fallback、status page、security split 與 customer communication 的邊界。&lt;/p>
&lt;h2 id="artifact-契約">Artifact 契約&lt;/h2>
&lt;p>Artifact 契約的責任是讓每個環節都有可交接輸出。這些 artifact 可以先用 Markdown、ticket 欄位或 incident template 表達，等流程跑通後再導入工具自動化。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Artifact&lt;/th>
 &lt;th>最小欄位&lt;/th>
 &lt;th>來源章節&lt;/th>
 &lt;th>下游使用&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Observability evidence package&lt;/td>
 &lt;td>source、time range、query link、owner、data quality、confidence、known gap&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20&lt;/a>&lt;/td>
 &lt;td>triage、release gate、PIR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Verification evidence handoff&lt;/td>
 &lt;td>hypothesis、scope、steady state、workload / fault、result、decision、owner&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23&lt;/a>&lt;/td>
 &lt;td>release gate、runbook、drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident decision log&lt;/td>
 &lt;td>timestamp、decision、context、evidence、owner、expected effect、rollback condition&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19&lt;/a>&lt;/td>
 &lt;td>handoff、stakeholder update、PIR&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident evidence write-back&lt;/td>
 &lt;td>finding、evidence、target artifact、owner、closure signal、review date&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22&lt;/a>&lt;/td>
 &lt;td>dashboard、experiment、runbook&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Observability evidence package 是第一個 artifact。它保存查詢、時間窗、資料品質與 owner，讓後面的驗證與事故流程使用同一組事實。&lt;/p></description><content:encoded><![CDATA[<p>操作控制 vertical slice 的核心責任是把「看得見、驗得過、接得住、回寫得動」落到同一個服務流程。這一章把 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>、<a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 與 action item closure 串成第一個可實作切片。</p>
<h2 id="大綱">大綱</h2>
<ul>
<li>實作目標：選一個核心 user journey，建立最小操作控制閉環</li>
<li>輸入：服務入口、核心依賴、SLO / SLI、告警、驗證場景、事故流程</li>
<li>產出：evidence package、verification evidence handoff、incident decision log、write-back item</li>
<li>邊界：先做 artifact 與路由，工具與語言實作留給 04 / 06 / 08 與語言教材</li>
<li>驗收：能從一次異常走完 triage、verification、decision、write-back</li>
</ul>
<h2 id="實作目標">實作目標</h2>
<p>Vertical slice 的目標是先做一條可回放的操作控制路徑。選一個核心 user journey，例如 checkout、message delivery、document publish、login 或 invoice generation，讓這條路徑同時具備觀測證據、驗證門檻、事故決策與回寫機制。</p>
<p>這一輪的交付是 artifact 與流程責任。工具可以是現有 log search、dashboard、ticket、runbook repository 與 chat；重點是資料欄位與流程責任先成立，後續才判斷是否需要 Prometheus、OpenTelemetry backend、PagerDuty、incident.io 或 chaos tooling。</p>
<h2 id="選擇服務切片">選擇服務切片</h2>
<p>服務切片的選擇責任是找到最能暴露 04 / 06 / 08 交接問題的路徑。第一條 slice 應該具備使用者影響、依賴邊界、可量測訊號與可驗證失敗模式。</p>
<table>
  <thead>
      <tr>
          <th>候選切片</th>
          <th>適合原因</th>
          <th>常見失敗模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Checkout</td>
          <td>直接連到收入與客戶痛點</td>
          <td>payment timeout、inventory lag</td>
      </tr>
      <tr>
          <td>Message delivery</td>
          <td>同時包含同步入口與非同步處理</td>
          <td>queue lag、redelivery loop</td>
      </tr>
      <tr>
          <td>Login</td>
          <td>影響所有後續功能</td>
          <td>identity provider outage</td>
      </tr>
      <tr>
          <td>Document publish</td>
          <td>涵蓋寫入、背景工作與通知</td>
          <td>stale read、worker backlog</td>
      </tr>
      <tr>
          <td>Invoice</td>
          <td>牽涉正確性與客戶信任</td>
          <td>duplicate charge、missing file</td>
      </tr>
  </tbody>
</table>
<p>Checkout 適合第一輪，因為它同時暴露 latency、dependency failure、customer impact 與 rollback decision。若團隊沒有交易路徑，可以選 message delivery 或 login；判準是這條路徑一旦失效，on-call 需要在 15 分鐘內做出明確決策。</p>
<p>Message delivery 適合用來驗證 async observability。它能暴露 request id、correlation id、queue lag、DLQ、retry policy 與 replay runbook 的交接品質。</p>
<p>Login 適合用來驗證外部依賴事故。它能暴露 identity provider、fallback、status page、security split 與 customer communication 的邊界。</p>
<h2 id="artifact-契約">Artifact 契約</h2>
<p>Artifact 契約的責任是讓每個環節都有可交接輸出。這些 artifact 可以先用 Markdown、ticket 欄位或 incident template 表達，等流程跑通後再導入工具自動化。</p>
<table>
  <thead>
      <tr>
          <th>Artifact</th>
          <th>最小欄位</th>
          <th>來源章節</th>
          <th>下游使用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Observability evidence package</td>
          <td>source、time range、query link、owner、data quality、confidence、known gap</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a></td>
          <td>triage、release gate、PIR</td>
      </tr>
      <tr>
          <td>Verification evidence handoff</td>
          <td>hypothesis、scope、steady state、workload / fault、result、decision、owner</td>
          <td><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23</a></td>
          <td>release gate、runbook、drill</td>
      </tr>
      <tr>
          <td>Incident decision log</td>
          <td>timestamp、decision、context、evidence、owner、expected effect、rollback condition</td>
          <td><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a></td>
          <td>handoff、stakeholder update、PIR</td>
      </tr>
      <tr>
          <td>Incident evidence write-back</td>
          <td>finding、evidence、target artifact、owner、closure signal、review date</td>
          <td><a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
          <td>dashboard、experiment、runbook</td>
      </tr>
  </tbody>
</table>
<p>Observability evidence package 是第一個 artifact。它保存查詢、時間窗、資料品質與 owner，讓後面的驗證與事故流程使用同一組事實。</p>
<p>Verification evidence handoff 是第二個 artifact。它把一次 load test、chaos drill、DR rehearsal 或 readiness review 的結果轉成 release gate 與 incident drill 可用的證據。</p>
<p>Incident decision log 是第三個 artifact。它把事中決策、證據、預期效果與回退條件保存下來，讓交班與復盤可以直接引用。</p>
<p>Incident evidence write-back 是第四個 artifact。它把事故學習轉成 dashboard、alert、SLO、experiment、runbook 或 automation boundary 的修改項。</p>
<h2 id="實作步驟">實作步驟</h2>
<p>實作步驟的責任是讓 slice 能被單次演練走完。每一步都產生一個可檢查輸出，避免流程只停在口頭共識。</p>
<ol>
<li>選定服務切片與核心 user journey。</li>
<li>定義 steady state：success rate、latency、queue lag、data correctness、customer impact。</li>
<li>補 observability evidence package：dashboard、query、trace、log、audit、data quality。</li>
<li>補 verification evidence handoff：load、chaos、DR 或 rollback rehearsal 的 hypothesis 與 result。</li>
<li>建 incident intake template：source、confidence、impact scope、evidence link、severity candidate。</li>
<li>建 incident decision log template：decision、owner、expected effect、rollback condition。</li>
<li>建 write-back template：finding、target artifact、closure signal、review date。</li>
<li>跑一次 tabletop 或 game day，確認 artifact 能被實際填寫。</li>
<li>把缺口回寫到 04 readiness、06 experiment 或 08 runbook。</li>
</ol>
<p>第一步要避免選太大的系統。選「checkout」比選「整個支付平台」更好，因為 slice 需要在一輪演練中跑完。</p>
<p>第二步要先定義穩態。沒有 steady state，load test、chaos 與 incident recovery 都會缺少共同終點。</p>
<p>第三步要保留 data quality 限制。若 trace sampling、log drop 或 metric ingest delay 會影響判讀，限制要跟 evidence 一起交接。</p>
<p>第四步要把驗證結果變成下游可用語言。Pass、conditional、fail 都要附上 scope、hypothesis 與下一步路由。</p>
<p>第五到第七步要先用輕量 template。template 跑通後，再把欄位搬進 incident tool、ticket system 或 runbook platform。</p>
<p>第八步要實際演練。tabletop 可以先驗證欄位與角色，game day 再驗證工具與訊號。</p>
<h2 id="最小-template">最小 template</h2>
<p>最小 template 的責任是讓第一輪不用等待工具導入。以下欄位可以直接放進 Markdown、ticket、incident doc 或 runbook。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">service_slice</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">journey</span><span class="p">:</span><span class="w"> </span><span class="l">checkout</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">payments-team</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">steady_state</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">success_rate</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&gt;= 99.9% over 30m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">latency</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;p95 &lt;= 800ms&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">queue_lag</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&lt;= 5m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">customer_impact</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;failed checkout count &lt;= threshold&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="nt">evidence_package</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;dashboard / log query / trace / audit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">time_range</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;incident window plus baseline&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">query_link</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;stable query URL or saved query name&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;service or platform owner&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">data_quality</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;sampling, freshness, missing fields&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">confidence</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;confirmed / suspected / weak&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">  </span><span class="nt">known_gap</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;missing signal or schema drift&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="nt">verification_handoff</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">  </span><span class="nt">hypothesis</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;payment provider timeout triggers fallback within 2m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">  </span><span class="nt">scope</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;staging or 10% production traffic&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="nt">workload_or_fault</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;timeout injection against provider adapter&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">  </span><span class="nt">result</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pass / conditional / fail&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;release / block / follow-up / runbook update&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;closure owner&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w"></span><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2026-05-07T10:15:00Z&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;enable checkout fallback&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;provider timeout and rising failed checkout&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;evidence_package link&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;incident commander or service owner&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;failed checkout drops within 10m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;fallback stale data exceeds threshold&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="w"></span><span class="nt">write_back</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="w">  </span><span class="nt">finding</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;provider timeout alert lacks tenant dimension&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">38</span><span class="cl"><span class="w">  </span><span class="nt">target_artifact</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;dashboard / alert / experiment / runbook&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="w">  </span><span class="nt">closure_signal</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;game day triggers tenant-scoped alert within 5m&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="w">  </span><span class="nt">review_date</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;next readiness review&#34;</span></span></span></code></pre></div><p>這份 template 的價值是把四個 artifact 放在同一份文件中。第一輪可以手動填寫，第二輪再拆到不同工具。</p>
<h2 id="驗收門檻">驗收門檻</h2>
<p>驗收門檻的責任是判斷 slice 是否已經能支援實際事故。完成狀態要由團隊能否沿著 artifact 做出同一組判斷來確認。</p>
<table>
  <thead>
      <tr>
          <th>驗收項目</th>
          <th>通過訊號</th>
          <th>回寫位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Triage</td>
          <td>on-call 能用 evidence 判斷是否啟動事故</td>
          <td>8.18 intake</td>
      </tr>
      <tr>
          <td>Verification</td>
          <td>release owner 能讀 handoff 做放行判斷</td>
          <td>6.8 release gate</td>
      </tr>
      <tr>
          <td>Decision</td>
          <td>IC 能用 decision log 交班與回退</td>
          <td>8.19 decision log</td>
      </tr>
      <tr>
          <td>Communication</td>
          <td>stakeholder update 能引用同一組 impact</td>
          <td>8.10 comms</td>
      </tr>
      <tr>
          <td>Write-back</td>
          <td>PIR action item 有 target 與 closure</td>
          <td>8.22 write-back</td>
      </tr>
  </tbody>
</table>
<p>Triage 通過代表 evidence 能支援事故啟動。若 on-call 還需要臨場重新找資料，回到 4.16 readiness 與 4.20 evidence package。</p>
<p>Verification 通過代表驗證結果能支援 release 決策。若 release owner 只看到 pass / fail，回到 6.23 handoff 補 hypothesis、scope 與 data quality。</p>
<p>Decision 通過代表事故現場有共同記憶。若交班後需要重問背景，回到 8.19 decision log 補 context、evidence 與 rollback condition。</p>
<p>Write-back 通過代表事故學習有落點。若 action item 只有「補監控」或「更新文件」，回到 8.22 write-back 補 target artifact 與 closure signal。</p>
<h2 id="tripwire">Tripwire</h2>
<p>Tripwire 的責任是提醒團隊何時回到概念層補缺口。Vertical slice 的目的在於快速暴露 routing chain 哪裡斷掉，再用最小修正補上 artifact 與 owner。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>evidence 找不到 owner</td>
          <td>觀測 operating model 缺口</td>
          <td>回到 4.18 owner 與 review cadence</td>
      </tr>
      <tr>
          <td>pass / fail 缺少決策力</td>
          <td>verification handoff 缺口</td>
          <td>回到 6.23 補 scope、hypothesis、decision</td>
      </tr>
      <tr>
          <td>IC 交班缺少共同記憶</td>
          <td>decision log 缺口</td>
          <td>回到 8.19 補最近決策、未完成動作與 rollback 條件</td>
      </tr>
      <tr>
          <td>PIR action 缺少關閉力</td>
          <td>write-back 缺口</td>
          <td>回到 8.22 補 closure signal 與 review date</td>
      </tr>
      <tr>
          <td>template 填寫成本過高</td>
          <td>欄位過多或工具摩擦</td>
          <td>刪到最小欄位，再跑一次 tabletop</td>
      </tr>
  </tbody>
</table>
<p>這些 tripwire 出現時，先修 artifact 與流程，再考慮導入新工具。工具能降低填寫成本，但欄位責任與 owner 需要先清楚。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/00-service-selection/operations-control-service-selection/" data-link-title="0.12 觀測、可靠性與事故服務選型" data-link-desc="從訊號、驗證與響應三層能力判斷操作控制服務的選型順序">0.12 operations control service selection</a>：判斷目前缺的是訊號、驗證、響應還是閉環。</li>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence package</a>：建立可交接觀測證據。</li>
<li><a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">6.22 steady state definition</a>：定義實驗與事故共用成功條件。</li>
<li><a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">6.23 verification evidence handoff</a>：把驗證結果交給 release 與 incident。</li>
<li><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 incident decision log</a>：保存事中決策與回退條件。</li>
<li><a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 incident evidence write-back</a>：把事故學習回寫成可關閉改善。</li>
</ul>
]]></content:encoded></item><item><title>4.13 Service Topology 與 Dependency Map</title><link>https://tarrragon.github.io/blog/backend/04-observability/service-topology/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/service-topology/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何依賴拓撲需要獨立節點：人工維護的依賴圖永遠過時&lt;/li>
&lt;li>拓撲訊號的來源：trace（4.3）、service mesh（mTLS / sidecar）、network flow log&lt;/li>
&lt;li>服務 graph 的維度：呼叫頻率、latency、錯誤率、版本&lt;/li>
&lt;li>依賴變化告警：新增依賴、刪除依賴、依賴方向反轉&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 分析：上游失效時下游影響範圍預測&lt;/li>
&lt;li>動態叢集下的拓撲追蹤：擴縮事件如何回寫拓撲訊號&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing&lt;/a> 的分工：trace 是單 request、topology 是統計聚合&lt;/li>
&lt;li>跟 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 deployment platform&lt;/a> 的整合：service mesh 部署&lt;/li>
&lt;li>反模式：架構圖只在 wiki 上、跟實際流量漂移；新依賴上線缺 review；拓撲圖回答「這服務掛了誰受影響」需要人工追查&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Service topology 是把跨服務依賴從文件轉成可觀測資料的能力，責任是讓團隊能用實際呼叫關係判斷依賴、影響面與變更風險。&lt;/p>
&lt;p>這一頁處理的是服務關係圖。Trace 解釋單次 request、topology 解釋一段時間內的依賴結構；兩者合起來才能回答「這個服務壞了會影響誰」。&lt;/p>
&lt;p>人工維護的依賴圖在快速變動的微服務環境下會持續漂移。新服務上線、舊服務下架、依賴方向反轉、版本切換都會發生在 wiki 圖更新之前；事故時依賴 wiki 圖判讀 blast radius，會把過期的依賴結構誤當成當前事實。&lt;/p>
&lt;h2 id="拓撲訊號的來源">拓撲訊號的來源&lt;/h2>
&lt;p>Service topology 的可信度取決於資料來源是否反映真實流量。常見的訊號來源各有覆蓋範圍跟限制：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>來源&lt;/th>
 &lt;th>覆蓋範圍&lt;/th>
 &lt;th>主要限制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Trace（4.3）&lt;/td>
 &lt;td>應用層呼叫關係、含 latency / 錯誤率&lt;/td>
 &lt;td>需要 instrumentation 覆蓋、有採樣偏誤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service mesh&lt;/td>
 &lt;td>sidecar / mTLS 拦截的所有跨服務流量&lt;/td>
 &lt;td>依賴 mesh 部署、不含外部依賴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Network flow log&lt;/td>
 &lt;td>L3 / L4 連線記錄、含外部依賴&lt;/td>
 &lt;td>缺少應用語意、難判斷哪個 service&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>API gateway log&lt;/td>
 &lt;td>外部入口流量、含 client / API 維度&lt;/td>
 &lt;td>只看到 gateway 視角、不知道內部呼叫&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>實務上常用組合：trace 作為主要來源（提供應用語意跟錯誤率），service mesh 作為補充（補上未 instrument 的服務），network flow log 作為兜底（揭露未管理的外部依賴）。&lt;/p>
&lt;p>把不同來源的拓撲訊號合併時，要顯式記錄每段依賴的來源。當 trace 看不到某段依賴、service mesh 卻看得到時，可能意味著 instrumentation 缺失或服務 bypass mesh，這本身是治理訊號。&lt;/p>
&lt;h2 id="服務-graph-的維度">服務 Graph 的維度&lt;/h2>
&lt;p>服務 graph 的責任是把跨服務依賴量化成可判讀的訊號、支援事故決策跟容量規劃。每段依賴關係要帶上維度（頻率、latency、錯誤率、版本、可選性）、才能在事故時被直接使用、而非只能呈現拓撲輪廓。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>呼叫頻率&lt;/strong>：高頻依賴跟低頻依賴的失效影響不同。高頻依賴失效會立即放大成 5xx，低頻依賴失效可能要數小時才浮現。&lt;/li>
&lt;li>&lt;strong>Latency 分布&lt;/strong>：依賴 p50 / p99 latency 決定下游 timeout 應該設多少。沒有 latency 訊號的依賴圖無法支援 timeout 設計。&lt;/li>
&lt;li>&lt;strong>Error rate&lt;/strong>：依賴的錯誤率提供 budget 訊號。當某依賴錯誤率上升，下游應觸發降級、保護自身可用性、避免進入無限重試放大故障。&lt;/li>
&lt;li>&lt;strong>版本 / API contract&lt;/strong>：依賴的版本變化跟 API contract 變更要進拓撲訊號。版本升級後若某段依賴消失，可能是 contract breaking。&lt;/li>
&lt;li>&lt;strong>方向跟可選性&lt;/strong>：是必要依賴（失效 = 服務失敗）還是可選依賴（失效 = 功能降級），影響事故分級。&lt;/li>
&lt;/ul>
&lt;p>這些維度進入拓撲訊號後，配合 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget&lt;/a> 才能把依賴可靠性變成可量化決策。&lt;/p>
&lt;h2 id="依賴變化的治理">依賴變化的治理&lt;/h2>
&lt;p>依賴關係的變化本身是訊號。新增依賴、刪除依賴、依賴方向反轉，都是值得告警的事件。沒有依賴變化偵測時，新服務接入往往跳過依賴 review，事故發生才從 trace 反查到「原來這條 path 已經接了三個月」。&lt;/p>
&lt;p>可操作的依賴變化告警：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>新增依賴 alert&lt;/strong>：當 trace 出現新的 service-to-service 呼叫，觸發 review。新依賴是否在預期內、是否經過 contract review、是否有 fallback。&lt;/li>
&lt;li>&lt;strong>依賴消失 alert&lt;/strong>：某段穩定存在的依賴在 N 分鐘內 trace 看不到，可能是 instrumentation 漏、可能是上游被誤改、可能是真實事故的早期訊號。&lt;/li>
&lt;li>&lt;strong>依賴方向反轉&lt;/strong>：A → B 變成 B → A 通常意味著 refactor 或誤改、應該觸發 review。&lt;/li>
&lt;li>&lt;strong>循環依賴偵測&lt;/strong>：環狀依賴會在事故時放大恢復難度、應該在拓撲訊號層級就阻擋。&lt;/li>
&lt;/ol>
&lt;h2 id="動態叢集下的拓撲訊號">動態叢集下的拓撲訊號&lt;/h2>
&lt;p>動態叢集下拓撲訊號的責任是讓觀測模型追上實際依賴結構的變化。Pod 數量浮動、node 換代、service IP 變化、跨 cluster 流量重新分配都會在分鐘級內改變服務間的可達性、若拓撲訊號停留在週期性快照、事故時看到的會是過期結構。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何依賴拓撲需要獨立節點：人工維護的依賴圖永遠過時</li>
<li>拓撲訊號的來源：trace（4.3）、service mesh（mTLS / sidecar）、network flow log</li>
<li>服務 graph 的維度：呼叫頻率、latency、錯誤率、版本</li>
<li>依賴變化告警：新增依賴、刪除依賴、依賴方向反轉</li>
<li><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 分析：上游失效時下游影響範圍預測</li>
<li>動態叢集下的拓撲追蹤：擴縮事件如何回寫拓撲訊號</li>
<li>跟 <a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing</a> 的分工：trace 是單 request、topology 是統計聚合</li>
<li>跟 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 deployment platform</a> 的整合：service mesh 部署</li>
<li>反模式：架構圖只在 wiki 上、跟實際流量漂移；新依賴上線缺 review；拓撲圖回答「這服務掛了誰受影響」需要人工追查</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Service topology 是把跨服務依賴從文件轉成可觀測資料的能力，責任是讓團隊能用實際呼叫關係判斷依賴、影響面與變更風險。</p>
<p>這一頁處理的是服務關係圖。Trace 解釋單次 request、topology 解釋一段時間內的依賴結構；兩者合起來才能回答「這個服務壞了會影響誰」。</p>
<p>人工維護的依賴圖在快速變動的微服務環境下會持續漂移。新服務上線、舊服務下架、依賴方向反轉、版本切換都會發生在 wiki 圖更新之前；事故時依賴 wiki 圖判讀 blast radius，會把過期的依賴結構誤當成當前事實。</p>
<h2 id="拓撲訊號的來源">拓撲訊號的來源</h2>
<p>Service topology 的可信度取決於資料來源是否反映真實流量。常見的訊號來源各有覆蓋範圍跟限制：</p>
<table>
  <thead>
      <tr>
          <th>來源</th>
          <th>覆蓋範圍</th>
          <th>主要限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Trace（4.3）</td>
          <td>應用層呼叫關係、含 latency / 錯誤率</td>
          <td>需要 instrumentation 覆蓋、有採樣偏誤</td>
      </tr>
      <tr>
          <td>Service mesh</td>
          <td>sidecar / mTLS 拦截的所有跨服務流量</td>
          <td>依賴 mesh 部署、不含外部依賴</td>
      </tr>
      <tr>
          <td>Network flow log</td>
          <td>L3 / L4 連線記錄、含外部依賴</td>
          <td>缺少應用語意、難判斷哪個 service</td>
      </tr>
      <tr>
          <td>API gateway log</td>
          <td>外部入口流量、含 client / API 維度</td>
          <td>只看到 gateway 視角、不知道內部呼叫</td>
      </tr>
  </tbody>
</table>
<p>實務上常用組合：trace 作為主要來源（提供應用語意跟錯誤率），service mesh 作為補充（補上未 instrument 的服務），network flow log 作為兜底（揭露未管理的外部依賴）。</p>
<p>把不同來源的拓撲訊號合併時，要顯式記錄每段依賴的來源。當 trace 看不到某段依賴、service mesh 卻看得到時，可能意味著 instrumentation 缺失或服務 bypass mesh，這本身是治理訊號。</p>
<h2 id="服務-graph-的維度">服務 Graph 的維度</h2>
<p>服務 graph 的責任是把跨服務依賴量化成可判讀的訊號、支援事故決策跟容量規劃。每段依賴關係要帶上維度（頻率、latency、錯誤率、版本、可選性）、才能在事故時被直接使用、而非只能呈現拓撲輪廓。</p>
<ul>
<li><strong>呼叫頻率</strong>：高頻依賴跟低頻依賴的失效影響不同。高頻依賴失效會立即放大成 5xx，低頻依賴失效可能要數小時才浮現。</li>
<li><strong>Latency 分布</strong>：依賴 p50 / p99 latency 決定下游 timeout 應該設多少。沒有 latency 訊號的依賴圖無法支援 timeout 設計。</li>
<li><strong>Error rate</strong>：依賴的錯誤率提供 budget 訊號。當某依賴錯誤率上升，下游應觸發降級、保護自身可用性、避免進入無限重試放大故障。</li>
<li><strong>版本 / API contract</strong>：依賴的版本變化跟 API contract 變更要進拓撲訊號。版本升級後若某段依賴消失，可能是 contract breaking。</li>
<li><strong>方向跟可選性</strong>：是必要依賴（失效 = 服務失敗）還是可選依賴（失效 = 功能降級），影響事故分級。</li>
</ul>
<p>這些維度進入拓撲訊號後，配合 <a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget</a> 才能把依賴可靠性變成可量化決策。</p>
<h2 id="依賴變化的治理">依賴變化的治理</h2>
<p>依賴關係的變化本身是訊號。新增依賴、刪除依賴、依賴方向反轉，都是值得告警的事件。沒有依賴變化偵測時，新服務接入往往跳過依賴 review，事故發生才從 trace 反查到「原來這條 path 已經接了三個月」。</p>
<p>可操作的依賴變化告警：</p>
<ol>
<li><strong>新增依賴 alert</strong>：當 trace 出現新的 service-to-service 呼叫，觸發 review。新依賴是否在預期內、是否經過 contract review、是否有 fallback。</li>
<li><strong>依賴消失 alert</strong>：某段穩定存在的依賴在 N 分鐘內 trace 看不到，可能是 instrumentation 漏、可能是上游被誤改、可能是真實事故的早期訊號。</li>
<li><strong>依賴方向反轉</strong>：A → B 變成 B → A 通常意味著 refactor 或誤改、應該觸發 review。</li>
<li><strong>循環依賴偵測</strong>：環狀依賴會在事故時放大恢復難度、應該在拓撲訊號層級就阻擋。</li>
</ol>
<h2 id="動態叢集下的拓撲訊號">動態叢集下的拓撲訊號</h2>
<p>動態叢集下拓撲訊號的責任是讓觀測模型追上實際依賴結構的變化。Pod 數量浮動、node 換代、service IP 變化、跨 cluster 流量重新分配都會在分鐘級內改變服務間的可達性、若拓撲訊號停留在週期性快照、事故時看到的會是過期結構。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb K8s 規模化下的觀測訊號治理</a>：揭露「叢集擴縮跟工作負載變動需要回寫觀測模型」「叢集層指標跟服務層指標要分開治理」「擴縮事件跟事故關聯要可回溯」三個方向（case 直接列出）；以下展開的 service 層級節點、跨 cluster failover、drill-down 設計屬通用 K8s observability 經驗、case 本身未細說。</p>
<p>動態叢集對拓撲訊號的挑戰有三個面向、性質不同、各自的對應做法也不同。</p>
<p><strong>拓撲節點不穩定</strong> 是資料模型層的問題。Pod 短暫存在、IP 不固定、若直接把 Pod 當拓撲節點、graph 會分鐘級持續抖動、事故時看到的依賴結構不可信。對應做法是把節點層級從 Pod / IP 提升到 service（service name + version + region）、把 instance / Pod 層級放到 dashboard drill-down、讓主拓撲圖反映穩定的服務依賴而非瞬時實例分布。</p>
<p><strong>擴縮事件 vs 真實事故區分</strong> 是訊號分辨層的問題。HPA scale-up / scale-down、cluster autoscaler 加 node 失敗、Pod 重啟、health check 短暫失敗，這些擴縮動作本身會產生跟事故相似的訊號（5xx 短暫升高、reconnect、依賴連線中斷）、若沒分辨機制、值班會把擴縮過程的正常波動誤判成事故、或把真正的事故誤判成擴縮。對應做法是把擴縮事件本身打進 timeline、跟事故 timeline 共用同一張圖、判讀時對齊看。</p>
<p><strong>跨 cluster 流量變化</strong> 是視角層的問題。multi-cluster 部署下、流量可能因 cluster 變更從 cluster A 切到 cluster B、若拓撲圖只看單 cluster 視角、B cluster 突增的流量會被解讀為 traffic spike、漏掉真正的 failover 事件。對應做法是讓拓撲圖呈現跨 cluster 邊界、把 cluster 間流量變化也標到圖上、避免 cluster 邊界成為觀測盲區。</p>
<p>把叢集層指標（node count、Pod count、HPA event）跟服務層指標（call rate、error rate、latency）分開治理，是動態叢集環境的基本要求。叢集層指標的 owner 通常是 platform team、服務層指標的 owner 通常是 service team，兩者放在同一 dashboard 上要清楚標示來源跟責任。</p>
<p>擴縮事件回溯到事故關聯的另一個價值是 capacity retrospective。當 HPA 在事故前後觸發、scale-up 是否足夠、scale-down 是否過快，都需要把擴縮 timeline 跟事故 timeline 拼起來看，回到 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 容量成本</a> 跟 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃</a> 的回寫。</p>
<h2 id="blast-radius-推導">Blast Radius 推導</h2>
<p>Blast radius 分析的核心責任是回答「如果這個服務或依賴失效、哪些上游 / 下游會受影響、影響多深」。沒有實時拓撲訊號時，這個分析靠經驗、容易低估或高估。</p>
<p>實時 topology 加上依賴可選性標記後，blast radius 可以分層推導：</p>
<ul>
<li><strong>直接下游</strong>：直接呼叫該服務的服務、立即受影響。</li>
<li><strong>間接下游</strong>：透過中間服務間接依賴、影響時間延後。</li>
<li><strong>可降級下游</strong>：依賴是 optional、失效會觸發降級但不失敗。</li>
<li><strong>必要下游</strong>：依賴是 mandatory、失效會傳播成服務失敗。</li>
</ul>
<p>事故時把 blast radius 從拓撲推導出來、再對照實際看到的 5xx 跟 SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、能驗證影響面是否符合預期。當實際影響超出推導 blast radius、通常意味著存在未紀錄依賴。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 topology 時，先看資料是否來自真實流量，再看依賴變化是否能被治理。</p>
<p>重點訊號包括：</p>
<ul>
<li>service graph 是否包含呼叫方向、頻率、latency 與 error rate</li>
<li>新增依賴是否能觸發 review 或 alert</li>
<li><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 是否能從上游 / 下游關係推導</li>
<li>topology 是否能餵給 dependency budget 與事故型態判讀</li>
<li>動態擴縮事件是否打進 timeline、能跟事故區分</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故時回答「誰呼叫這服務」需要人工追查</li>
<li>新服務接入無依賴 review、出事後才發現連結</li>
<li>架構文件跟實際呼叫關係漂移、半年沒更新</li>
<li>service mesh 部署但拓撲訊號未被使用</li>
<li>循環依賴存在但無人發現</li>
<li>擴縮事件造成的短暫錯誤被誤判成事故</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Wiki 架構圖</td>
          <td>圖跟實際流量漂移半年</td>
          <td>從 trace / mesh 自動生成、持續更新</td>
      </tr>
      <tr>
          <td>新依賴無 review</td>
          <td>trace 出現新依賴沒人知道</td>
          <td>新依賴 alert、依賴 review 進 release flow</td>
      </tr>
      <tr>
          <td>拓撲節點用 Pod / Instance</td>
          <td>動態叢集下圖持續抖動</td>
          <td>service 層級節點、Pod 放 drill-down</td>
      </tr>
      <tr>
          <td>叢集跟服務指標混在一張圖</td>
          <td>platform 跟 service 責任不清</td>
          <td>分層 dashboard、明確 owner</td>
      </tr>
      <tr>
          <td>Blast radius 靠經驗推導</td>
          <td>影響面評估不準、事後才發現遺漏</td>
          <td>從拓撲訊號自動推導、跟實際影響對照</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing</a>：拓撲訊號的原始來源</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：叢集層 / 服務層 ownership 分工</li>
<li><a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署</a>：service mesh 配置</li>
<li>6.5 pre-mortem（規劃中）：依賴失效路徑分析</li>
<li><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity cost</a>：擴縮事件 retrospective</li>
<li><a href="/blog/backend/06-reliability/dependency-reliability-budget/" data-link-title="6.14 Dependency Reliability Budget" data-link-desc="把內外依賴的可靠性納入 SLO 計算與設計約束">6.14 dependency budget</a>：拓撲是依賴可靠性評估的資料來源</li>
<li><a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">8.9 事故型態庫</a>：<a href="/blog/backend/knowledge-cards/cascading-failure/" data-link-title="Cascading Failure" data-link-desc="說明局部故障如何透過等待、重試與資源耗盡擴散到整個系統">cascading failure</a> 型態的拓撲依據</li>
</ul>
]]></content:encoded></item><item><title>4.14 Anomaly Detection</title><link>https://tarrragon.github.io/blog/backend/04-observability/anomaly-detection/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/anomaly-detection/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>Anomaly detection 跟 rule-based alert 的分工&lt;/li>
&lt;li>Baseline 模型類別&lt;/li>
&lt;li>Anomaly 訊號的處理路徑&lt;/li>
&lt;li>False positive 與 alert noise 共用預算&lt;/li>
&lt;li>Explainability：anomaly 要能定位到維度&lt;/li>
&lt;li>Vendor 定位&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Anomaly detection 是用統計基線或模型找出偏離常態的訊號，責任是補上 rule-based alert 難以事先列舉的變化。&lt;/p>
&lt;p>Rule-based alert 抓已知模式 — 團隊事先定義「error rate &amp;gt; 1% 就告警」。Anomaly detection 抓未知模式 — 系統觀察到「今天的 latency 分布跟過去 30 天的同時段不同」。兩者互補：rule-based 精確但只能抓團隊已預見的問題，anomaly detection 有噪音但能發現團隊沒想到的退化。&lt;/p>
&lt;p>Anomaly 適合作為提示層（hint），通常先進 dashboard 或低 severity 路由，再由 SLO 判讀或人工確認決定是否升級。把 anomaly 直接接 page 是噪音爆量的常見原因。&lt;/p>
&lt;h2 id="跟-rule-based-alert-的分工">跟 Rule-based Alert 的分工&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Rule-based alert&lt;/th>
 &lt;th>Anomaly detection&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>觸發條件&lt;/td>
 &lt;td>固定閾值或 burn rate&lt;/td>
 &lt;td>偏離統計基線&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>抓什麼&lt;/td>
 &lt;td>已知模式（團隊事先定義）&lt;/td>
 &lt;td>未知模式（歷史基線判斷）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>精確度&lt;/td>
 &lt;td>高（閾值明確）&lt;/td>
 &lt;td>低到中（統計偏差 = 候選，需要確認）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>False positive&lt;/td>
 &lt;td>閾值對齊時低&lt;/td>
 &lt;td>較高（季節性未建模、促銷、release）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合的 severity&lt;/td>
 &lt;td>Critical / Warning&lt;/td>
 &lt;td>Info / Warning（確認後才升級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>維護成本&lt;/td>
 &lt;td>隨服務變化需調整閾值&lt;/td>
 &lt;td>模型要持續 retrain 或校正&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>最有效的整合方式：rule-based alert 處理已知的 SLO violation（symptom-based、高 severity），anomaly detection 處理趨勢異常跟 novel failure mode（低 severity、dashboard widget）。兩者共用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a> 的 noise budget — anomaly 的 false positive 也算進整體 noise rate。&lt;/p>
&lt;h2 id="baseline-模型類別">Baseline 模型類別&lt;/h2>
&lt;h3 id="seasonal-baseline">Seasonal baseline&lt;/h3>
&lt;p>按日夜、週末、節慶、促銷等週期建立基線。同一個指標的「正常範圍」在週一上午跟週日凌晨不同。Seasonal model 用歷史同期資料建立預期帶（expected band），偏離帶外視為 anomaly。&lt;/p>
&lt;p>Seasonal baseline 的失敗模式是週期性假設錯誤 — 業務改變後流量模式跟歷史不同（新產品上線改變了週末流量），模型用錯誤的基線判斷。需要定期驗證模型跟實際流量的吻合度。&lt;/p>
&lt;h3 id="moving-window-baseline">Moving window baseline&lt;/h3>
&lt;p>用過去 N 分鐘 / 小時的資料建立動態基線。比 seasonal model 簡單、延遲更低，但對突發變化更敏感（release 後 latency 自然變化可能觸發 anomaly）。&lt;/p>
&lt;p>Moving window 適合不需要週期性建模的指標 — 連線數、queue depth、goroutine count 等「預期穩定、突變代表問題」的指標。&lt;/p>
&lt;h3 id="ml-basedforecast--clustering">ML-based（forecast / clustering）&lt;/h3>
&lt;p>用機器學習模型做時間序列預測（Prophet、ARIMA）或高維度聚類（isolation forest、DBSCAN）。能處理複雜的多變量異常（A 指標上升 + B 指標下降 = 異常，但各自單獨看都在正常範圍）。&lt;/p>
&lt;p>ML 模型的成本是訓練、retrain、模型版本管理跟 explainability。多數團隊的起步方式是先用 seasonal + moving window（不需要 ML pipeline），等 false positive 管理穩定後再引入 ML。&lt;/p>
&lt;h2 id="anomaly-訊號的處理路徑">Anomaly 訊號的處理路徑&lt;/h2>
&lt;p>Anomaly detection 的輸出是「這個指標在這段時間偏離基線」— 候選訊號，不是確認的問題。處理路徑決定 anomaly 是有用的提示還是噪音來源。&lt;/p>
&lt;p>&lt;strong>Dashboard widget&lt;/strong>：anomaly 標記在 time series panel 上（標色、annotation），讓巡視 dashboard 的工程師注意到。低成本、零噪音（不通知任何人）、但需要有人主動看。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>Anomaly detection 跟 rule-based alert 的分工</li>
<li>Baseline 模型類別</li>
<li>Anomaly 訊號的處理路徑</li>
<li>False positive 與 alert noise 共用預算</li>
<li>Explainability：anomaly 要能定位到維度</li>
<li>Vendor 定位</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Anomaly detection 是用統計基線或模型找出偏離常態的訊號，責任是補上 rule-based alert 難以事先列舉的變化。</p>
<p>Rule-based alert 抓已知模式 — 團隊事先定義「error rate &gt; 1% 就告警」。Anomaly detection 抓未知模式 — 系統觀察到「今天的 latency 分布跟過去 30 天的同時段不同」。兩者互補：rule-based 精確但只能抓團隊已預見的問題，anomaly detection 有噪音但能發現團隊沒想到的退化。</p>
<p>Anomaly 適合作為提示層（hint），通常先進 dashboard 或低 severity 路由，再由 SLO 判讀或人工確認決定是否升級。把 anomaly 直接接 page 是噪音爆量的常見原因。</p>
<h2 id="跟-rule-based-alert-的分工">跟 Rule-based Alert 的分工</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Rule-based alert</th>
          <th>Anomaly detection</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發條件</td>
          <td>固定閾值或 burn rate</td>
          <td>偏離統計基線</td>
      </tr>
      <tr>
          <td>抓什麼</td>
          <td>已知模式（團隊事先定義）</td>
          <td>未知模式（歷史基線判斷）</td>
      </tr>
      <tr>
          <td>精確度</td>
          <td>高（閾值明確）</td>
          <td>低到中（統計偏差 = 候選，需要確認）</td>
      </tr>
      <tr>
          <td>False positive</td>
          <td>閾值對齊時低</td>
          <td>較高（季節性未建模、促銷、release）</td>
      </tr>
      <tr>
          <td>適合的 severity</td>
          <td>Critical / Warning</td>
          <td>Info / Warning（確認後才升級）</td>
      </tr>
      <tr>
          <td>維護成本</td>
          <td>隨服務變化需調整閾值</td>
          <td>模型要持續 retrain 或校正</td>
      </tr>
  </tbody>
</table>
<p>最有效的整合方式：rule-based alert 處理已知的 SLO violation（symptom-based、高 severity），anomaly detection 處理趨勢異常跟 novel failure mode（低 severity、dashboard widget）。兩者共用 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 的 noise budget — anomaly 的 false positive 也算進整體 noise rate。</p>
<h2 id="baseline-模型類別">Baseline 模型類別</h2>
<h3 id="seasonal-baseline">Seasonal baseline</h3>
<p>按日夜、週末、節慶、促銷等週期建立基線。同一個指標的「正常範圍」在週一上午跟週日凌晨不同。Seasonal model 用歷史同期資料建立預期帶（expected band），偏離帶外視為 anomaly。</p>
<p>Seasonal baseline 的失敗模式是週期性假設錯誤 — 業務改變後流量模式跟歷史不同（新產品上線改變了週末流量），模型用錯誤的基線判斷。需要定期驗證模型跟實際流量的吻合度。</p>
<h3 id="moving-window-baseline">Moving window baseline</h3>
<p>用過去 N 分鐘 / 小時的資料建立動態基線。比 seasonal model 簡單、延遲更低，但對突發變化更敏感（release 後 latency 自然變化可能觸發 anomaly）。</p>
<p>Moving window 適合不需要週期性建模的指標 — 連線數、queue depth、goroutine count 等「預期穩定、突變代表問題」的指標。</p>
<h3 id="ml-basedforecast--clustering">ML-based（forecast / clustering）</h3>
<p>用機器學習模型做時間序列預測（Prophet、ARIMA）或高維度聚類（isolation forest、DBSCAN）。能處理複雜的多變量異常（A 指標上升 + B 指標下降 = 異常，但各自單獨看都在正常範圍）。</p>
<p>ML 模型的成本是訓練、retrain、模型版本管理跟 explainability。多數團隊的起步方式是先用 seasonal + moving window（不需要 ML pipeline），等 false positive 管理穩定後再引入 ML。</p>
<h2 id="anomaly-訊號的處理路徑">Anomaly 訊號的處理路徑</h2>
<p>Anomaly detection 的輸出是「這個指標在這段時間偏離基線」— 候選訊號，不是確認的問題。處理路徑決定 anomaly 是有用的提示還是噪音來源。</p>
<p><strong>Dashboard widget</strong>：anomaly 標記在 time series panel 上（標色、annotation），讓巡視 dashboard 的工程師注意到。低成本、零噪音（不通知任何人）、但需要有人主動看。</p>
<p><strong>Low severity alert（info / warning）</strong>：anomaly 進入 alerting pipeline，但 severity 設為 info 或 warning。不 page on-call、但記錄在 alert history 中。事故發生後可以回溯「事故前有沒有 anomaly 提早預警」。</p>
<p><strong>Conditional escalation</strong>：anomaly 搭配 rule-based 條件升級。「Latency 偏離基線 + error rate 超過 SLO <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>」→ 升級為 critical。單獨的 anomaly 不足以 page，但跟其他訊號組合時有判讀價值。</p>
<h2 id="explainability">Explainability</h2>
<p>Anomaly 觸發時，工程師需要回答「為什麼異常」 — 是哪個服務、哪個 endpoint、哪個 tenant、哪個地區導致的。只告訴你「overall latency 異常」但不說維度，診斷價值有限。</p>
<p>可操作的 explainability 有兩層：</p>
<p><strong>維度歸因</strong>：anomaly detection 系統自動拆分異常到子維度 — 「overall latency 異常，主要來自 region=us-east + endpoint=/api/search」。Datadog Watchdog 跟 New Relic AI 提供這種維度下鑽能力。</p>
<p><strong>Root cause hint</strong>：anomaly 跟其他訊號（deploy event、config change、dependency error spike）的時間關聯。「Latency anomaly 開始的時間跟 v2.3.1 deploy 吻合」— 提示 root cause 可能跟 deploy 有關。</p>
<h2 id="vendor-定位">Vendor 定位</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>定位</th>
          <th>特點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog Watchdog</td>
          <td>託管 anomaly + 維度歸因</td>
          <td>跟 APM / log / metric 整合、auto-detect</td>
      </tr>
      <tr>
          <td>New Relic AI</td>
          <td>託管 anomaly + root cause suggest</td>
          <td>全棧觀測整合</td>
      </tr>
      <tr>
          <td>Prophet（自建）</td>
          <td>開源 time series forecast</td>
          <td>需要自建 pipeline、training、serving</td>
      </tr>
      <tr>
          <td>Anomalo</td>
          <td>資料品質 anomaly</td>
          <td>偏 data pipeline、非 infra 觀測</td>
      </tr>
  </tbody>
</table>
<p>自建 vs 託管的判準：團隊是否有 ML pipeline 維運能力。託管方案的好處是零 ML 運維、跟觀測平台深度整合；自建的好處是可控性高、可以針對業務邏輯客製模型。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Anomaly detection 最常見的失敗是 baseline 沒對齊流量週期（週末自然下降被判成異常）跟異常觸發後無法歸因到具體維度（只知道「latency 異常」但看不出是哪個 service、哪個 region）。</p>
<p>重點訊號包括：</p>
<ul>
<li>Baseline 是否理解日夜、週末、節慶與促銷週期</li>
<li>Anomaly 是否能指出 service、tenant、region 或 endpoint 維度</li>
<li>False positive 是否納入 alert noise governance</li>
<li>Anomaly 與 rule-based alert 是否有清楚分工</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Alert 規則寫到數百條、仍漏掉 novel failure mode</li>
<li>已知 anomaly 訊號被忽略、靠人工巡視 dashboard</li>
<li>Anomaly 觸發後無人能解釋「為什麼異常」</li>
<li>模型未對齊週期性（週末 / 節慶 / promo）造成噪音</li>
<li>同一指標 anomaly + rule alert 重複觸發、無協調</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Anomaly 直接接 page</td>
          <td>On-call 被統計偏差淹沒</td>
          <td>Anomaly 先走 info/warning、conditional 才升級</td>
      </tr>
      <tr>
          <td>Baseline 沒對齊季節性</td>
          <td>週末 / 節慶流量自然變化觸發 false positive</td>
          <td>用 seasonal model 或 exclude 已知事件窗口</td>
      </tr>
      <tr>
          <td>Anomaly 跟 rule alert 重複</td>
          <td>同一問題兩個來源觸發、noise 翻倍</td>
          <td>共用 noise budget、anomaly 在 rule 已觸發時抑制</td>
      </tr>
      <tr>
          <td>模型不可解釋</td>
          <td>Anomaly fired 但工程師不知道看什麼</td>
          <td>要求維度歸因能力、否則只作 dashboard widget</td>
      </tr>
      <tr>
          <td>自建 ML 但無 retrain pipeline</td>
          <td>模型用半年前的 baseline、precision 持續下降</td>
          <td>建立定期 retrain 或改用託管方案</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a>：anomaly 升級 alert 的條件</li>
<li><a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO</a>：跟 SLO burn rate 的訊號分工</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal governance</a>：anomaly false positive 的淘汰</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：anomaly 系統的 ownership</li>
</ul>
]]></content:encoded></item><item><title>4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/observability-cost-governance-at-scale/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/observability-cost-governance-at-scale/</guid><description>&lt;p>觀測成本治理案例來自多家企業的共同經驗：觀測平台帳單每季成長 30%，管理層問「為什麼監控這麼貴」但沒人能歸因。問題的核心不是「花太多」而是「花在哪不知道」— 沒有 per-team cost attribution 的觀測平台，成本優化只能靠全域砍 retention 或降 sampling，兩者都會傷害觀測品質。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>這個案例綜合三個組織的經驗模式：&lt;/p>
&lt;p>一家中型 SaaS 公司用 Datadog 做全端觀測（APM + logs + metrics + RUM）。月帳單從 $15K 成長到 $60K，兩年內四倍。CFO 問 CTO「這筆錢買到什麼」，CTO 轉問 platform team，platform team 說不出哪些團隊佔多少。&lt;/p>
&lt;p>一家金融科技公司自建 Grafana Stack（Prometheus + Loki + Tempo + Mimir）。自建沒有 SaaS 帳單，但 Kubernetes 節點跟 storage 的成本持續增加。infra team 知道 Mimir 的 storage 在成長，但不知道是哪些 metric label 造成的 cardinality 爆炸。&lt;/p>
&lt;p>一家遊戲公司用 CloudWatch 做 AWS 原生觀測。Logs 的 ingestion 費用佔帳單 70%，但追查後發現 90% 是 debug-level log，只在排錯時用到，平常沒人查。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="沒有-cost-attribution">沒有 cost attribution&lt;/h3>
&lt;p>觀測帳單通常是 organization-level 的一筆支出。SaaS 帳單按 hosts、custom metrics、log volume、APM spans 計費；自建平台按 compute 跟 storage 計費。兩種模式都缺少「這些費用是哪個 team / service 造成的」的歸因。&lt;/p>
&lt;p>沒有 attribution 的後果是所有優化都是全域操作 — 砍 retention 從 30 天到 7 天影響所有人，降 sampling 從 100% 到 10% 影響所有服務。需要觀測資料的團隊被平均到成本節省裡，不需要的團隊搭便車。&lt;/p>
&lt;h3 id="cardinality-爆炸">Cardinality 爆炸&lt;/h3>
&lt;p>Metrics 成本的主要 driver 是 cardinality — unique label combination 的數量。常見的 cardinality 爆炸來源：&lt;/p>
&lt;ul>
&lt;li>把 user ID 或 request ID 放進 metric label（每個 unique user 產生一組 series）&lt;/li>
&lt;li>動態的 endpoint path（&lt;code>/api/users/123&lt;/code> 每個 user ID 是一個 label value）&lt;/li>
&lt;li>多租戶 label 過細（tenant × region × service × endpoint 的笛卡兒積）&lt;/li>
&lt;/ul>
&lt;p>一個失控的 label 可以讓 series 數量從 10 萬跳到 1000 萬。SaaS 的計費是 per custom metric，自建的代價是 Prometheus / Mimir 的 memory 跟 storage。&lt;/p>
&lt;h3 id="log-volume-失控">Log volume 失控&lt;/h3>
&lt;p>Debug-level log 在開發階段有用，但 production 環境裡通常只在排錯時被查。全量 debug log 送進 hot tier（Elasticsearch、Loki、CloudWatch Logs）的 ingestion 跟 storage 成本是最大的 log 成本來源。&lt;/p>
&lt;p>問題是沒人敢降 debug log — 「萬一出事需要 debug log 怎麼辦」。恐懼驅動的 log level 設定讓 log volume 只升不降。&lt;/p></description><content:encoded><![CDATA[<p>觀測成本治理案例來自多家企業的共同經驗：觀測平台帳單每季成長 30%，管理層問「為什麼監控這麼貴」但沒人能歸因。問題的核心不是「花太多」而是「花在哪不知道」— 沒有 per-team cost attribution 的觀測平台，成本優化只能靠全域砍 retention 或降 sampling，兩者都會傷害觀測品質。</p>
<h2 id="業務背景">業務背景</h2>
<p>這個案例綜合三個組織的經驗模式：</p>
<p>一家中型 SaaS 公司用 Datadog 做全端觀測（APM + logs + metrics + RUM）。月帳單從 $15K 成長到 $60K，兩年內四倍。CFO 問 CTO「這筆錢買到什麼」，CTO 轉問 platform team，platform team 說不出哪些團隊佔多少。</p>
<p>一家金融科技公司自建 Grafana Stack（Prometheus + Loki + Tempo + Mimir）。自建沒有 SaaS 帳單，但 Kubernetes 節點跟 storage 的成本持續增加。infra team 知道 Mimir 的 storage 在成長，但不知道是哪些 metric label 造成的 cardinality 爆炸。</p>
<p>一家遊戲公司用 CloudWatch 做 AWS 原生觀測。Logs 的 ingestion 費用佔帳單 70%，但追查後發現 90% 是 debug-level log，只在排錯時用到，平常沒人查。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="沒有-cost-attribution">沒有 cost attribution</h3>
<p>觀測帳單通常是 organization-level 的一筆支出。SaaS 帳單按 hosts、custom metrics、log volume、APM spans 計費；自建平台按 compute 跟 storage 計費。兩種模式都缺少「這些費用是哪個 team / service 造成的」的歸因。</p>
<p>沒有 attribution 的後果是所有優化都是全域操作 — 砍 retention 從 30 天到 7 天影響所有人，降 sampling 從 100% 到 10% 影響所有服務。需要觀測資料的團隊被平均到成本節省裡，不需要的團隊搭便車。</p>
<h3 id="cardinality-爆炸">Cardinality 爆炸</h3>
<p>Metrics 成本的主要 driver 是 cardinality — unique label combination 的數量。常見的 cardinality 爆炸來源：</p>
<ul>
<li>把 user ID 或 request ID 放進 metric label（每個 unique user 產生一組 series）</li>
<li>動態的 endpoint path（<code>/api/users/123</code> 每個 user ID 是一個 label value）</li>
<li>多租戶 label 過細（tenant × region × service × endpoint 的笛卡兒積）</li>
</ul>
<p>一個失控的 label 可以讓 series 數量從 10 萬跳到 1000 萬。SaaS 的計費是 per custom metric，自建的代價是 Prometheus / Mimir 的 memory 跟 storage。</p>
<h3 id="log-volume-失控">Log volume 失控</h3>
<p>Debug-level log 在開發階段有用，但 production 環境裡通常只在排錯時被查。全量 debug log 送進 hot tier（Elasticsearch、Loki、CloudWatch Logs）的 ingestion 跟 storage 成本是最大的 log 成本來源。</p>
<p>問題是沒人敢降 debug log — 「萬一出事需要 debug log 怎麼辦」。恐懼驅動的 log level 設定讓 log volume 只升不降。</p>
<h3 id="trace-sampling-恐懼">Trace sampling 恐懼</h3>
<p>類似的恐懼存在於 trace sampling — 「如果剛好那筆有問題的 request 被 sample 掉怎麼辦」。100% tracing 的成本在中等規模（每秒數萬 request）就開始顯著。</p>
<h2 id="解法">解法</h2>
<h3 id="cost-attribution-by-team--service">Cost attribution by team / service</h3>
<p>第一步是讓成本可見，歸因先於優化。</p>
<p>SaaS 平台：用 Datadog 的 usage attribution 或 Grafana Cloud 的 usage reporting 把 ingestion 按 service tag / team tag 拆分。每個 team 看到自己的 metric series、log volume 跟 span 數量。</p>
<p>自建平台：在 Mimir / Loki 的 tenant 維度或 Prometheus 的 namespace 維度拆分 storage 跟 query cost。用 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a> 的框架把 infra cost 按 service ownership 分配。</p>
<p>Attribution 本身就能驅動行為改變 — 當團隊看到自己佔了 40% 的 log volume、而且 95% 是 debug level 時，他們會主動調 log level。</p>
<h3 id="cardinality-budget-per-team">Cardinality budget per team</h3>
<p>Attribution 之後，為每個 team / service 設定 cardinality budget（active series 上限）。超出 budget 的 series 進入 review 流程 — team 決定哪些 label 可以 aggregate 或移除，而非由 platform 單方面 drop。</p>
<p>Budget 的設定依據是 baseline measurement + growth rate，不是拍腦袋。先觀察 3 個月的 cardinality 趨勢，把 budget 設在 baseline 的 1.5 倍，每季 review。</p>
<h3 id="log-tiering">Log tiering</h3>
<p>把 log 從「全部進 hot tier」改成分層：</p>
<table>
  <thead>
      <tr>
          <th>Log level</th>
          <th>目的地</th>
          <th>Retention</th>
          <th>查詢延遲</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Error / Warn</td>
          <td>Hot tier（Loki / Elasticsearch）</td>
          <td>30 天</td>
          <td>即時</td>
      </tr>
      <tr>
          <td>Info</td>
          <td>Warm tier（壓縮 + 延遲查詢）</td>
          <td>14 天</td>
          <td>秒到分鐘</td>
      </tr>
      <tr>
          <td>Debug</td>
          <td>Cold archive（object storage）</td>
          <td>7 天</td>
          <td>分鐘到小時</td>
      </tr>
  </tbody>
</table>
<p>Debug log 仍然保留，但不進昂貴的 hot tier。需要排錯時從 cold archive 拉回 — 多等幾分鐘的代價遠低於全量 hot tier 的持續成本。</p>
<h3 id="adaptive-sampling">Adaptive sampling</h3>
<p>Trace sampling 從 uniform 改成 adaptive：</p>
<ul>
<li>錯誤 request 100% 保留</li>
<li>高 latency request（&gt; p99）100% 保留</li>
<li>正常 request 依 traffic volume adaptive sampling（高流量 endpoint 低 sample rate、低流量 endpoint 高 sample rate）</li>
</ul>
<p>Adaptive sampling 保留了排錯最需要的 trace（error 跟 outlier），砍的是正常 request 的重複 trace。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>不治理</th>
          <th>治理後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>成本趨勢</td>
          <td>隨 traffic 超線性成長</td>
          <td>跟 traffic 線性成長或低於線性</td>
      </tr>
      <tr>
          <td>觀測覆蓋</td>
          <td>全量（但可能是低品質的全量）</td>
          <td>分層（high-value 資料保留全量、low-value 降級）</td>
      </tr>
      <tr>
          <td>Debug 體驗</td>
          <td>所有資料都在 hot tier、查得快</td>
          <td>部分資料要從 cold archive 拉、多等幾分鐘</td>
      </tr>
      <tr>
          <td>團隊自主性</td>
          <td>無限制（cardinality 跟 log level 隨意）</td>
          <td>有 budget 跟 policy 約束</td>
      </tr>
      <tr>
          <td>治理人力</td>
          <td>零（直到帳單爆炸才開始）</td>
          <td>需要 platform team 持續維護 attribution + budget + policy</td>
      </tr>
  </tbody>
</table>
<p>治理的最大風險是「砍過頭」— 在事故期間發現 debug log 被移到 cold archive 查不到、或 trace 被 sample 掉找不到問題 request。Adaptive sampling 跟 error retention 100% 是安全網，但安全網的設計本身需要定期 review（例如 error 的定義是否涵蓋了所有異常模式）。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a>：per-team cost visibility 是治理的起點。</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理</a>：cardinality budget 跟 label review 的操作流程。</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a>：log tiering 跟 adaptive sampling 是 pipeline 的 routing 跟 processing 層配置。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>觀測帳單每季成長 &gt; 20%，但服務的 request volume 成長遠小於此 — cardinality 或 log volume 可能在失控成長</li>
<li>管理層問「監控花多少錢、誰在用」但沒人能回答</li>
<li>曾經做過「全域降 retention」或「全域降 sampling」的成本優化，但幾個月後成本回升</li>
<li>Platform team 花大量時間處理「Prometheus OOM」或「Elasticsearch disk full」而非改善觀測品質</li>
<li>團隊的 debug log level 在 production 預設開著，理由是「不知道什麼時候需要」</li>
</ul>
]]></content:encoded></item><item><title>1.14 Production Slow Log Closed Loop</title><link>https://tarrragon.github.io/blog/backend/01-database/production-slow-log-loop/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/production-slow-log-loop/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式&lt;/a> 列出了 query 反模式清單跟每請求預算、但沒覆蓋一件事：&lt;strong>production slow log 怎麼從「事故時才看」變成「定期審視能 catch 反模式」&lt;/strong>。本章把 slow log 包成 closed loop — 採集、分析、PR review 整合、regression 偵測四個動作串起來、讓反模式在進 production 之前就被攔下。&lt;/p>
&lt;h2 id="slow-log-的兩種讀法">Slow log 的兩種讀法&lt;/h2>
&lt;p>多數團隊把 slow log 當「事故診斷工具」— 服務變慢時去翻一下、找出當下的罪魁禍首。這條讀法在事故時有效、但有 systemic 缺陷：所有 catch 到的反模式都已經影響使用者一段時間。&lt;/p>
&lt;p>另一條讀法是把 slow log 當「定期審視訊號」— 每週 / 每 release cycle 抓 slow log top-N、看哪些 query 模式持續存在、哪些是新出現的。這條讀法的關鍵在於「對比基線」、不是「找絕對閾值」。&lt;/p>
&lt;p>兩種讀法的對比決定了 closed loop 的設計方向：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>事故診斷工具&lt;/th>
 &lt;th>定期審視訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>觸發時機&lt;/td>
 &lt;td>服務變慢時被動翻&lt;/td>
 &lt;td>排程定期掃&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>比較對象&lt;/td>
 &lt;td>跟絕對閾值比（query &amp;gt; 1 秒）&lt;/td>
 &lt;td>跟上週 / 上次 release 的 slow log 分布比&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>處理路徑&lt;/td>
 &lt;td>找出 root cause → 立即修&lt;/td>
 &lt;td>收進 PR backlog → 排序 → 規律修&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>介入點&lt;/td>
 &lt;td>事故發生後&lt;/td>
 &lt;td>反模式被引入後、影響使用者前&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對應角色&lt;/td>
 &lt;td>On-call / SRE&lt;/td>
 &lt;td>整個團隊（每週輪流 review）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>定期審視這條讀法是本章的核心、後續四個動作都環繞它建立。&lt;/p>
&lt;h2 id="loop-第一步採集">Loop 第一步：採集&lt;/h2>
&lt;p>Slow log 採集的設計關鍵是「採集標準要穩定、retention 要夠長」。常見的採集配置選擇：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Threshold 設定&lt;/strong>：MySQL &lt;code>long_query_time&lt;/code>、PostgreSQL &lt;code>log_min_duration_statement&lt;/code> 設多久才記？常見 default 1 秒太寬鬆、會漏掉「200ms-1s」這層慢但累積成大量壓力的 query。建議 100ms 或更低（依 application 需求）。&lt;/li>
&lt;li>&lt;strong>採集對象&lt;/strong>：純 SELECT 慢？還是含 INSERT/UPDATE/DELETE？寫路徑慢通常代表 lock contention 或 transaction 範圍問題、跟讀路徑反模式不同、要分開分析。&lt;/li>
&lt;li>&lt;strong>Retention&lt;/strong>：log 保留多久？至少 30 天（覆蓋一個 sprint）、有資源的話 90 天（覆蓋季度 regression 對比）。雲端 managed DB（RDS / Aurora）的 slow log 通常自動匯出到 CloudWatch / S3、設定 retention policy 而不是依賴 DB instance 本身的 log。&lt;/li>
&lt;li>&lt;strong>Sample rate&lt;/strong>：高流量服務全採會把 disk I/O 拖垮。Production 環境用 sampling（如 10% 取樣）平衡採集完整度跟系統壓力。&lt;/li>
&lt;/ul>
&lt;p>採集出來的 raw log 不適合直接讀、要先 normalize。&lt;/p>
&lt;h2 id="loop-第二步normalize-與聚合">Loop 第二步：Normalize 與聚合&lt;/h2>
&lt;p>Raw slow log 每筆都帶具體參數（&lt;code>WHERE user_id = 12345&lt;/code>、&lt;code>WHERE user_id = 67890&lt;/code>），直接看會看到上千筆「不同 query」。實際上多數是同一個 query template 的不同參數實例。&lt;/p>
&lt;p>Normalize 動作把參數抽掉、留 query shape：&lt;/p>
&lt;ul>
&lt;li>&lt;code>WHERE user_id = 12345&lt;/code> → &lt;code>WHERE user_id = ?&lt;/code>&lt;/li>
&lt;li>&lt;code>IN (1, 2, 3, 4, 5)&lt;/code> → &lt;code>IN (?)&lt;/code>&lt;/li>
&lt;li>字串常數同樣抽掉&lt;/li>
&lt;/ul>
&lt;p>工具上：MySQL 用 &lt;code>pt-query-digest&lt;/code>（Percona Toolkit）；PostgreSQL 用 &lt;code>pg_stat_statements&lt;/code> extension（已內建 normalize）；雲端用 vendor 工具（AWS Performance Insights、GCP Query Insights、Azure SQL Insights）。Normalize 後可以按 query shape 聚合、看哪些 shape 累計時間最長、出現次數最多、平均延遲最高。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a> 列出了 query 反模式清單跟每請求預算、但沒覆蓋一件事：<strong>production slow log 怎麼從「事故時才看」變成「定期審視能 catch 反模式」</strong>。本章把 slow log 包成 closed loop — 採集、分析、PR review 整合、regression 偵測四個動作串起來、讓反模式在進 production 之前就被攔下。</p>
<h2 id="slow-log-的兩種讀法">Slow log 的兩種讀法</h2>
<p>多數團隊把 slow log 當「事故診斷工具」— 服務變慢時去翻一下、找出當下的罪魁禍首。這條讀法在事故時有效、但有 systemic 缺陷：所有 catch 到的反模式都已經影響使用者一段時間。</p>
<p>另一條讀法是把 slow log 當「定期審視訊號」— 每週 / 每 release cycle 抓 slow log top-N、看哪些 query 模式持續存在、哪些是新出現的。這條讀法的關鍵在於「對比基線」、不是「找絕對閾值」。</p>
<p>兩種讀法的對比決定了 closed loop 的設計方向：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>事故診斷工具</th>
          <th>定期審視訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>觸發時機</td>
          <td>服務變慢時被動翻</td>
          <td>排程定期掃</td>
      </tr>
      <tr>
          <td>比較對象</td>
          <td>跟絕對閾值比（query &gt; 1 秒）</td>
          <td>跟上週 / 上次 release 的 slow log 分布比</td>
      </tr>
      <tr>
          <td>處理路徑</td>
          <td>找出 root cause → 立即修</td>
          <td>收進 PR backlog → 排序 → 規律修</td>
      </tr>
      <tr>
          <td>介入點</td>
          <td>事故發生後</td>
          <td>反模式被引入後、影響使用者前</td>
      </tr>
      <tr>
          <td>對應角色</td>
          <td>On-call / SRE</td>
          <td>整個團隊（每週輪流 review）</td>
      </tr>
  </tbody>
</table>
<p>定期審視這條讀法是本章的核心、後續四個動作都環繞它建立。</p>
<h2 id="loop-第一步採集">Loop 第一步：採集</h2>
<p>Slow log 採集的設計關鍵是「採集標準要穩定、retention 要夠長」。常見的採集配置選擇：</p>
<ul>
<li><strong>Threshold 設定</strong>：MySQL <code>long_query_time</code>、PostgreSQL <code>log_min_duration_statement</code> 設多久才記？常見 default 1 秒太寬鬆、會漏掉「200ms-1s」這層慢但累積成大量壓力的 query。建議 100ms 或更低（依 application 需求）。</li>
<li><strong>採集對象</strong>：純 SELECT 慢？還是含 INSERT/UPDATE/DELETE？寫路徑慢通常代表 lock contention 或 transaction 範圍問題、跟讀路徑反模式不同、要分開分析。</li>
<li><strong>Retention</strong>：log 保留多久？至少 30 天（覆蓋一個 sprint）、有資源的話 90 天（覆蓋季度 regression 對比）。雲端 managed DB（RDS / Aurora）的 slow log 通常自動匯出到 CloudWatch / S3、設定 retention policy 而不是依賴 DB instance 本身的 log。</li>
<li><strong>Sample rate</strong>：高流量服務全採會把 disk I/O 拖垮。Production 環境用 sampling（如 10% 取樣）平衡採集完整度跟系統壓力。</li>
</ul>
<p>採集出來的 raw log 不適合直接讀、要先 normalize。</p>
<h2 id="loop-第二步normalize-與聚合">Loop 第二步：Normalize 與聚合</h2>
<p>Raw slow log 每筆都帶具體參數（<code>WHERE user_id = 12345</code>、<code>WHERE user_id = 67890</code>），直接看會看到上千筆「不同 query」。實際上多數是同一個 query template 的不同參數實例。</p>
<p>Normalize 動作把參數抽掉、留 query shape：</p>
<ul>
<li><code>WHERE user_id = 12345</code> → <code>WHERE user_id = ?</code></li>
<li><code>IN (1, 2, 3, 4, 5)</code> → <code>IN (?)</code></li>
<li>字串常數同樣抽掉</li>
</ul>
<p>工具上：MySQL 用 <code>pt-query-digest</code>（Percona Toolkit）；PostgreSQL 用 <code>pg_stat_statements</code> extension（已內建 normalize）；雲端用 vendor 工具（AWS Performance Insights、GCP Query Insights、Azure SQL Insights）。Normalize 後可以按 query shape 聚合、看哪些 shape 累計時間最長、出現次數最多、平均延遲最高。</p>
<p>聚合後產出三條訊號：</p>
<ol>
<li><strong>Top-N by total time</strong>：累計時間最長的 query — 改一條就能省最多 DB 壓力</li>
<li><strong>Top-N by count</strong>：出現次數最多的 query — 改一條就能降最多 connection 占用</li>
<li><strong>Top-N by avg latency</strong>：平均延遲最高的 query — 個別 request 體驗最差的</li>
</ol>
<p>三條訊號可能指向不同 query、各自值得 attention。</p>
<h2 id="loop-第三步pr-review-整合">Loop 第三步：PR review 整合</h2>
<p>把 slow log 的 top-N 帶回 PR review 是 closed loop 的關鍵。常見三種整合機制：</p>
<ul>
<li><strong>每週 slow log review 會議</strong>：固定時段（每週 30 分鐘）、團隊輪流 owner、把 top-10 過一輪、決定每筆是修 / 留 / 標 acceptable。產出進 backlog、不是當場修。</li>
<li><strong>PR-level query budget check</strong>：CI 加 middleware 統計每個 endpoint 的 query 數（per <a href="/blog/backend/01-database/query-anti-patterns/#%e6%af%8f%e8%ab%8b%e6%b1%82%e7%9a%84-query-%e9%a0%90%e7%ae%97" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 預算</a>）、超過閾值的 PR 在 review 時觸發討論。這層比 slow log 早、catch 的是「新引入」反模式。</li>
<li><strong>Production regression alert</strong>：當某個 query shape 的 P99 latency 比上週 baseline 偏高 50%+、自動發 alert 給該服務 owner。這層 catch 的是「漸進惡化」反模式（如資料量增加、index 失效）。</li>
</ul>
<p>三層機制按介入點分層：PR check 是「進 production 前」、weekly review 是「進 production 後的固定盤點」、regression alert 是「漸進惡化的訊號偵測」。三層覆蓋率最高、單跑任一層都會漏。</p>
<h2 id="loop-第四步regression-偵測">Loop 第四步：Regression 偵測</h2>
<p>Slow log 的對比基線需要主動維護。沒有基線、定期審視會退化成「每次都看到同樣的 top-10、習以為常」。建立基線的常見做法：</p>
<ul>
<li><strong>每 release 凍結 baseline</strong>：上線新版本前抓一份 slow log snapshot、release 後跟它比。新增的 query shape 跟惡化的 query shape 都會浮出來。</li>
<li><strong>資料量分位點 marker</strong>：在 schema 加註「這張表預期 1M / 10M / 100M 行的 query 計畫」、實際成長到對應規模時驗證 plan 是否還對。Index 失效常常是「資料量過某個門檻、optimizer 改用 full scan」造成的。</li>
<li><strong>跨 release 趨勢圖</strong>：把 slow log top-10 的累計時間做時序圖、看一年的趨勢。穩定升高代表反模式 / 資料成長壓力、突然升高代表新引入問題。</li>
</ul>
<p>Regression 偵測的 false-positive 風險是「業務本身在變、流量本身在長」、不是反模式造成的。用「query shape 佔比」而非「絕對延遲」當訊號可以降低 false positive — 某個 query shape 從佔 5% 變成佔 30%，不論絕對延遲是否升高、都值得審視。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slow log top-10 一直是同一批 query</td>
          <td>Closed loop 沒形成、review 退化成擺設</td>
          <td>啟動 PR-level query budget check 或 weekly review</td>
      </tr>
      <tr>
          <td>某個 query shape 突然從 top-100 升到 top-10</td>
          <td>新版本引入反模式 / 流量結構變化</td>
          <td>對照最近 release diff、找出引入時點</td>
      </tr>
      <tr>
          <td>Top-N 累計時間穩定升高、但 query shape 沒變</td>
          <td>資料量增加、index 退化或 query 計畫漂移</td>
          <td>EXPLAIN 對比、檢查是否該加 covering index 或 partition</td>
      </tr>
      <tr>
          <td>Slow log 異常稀少（&lt; 預期）</td>
          <td>Threshold 設太寬、或採集 sample rate 太低</td>
          <td>降 threshold、提高 sample rate</td>
      </tr>
      <tr>
          <td>同一個 endpoint 在 PR check 過、production 卻爆</td>
          <td>PR 環境資料量太小、CI 無法 catch 大資料量退化</td>
          <td>加 production-like load test、或在 CI 用 anonymized prod data</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 slow log 當「事故工具」、不做定期審視。事故時的 slow log 是 lagging indicator — 反模式已經影響使用者一段時間才被看見。定期審視是把它變成 leading indicator 的關鍵。</p>
<p>把 threshold 設太鬆（1 秒、5 秒）。多數反模式落在 100ms-1s 區間、設 1 秒會漏掉。Threshold 應該對齊「user-perceived 慢」門檻、通常 100-500ms。</p>
<p>把 top-10 當「不能動」。一些 top-10 是業務本質慢（複雜 report、bulk write）、改起來代價遠超效益。Review 時要明示標記「acceptable」、避免下週又被當未解決問題討論。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「production slow log 怎麼變成 closed loop」。當問題進入具體反模式分析（這條 query 是哪種反模式？怎麼改？）、回到 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a>；進入 EXPLAIN 解讀細節、回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 schema design</a>；進入 application-side query 數量控制機制（ORM middleware、query log 觀察），跨到 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 模組。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 案例庫中、slow log closed loop 直接示範的案例稀少（多數案例談規模 / vendor、不談 ops loop 設計）。可用以下案例反向追問：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash：Aurora Postgres 寫入瓶頸</a> — 寫入飽和被識別為 vendor 層問題、但若 production slow log loop 早期就 catch 到 transaction 範圍跟熱 row 競爭、可能延後遷移時點。對照本章可問：DoorDash 在啟動遷移前、是否有定期 slow log review 機制？</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered：合規驅動容量規劃</a> — 容量規劃以合規為驅動、但 query 預算假設若無 production 驗證、規劃出的 TPS 上限會偏低。對照本章「Regression 偵測」段：合規 cluster 是否有 query shape 趨勢圖？</li>
</ul>
<p>反向追問框架（per <a href="/blog/report/case-misalignment-reverse-inquiry/" data-link-title="案例庫不對齊章節主題時用反向追問取代強掛" data-link-desc="當案例庫主軸跟章節主題不在同一維度時、引用框架要從『正向掛入』切換到『反向追問』；強掛 case 的根因是『想填滿案例段』的模板配額、而非『想讓讀者看到證據』；反向追問把案例庫的限制當 first-class 訊息傳給讀者、case 變成『沒做 X 的後果』的反證、不是 X 的示範；reviewer 第一輪 fact-check 就能抓出強掛、修正成本高；判讀徵兆是引用句寫不出 case 具體段落 / 多個 case 句型雷同 / 章節主題跟 case 庫主軸不同維度">#146</a>）：案例本身不直接示範 closed loop、但用「啟動 vendor 升級前、closed loop 能不能延後撞牆」這條追問、能看出 slow log loop 的事前價值。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a> 的交接：1.13 給反模式清單、本章給「定期 catch 它們」的機制。</li>
<li>與 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a> 的交接：slow log 採集跟聚合是 observability 的子問題、跨服務的 query trace 需要 04 的 telemetry pipeline。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位</a> 的交接：9.5 用 USE / RED method 定位、本章用 slow log 在 DB 層做更精細的 query-level 定位。</li>
<li>與 <a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">06 reliability ci-pipeline</a> 的交接：PR-level query budget check 是 CI 環節、屬 06 模組的 release gate 設計。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看具體反模式怎麼修、回 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a>。要把 query 觀測接進完整 telemetry pipeline、進 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 observability</a>。要看 PR-level check 怎麼接 release gate、進 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>。</p>
]]></content:encoded></item><item><title>4.15 Cost Attribution / Chargeback</title><link>https://tarrragon.github.io/blog/backend/04-observability/cost-attribution/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cost-attribution/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>為何需要 attribution：共享平台模式下成本無人擁有&lt;/li>
&lt;li>拆分維度：team / service / environment / tenant / cost driver&lt;/li>
&lt;li>拆分的訊號來源：metric label / log tag / span attribute&lt;/li>
&lt;li>Showback vs chargeback&lt;/li>
&lt;li>Attribution dashboard 設計&lt;/li>
&lt;li>Vendor 帳單拆分能力&lt;/li>
&lt;li>反模式&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Cost attribution 是把 observability 成本拆回團隊、服務、環境與成本來源的治理能力，責任是讓使用訊號的人也看見訊號成本。&lt;/p>
&lt;p>Observability 平台（自架或託管）的成本來自三個層面：ingestion（收了多少資料）、storage / retention（保留了多久）、query（查了多少次跟多大範圍）。沒有 attribution 時，這三層的成本由平台團隊背，產品團隊把 observability 當免費資源 — 新增 metric label、延長 retention、加 dashboard panel 都沒有成本意識。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality&lt;/a> 的分工：4.7 是技術治理工具（控制 cardinality、sampling、retention 階梯），4.15 是組織治理工具（讓成本對應到 owner、驅動 owner 採取行動）。&lt;/p>
&lt;h2 id="拆分維度">拆分維度&lt;/h2>
&lt;h3 id="按-service--team">按 service / team&lt;/h3>
&lt;p>最基本的拆分。每個服務產生的 ingestion 量（events/sec、series count、log volume）歸到服務 owner。團隊是多個服務的集合。&lt;/p>
&lt;p>實作方式：metric 跟 log 的 &lt;code>service&lt;/code> label / tag 是拆分的基礎。如果 label 穩定且全覆蓋，用 &lt;code>sum by (service)&lt;/code> 就能拆分 ingestion 成本。Label 不穩定（部分服務沒打 service tag）或 label 值漂移（service name 改名但 cost 系統沒更新）會讓拆分不準。&lt;/p>
&lt;h3 id="按-environment">按 environment&lt;/h3>
&lt;p>Production / staging / dev 環境的成本各自歸因。常見發現是 staging 環境的 observability 成本跟 production 相當 — staging 開了跟 production 一樣的 retention、sampling 率、dashboard，但 staging 的觀測需求遠低於 production。&lt;/p>
&lt;p>可操作的做法：staging 跟 dev 環境用更短的 retention（7 天 vs production 的 30 天）、更高的 sampling 比例、關閉不需要的 dashboard。把 environment 的成本差異展示在 attribution dashboard 上，讓團隊自行判斷 staging 的 observability 是否過度。&lt;/p>
&lt;h3 id="按-cost-driver-type">按 cost driver type&lt;/h3>
&lt;p>Ingestion / storage / query 三層的成本增長模式不同、控制手段也不同。&lt;/p>
&lt;p>&lt;strong>Ingestion 成本&lt;/strong>：跟 events/sec 跟 series count 成正比。控制手段是 sampling、cardinality 限制、低價值訊號過濾。歸因到產生訊號的服務。&lt;/p>
&lt;p>&lt;strong>Storage / retention 成本&lt;/strong>：跟資料量 × 保留期成正比。控制手段是 retention 階梯（&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7&lt;/a>）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a>。歸因到資料保留政策的 owner。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>為何需要 attribution：共享平台模式下成本無人擁有</li>
<li>拆分維度：team / service / environment / tenant / cost driver</li>
<li>拆分的訊號來源：metric label / log tag / span attribute</li>
<li>Showback vs chargeback</li>
<li>Attribution dashboard 設計</li>
<li>Vendor 帳單拆分能力</li>
<li>反模式</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>Cost attribution 是把 observability 成本拆回團隊、服務、環境與成本來源的治理能力，責任是讓使用訊號的人也看見訊號成本。</p>
<p>Observability 平台（自架或託管）的成本來自三個層面：ingestion（收了多少資料）、storage / retention（保留了多久）、query（查了多少次跟多大範圍）。沒有 attribution 時，這三層的成本由平台團隊背，產品團隊把 observability 當免費資源 — 新增 metric label、延長 retention、加 dashboard panel 都沒有成本意識。</p>
<p>跟 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a> 的分工：4.7 是技術治理工具（控制 cardinality、sampling、retention 階梯），4.15 是組織治理工具（讓成本對應到 owner、驅動 owner 採取行動）。</p>
<h2 id="拆分維度">拆分維度</h2>
<h3 id="按-service--team">按 service / team</h3>
<p>最基本的拆分。每個服務產生的 ingestion 量（events/sec、series count、log volume）歸到服務 owner。團隊是多個服務的集合。</p>
<p>實作方式：metric 跟 log 的 <code>service</code> label / tag 是拆分的基礎。如果 label 穩定且全覆蓋，用 <code>sum by (service)</code> 就能拆分 ingestion 成本。Label 不穩定（部分服務沒打 service tag）或 label 值漂移（service name 改名但 cost 系統沒更新）會讓拆分不準。</p>
<h3 id="按-environment">按 environment</h3>
<p>Production / staging / dev 環境的成本各自歸因。常見發現是 staging 環境的 observability 成本跟 production 相當 — staging 開了跟 production 一樣的 retention、sampling 率、dashboard，但 staging 的觀測需求遠低於 production。</p>
<p>可操作的做法：staging 跟 dev 環境用更短的 retention（7 天 vs production 的 30 天）、更高的 sampling 比例、關閉不需要的 dashboard。把 environment 的成本差異展示在 attribution dashboard 上，讓團隊自行判斷 staging 的 observability 是否過度。</p>
<h3 id="按-cost-driver-type">按 cost driver type</h3>
<p>Ingestion / storage / query 三層的成本增長模式不同、控制手段也不同。</p>
<p><strong>Ingestion 成本</strong>：跟 events/sec 跟 series count 成正比。控制手段是 sampling、cardinality 限制、低價值訊號過濾。歸因到產生訊號的服務。</p>
<p><strong>Storage / retention 成本</strong>：跟資料量 × 保留期成正比。控制手段是 retention 階梯（<a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>）、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 跟 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a>。歸因到資料保留政策的 owner。</p>
<p><strong>Query 成本</strong>：跟查詢次數 × 掃描量成正比。控制手段是 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a>、query cache、query cost estimation（<a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23</a>）。歸因到 dashboard 跟 alert rule 的 owner。</p>
<p>三層分開歸因的價值是精確定位成本增長來源。「這個月成本增長 30%」→ 是 ingestion 增長（某服務開了新 metric）還是 query 增長（某人加了 heavy dashboard panel）？分層歸因讓回答這個問題只需要查一個 dashboard。</p>
<h3 id="按-tenant多租戶場景">按 tenant（多租戶場景）</h3>
<p>Multi-tenant 平台的 observability 成本跟 tenant 的活躍度有關。大 tenant 產生的事件量可能是小 tenant 的 100 倍，但如果 observability 成本平攤，小 tenant 補貼大 tenant。</p>
<p>Tenant-level attribution 需要 metric / log / trace 帶 tenant label。Label 的 cardinality 問題在 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a> 處理 — tenant label 在 metric 層通常過高 cardinality（每個 tenant 一條 series），可以改在 log 或 trace 層按 tenant 統計 ingestion 量。</p>
<h2 id="showback-vs-chargeback">Showback vs Chargeback</h2>
<p><strong>Showback</strong>：讓團隊看到自己產生的 observability 成本，但不實際扣款。透明化驅動行為改變 — 當 team A 發現自己的 log ingestion 成本是其他團隊的 5 倍時，自然會開始檢視「是不是 debug log 開太多」。</p>
<p><strong>Chargeback</strong>：把 observability 成本從團隊的預算中實際扣除。驅動力更強，但需要精確的 attribution（誤差會讓團隊不信任系統）跟組織層面的支持（財務流程、管理層買單）。</p>
<p>多數團隊的起步方式是 showback。Showback 的 attribution 精度要求比 chargeback 低 — 差 10-20% 的歸因不影響行為改變的驅動力。Chargeback 需要差 &lt; 5% 才能讓團隊接受。</p>
<h2 id="attribution-dashboard-設計">Attribution Dashboard 設計</h2>
<p>Attribution dashboard 回答三個問題：</p>
<ol>
<li><strong>誰在燒？</strong> — 按 service / team 排序的成本排行榜。前 10 個服務通常佔 70-80% 的成本。</li>
<li><strong>燒在哪一層？</strong> — 前 10 個服務的 ingestion / storage / query 成本比例。</li>
<li><strong>趨勢是什麼？</strong> — 月對月的成本趨勢、哪些服務的成本增長最快。</li>
</ol>
<p>Dashboard 的更新頻率可以低（每天或每週），因為 attribution 驅動的是策略決策而非即時操作。Panel 讀 pre-aggregated 資料（daily cost summary table），查詢成本本身很低。</p>
<p>Attribution dashboard 的 owner 是 observability platform team，但 actionable insight 的 owner 是各服務團隊。Platform team 負責維護 attribution 的精確性跟 dashboard 的正確性；服務團隊負責看自己的成本趨勢跟採取控制行動。</p>
<h2 id="vendor-帳單拆分能力">Vendor 帳單拆分能力</h2>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>帳單拆分能力</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog</td>
          <td>Usage attribution by tag（service / team / env）</td>
          <td>需要事先定義 attribution tag</td>
      </tr>
      <tr>
          <td>Honeycomb</td>
          <td>Team-based usage tracking</td>
          <td>按 dataset 拆分、不按 service</td>
      </tr>
      <tr>
          <td>Grafana Cloud</td>
          <td>Usage dashboard by data source</td>
          <td>需自建 attribution layer</td>
      </tr>
      <tr>
          <td>自架 Prometheus + Loki</td>
          <td>自建 cost model（series count × price / log volume × price）</td>
          <td>完全自定義但維護成本高</td>
      </tr>
  </tbody>
</table>
<p>自架的 attribution 精度最高（因為完全可控），但維護成本也最高。託管 vendor 通常提供 service 或 team 級的 usage attribution，但跨 ingestion / storage / query 的分層拆分需要用 vendor API 自建 dashboard。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>Cost attribution 的核心目標是讓成本對應到能採取行動的 <a href="/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">owner</a> — 成本只有總額而無歸屬時，沒有團隊有動力控制。</p>
<p>重點訊號包括：</p>
<ul>
<li>Ingestion、retention、query 是否能分開歸因</li>
<li>Team / service / environment label 是否穩定</li>
<li>Showback 是否足以改變行為，或需要 chargeback</li>
<li>高成本訊號是否能對應事故、SLO 或除錯價值</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>成本季度增長、無人能說「哪個團隊 / 服務在燒」</li>
<li>高成本服務跟高價值服務不對應、無 ROI 視角</li>
<li>平台團隊背所有預算、產品團隊把 observability 當免費資源</li>
<li>Attribution dashboard 存在但無 owner、半年沒看</li>
<li>Vendor 帳單只有總額、無服務級拆分</li>
<li>Staging 的 observability 成本跟 production 相當但無人注意</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>平台吸收所有成本</td>
          <td>產品團隊沒成本意識、ingestion 無限增長</td>
          <td>Showback 起步、讓團隊看到自己的成本</td>
      </tr>
      <tr>
          <td>Attribution 顆粒度太粗</td>
          <td>只有總額、定位成本來源要人工拆帳</td>
          <td>按 service + cost driver type 拆分</td>
      </tr>
      <tr>
          <td>Chargeback 精度不夠</td>
          <td>團隊質疑歸因結果、不信任系統</td>
          <td>先用 showback、精度穩定後再轉 chargeback</td>
      </tr>
      <tr>
          <td>Attribution label 漂移</td>
          <td>Service name 改了但 cost 系統沒更新</td>
          <td>Label 同步機制 + 定期 reconciliation</td>
      </tr>
      <tr>
          <td>成本只看帳單不看 ROI</td>
          <td>砍最貴的 metric 但那是 SLO 唯一訊號來源</td>
          <td>成本決策同時評估「砍掉後事故定位會變慢多少」</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：技術層面的成本治理工具</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a>：pipeline 各層的成本歸屬</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：platform team 跟 service team 的 cost ownership</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：query 成本的 estimation 跟治理</li>
<li><a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity / cost</a>：observability 成本作為整體容量規劃的一部分</li>
<li><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">4.C14 觀測平台成本治理</a>：從帳單驚嚇到可預測成本的綜合情境</li>
</ul>
]]></content:encoded></item><item><title>4.16 Observability Readiness Review</title><link>https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/observability-readiness-review/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>readiness review 的責任：在 production 前確認訊號能支援分級、定位、回復與復盤&lt;/li>
&lt;li>檢查面向：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>&lt;/li>
&lt;li>上線前判準：核心 user journey 是否有 SLI、錯誤是否有 correlation key、依賴是否可追蹤&lt;/li>
&lt;li>變更前判準：新依賴、新 queue、新 feature flag 是否帶出新訊號需求&lt;/li>
&lt;li>演練前判準：game day / chaos / DR drill 是否能被 04 訊號觀察&lt;/li>
&lt;li>跟 06 的交接：readiness 缺口進入 reliability readiness / release gate&lt;/li>
&lt;li>跟 08 的交接：readiness 缺口影響 severity trigger、runbook 與 decision log&lt;/li>
&lt;li>反模式：服務先上線、事故後才補 dashboard；alert 有通知但缺定位欄位；trace 需要人工對回 log&lt;/li>
&lt;/ul>
&lt;p>Observability readiness review 的價值在於把「事故時才會被問到的問題」提前成上線條件。服務進 production 前，團隊需要先確認訊號能回答三件事：哪裡出問題、影響到誰、下一步由誰處理。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Observability readiness review 是把「訊號是否足以支援操作」變成上線前檢查的流程，責任是讓服務進入 production 前已具備基本診斷能力。&lt;/p>
&lt;p>這一頁處理的是準備度。工具已存在時，仍需要確認訊號是否對應使用者旅程、依賴邊界、事故分級與復盤證據。&lt;/p>
&lt;p>readiness review 不等於打勾清單。它是一次跨角色對齊：服務團隊確認事件語意，平台團隊確認採集與查詢路徑，on-call 確認事故前 10 分鐘真的能定位。三者同時成立，才算可操作準備度。&lt;/p>
&lt;h2 id="適用情境">適用情境&lt;/h2>
&lt;p>Observability readiness review 適合放在服務生命週期的高風險節點。這些節點共同特徵是：一旦變更進入 production，第一次異常就會依賴既有訊號做判讀。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>檢查重點&lt;/th>
 &lt;th>缺口代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>新服務上線&lt;/td>
 &lt;td>核心旅程、依賴、owner 是否可觀測&lt;/td>
 &lt;td>事故初期只能靠人工猜測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重大變更&lt;/td>
 &lt;td>新 queue、新依賴、新 flag 的訊號&lt;/td>
 &lt;td>新風險進 production 後才暴露&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>架構拆分&lt;/td>
 &lt;td>trace、correlation、service name&lt;/td>
 &lt;td>事件鏈跨服務後斷裂&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>演練前&lt;/td>
 &lt;td>chaos、load、DR 行為是否可被看見&lt;/td>
 &lt;td>演練結果缺少可驗證證據&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故後&lt;/td>
 &lt;td>復盤缺口是否回寫成新訊號&lt;/td>
 &lt;td>同類事故仍以相同盲區重演&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>新服務上線時，readiness review 的責任是確認基本診斷能力已經存在。典型服務至少要能從 request、tenant、region、dependency 與錯誤分類回到同一條事件鏈，讓 on-call 能在前 10 分鐘判斷影響範圍。&lt;/p>
&lt;p>重大變更時，readiness review 的責任是確認變更帶來的新風險已有訊號。加入新的外部 API、queue、background job、feature flag 或資料同步流程，都會增加新的失效面；每個失效面都應有對應 log、metric、trace 或 alert。&lt;/p>
&lt;p>演練前，readiness review 的責任是確認驗證行為能被觀測。chaos experiment、load test 或 DR drill 需要同時產生故障與判讀證據，讓團隊能確認 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a> 與回復狀態。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 observability readiness 時，先看服務的核心旅程是否有訊號，再看事故時能否從症狀走到原因。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>核心 user journey 是否有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO&lt;/a> 與 error rate&lt;/li>
&lt;li>log 是否有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 與 tenant 欄位&lt;/li>
&lt;li>trace 是否覆蓋同步、async、queue 與 background job 邊界&lt;/li>
&lt;li>dashboard 是否能支援 on-call 的前 10 分鐘判讀&lt;/li>
&lt;li>alert 是否能連到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與 owner&lt;/li>
&lt;/ul>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>檢查面向&lt;/th>
 &lt;th>最小可用判準&lt;/th>
 &lt;th>常見失真&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>事件關聯&lt;/td>
 &lt;td>request / trace / tenant 可串成同一條事件鏈&lt;/td>
 &lt;td>欄位命名不一致、跨服務拼接失敗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務健康&lt;/td>
 &lt;td>SLI 與 error rate 能反映核心旅程&lt;/td>
 &lt;td>指標只反映系統資源、不反映用戶結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>路徑可視&lt;/td>
 &lt;td>trace 能覆蓋 sync + async + queue&lt;/td>
 &lt;td>background job 與 queue 邊界斷鏈&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>操作入口&lt;/td>
 &lt;td>dashboard / alert 能支撐前 10 分鐘&lt;/td>
 &lt;td>告警有通知、沒有定位與下一步&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="review-流程">Review 流程&lt;/h2>
&lt;p>Readiness review 的流程是從使用者旅程走向操作路由。先從服務承諾的體驗開始，再反推工具與訊號清單，才能讓監控資產對應事故時的實際判讀。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>readiness review 的責任：在 production 前確認訊號能支援分級、定位、回復與復盤</li>
<li>檢查面向：<a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a>、<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a>、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a></li>
<li>上線前判準：核心 user journey 是否有 SLI、錯誤是否有 correlation key、依賴是否可追蹤</li>
<li>變更前判準：新依賴、新 queue、新 feature flag 是否帶出新訊號需求</li>
<li>演練前判準：game day / chaos / DR drill 是否能被 04 訊號觀察</li>
<li>跟 06 的交接：readiness 缺口進入 reliability readiness / release gate</li>
<li>跟 08 的交接：readiness 缺口影響 severity trigger、runbook 與 decision log</li>
<li>反模式：服務先上線、事故後才補 dashboard；alert 有通知但缺定位欄位；trace 需要人工對回 log</li>
</ul>
<p>Observability readiness review 的價值在於把「事故時才會被問到的問題」提前成上線條件。服務進 production 前，團隊需要先確認訊號能回答三件事：哪裡出問題、影響到誰、下一步由誰處理。</p>
<h2 id="概念定位">概念定位</h2>
<p>Observability readiness review 是把「訊號是否足以支援操作」變成上線前檢查的流程，責任是讓服務進入 production 前已具備基本診斷能力。</p>
<p>這一頁處理的是準備度。工具已存在時，仍需要確認訊號是否對應使用者旅程、依賴邊界、事故分級與復盤證據。</p>
<p>readiness review 不等於打勾清單。它是一次跨角色對齊：服務團隊確認事件語意，平台團隊確認採集與查詢路徑，on-call 確認事故前 10 分鐘真的能定位。三者同時成立，才算可操作準備度。</p>
<h2 id="適用情境">適用情境</h2>
<p>Observability readiness review 適合放在服務生命週期的高風險節點。這些節點共同特徵是：一旦變更進入 production，第一次異常就會依賴既有訊號做判讀。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>檢查重點</th>
          <th>缺口代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新服務上線</td>
          <td>核心旅程、依賴、owner 是否可觀測</td>
          <td>事故初期只能靠人工猜測</td>
      </tr>
      <tr>
          <td>重大變更</td>
          <td>新 queue、新依賴、新 flag 的訊號</td>
          <td>新風險進 production 後才暴露</td>
      </tr>
      <tr>
          <td>架構拆分</td>
          <td>trace、correlation、service name</td>
          <td>事件鏈跨服務後斷裂</td>
      </tr>
      <tr>
          <td>演練前</td>
          <td>chaos、load、DR 行為是否可被看見</td>
          <td>演練結果缺少可驗證證據</td>
      </tr>
      <tr>
          <td>事故後</td>
          <td>復盤缺口是否回寫成新訊號</td>
          <td>同類事故仍以相同盲區重演</td>
      </tr>
  </tbody>
</table>
<p>新服務上線時，readiness review 的責任是確認基本診斷能力已經存在。典型服務至少要能從 request、tenant、region、dependency 與錯誤分類回到同一條事件鏈，讓 on-call 能在前 10 分鐘判斷影響範圍。</p>
<p>重大變更時，readiness review 的責任是確認變更帶來的新風險已有訊號。加入新的外部 API、queue、background job、feature flag 或資料同步流程，都會增加新的失效面；每個失效面都應有對應 log、metric、trace 或 alert。</p>
<p>演練前，readiness review 的責任是確認驗證行為能被觀測。chaos experiment、load test 或 DR drill 需要同時產生故障與判讀證據，讓團隊能確認 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a> 與回復狀態。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 observability readiness 時，先看服務的核心旅程是否有訊號，再看事故時能否從症狀走到原因。</p>
<p>重點訊號包括：</p>
<ul>
<li>核心 user journey 是否有 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI/SLO</a> 與 error rate</li>
<li>log 是否有 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 與 tenant 欄位</li>
<li>trace 是否覆蓋同步、async、queue 與 background job 邊界</li>
<li>dashboard 是否能支援 on-call 的前 10 分鐘判讀</li>
<li>alert 是否能連到 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與 owner</li>
</ul>
<table>
  <thead>
      <tr>
          <th>檢查面向</th>
          <th>最小可用判準</th>
          <th>常見失真</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事件關聯</td>
          <td>request / trace / tenant 可串成同一條事件鏈</td>
          <td>欄位命名不一致、跨服務拼接失敗</td>
      </tr>
      <tr>
          <td>服務健康</td>
          <td>SLI 與 error rate 能反映核心旅程</td>
          <td>指標只反映系統資源、不反映用戶結果</td>
      </tr>
      <tr>
          <td>路徑可視</td>
          <td>trace 能覆蓋 sync + async + queue</td>
          <td>background job 與 queue 邊界斷鏈</td>
      </tr>
      <tr>
          <td>操作入口</td>
          <td>dashboard / alert 能支撐前 10 分鐘</td>
          <td>告警有通知、沒有定位與下一步</td>
      </tr>
  </tbody>
</table>
<h2 id="review-流程">Review 流程</h2>
<p>Readiness review 的流程是從使用者旅程走向操作路由。先從服務承諾的體驗開始，再反推工具與訊號清單，才能讓監控資產對應事故時的實際判讀。</p>
<ol>
<li>定義核心旅程與失敗後果。</li>
<li>對每個旅程列出依賴、async workflow 與資料寫入點。</li>
<li>為每個失效點指定 log、metric、trace 或 dashboard。</li>
<li>驗證 alert 是否連到 owner、runbook 與下一步動作。</li>
<li>標記尚未補齊的訊號缺口，決定是否阻擋上線或納入 follow-up。</li>
</ol>
<p>核心旅程是 readiness review 的錨點。購物服務的核心旅程可能是 checkout、payment、order confirmation；內容平台可能是 upload、publish、read path；B2B API 可能是 authentication、request processing、webhook delivery。訊號需要優先對到這些旅程，再補 CPU、memory 與 pod restart 等資源層訊號。</p>
<p>依賴圖是 readiness review 的第二層。每個資料庫、cache、broker、third-party API、object storage 與 internal service 都應能被定位為 upstream 或 downstream，並且在 trace、metric 或 log 中留下可查詢欄位。</p>
<p>操作路由是 readiness review 的交付物。當 alert 觸發時，on-call 需要知道先看哪個 dashboard、用哪個 query、找哪個 owner、用哪個 runbook、何時升級到 incident commander。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>服務上線 checklist 有監控項目，但沒有事故判讀欄位</li>
<li>新依賴上線後，dashboard 看不到 upstream / downstream 影響</li>
<li>alert 觸發後仍需要人工 grep 多個系統拼事件鏈</li>
<li>chaos 或 DR 演練產生故障，但 04 訊號沒有反映出預期現象</li>
<li>事故復盤 action item 反覆要求「補監控」</li>
</ul>
<p>在真實服務中，最常見的 readiness 缺口是工具已存在，但工具沒有對到決策。例如 alert 可以 page on-call，但查詢第一步就要跨三個系統手動對帳，代表 readiness 還停在可見層，尚未進入可操作層。</p>
<h2 id="控制面">控制面</h2>
<p>Readiness review 的控制面是把檢查結果轉成可執行決策。每個缺口都要被分類為阻擋、降級接受或後續改善，並且留下 owner 與期限。</p>
<table>
  <thead>
      <tr>
          <th>缺口類型</th>
          <th>判斷方式</th>
          <th>處理路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>阻擋</td>
          <td>影響核心旅程、事故時無替代判讀</td>
          <td>暫停上線，補 04 訊號或 06 readiness</td>
      </tr>
      <tr>
          <td>降級接受</td>
          <td>風險可被 runbook 或人工查證承接</td>
          <td>標記限制，接到 08 intake 與 decision log</td>
      </tr>
      <tr>
          <td>後續改善</td>
          <td>不影響首輪定位，但影響長期治理</td>
          <td>進入 04.8 signal governance loop</td>
      </tr>
      <tr>
          <td>淘汰整理</td>
          <td>舊 dashboard 或 alert 干擾判讀</td>
          <td>進入 4.18 operating model</td>
      </tr>
  </tbody>
</table>
<p>阻擋條件應該以「事故時是否能決策」為核心。核心旅程 SLI、request correlation、upstream / downstream 分辨能力與 alert owner 都是第一次事故能否被接住的基本條件。</p>
<p>降級接受需要明確寫出限制。若某個低流量背景任務暫時缺 trace，但有 log query、DLQ dashboard 與人工 replay 流程可以承接，團隊可以接受短期限制；限制需要進入 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a>，避免事中被誤讀為完整訊號。</p>
<p>後續改善適合處理長期品質問題。dashboard 可用但查詢成本過高、alert 可行但 noise 偏高、欄位命名需要統一，這些缺口適合進入 signal governance，讓上線決策與長期治理分流。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Observability readiness 的反模式通常來自把「有監控」誤當成「可操作」。監控存在只是起點，能支援判讀、路由與回復才是 readiness。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事後補 dashboard</td>
          <td>事故發生後才知道缺哪些面板</td>
          <td>把核心旅程面板列為上線條件</td>
      </tr>
      <tr>
          <td>告警只有通知</td>
          <td>on-call 收到 page 後仍需重新找證據</td>
          <td>alert 必須帶 owner 與 runbook</td>
      </tr>
      <tr>
          <td>trace 需要人工拼 log</td>
          <td>跨服務路徑靠 request id 手動對回</td>
          <td>統一 trace context 與 log 欄位</td>
      </tr>
      <tr>
          <td>readiness 只看平台工具</td>
          <td>平台 green，但服務旅程不可判讀</td>
          <td>從 user journey 反推訊號需求</td>
      </tr>
      <tr>
          <td>checklist 無阻擋條件</td>
          <td>每次都勾選通過，但缺口持續存在</td>
          <td>定義 block / accept / follow-up</td>
      </tr>
  </tbody>
</table>
<p>事後補 dashboard 的風險是把第一次事故變成探索行為。事故期間的主要工作應是止血與決策；如果團隊還在建立第一個查詢、猜欄位語意、找 owner，代表 readiness 沒有完成。</p>
<p>告警只有通知會把壓力丟給 on-call。有效 alert 應該同時提供症狀、範圍、第一個查詢入口與下一步路由，讓值班者能直接進入判讀流程。</p>
<h2 id="與-06-和-08-的關係">與 06 和 08 的關係</h2>
<p>Observability readiness 是可靠性驗證與事故處理的輸入層。06 需要用它判斷驗證前提是否成立，08 需要用它判斷事故 evidence 是否足以啟動流程。</p>
<p>在 06 中，readiness 缺口會影響 load test、chaos、DR drill 與 release gate。驗證行為需要可觀測訊號支撐，測試結果才足以證明系統維持在可接受狀態內。</p>
<p>在 08 中，readiness 缺口會影響 severity trigger、incident intake 與 decision log。若 evidence 不完整，事故指揮需要先標記資料限制，再決定是否升級、降級或等待更多證據。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.1 log schema：補事件關聯欄位</li>
<li>04.2 metrics：補服務健康與容量指標</li>
<li>04.3 tracing：補跨服務與 async context</li>
<li>04.4 dashboard / alert：補操作入口與通知條件</li>
<li><a href="/blog/backend/04-observability/attacker-view-observability-risks/" data-link-title="4.5 可觀測性威脅建模（Threat Modeling）" data-link-desc="從觀測盲區、告警失真與資料暴露風險，盤點 observability 的主要弱點">4.5 威脅建模</a>：觀測盲區跟資料暴露的上線前檢查</li>
<li>06.19 reliability readiness：把觀測準備度納入上線前門檻</li>
<li>08.18 incident intake：把訊號接進事故 intake 與 evidence triage</li>
</ul>
]]></content:encoded></item><item><title>4.17 Telemetry Data Quality</title><link>https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>telemetry data quality 的責任：確認觀測資料本身可信&lt;/li>
&lt;li>缺漏類型：missing signal、partial trace、dropped log、stale metric&lt;/li>
&lt;li>漂移類型：schema drift、label drift、service name drift、semantic convention drift&lt;/li>
&lt;li>偏誤類型：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> bias、low-traffic bias、high-cardinality truncation&lt;/li>
&lt;li>時間類型：clock skew、ingest delay、out-of-order event、timezone mismatch&lt;/li>
&lt;li>品質指標：completeness、freshness、consistency、accuracy、coverage&lt;/li>
&lt;li>跟 4.11 telemetry pipeline 的分工：pipeline 看路徑，data quality 看資料可信度&lt;/li>
&lt;li>反模式：dashboard 看起來正常但資料少一半；trace sample 漏掉錯誤；timestamp 導致 timeline 錯序&lt;/li>
&lt;/ul>
&lt;p>Telemetry data quality 的核心是把「觀測資料失真」當成一級事件。服務事故判讀建立在觀測資料上，資料品質不穩時，團隊會把資料缺口誤讀成系統行為，進而做出錯誤分級、錯誤回復或錯誤 SLO 判斷。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Telemetry data quality 是把觀測資料當成資料產品治理的能力，責任是讓 log、metric、trace 與 alert 的判讀建立在可信資料上。&lt;/p>
&lt;p>這一頁處理的是資料可信度。訊號存在不等於訊號可信；缺漏、漂移、偏誤與時間錯位都會讓事故判讀走向錯誤路徑。&lt;/p>
&lt;p>資料品質治理最有效的做法是把品質指標產品化：讓 completeness、freshness、drift、sampling coverage 也進 dashboard 與告警，讓團隊在事故前就能看見資料限制。&lt;/p>
&lt;h2 id="品質模型">品質模型&lt;/h2>
&lt;p>Telemetry data quality 的品質模型由五個面向組成。這五個面向分別回答資料是否存在、是否及時、是否一致、是否代表真實流量，以及是否足以覆蓋關鍵旅程。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>品質面向&lt;/th>
 &lt;th>核心問題&lt;/th>
 &lt;th>常見資料&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Completeness&lt;/td>
 &lt;td>該出現的訊號是否完整出現&lt;/td>
 &lt;td>drop rate、coverage、gap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Freshness&lt;/td>
 &lt;td>訊號是否足夠接近事件發生時間&lt;/td>
 &lt;td>ingest delay、stale metric&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consistency&lt;/td>
 &lt;td>欄位、命名與語意是否跨服務一致&lt;/td>
 &lt;td>schema drift、label drift&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Accuracy&lt;/td>
 &lt;td>數值與事件語意是否反映真實狀態&lt;/td>
 &lt;td>duplicate event、wrong unit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Coverage&lt;/td>
 &lt;td>高風險旅程與低流量邊界是否被涵蓋&lt;/td>
 &lt;td>sampling policy、trace ratio&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Completeness 是事故判讀的基礎。log、metric 或 trace 的缺口如果沒有被標示，dashboard 會呈現一條看似平順的線，實際上可能只是 ingestion pipeline 丟了資料。&lt;/p>
&lt;p>Freshness 決定資料能否支援事中決策。告警延遲、metric scrape delay、trace export queue backlog 與 log indexing lag 都會讓 incident commander 用過期資料判斷是否擴大或回復。&lt;/p>
&lt;p>Consistency 決定資料能否跨服務拼接。service name、region、tenant、environment、error class 與 semantic convention 若在不同系統漂移，單一服務看起來正常，跨服務事件鏈卻會斷裂。&lt;/p>
&lt;p>Accuracy 決定資料能否代表真實狀態。常見問題包含錯誤單位、重複計數、counter reset 誤判、histogram bucket 設錯與 status code mapping 錯誤。&lt;/p>
&lt;p>Coverage 決定資料能否覆蓋高風險邊界。低流量服務、VIP tenant、錯誤樣本、長尾 latency 與 rare dependency failure 常被 sampling 或聚合策略稀釋。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 telemetry data quality 時，先看資料是否完整與新鮮，再看不同訊號之間是否能互相對齊。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>log / metric / trace 是否有 coverage 與 drop rate&lt;/li>
&lt;li>schema 是否有版本與 drift 偵測&lt;/li>
&lt;li>sampling 是否保留錯誤、高延遲與低流量樣本&lt;/li>
&lt;li>timestamp 是否能支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 還原&lt;/li>
&lt;li>dashboard 是否標示資料延遲、缺口與查詢範圍&lt;/li>
&lt;/ul>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>品質面向&lt;/th>
 &lt;th>最小可用判準&lt;/th>
 &lt;th>失真後果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>完整性&lt;/td>
 &lt;td>drop rate、coverage 可被量測&lt;/td>
 &lt;td>事故定位依賴不完整證據&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>一致性&lt;/td>
 &lt;td>欄位語意與命名跨服務一致&lt;/td>
 &lt;td>事件鏈需要人工拼接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>代表性&lt;/td>
 &lt;td>sampling 覆蓋高風險樣本&lt;/td>
 &lt;td>錯誤被平均化，誤判風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>時間性&lt;/td>
 &lt;td>timestamp 與 delay 可追蹤&lt;/td>
 &lt;td>timeline 錯序，決策先後顛倒&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="缺漏與漂移">缺漏與漂移&lt;/h2>
&lt;p>缺漏是 telemetry data quality 最容易造成錯誤安全感的問題。缺漏發生時，圖表通常不會直接報錯，而是呈現較低的流量、較少的錯誤或不完整的 trace。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>telemetry data quality 的責任：確認觀測資料本身可信</li>
<li>缺漏類型：missing signal、partial trace、dropped log、stale metric</li>
<li>漂移類型：schema drift、label drift、service name drift、semantic convention drift</li>
<li>偏誤類型：<a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> bias、low-traffic bias、high-cardinality truncation</li>
<li>時間類型：clock skew、ingest delay、out-of-order event、timezone mismatch</li>
<li>品質指標：completeness、freshness、consistency、accuracy、coverage</li>
<li>跟 4.11 telemetry pipeline 的分工：pipeline 看路徑，data quality 看資料可信度</li>
<li>反模式：dashboard 看起來正常但資料少一半；trace sample 漏掉錯誤；timestamp 導致 timeline 錯序</li>
</ul>
<p>Telemetry data quality 的核心是把「觀測資料失真」當成一級事件。服務事故判讀建立在觀測資料上，資料品質不穩時，團隊會把資料缺口誤讀成系統行為，進而做出錯誤分級、錯誤回復或錯誤 SLO 判斷。</p>
<h2 id="概念定位">概念定位</h2>
<p>Telemetry data quality 是把觀測資料當成資料產品治理的能力，責任是讓 log、metric、trace 與 alert 的判讀建立在可信資料上。</p>
<p>這一頁處理的是資料可信度。訊號存在不等於訊號可信；缺漏、漂移、偏誤與時間錯位都會讓事故判讀走向錯誤路徑。</p>
<p>資料品質治理最有效的做法是把品質指標產品化：讓 completeness、freshness、drift、sampling coverage 也進 dashboard 與告警，讓團隊在事故前就能看見資料限制。</p>
<h2 id="品質模型">品質模型</h2>
<p>Telemetry data quality 的品質模型由五個面向組成。這五個面向分別回答資料是否存在、是否及時、是否一致、是否代表真實流量，以及是否足以覆蓋關鍵旅程。</p>
<table>
  <thead>
      <tr>
          <th>品質面向</th>
          <th>核心問題</th>
          <th>常見資料</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Completeness</td>
          <td>該出現的訊號是否完整出現</td>
          <td>drop rate、coverage、gap</td>
      </tr>
      <tr>
          <td>Freshness</td>
          <td>訊號是否足夠接近事件發生時間</td>
          <td>ingest delay、stale metric</td>
      </tr>
      <tr>
          <td>Consistency</td>
          <td>欄位、命名與語意是否跨服務一致</td>
          <td>schema drift、label drift</td>
      </tr>
      <tr>
          <td>Accuracy</td>
          <td>數值與事件語意是否反映真實狀態</td>
          <td>duplicate event、wrong unit</td>
      </tr>
      <tr>
          <td>Coverage</td>
          <td>高風險旅程與低流量邊界是否被涵蓋</td>
          <td>sampling policy、trace ratio</td>
      </tr>
  </tbody>
</table>
<p>Completeness 是事故判讀的基礎。log、metric 或 trace 的缺口如果沒有被標示，dashboard 會呈現一條看似平順的線，實際上可能只是 ingestion pipeline 丟了資料。</p>
<p>Freshness 決定資料能否支援事中決策。告警延遲、metric scrape delay、trace export queue backlog 與 log indexing lag 都會讓 incident commander 用過期資料判斷是否擴大或回復。</p>
<p>Consistency 決定資料能否跨服務拼接。service name、region、tenant、environment、error class 與 semantic convention 若在不同系統漂移，單一服務看起來正常，跨服務事件鏈卻會斷裂。</p>
<p>Accuracy 決定資料能否代表真實狀態。常見問題包含錯誤單位、重複計數、counter reset 誤判、histogram bucket 設錯與 status code mapping 錯誤。</p>
<p>Coverage 決定資料能否覆蓋高風險邊界。低流量服務、VIP tenant、錯誤樣本、長尾 latency 與 rare dependency failure 常被 sampling 或聚合策略稀釋。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 telemetry data quality 時，先看資料是否完整與新鮮，再看不同訊號之間是否能互相對齊。</p>
<p>重點訊號包括：</p>
<ul>
<li>log / metric / trace 是否有 coverage 與 drop rate</li>
<li>schema 是否有版本與 drift 偵測</li>
<li>sampling 是否保留錯誤、高延遲與低流量樣本</li>
<li>timestamp 是否能支援 <a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 還原</li>
<li>dashboard 是否標示資料延遲、缺口與查詢範圍</li>
</ul>
<table>
  <thead>
      <tr>
          <th>品質面向</th>
          <th>最小可用判準</th>
          <th>失真後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>完整性</td>
          <td>drop rate、coverage 可被量測</td>
          <td>事故定位依賴不完整證據</td>
      </tr>
      <tr>
          <td>一致性</td>
          <td>欄位語意與命名跨服務一致</td>
          <td>事件鏈需要人工拼接</td>
      </tr>
      <tr>
          <td>代表性</td>
          <td>sampling 覆蓋高風險樣本</td>
          <td>錯誤被平均化，誤判風險</td>
      </tr>
      <tr>
          <td>時間性</td>
          <td>timestamp 與 delay 可追蹤</td>
          <td>timeline 錯序，決策先後顛倒</td>
      </tr>
  </tbody>
</table>
<h2 id="缺漏與漂移">缺漏與漂移</h2>
<p>缺漏是 telemetry data quality 最容易造成錯誤安全感的問題。缺漏發生時，圖表通常不會直接報錯，而是呈現較低的流量、較少的錯誤或不完整的 trace。</p>
<table>
  <thead>
      <tr>
          <th>缺漏類型</th>
          <th>真實服務樣貌</th>
          <th>判讀風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Missing signal</td>
          <td>新服務路徑沒有 instrument</td>
          <td>核心旅程失敗但 dashboard 正常</td>
      </tr>
      <tr>
          <td>Partial trace</td>
          <td>async job 或 queue consumer 缺 span</td>
          <td>事件鏈停在同步 request</td>
      </tr>
      <tr>
          <td>Dropped log</td>
          <td>ingest burst 時 log 被丟棄</td>
          <td>錯誤率下降被誤判為恢復</td>
      </tr>
      <tr>
          <td>Stale metric</td>
          <td>scrape 成功但資料停在舊 timestamp</td>
          <td>incident timeline 被拉歪</td>
      </tr>
  </tbody>
</table>
<p>Missing signal 代表觀測需求沒有覆蓋服務路徑。常見場景是新 feature flag 開啟後走到新 code path，但 SLI、log schema 與 trace 還停在舊路徑。</p>
<p>Partial trace 代表跨邊界 context 缺少完整傳遞。request 進入 queue 後，如果 message 缺少 correlation id 或 consumer 缺少 span，團隊只能知道 request 發出去，背景流程的失敗時間與失敗點會留在盲區。</p>
<p>Dropped log 代表資料流量超過 pipeline 或成本限制。burst error 發生時，如果 log pipeline 開始 sampling 或丟棄，事故團隊看到的錯誤量會比真實狀態少。</p>
<p>Schema drift 是長期維護最常見的品質問題。欄位改名、label 粒度改變、service name 不一致、semantic convention 升級，都會讓查詢與 dashboard 在沒有明顯錯誤的情況下失準。</p>
<h2 id="sampling-與代表性">Sampling 與代表性</h2>
<p>本段聚焦 sampling 對資料品質的失真風險；sampling 策略（Head / Tail / Adaptive / Exemplar）的 SSoT 在 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Sampling 策略</a>。</p>
<p>Sampling 的責任是控制觀測成本，同時保留足以判讀的高價值樣本。sampling policy 若只按固定比例抽樣，最容易丟掉低頻但高風險的事件。</p>
<table>
  <thead>
      <tr>
          <th>Sampling 風險</th>
          <th>失真方式</th>
          <th>控制面</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Low-traffic bias</td>
          <td>低流量服務樣本太少</td>
          <td>對低流量服務設定 minimum sample floor</td>
      </tr>
      <tr>
          <td>Error sample loss</td>
          <td>錯誤 request 被普通比例抽掉</td>
          <td>對 error、timeout、high latency 強制保留</td>
      </tr>
      <tr>
          <td>Tenant skew</td>
          <td>大 tenant 壓過小 tenant</td>
          <td>以 tenant 或 plan 做分層 sampling</td>
      </tr>
      <tr>
          <td>Cardinality truncation</td>
          <td>高維度 label 被截斷或合併</td>
          <td>標示 truncation，保留 top-K 與 overflow</td>
      </tr>
      <tr>
          <td>Tail latency loss</td>
          <td>長尾 latency 被平均值掩蓋</td>
          <td>使用 histogram 與 exemplar</td>
      </tr>
  </tbody>
</table>
<p>Low-traffic bias 會讓小服務或小 tenant 的問題長期不可見。這些路徑平時量小，但可能承擔高價值客戶、管理操作或資安事件；抽樣策略需要保留最低樣本量。</p>
<p>Error sample loss 會直接破壞事故判讀。錯誤、timeout、retry exhausted、DLQ、payment failure 與 authorization failure 應該有更高保留權重，因為它們代表決策價值高於普通成功 request。</p>
<p>Cardinality truncation 需要明確揭露。當平台為了成本截斷 label 或聚合 tenant 維度時，dashboard 應標示資料限制，讓讀者知道當下看的是聚合視角與可用粒度。</p>
<h2 id="時間對齊">時間對齊</h2>
<p>時間對齊是 incident timeline 的基礎能力。事件發生時間、採集時間、寫入時間、查詢時間與顯示時區若未分清，事故復盤會把原因與結果順序看反。</p>
<table>
  <thead>
      <tr>
          <th>時間問題</th>
          <th>常見來源</th>
          <th>事故後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Clock skew</td>
          <td>host、container、client 時鐘不同</td>
          <td>事件先後被重排</td>
      </tr>
      <tr>
          <td>Ingest delay</td>
          <td>exporter queue 或 indexing lag</td>
          <td>告警與圖表晚於真實事件</td>
      </tr>
      <tr>
          <td>Out-of-order event</td>
          <td>async pipeline 或 retry 寫入</td>
          <td>同一 trace 的 span 順序錯亂</td>
      </tr>
      <tr>
          <td>Timezone mismatch</td>
          <td>人工紀錄與平台顯示時區不同</td>
          <td>對外通訊與內部 timeline 衝突</td>
      </tr>
  </tbody>
</table>
<p>Clock skew 會讓跨服務事件鏈失去可信度。若 API、worker、database proxy 與 observability collector 的時間基準不同，trace 中的等待點可能看起來是負時間或錯誤順序。</p>
<p>Ingest delay 會影響事中決策。incident commander 看到 error rate 下降時，需要知道資料是即時下降，還是 pipeline 還沒收完高峰區段。</p>
<p>Timezone mismatch 常出現在 status page、support ticket、vendor notice 與內部 timeline 對接時。所有事故證據都應保留原始時間與標準化時間，避免復盤時重排錯誤。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>同一事故在 log、metric、trace 中呈現不同時間線</li>
<li>service name / region / tenant label 在不同系統拼不起來</li>
<li>低流量服務的錯誤被 sampling 稀釋</li>
<li>pipeline drop 發生但 dashboard 沒提示資料缺口</li>
<li>post-incident review 發現判讀基於不完整資料</li>
</ul>
<p>常見場景是「圖看起來穩，但資料在悄悄掉」。例如 ingest 層 partial drop 後 error rate 下降，看似健康，實際是訊號少了高風險區段。這類情況若沒有資料品質指標，會讓事故決策建立在錯誤安全感上。</p>
<h2 id="控制面">控制面</h2>
<p>Telemetry data quality 的控制面是把資料限制顯性化。資料品質不需要追求完美，但需要讓讀者知道目前能相信什麼、限制在哪裡、何時需要改用其他 evidence。</p>
<ol>
<li>為每種 telemetry 設定品質指標。</li>
<li>在 dashboard 標示 freshness、coverage 與 known gap。</li>
<li>對 schema drift、drop rate 與 sampling policy 建立告警。</li>
<li>在 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log</a> 記錄資料限制。</li>
<li>在 post-incident review 中回寫造成判讀錯誤的資料品質缺口。</li>
</ol>
<p>品質指標本身也需要 owner。平台團隊可以維護 pipeline drop、ingest delay 與 semantic convention；服務團隊需要維護 service-specific schema、business event 與 user journey coverage。</p>
<p>資料限制應直接出現在操作入口。若某 dashboard 的 trace sample 只保留 10%、某 tenant label 被聚合、某時間區段有 log gap，讀者應在同一個畫面看到限制，並把限制納入當下決策。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Telemetry data quality 的反模式來自把查詢結果視為事實本身。查詢結果只是資料產品的輸出，仍然受採集、轉換、抽樣、儲存與查詢限制影響。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>dashboard 即事實</td>
          <td>圖表下降就判斷服務恢復</td>
          <td>顯示資料延遲與 coverage</td>
      </tr>
      <tr>
          <td>schema 漂移無治理</td>
          <td>查詢突然少資料但沒人知道</td>
          <td>欄位版本與 drift 偵測</td>
      </tr>
      <tr>
          <td>sampling policy 黑箱</td>
          <td>錯誤樣本被抽掉仍用比例推估</td>
          <td>公開 sampling policy 與例外規則</td>
      </tr>
      <tr>
          <td>timeline 單時間戳</td>
          <td>只記顯示時間，不記事件原始時間</td>
          <td>同時保留 event / ingest / query</td>
      </tr>
      <tr>
          <td>成本截斷不標示</td>
          <td>高 cardinality 被合併但仍當完整資料</td>
          <td>標示 truncation 與聚合粒度</td>
      </tr>
  </tbody>
</table>
<p>dashboard 即事實會讓事故決策失去資料謙遜。圖表顯示健康時，仍要確認資料有沒有缺口、延遲或抽樣偏誤，尤其在 pipeline 自身承受壓力時。</p>
<p>sampling policy 黑箱會降低服務團隊的風險判讀品質。平台可以為成本抽樣，但抽樣規則要能被服務團隊理解，並且允許錯誤、高延遲與低流量關鍵路徑保留更高權重。</p>
<h2 id="遷移期的雙軌對照驗證">遷移期的雙軌對照驗證</h2>
<p>觀測平台遷移是資料品質最容易失分的窗口。新舊管線並存期間，若沒有顯式對照驗證，語意漂移會在 dashboard 看起來「都有資料」的情況下緩慢偏離，直到事故時才浮現。</p>
<p>雙軌對照的核心責任是把新管線當被檢驗的對象、用舊管線作為對照基準。新舊管線同時採集相同訊號、用相同 query 對照 error rate、p95 latency、<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a>、trace coverage 是否一致；偏差超過閾值時先停止下一步遷移、保留證據後再決定下一步。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel 相容遷移實務</a>：揭露「先建立雙軌採集對照、用品質指標決定何時關閉舊管線」的做法。對應 <a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9 OTel 遷移訊號漂移反例</a>：揭露遷移失敗的主要風險來自語意漂移 — metric 名稱、label、sampling、aggregation 在新舊管線間出現微小差異，導致同一現象被歸到不同 service / label / latency bucket。</p>
<p>可重複套用的對照驗證做法：</p>
<ol>
<li><strong>固定一組 baseline query</strong>：選定關鍵服務的核心 SLI query（error rate、p99 latency、throughput），新舊管線各跑一份、定期比對。</li>
<li><strong>設定偏差閾值</strong>：每個 SLI 設可接受偏差（例如 ±5%）。超過閾值的時段標記為待調查，不能無視。</li>
<li><strong>追蹤 missing signal 比例</strong>：missing span、missing metric、missing log 的比例是漂移的早期指標。比例持續上升時，停止下一批服務切換。</li>
<li><strong>退出條件顯式化</strong>：「對照偏差連續 N 天 &lt; X%」作為關閉舊管線的退出條件，把雙軌期變成有界的、不是無限延長。</li>
</ol>
<p>遷移期的告警條件本身也是治理項目。新舊管線對同服務的 error rate 長期偏離、missing span / missing metric 比例持續上升、同一事件在兩套 dashboard 得到相反結論、這些都該成為高優先告警、讓漂移在發生當下即時可見、避免堆積到 retrospective 才被注意。</p>
<p>雙軌期的成本是顯而易見的：兩份採集、兩份儲存、兩份查詢。但放棄對照的代價更大 — 沒有對照證據，事故時無法分辨是「服務問題」還是「遷移問題」，回退也失去依據。詳細的回退判讀流程由 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 處理，本章關注的是品質指標的對照設計。</p>
<h2 id="與-slo-和事故的關係">與 SLO 和事故的關係</h2>
<p>Telemetry data quality 是 SLO 與事故 evidence 的可信度前提。SLI 若建立在失真資料上，<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a>、burn rate alert 與 release freeze 都會被錯誤資料牽動。</p>
<p>在 SLO 場景中，資料品質缺口會直接改變可靠性政策。若 availability SLI 漏掉 mobile client、region label 漂移、error sample 被抽掉，團隊會高估可靠性並繼續放行高風險變更。</p>
<p>在事故場景中，資料品質限制需要進入 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">incident decision log</a>。當 IC 做出升級、降級、等待或 rollback 決策時，應同時記錄當下 evidence 的 completeness、freshness 與 confidence。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>：治理欄位漂移</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：sampling 策略矩陣、高維度截斷與成本取捨</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a>：追查 drop、delay 與 ingest 問題</li>
<li><a href="/blog/backend/04-observability/anomaly-detection/" data-link-title="4.14 Anomaly Detection" data-link-desc="把 ML / statistical baseline 訊號跟 rule-based alert 整合">4.14 anomaly detection</a>：避免模型學到偏誤資料</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：品質指標的 platform / service ownership 邊界</li>
<li><a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 incident decision log</a>：標記事中判讀使用的資料品質限制</li>
<li><a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>：pre-aggregation 跟 raw data 的一致性驗證</li>
<li><a href="/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">4.C13 Discord 儲存→觀測缺口</a>：儲存演進反覆暴露觀測盲區的教訓</li>
</ul>
]]></content:encoded></item><item><title>4.18 Observability Operating Model</title><link>https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>operating model 的責任：定義誰擁有訊號、誰維護 dashboard、誰處理 alert、誰承擔成本&lt;/li>
&lt;li>角色分工：platform team、service team、on-call、incident commander、security / compliance&lt;/li>
&lt;li>ownership 欄位：owner、review cadence、retention、cost center、runbook link、deprecation date&lt;/li>
&lt;li>生命週期：新增、審核、使用、修訂、淘汰&lt;/li>
&lt;li>治理節奏：dashboard review、alert review、cost review、post-incident write-back&lt;/li>
&lt;li>跟 4.15 cost attribution 的關係：成本歸屬是 operating model 的一部分&lt;/li>
&lt;li>跟 08 的關係：事故時使用同一組 owner 與 escalation route&lt;/li>
&lt;li>反模式：平台團隊擁有所有 alert；service team 不看 dashboard；成本無 owner&lt;/li>
&lt;/ul>
&lt;p>Observability operating model 的價值是把觀測從「工具責任」改成「服務責任」。平台團隊提供共用能力，服務團隊提供業務語意，on-call 使用這些資產做決策；operating model 負責固定三者的接口。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Observability operating model 是把觀測資產的責任分配明確化的治理模型，責任是讓訊號有人維護、告警有人回應、成本有人決策。&lt;/p>
&lt;p>這一頁處理的是 ownership。可觀測性需要平台工具、服務脈絡、操作責任與淘汰條件一起維持。&lt;/p>
&lt;p>這層的判準是事故當下能否立刻知道誰要看哪個面板、誰有權調整閾值、誰負責決定淘汰過期訊號。dashboard 數量與 alert 覆蓋率只是輔助訊號。&lt;/p>
&lt;h2 id="角色分工">角色分工&lt;/h2>
&lt;p>Observability operating model 的角色分工以「誰能做決策」為核心。owner 是有權維護、調整、下架或升級觀測資產的人，名義聯絡人只能作為補充欄位。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>角色&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;th>決策權限&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Platform team&lt;/td>
 &lt;td>採集、儲存、查詢、成本與標準&lt;/td>
 &lt;td>pipeline、schema convention、quota&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service team&lt;/td>
 &lt;td>服務語意、核心旅程與業務事件&lt;/td>
 &lt;td>service dashboard、SLI、alert rule&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>On-call&lt;/td>
 &lt;td>事中判讀、runbook 使用與升級&lt;/td>
 &lt;td>silence、escalate、incident intake&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Incident commander&lt;/td>
 &lt;td>事故優先序、通訊節奏與決策紀錄&lt;/td>
 &lt;td>severity、rollback、status update&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Security / compliance&lt;/td>
 &lt;td>audit log、PII、retention 與 evidence&lt;/td>
 &lt;td>retention、masking、access review&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Finance / cost owner&lt;/td>
 &lt;td>成本歸屬、預算與 chargeback&lt;/td>
 &lt;td>quota、retention tier、cost review&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Platform team 的責任是維持共同語言。它需要定義 service name、environment、region、tenant、trace context、retention tier 與成本政策，讓跨服務查詢可行。&lt;/p>
&lt;p>Service team 的責任是維持服務語意。它需要定義哪些 user journey 是核心、哪些錯誤影響用戶、哪些 dependency failure 需要 alert、哪些 dashboard 仍有操作價值。&lt;/p>
&lt;p>On-call 的責任是把資產用在事中決策。alert 應能帶到 dashboard、runbook 與 owner，讓 operating model 真正進入操作流程。&lt;/p>
&lt;p>Security / compliance 的責任是把觀測資料的證據價值與資料風險同時納入治理。audit log、PII redaction、retention 與 access review 需要在觀測模型中有明確 owner。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 operating model 時，先看每個觀測資產是否有 owner，再看 owner 是否有權限與節奏採取行動。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>dashboard 是否有明確使用者與 review cadence&lt;/li>
&lt;li>alert 是否有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>、owner 與 escalation path&lt;/li>
&lt;li>高成本訊號是否能對應服務價值與成本中心&lt;/li>
&lt;li>post-incident review 是否能回寫到訊號 owner&lt;/li>
&lt;li>orphan dashboard 與 stale alert 是否有清理流程&lt;/li>
&lt;/ul>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資產類型&lt;/th>
 &lt;th>Owner&lt;/th>
 &lt;th>週期&lt;/th>
 &lt;th>關閉條件&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Dashboard&lt;/td>
 &lt;td>service team + on-call&lt;/td>
 &lt;td>月檢&lt;/td>
 &lt;td>無使用者、無判讀價值&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Alert&lt;/td>
 &lt;td>service owner&lt;/td>
 &lt;td>週檢&lt;/td>
 &lt;td>重複、誤報高、無行動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Query / Schema&lt;/td>
 &lt;td>platform + service&lt;/td>
 &lt;td>變更檢&lt;/td>
 &lt;td>欄位漂移、查詢成本失控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cost Attribution&lt;/td>
 &lt;td>cost owner&lt;/td>
 &lt;td>月檢&lt;/td>
 &lt;td>成本缺少服務價值對應&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="觀測資產欄位">觀測資產欄位&lt;/h2>
&lt;p>Observability asset 需要像服務 artifact 一樣有 metadata。沒有 metadata 的 dashboard、alert、query 與 schema 會在幾個月後變成無人敢刪、無人敢改、也無人信任的資產。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>operating model 的責任：定義誰擁有訊號、誰維護 dashboard、誰處理 alert、誰承擔成本</li>
<li>角色分工：platform team、service team、on-call、incident commander、security / compliance</li>
<li>ownership 欄位：owner、review cadence、retention、cost center、runbook link、deprecation date</li>
<li>生命週期：新增、審核、使用、修訂、淘汰</li>
<li>治理節奏：dashboard review、alert review、cost review、post-incident write-back</li>
<li>跟 4.15 cost attribution 的關係：成本歸屬是 operating model 的一部分</li>
<li>跟 08 的關係：事故時使用同一組 owner 與 escalation route</li>
<li>反模式：平台團隊擁有所有 alert；service team 不看 dashboard；成本無 owner</li>
</ul>
<p>Observability operating model 的價值是把觀測從「工具責任」改成「服務責任」。平台團隊提供共用能力，服務團隊提供業務語意，on-call 使用這些資產做決策；operating model 負責固定三者的接口。</p>
<h2 id="概念定位">概念定位</h2>
<p>Observability operating model 是把觀測資產的責任分配明確化的治理模型，責任是讓訊號有人維護、告警有人回應、成本有人決策。</p>
<p>這一頁處理的是 ownership。可觀測性需要平台工具、服務脈絡、操作責任與淘汰條件一起維持。</p>
<p>這層的判準是事故當下能否立刻知道誰要看哪個面板、誰有權調整閾值、誰負責決定淘汰過期訊號。dashboard 數量與 alert 覆蓋率只是輔助訊號。</p>
<h2 id="角色分工">角色分工</h2>
<p>Observability operating model 的角色分工以「誰能做決策」為核心。owner 是有權維護、調整、下架或升級觀測資產的人，名義聯絡人只能作為補充欄位。</p>
<table>
  <thead>
      <tr>
          <th>角色</th>
          <th>核心責任</th>
          <th>決策權限</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Platform team</td>
          <td>採集、儲存、查詢、成本與標準</td>
          <td>pipeline、schema convention、quota</td>
      </tr>
      <tr>
          <td>Service team</td>
          <td>服務語意、核心旅程與業務事件</td>
          <td>service dashboard、SLI、alert rule</td>
      </tr>
      <tr>
          <td>On-call</td>
          <td>事中判讀、runbook 使用與升級</td>
          <td>silence、escalate、incident intake</td>
      </tr>
      <tr>
          <td>Incident commander</td>
          <td>事故優先序、通訊節奏與決策紀錄</td>
          <td>severity、rollback、status update</td>
      </tr>
      <tr>
          <td>Security / compliance</td>
          <td>audit log、PII、retention 與 evidence</td>
          <td>retention、masking、access review</td>
      </tr>
      <tr>
          <td>Finance / cost owner</td>
          <td>成本歸屬、預算與 chargeback</td>
          <td>quota、retention tier、cost review</td>
      </tr>
  </tbody>
</table>
<p>Platform team 的責任是維持共同語言。它需要定義 service name、environment、region、tenant、trace context、retention tier 與成本政策，讓跨服務查詢可行。</p>
<p>Service team 的責任是維持服務語意。它需要定義哪些 user journey 是核心、哪些錯誤影響用戶、哪些 dependency failure 需要 alert、哪些 dashboard 仍有操作價值。</p>
<p>On-call 的責任是把資產用在事中決策。alert 應能帶到 dashboard、runbook 與 owner，讓 operating model 真正進入操作流程。</p>
<p>Security / compliance 的責任是把觀測資料的證據價值與資料風險同時納入治理。audit log、PII redaction、retention 與 access review 需要在觀測模型中有明確 owner。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 operating model 時，先看每個觀測資產是否有 owner，再看 owner 是否有權限與節奏採取行動。</p>
<p>重點訊號包括：</p>
<ul>
<li>dashboard 是否有明確使用者與 review cadence</li>
<li>alert 是否有 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>、owner 與 escalation path</li>
<li>高成本訊號是否能對應服務價值與成本中心</li>
<li>post-incident review 是否能回寫到訊號 owner</li>
<li>orphan dashboard 與 stale alert 是否有清理流程</li>
</ul>
<table>
  <thead>
      <tr>
          <th>資產類型</th>
          <th>Owner</th>
          <th>週期</th>
          <th>關閉條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dashboard</td>
          <td>service team + on-call</td>
          <td>月檢</td>
          <td>無使用者、無判讀價值</td>
      </tr>
      <tr>
          <td>Alert</td>
          <td>service owner</td>
          <td>週檢</td>
          <td>重複、誤報高、無行動</td>
      </tr>
      <tr>
          <td>Query / Schema</td>
          <td>platform + service</td>
          <td>變更檢</td>
          <td>欄位漂移、查詢成本失控</td>
      </tr>
      <tr>
          <td>Cost Attribution</td>
          <td>cost owner</td>
          <td>月檢</td>
          <td>成本缺少服務價值對應</td>
      </tr>
  </tbody>
</table>
<h2 id="觀測資產欄位">觀測資產欄位</h2>
<p>Observability asset 需要像服務 artifact 一樣有 metadata。沒有 metadata 的 dashboard、alert、query 與 schema 會在幾個月後變成無人敢刪、無人敢改、也無人信任的資產。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>判讀用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Owner</td>
          <td>指定維護與決策責任</td>
          <td>事故時知道找誰</td>
      </tr>
      <tr>
          <td>User</td>
          <td>說明誰會使用這個資產</td>
          <td>判斷是否仍有操作價值</td>
      </tr>
      <tr>
          <td>Runbook link</td>
          <td>連到下一步操作</td>
          <td>讓 alert 能轉成行動</td>
      </tr>
      <tr>
          <td>Review cadence</td>
          <td>定義檢視頻率</td>
          <td>避免 stale dashboard / alert</td>
      </tr>
      <tr>
          <td>Cost center</td>
          <td>對應服務或團隊成本</td>
          <td>支援 chargeback 與 retention 決策</td>
      </tr>
      <tr>
          <td>Retention tier</td>
          <td>指定保存時間與查詢粒度</td>
          <td>平衡法規、事故與成本</td>
      </tr>
      <tr>
          <td>Deprecation date</td>
          <td>標示預計下架或重檢日期</td>
          <td>避免觀測資產永久堆積</td>
      </tr>
      <tr>
          <td>Data limitation</td>
          <td>標示抽樣、缺口與聚合限制</td>
          <td>避免事中誤讀資料</td>
      </tr>
  </tbody>
</table>
<p>Owner 欄位要搭配權限才有意義。有效 owner 需要能調整 threshold、更新 dashboard、下架 query 或決定 retention，讓 ownership 成為可執行責任。</p>
<p>User 欄位能避免 dashboard 變成展示資產。面板若沒有明確使用者，例如 on-call、service owner、capacity planner 或 compliance reviewer，就很難判斷它是否仍值得維護。</p>
<p>Runbook link 是 alert 從通知變成行動的關鍵。每個可 page 的 alert 都應連到第一步查詢、初始判讀、升級條件與 rollback / degrade / wait 的決策路由。</p>
<p>Cost center 讓觀測成本有業務語意。高 cardinality、長 retention、full-fidelity trace 與大量 log indexing 都有價值，但價值需要由能受益的服務或團隊承擔與檢視。</p>
<h2 id="生命週期">生命週期</h2>
<p>Observability operating model 的生命週期是新增、審核、使用、修訂與淘汰。這個生命週期讓訊號保持有用，並讓觀測資產累積在可治理範圍內。</p>
<ol>
<li>新增：服務變更、事故復盤、演練需求或合規要求產生新訊號。</li>
<li>審核：確認 schema、成本、owner、runbook 與 retention。</li>
<li>使用：進入 dashboard、alert、incident intake 或 SLO 計算。</li>
<li>修訂：根據噪音、缺口、成本與使用頻率調整。</li>
<li>淘汰：移除 stale alert、orphan dashboard、過期 query 與無價值高成本訊號。</li>
</ol>
<p>新增訊號需要清楚的需求來源。最好的來源是 user journey、SLO、incident review、game day 或 audit requirement；最弱的來源是「可能有用」。</p>
<p>審核訊號需要同時看語意與成本。欄位是否穩定、cardinality 是否可控、retention 是否合理、PII 是否被遮罩、owner 是否能維護，都是訊號上線前的固定問題。</p>
<p>淘汰是 operating model 的必要能力。舊 alert 沒有人敢關，會增加 alert fatigue；舊 dashboard 沒有人敢刪，會讓事故時不知道哪個面板可信。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>alert 觸發後沒人知道該由平台或服務團隊處理</li>
<li>dashboard 存在但半年無人打開</li>
<li>成本暴增時只能找平台團隊吸收</li>
<li>post-incident review 指派 action item，但沒有訊號 owner</li>
<li>service team 調整欄位後，平台查詢與 dashboard 斷裂</li>
</ul>
<p>實務上常見的治理斷點是「有 owner 名字，缺 owner 權限」。owner 需要能調整 alert、建立或下架 dashboard、分配成本，治理流程才會停在資產責任人，減少回流到平台集中處理的積壓。</p>
<h2 id="治理節奏">治理節奏</h2>
<p>Operating model 的治理節奏把觀測資產拉回日常工程流程。review cadence 的重點是定期回答「這個資產還能支援決策嗎」，會議只是其中一種執行形式。</p>
<table>
  <thead>
      <tr>
          <th>節奏</th>
          <th>核心問題</th>
          <th>典型輸出</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dashboard review</td>
          <td>面板是否仍有人用、是否對應旅程</td>
          <td>更新、合併、下架</td>
      </tr>
      <tr>
          <td>Alert review</td>
          <td>alert 是否可行動、噪音是否可接受</td>
          <td>threshold 調整、silence、runbook</td>
      </tr>
      <tr>
          <td>Cost review</td>
          <td>成本是否對應服務價值</td>
          <td>retention tier、sampling policy</td>
      </tr>
      <tr>
          <td>Schema review</td>
          <td>欄位是否穩定、是否跨服務一致</td>
          <td>schema migration、drift 修正</td>
      </tr>
      <tr>
          <td>Post-incident write-back</td>
          <td>復盤缺口是否回寫到訊號與 owner</td>
          <td>新 alert、新 dashboard、新 runbook</td>
      </tr>
  </tbody>
</table>
<p>Dashboard review 應看使用情境與操作價值。面板需要支援 on-call 的前 10 分鐘、capacity planning 或 SLO review；脫離這些用途的面板適合合併、重命名或下架。</p>
<p>Alert review 應看行動品質。alert 若經常觸發但缺少明確處置，通常更適合變成 dashboard signal、ticket 或長期治理項。</p>
<p>Cost review 應看服務價值。觀測成本上升不一定是壞事，但需要能說明這些成本降低了哪一種事故風險、合規風險或容量風險。</p>
<h2 id="規模差異下的角色配置">規模差異下的角色配置</h2>
<p>Operating model 的角色配置隨組織規模調整。可投入的治理人力、可承受的協調成本、可維持的審核頻率三項一起決定當前該採哪種配置。把大組織的治理模型套到小團隊會造成過度治理；把小團隊的鬆散模型套到大組織會造成責任懸空。</p>
<p>本段聚焦常態 ownership 配置（不同規模下角色矩陣的差異）；遷移期的節奏取捨由 <a href="/blog/backend/04-observability/telemetry-pipeline/#%e8%a6%8f%e6%a8%a1%e5%b7%ae%e7%95%b0%e4%b8%8b%e7%9a%84%e9%81%b7%e7%a7%bb%e7%af%80%e5%a5%8f" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 規模差異下的遷移節奏</a> 處理、兩者 lens 不同。</p>
<p>對應 <a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10 規模差異下觀測遷移</a>：揭露「規模差異會放大不同治理失分模式」的方向；case 主場景是觀測遷移、本章將此 frame 借用到常態 operating model 場景、以下展開屬通用工程知識補充。</p>
<p>小型組織的 operating model 重點是「角色合一、節奏明確」。一個 SRE 同時承擔 platform、service、on-call、cost owner 多重身份。治理重點是顯式記錄當前 ownership 跟 review cadence、避免角色合一被誤讀成默契傳遞（「大家都管 = 沒人管」是典型失敗）。Dashboard review、alert review、cost review 可以合併在同一個月會中，但要有具體的決議紀錄。</p>
<p>中型組織開始出現 platform 跟 service team 的分化，治理失分集中在介面定義。schema convention、cardinality 限制、cost center 命名規約若未在 platform / service 之間明確化，會在跨服務查詢時持續出現拼接斷裂。中型組織適合先固化「平台保證什麼、服務保證什麼」的契約，再擴大角色拆分。</p>
<p>大型組織的 operating model 牽涉多層 platform team、跨地區 on-call、合規 / 安全 / 財務的橫切責任。治理失分的核心來源是審核節奏跟不上資產成長速度 — 角色分工通常已經清晰，但每週 / 每月人工 review 數百個 dashboard / alert 不切實際。大型組織需要自動化的 stale dashboard 偵測、orphan alert 提示、retention compliance 報表，把 review 從手動週期變成事件驅動，讓治理隨資產數量自動擴展。</p>
<p>三類組織的共同前提是先把 ownership 視為可演進的、再決定當前該採哪種配置。組織成長過程中 ownership 矩陣會反覆調整，每次調整都要把新配置寫進文件、進入 release / runbook 流程、讓 ownership 變更跟釋出流程同步可見。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Observability operating model 的反模式通常是責任集中或責任懸空。前者讓平台團隊成為所有訊號的瓶頸，後者讓服務團隊在事故時找不到可信入口。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>平台擁有所有 alert</td>
          <td>服務語意缺失，告警只能看基礎設施</td>
          <td>service owner 擁有服務級 alert</td>
      </tr>
      <tr>
          <td>服務各自為政</td>
          <td>欄位、命名、retention 不一致</td>
          <td>platform 提供 schema convention</td>
      </tr>
      <tr>
          <td>owner 缺權限</td>
          <td>只能被追責，缺少資產修正能力</td>
          <td>owner 取得調整、下架與預算權限</td>
      </tr>
      <tr>
          <td>成本無歸屬</td>
          <td>高成本訊號由平台吸收</td>
          <td>cost center 與 retention tier</td>
      </tr>
      <tr>
          <td>復盤無回寫</td>
          <td>action item 停在文件</td>
          <td>write-back 到 dashboard / alert</td>
      </tr>
  </tbody>
</table>
<p>平台擁有所有 alert 會讓服務語意被削弱。平台知道 pipeline 與 infra，但通常不知道某個錯誤是否影響 checkout、資料同步、帳單或客戶 SLA。</p>
<p>服務各自為政會讓跨服務事故難以判讀。每個服務都可以有自己的 dashboard，但 service name、environment、region、tenant、error class 與 trace context 需要共用標準。</p>
<p>復盤無回寫會讓 operating model 停在文件。post-incident review 揭露的偵測缺口、runbook 缺口與成本缺口都應回到對應 owner 的資產生命週期。</p>
<h2 id="與事故流程的關係">與事故流程的關係</h2>
<p>Observability operating model 是事故流程的責任基礎。事故期間，IC 需要知道哪些訊號可信、哪個 owner 能解釋欄位、誰能調整 alert、誰能決定保留或匯出 evidence。</p>
<p>在 incident command 中，observability owner 不一定是 incident commander，但必須能提供訊號解釋與操作建議。當 telemetry data quality 有限制時，owner 需要把限制交給 scribe 或 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">decision log</a>。</p>
<p>在 runbook lifecycle 中，dashboard、alert 與 query 都應被視為 runbook 的依賴。runbook 更新時，如果沒有同步更新觀測資產，下一次事故仍會走到舊入口。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard / alert</a>：設計 owner、runbook 與停止條件</li>
<li><a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 signal governance loop</a>：淘汰 stale alert 與 orphan dashboard</li>
<li><a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">4.13 service topology</a>：動態叢集環境下、cluster 層 vs 服務層的 ownership 路由</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>：把成本接回 owner 與服務</li>
<li>08.2 incident command roles：事故時使用相同 ownership 模型</li>
<li>08.16 runbook lifecycle：把觀測資產接進 runbook 版本治理</li>
</ul>
]]></content:encoded></item><item><title>4.19 Debuggability by Design</title><link>https://tarrragon.github.io/blog/backend/04-observability/debuggability-by-design/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/debuggability-by-design/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>debuggability by design 的責任：讓系統設計本身支援定位、重現與證據收集&lt;/li>
&lt;li>API 設計：request id、error code、idempotency key、semantic status&lt;/li>
&lt;li>async workflow：message id、correlation id、retry count、dead-letter reason&lt;/li>
&lt;li>dependency call：timeout、fallback、upstream response、circuit state&lt;/li>
&lt;li>error model：可分類錯誤、可追蹤錯誤鏈、可對應使用者影響&lt;/li>
&lt;li>診斷入口：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">diagnostic endpoint&lt;/a>、health check、probe&lt;/li>
&lt;li>跟語言教材的分工：語言處理 logger / error chain，04 處理跨服務診斷能力&lt;/li>
&lt;li>反模式：事後補 log；錯誤只回 500；async 任務缺 correlation id；依賴失敗無上下文&lt;/li>
&lt;/ul>
&lt;p>Debuggability by design 的核心是讓系統在設計時就暴露足夠上下文。事故時需要的資訊若沒有在 API、message、dependency call 與 error model 層留下來，後端平台再完整也只能收集到片段訊號。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Debuggability by design 是把可診斷性當成服務設計輸入的做法，責任是讓系統在出問題時自然留下定位所需的脈絡。&lt;/p>
&lt;p>這一頁處理的是設計前移。觀測工具只能收集系統吐出的訊號；如果 API、async workflow、dependency call 與 error model 沒有診斷欄位，事後補平台也只能看到破碎片段。&lt;/p>
&lt;p>這層與可觀測平台互補：平台負責收、存、查，設計負責產生可判讀語意。兩者任一缺失，都會讓事故定位時間呈倍數增加。&lt;/p>
&lt;h2 id="設計輸入">設計輸入&lt;/h2>
&lt;p>Debuggability by design 的設計輸入是「未來出問題時需要回答什麼問題」。系統設計時先列出這些問題，才能決定 API、message、dependency call 與 error model 要留下哪些欄位。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>問題&lt;/th>
 &lt;th>需要的設計輸入&lt;/th>
 &lt;th>常見位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>這次失敗影響哪個請求或用戶&lt;/td>
 &lt;td>request id、tenant、user journey&lt;/td>
 &lt;td>API、log schema、trace&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>這個 async 任務從哪裡來&lt;/td>
 &lt;td>correlation id、message id、causation id&lt;/td>
 &lt;td>queue、worker、event log&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗來自本服務還是外部依賴&lt;/td>
 &lt;td>upstream name、timeout、response class&lt;/td>
 &lt;td>HTTP client、adapter&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>這個錯誤能否重試或回放&lt;/td>
 &lt;td>retry count、idempotency key、DLQ reason&lt;/td>
 &lt;td>worker、consumer、DLQ&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>事故時能否安全查系統狀態&lt;/td>
 &lt;td>diagnostic endpoint、probe、read-only view&lt;/td>
 &lt;td>admin / diagnostic surface&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Request id 與 trace id 的責任不同。request id 通常對應對外請求與支援查詢，trace id 對應跨服務路徑；兩者互相連結時，支援查詢與工程診斷都會有穩定入口。&lt;/p>
&lt;p>Correlation id 與 causation id 能讓 async workflow 保留因果。事件進入 queue、fan-out、retry、DLQ 或 replay 後，團隊需要知道它從哪個 request 或上游事件來，並且知道目前是哪一次處理嘗試。&lt;/p>
&lt;p>Diagnostic endpoint 的責任是提供低風險查詢入口。它是受權限、速率、遮罩與審計保護的操作面，讓 on-call 能查健康、依賴、queue、cache 或 feature flag 狀態。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀 debuggability 時，先看關鍵流程是否保留 correlation，再看錯誤是否能路由到下一步。&lt;/p>
&lt;p>重點訊號包括：&lt;/p>
&lt;ul>
&lt;li>API request 是否有穩定 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 與錯誤分類&lt;/li>
&lt;li>async message 是否有 correlation id、retry count 與 DLQ reason&lt;/li>
&lt;li>dependency call 是否記錄 upstream、timeout、fallback 與 response class&lt;/li>
&lt;li>error chain 是否能連到 trace、log 與 user impact&lt;/li>
&lt;li>diagnostic endpoint 是否能支援 on-call 的低風險查詢&lt;/li>
&lt;/ul>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設計層&lt;/th>
 &lt;th>最小可診斷欄位&lt;/th>
 &lt;th>事故價值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>API&lt;/td>
 &lt;td>request id、error code、idempotency key&lt;/td>
 &lt;td>快速對齊請求與結果&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Async / Queue&lt;/td>
 &lt;td>message id、correlation id、retry reason&lt;/td>
 &lt;td>還原跨流程事件鏈&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Dependency&lt;/td>
 &lt;td>upstream、timeout、fallback state&lt;/td>
 &lt;td>分辨本地問題與外部依賴問題&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Error Model&lt;/td>
 &lt;td>error class、context、impact hint&lt;/td>
 &lt;td>路由到正確處理流程&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="api-可診斷性">API 可診斷性&lt;/h2>
&lt;p>API 可診斷性的責任是讓每一次 request 都能被支援、工程與事故流程共同定位。API 不只回傳成功或失敗，也要留下足夠語意讓團隊知道錯在哪個層級。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>debuggability by design 的責任：讓系統設計本身支援定位、重現與證據收集</li>
<li>API 設計：request id、error code、idempotency key、semantic status</li>
<li>async workflow：message id、correlation id、retry count、dead-letter reason</li>
<li>dependency call：timeout、fallback、upstream response、circuit state</li>
<li>error model：可分類錯誤、可追蹤錯誤鏈、可對應使用者影響</li>
<li>診斷入口：<a href="/blog/backend/knowledge-cards/diagnostic-endpoint/" data-link-title="Diagnostic Endpoint" data-link-desc="說明健康檢查、診斷與調試入口如何控制暴露面">diagnostic endpoint</a>、health check、probe</li>
<li>跟語言教材的分工：語言處理 logger / error chain，04 處理跨服務診斷能力</li>
<li>反模式：事後補 log；錯誤只回 500；async 任務缺 correlation id；依賴失敗無上下文</li>
</ul>
<p>Debuggability by design 的核心是讓系統在設計時就暴露足夠上下文。事故時需要的資訊若沒有在 API、message、dependency call 與 error model 層留下來，後端平台再完整也只能收集到片段訊號。</p>
<h2 id="概念定位">概念定位</h2>
<p>Debuggability by design 是把可診斷性當成服務設計輸入的做法，責任是讓系統在出問題時自然留下定位所需的脈絡。</p>
<p>這一頁處理的是設計前移。觀測工具只能收集系統吐出的訊號；如果 API、async workflow、dependency call 與 error model 沒有診斷欄位，事後補平台也只能看到破碎片段。</p>
<p>這層與可觀測平台互補：平台負責收、存、查，設計負責產生可判讀語意。兩者任一缺失，都會讓事故定位時間呈倍數增加。</p>
<h2 id="設計輸入">設計輸入</h2>
<p>Debuggability by design 的設計輸入是「未來出問題時需要回答什麼問題」。系統設計時先列出這些問題，才能決定 API、message、dependency call 與 error model 要留下哪些欄位。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>需要的設計輸入</th>
          <th>常見位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>這次失敗影響哪個請求或用戶</td>
          <td>request id、tenant、user journey</td>
          <td>API、log schema、trace</td>
      </tr>
      <tr>
          <td>這個 async 任務從哪裡來</td>
          <td>correlation id、message id、causation id</td>
          <td>queue、worker、event log</td>
      </tr>
      <tr>
          <td>失敗來自本服務還是外部依賴</td>
          <td>upstream name、timeout、response class</td>
          <td>HTTP client、adapter</td>
      </tr>
      <tr>
          <td>這個錯誤能否重試或回放</td>
          <td>retry count、idempotency key、DLQ reason</td>
          <td>worker、consumer、DLQ</td>
      </tr>
      <tr>
          <td>事故時能否安全查系統狀態</td>
          <td>diagnostic endpoint、probe、read-only view</td>
          <td>admin / diagnostic surface</td>
      </tr>
  </tbody>
</table>
<p>Request id 與 trace id 的責任不同。request id 通常對應對外請求與支援查詢，trace id 對應跨服務路徑；兩者互相連結時，支援查詢與工程診斷都會有穩定入口。</p>
<p>Correlation id 與 causation id 能讓 async workflow 保留因果。事件進入 queue、fan-out、retry、DLQ 或 replay 後，團隊需要知道它從哪個 request 或上游事件來，並且知道目前是哪一次處理嘗試。</p>
<p>Diagnostic endpoint 的責任是提供低風險查詢入口。它是受權限、速率、遮罩與審計保護的操作面，讓 on-call 能查健康、依賴、queue、cache 或 feature flag 狀態。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀 debuggability 時，先看關鍵流程是否保留 correlation，再看錯誤是否能路由到下一步。</p>
<p>重點訊號包括：</p>
<ul>
<li>API request 是否有穩定 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 與錯誤分類</li>
<li>async message 是否有 correlation id、retry count 與 DLQ reason</li>
<li>dependency call 是否記錄 upstream、timeout、fallback 與 response class</li>
<li>error chain 是否能連到 trace、log 與 user impact</li>
<li>diagnostic endpoint 是否能支援 on-call 的低風險查詢</li>
</ul>
<table>
  <thead>
      <tr>
          <th>設計層</th>
          <th>最小可診斷欄位</th>
          <th>事故價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>API</td>
          <td>request id、error code、idempotency key</td>
          <td>快速對齊請求與結果</td>
      </tr>
      <tr>
          <td>Async / Queue</td>
          <td>message id、correlation id、retry reason</td>
          <td>還原跨流程事件鏈</td>
      </tr>
      <tr>
          <td>Dependency</td>
          <td>upstream、timeout、fallback state</td>
          <td>分辨本地問題與外部依賴問題</td>
      </tr>
      <tr>
          <td>Error Model</td>
          <td>error class、context、impact hint</td>
          <td>路由到正確處理流程</td>
      </tr>
  </tbody>
</table>
<h2 id="api-可診斷性">API 可診斷性</h2>
<p>API 可診斷性的責任是讓每一次 request 都能被支援、工程與事故流程共同定位。API 不只回傳成功或失敗，也要留下足夠語意讓團隊知道錯在哪個層級。</p>
<table>
  <thead>
      <tr>
          <th>API 欄位</th>
          <th>設計責任</th>
          <th>事故價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Request ID</td>
          <td>對齊客訴、log、trace 與支援查詢</td>
          <td>從用戶回報回到後端事件</td>
      </tr>
      <tr>
          <td>Error code</td>
          <td>穩定分類錯誤語意</td>
          <td>分辨 validation、auth、quota</td>
      </tr>
      <tr>
          <td>Idempotency key</td>
          <td>保護重試與重播</td>
          <td>避免 recovery 時重複副作用</td>
      </tr>
      <tr>
          <td>Semantic status</td>
          <td>表達可重試、已接受、部分完成</td>
          <td>支援客戶端與後端一致處置</td>
      </tr>
      <tr>
          <td>Impact hint</td>
          <td>標示 user-facing 或 internal-only</td>
          <td>支援 severity 初判</td>
      </tr>
  </tbody>
</table>
<p>Request ID 是支援與工程之間的共同鑰匙。客戶只知道某次操作失敗，支援需要 request id 或可查詢等價欄位，才能把客訴轉成 incident intake evidence。</p>
<p>Error code 應該表達穩定語意，並保持內部實作封裝。<code>PAYMENT_PROVIDER_TIMEOUT</code>、<code>QUOTA_EXCEEDED</code>、<code>TOKEN_EXPIRED</code> 這類分類能支援路由；隨程式碼結構變動的錯誤字串則會讓查詢與客戶端處置不穩定。</p>
<p>Idempotency key 是 recovery 的診斷欄位。當 retry、rollback、replay 或補償流程啟動時，團隊需要知道哪些請求已被接受、哪些副作用已完成、哪些可以安全重送。</p>
<h2 id="async-workflow-可診斷性">Async Workflow 可診斷性</h2>
<p>Async workflow 可診斷性的責任是讓事件離開同步 request 後仍保留因果鏈。queue、worker、event handler 與 scheduled job 會把時間拉長、路徑拉開，欄位不足時最容易形成診斷斷點。</p>
<table>
  <thead>
      <tr>
          <th>Async 欄位</th>
          <th>設計責任</th>
          <th>事故價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Message ID</td>
          <td>標識單一訊息</td>
          <td>查詢 delivery、ack、redelivery</td>
      </tr>
      <tr>
          <td>Correlation ID</td>
          <td>串回原始 request 或 workflow</td>
          <td>還原跨流程事件鏈</td>
      </tr>
      <tr>
          <td>Retry count</td>
          <td>記錄處理嘗試次數</td>
          <td>分辨 transient 與 poison case</td>
      </tr>
      <tr>
          <td>DLQ reason</td>
          <td>記錄進入 dead-letter queue 原因</td>
          <td>支援 replay 與修復排序</td>
      </tr>
      <tr>
          <td>Consumer version</td>
          <td>標示處理程式版本</td>
          <td>追查 rollout 或 schema 相容性</td>
      </tr>
  </tbody>
</table>
<p>Message ID 讓團隊能看見單一訊息的生命週期。它應該能串到 publish、broker delivery、consumer ack、redelivery、DLQ 與 replay。</p>
<p>Correlation ID 讓 async 任務保留業務脈絡。缺少 correlation id 時，DLQ dashboard 只能顯示失敗數量，tenant、request 與 user journey 影響範圍會留在人工追查階段。</p>
<p>Retry count 與 DLQ reason 讓回復路徑可排序。高 retry count 可能代表下游依賴失效，也可能代表 poison message；兩者需要不同處置。</p>
<h2 id="dependency-call-可診斷性">Dependency Call 可診斷性</h2>
<p>Dependency call 可診斷性的責任是讓團隊分辨本地問題、下游問題與保護機制啟動。每一次外部依賴呼叫都應留下足夠上下文，支援等待、降級、切換或升級 vendor incident 的判斷。</p>
<table>
  <thead>
      <tr>
          <th>Dependency 欄位</th>
          <th>設計責任</th>
          <th>事故價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Upstream name</td>
          <td>穩定標示依賴服務</td>
          <td>分辨哪個下游失效</td>
      </tr>
      <tr>
          <td>Deadline</td>
          <td>標示呼叫預算</td>
          <td>判斷 timeout 設計是否合理</td>
      </tr>
      <tr>
          <td>Response class</td>
          <td>聚合成功、4xx、5xx、timeout</td>
          <td>支援 error rate 與 vendor triage</td>
      </tr>
      <tr>
          <td>Fallback state</td>
          <td>記錄是否進入降級</td>
          <td>判斷用戶影響是否被吸收</td>
      </tr>
      <tr>
          <td>Circuit state</td>
          <td>記錄 circuit breaker 狀態</td>
          <td>分辨保護機制或真實恢復</td>
      </tr>
  </tbody>
</table>
<p>Upstream name 需要是穩定維度。若每個 adapter 使用不同名稱，dashboard 與 trace 很難把同一個供應商或內部依賴聚合在一起。</p>
<p>Deadline 是 dependency call 的診斷欄位。timeout 發生時，團隊需要知道是下游慢、呼叫預算過短、queue backlog 導致開始太晚，還是 retry policy 放大壓力。</p>
<p>Fallback state 讓事故團隊知道保護是否生效。服務錯誤率可能沒上升，是因為 fallback 吸收了下游失敗；若沒有 fallback 訊號，團隊會低估風險。</p>
<h2 id="error-model-可診斷性">Error Model 可診斷性</h2>
<p>Error model 可診斷性的責任是把錯誤轉成可分類、可路由、可復盤的語意。錯誤不只服務於程式控制流，也服務於事故判讀與使用者影響評估。</p>
<table>
  <thead>
      <tr>
          <th>錯誤層級</th>
          <th>設計責任</th>
          <th>路由方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Validation error</td>
          <td>輸入不符合契約</td>
          <td>API contract / client 修正</td>
      </tr>
      <tr>
          <td>Authorization error</td>
          <td>身分或權限不足</td>
          <td>IAM / security triage</td>
      </tr>
      <tr>
          <td>Dependency error</td>
          <td>外部依賴回應失敗或超時</td>
          <td>vendor / downstream triage</td>
      </tr>
      <tr>
          <td>Capacity error</td>
          <td>資源、queue 或 quota 不足</td>
          <td>capacity / load shedding</td>
      </tr>
      <tr>
          <td>Data consistency error</td>
          <td>寫入、讀取或 migration 不一致</td>
          <td>reliability / migration gate</td>
      </tr>
  </tbody>
</table>
<p>錯誤分類應該讓下一步明確。<code>internal error</code> 適合作為最後防線；主要分類需要支援 on-call 判斷是重試、降級、rollback、升級資安，還是進入資料修復。</p>
<p>Error chain 需要保留上下文。過度包裝錯誤會讓原始 dependency、timeout、request id 或 schema version 消失；完全不包裝則會把底層細節直接丟給外部使用者。好的 error model 會分開內部診斷語意與外部穩定契約。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>事故時只能看到「500」，需要重跑才能定位原因</li>
<li>queue message 進 DLQ 後缺少原始 request 脈絡</li>
<li>外部 API timeout 無 upstream 名稱、耗時與 fallback 狀態</li>
<li>錯誤被包裝後 trace 與 error chain 斷裂</li>
<li>health check 顯示 healthy，但核心旅程已經失效</li>
</ul>
<p>典型情境是 queue 任務在三次重試後進 DLQ，但缺少 request 與 tenant 脈絡。工程師可以看到「失敗很多」，後續需要先補「誰受影響、哪個流程壞、該先修哪一段」的判讀資訊。這就是設計期缺欄位造成的診斷斷點。</p>
<h2 id="控制面">控制面</h2>
<p>Debuggability by design 的控制面是把診斷欄位納入設計審查與契約驗證。可診斷性若只靠事後補 log，會在每次新 API、新 workflow 或新 dependency 上重複遺漏。</p>
<ol>
<li>在 API design review 中檢查 request id、error code、idempotency 與 impact hint。</li>
<li>在 async workflow review 中檢查 message id、correlation、retry 與 DLQ reason。</li>
<li>在 dependency review 中檢查 timeout、deadline、fallback 與 upstream naming。</li>
<li>在 error model review 中檢查分類、內外部語意與 error chain。</li>
<li>在 contract testing 中驗證關鍵診斷欄位與錯誤語意。</li>
</ol>
<p>設計審查需要明確區分必填欄位與情境欄位。request id、trace context、error class 與 owner 通常是跨服務必填；idempotency key、DLQ reason、circuit state 則依 workflow 與依賴類型決定。</p>
<p>Contract testing 可以保護可診斷性。若 API 或 event schema 調整後移除了 correlation id、error code 或 retry metadata，測試應該阻擋這類破壞，因為它會讓事故判讀退回人工拼接。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Debuggability by design 的反模式是把診斷能力推遲到事故後。事故後補 log 可以修下一次，已發生事件的證據缺口則會留在復盤限制中。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>事後補 log</td>
          <td>每次事故才知道缺哪個欄位</td>
          <td>設計審查納入診斷欄位</td>
      </tr>
      <tr>
          <td>錯誤只回 500</td>
          <td>客戶、支援與 on-call 缺少分類</td>
          <td>建立穩定 error code 與 error class</td>
      </tr>
      <tr>
          <td>Async 缺 correlation</td>
          <td>DLQ 只有失敗數量，無業務脈絡</td>
          <td>message schema 保留因果欄位</td>
      </tr>
      <tr>
          <td>Dependency 黑箱</td>
          <td>timeout 只顯示本地錯誤</td>
          <td>adapter 統一 upstream 與 response class</td>
      </tr>
      <tr>
          <td>Diagnostic endpoint 無治理</td>
          <td>查詢有用但風險過高或無審計</td>
          <td>權限、遮罩、速率與 audit log</td>
      </tr>
  </tbody>
</table>
<p>事後補 log 的代價是已發生事故會留下復盤缺口。若缺少原始 request、tenant、message 或 dependency 欄位，工程師只能用間接推論重建時間線。</p>
<p>錯誤只回 500 會把所有問題導向同一條路由。validation、authorization、dependency、capacity 與 data consistency 的處置完全不同，錯誤模型應該支援這些分流。</p>
<p>Diagnostic endpoint 無治理會把可診斷性變成資安風險。診斷入口需要最小權限、資料遮罩、速率限制與 audit log，並且只提供事故判讀需要的 read-only 資訊。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>Debuggability by design 位在 Backend 服務設計層。語言教材負責如何在特定 runtime 中傳遞 context、包裝 error、實作 middleware、處理 async local storage 或 goroutine context；本章負責定義跨語言都需要保留的診斷語意。</p>
<p>同步 runtime 的重點是 thread-local、connection pool 與 blocking dependency call 是否能保留 request context。async runtime 的重點是 task、promise、callback 與 queue boundary 是否能保留 trace context。goroutine 或 lightweight task runtime 的重點是廉價並發是否放大下游壓力，並且是否保留 deadline 與 cancellation。</p>
<p>不同語言可以用不同實作方式，但 API、async workflow、dependency call 與 error model 的診斷責任相同。這也是 Backend 章節保留跨語言抽象的理由。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.1 log schema：定義診斷欄位</li>
<li>04.3 tracing：保留跨服務 context</li>
<li>04.11 telemetry pipeline：確保診斷訊號能被採集</li>
<li>06.10 contract testing：把錯誤模型與外部契約納入驗證</li>
<li>08.18 incident intake：把設計期留下的診斷欄位轉成 evidence</li>
</ul>
]]></content:encoded></item><item><title>4.20 LLM tracing 與 observability</title><link>https://tarrragon.github.io/blog/llm/04-applications/llm-tracing-and-observability/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/llm-tracing-and-observability/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/llm-tracing/" data-link-title="LLM Tracing" data-link-desc="把 LLM 應用的每次 LLM call / tool call / memory op 編成結構化 span、用 OpenTelemetry GenAI semantic conventions 標準化">LLM tracing&lt;/a> 把每次 LLM call / tool call / memory op / handoff 編成結構化 span、用 OpenTelemetry GenAI semantic conventions 標準化、是 production LLM 應用 debug / cost / quality 監控的事實標準。傳統 web app 的字串 logging 抓不到 LLM 應用的關鍵問題 — agent 為什麼選了那條路、reasoning trace 怎麼推導、tool call 為什麼 retry 三次、token 消耗為什麼比預期高 ×3。本章把 LLM tracing 的運作機制、OTel GenAI semconv、三大 use case（cost / latency / failure）跟 production eval 閉環拆成可操作的工程實務。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>解釋 LLM tracing 跟 traditional logging 的差異。&lt;/li>
&lt;li>用 OpenTelemetry GenAI semantic conventions 設計 span 結構。&lt;/li>
&lt;li>用 trace 做 cost / latency 監控跟 failure debug。&lt;/li>
&lt;li>把 production trace 餵回 &lt;a href="https://tarrragon.github.io/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">LLM-as-judge&lt;/a> 做品質迴路。&lt;/li>
&lt;li>對自己應用判斷該用 self-host vs SaaS observability platform。&lt;/li>
&lt;/ol>
&lt;h2 id="traditional-logging-為什麼不夠">Traditional logging 為什麼不夠&lt;/h2>
&lt;p>LLM 應用的 debug 問題對傳統 logging 太抽象：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Logging 看到&lt;/th>
 &lt;th>真正需要的資訊&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Agent 為什麼選 tool A 不選 tool B&lt;/td>
 &lt;td>&lt;code>tool=A&lt;/code> 一行&lt;/td>
 &lt;td>完整 reasoning trace + 當下 context + tool list&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Token cost 為什麼高&lt;/td>
 &lt;td>&lt;code>tokens=15234&lt;/code>&lt;/td>
 &lt;td>Input / output / cached token 分項 + 每 turn 累積&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Why TTFT 5 秒&lt;/td>
 &lt;td>&lt;code>ttft=5012ms&lt;/code>&lt;/td>
 &lt;td>Prefill 跟 cache miss、prompt length、queue time&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tool 為什麼 retry 三次&lt;/td>
 &lt;td>&lt;code>tool error retry&lt;/code>&lt;/td>
 &lt;td>每次 error message + LLM 的判讀 + retry 策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Agent 為什麼 infinite loop&lt;/td>
 &lt;td>大量重複 log&lt;/td>
 &lt;td>每 iteration 的 context + 為什麼沒判 terminate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>LLM tracing 用「結構化 span + parent-child 關係 + 標準化 attribute」直接編碼這些訊息。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/llm/knowledge-cards/llm-tracing/" data-link-title="LLM Tracing" data-link-desc="把 LLM 應用的每次 LLM call / tool call / memory op 編成結構化 span、用 OpenTelemetry GenAI semantic conventions 標準化">LLM tracing</a> 把每次 LLM call / tool call / memory op / handoff 編成結構化 span、用 OpenTelemetry GenAI semantic conventions 標準化、是 production LLM 應用 debug / cost / quality 監控的事實標準。傳統 web app 的字串 logging 抓不到 LLM 應用的關鍵問題 — agent 為什麼選了那條路、reasoning trace 怎麼推導、tool call 為什麼 retry 三次、token 消耗為什麼比預期高 ×3。本章把 LLM tracing 的運作機制、OTel GenAI semconv、三大 use case（cost / latency / failure）跟 production eval 閉環拆成可操作的工程實務。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>解釋 LLM tracing 跟 traditional logging 的差異。</li>
<li>用 OpenTelemetry GenAI semantic conventions 設計 span 結構。</li>
<li>用 trace 做 cost / latency 監控跟 failure debug。</li>
<li>把 production trace 餵回 <a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">LLM-as-judge</a> 做品質迴路。</li>
<li>對自己應用判斷該用 self-host vs SaaS observability platform。</li>
</ol>
<h2 id="traditional-logging-為什麼不夠">Traditional logging 為什麼不夠</h2>
<p>LLM 應用的 debug 問題對傳統 logging 太抽象：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Logging 看到</th>
          <th>真正需要的資訊</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Agent 為什麼選 tool A 不選 tool B</td>
          <td><code>tool=A</code> 一行</td>
          <td>完整 reasoning trace + 當下 context + tool list</td>
      </tr>
      <tr>
          <td>Token cost 為什麼高</td>
          <td><code>tokens=15234</code></td>
          <td>Input / output / cached token 分項 + 每 turn 累積</td>
      </tr>
      <tr>
          <td>Why TTFT 5 秒</td>
          <td><code>ttft=5012ms</code></td>
          <td>Prefill 跟 cache miss、prompt length、queue time</td>
      </tr>
      <tr>
          <td>Tool 為什麼 retry 三次</td>
          <td><code>tool error retry</code></td>
          <td>每次 error message + LLM 的判讀 + retry 策略</td>
      </tr>
      <tr>
          <td>Agent 為什麼 infinite loop</td>
          <td>大量重複 log</td>
          <td>每 iteration 的 context + 為什麼沒判 terminate</td>
      </tr>
  </tbody>
</table>
<p>LLM tracing 用「結構化 span + parent-child 關係 + 標準化 attribute」直接編碼這些訊息。</p>
<h2 id="opentelemetry-genai-semantic-conventions">OpenTelemetry GenAI semantic conventions</h2>
<p>OTel GenAI semconv 是 2024-2025 標準化中的 trace schema。核心概念：</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">Trace（一次 user query 從進來到 response）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  ├── Span: gen_ai.agent.invocation（agent loop iteration 1）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  │     ├── Span: gen_ai.client.operation（LLM call 1）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  │     │     attrs: model, temperature, input_tokens, output_tokens, cache_read
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  │     ├── Span: gen_ai.tool.execution（tool: read_file）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  │     │     attrs: tool_name, input, output, duration
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  │     └── Span: gen_ai.memory.read（retrieval）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  │           attrs: query, top_k, similarity_scores
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  ├── Span: gen_ai.agent.invocation（iteration 2）
</span></span><span class="line"><span class="ln">10</span><span class="cl">  │     └── ...
</span></span><span class="line"><span class="ln">11</span><span class="cl">  └── Span: gen_ai.agent.terminate
</span></span><span class="line"><span class="ln">12</span><span class="cl">        attrs: reason, total_tokens, total_cost</span></span></code></pre></div><p>主要 attribute 分類：</p>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>屬性 prefix</th>
          <th>典型內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Model</td>
          <td><code>gen_ai.request.*</code></td>
          <td>model, temperature, top_p, max_tokens, stream</td>
      </tr>
      <tr>
          <td>Usage</td>
          <td><code>gen_ai.usage.*</code></td>
          <td>input_tokens, output_tokens, cached_tokens</td>
      </tr>
      <tr>
          <td>Response</td>
          <td><code>gen_ai.response.*</code></td>
          <td>finish_reason, id</td>
      </tr>
      <tr>
          <td>Tool</td>
          <td><code>gen_ai.tool.*</code></td>
          <td>name, parameters, result</td>
      </tr>
      <tr>
          <td>Memory</td>
          <td><code>gen_ai.memory.*</code></td>
          <td>operation, store, query, hits</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td><code>gen_ai.cost.*</code></td>
          <td>usd, currency（vendor-specific）</td>
      </tr>
  </tbody>
</table>
<p>實作概要（Python 例）：</p>





<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">opentelemetry</span> <span class="kn">import</span> <span class="n">trace</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">openinference.semconv.trace</span> <span class="kn">import</span> <span class="n">SpanAttributes</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="n">tracer</span> <span class="o">=</span> <span class="n">trace</span><span class="o">.</span><span class="n">get_tracer</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;gen_ai.client.operation&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="n">SpanAttributes</span><span class="o">.</span><span class="n">LLM_MODEL_NAME</span><span class="p">,</span> <span class="s2">&#34;claude-sonnet-4-6&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="n">SpanAttributes</span><span class="o">.</span><span class="n">LLM_TEMPERATURE</span><span class="p">,</span> <span class="mf">0.7</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">llm_client</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="n">messages</span><span class="o">=...</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="n">SpanAttributes</span><span class="o">.</span><span class="n">LLM_TOKEN_COUNT_PROMPT</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">usage</span><span class="o">.</span><span class="n">input_tokens</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="n">SpanAttributes</span><span class="o">.</span><span class="n">LLM_TOKEN_COUNT_COMPLETION</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">usage</span><span class="o">.</span><span class="n">output_tokens</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;gen_ai.usage.cached_tokens&#34;</span><span class="p">,</span> <span class="n">response</span><span class="o">.</span><span class="n">usage</span><span class="o">.</span><span class="n">cache_read_tokens</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">)</span></span></span></code></pre></div><p>實務上多用 framework auto-instrumentation（LangChain / LlamaIndex / Anthropic SDK 都有 OTel integration）、不必手寫 span。</p>
<h2 id="use-case-1cost-monitoring">Use case 1：Cost monitoring</h2>
<p>Trace 是 LLM 應用 cost 監控的核心 — token usage attribute 內建、不必另外算。</p>
<p>實作模式：</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">1. Trace 端記錄 input_tokens / output_tokens / cached_tokens
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. Observability 平台用「per-model pricing table」算出 USD
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. Aggregate by：
</span></span><span class="line"><span class="ln">4</span><span class="cl">   - User（哪個 user 燒最多）
</span></span><span class="line"><span class="ln">5</span><span class="cl">   - Endpoint（哪條 API path 最貴）
</span></span><span class="line"><span class="ln">6</span><span class="cl">   - Feature（哪個 feature 最費 token）
</span></span><span class="line"><span class="ln">7</span><span class="cl">   - Time（哪天 spike）</span></span></code></pre></div><p>典型 dashboard 指標：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>直覺</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Total cost / day</td>
          <td>整體燒錢趨勢</td>
      </tr>
      <tr>
          <td>Cost per user</td>
          <td>找 power user 或 abuse</td>
      </tr>
      <tr>
          <td>Cost per request</td>
          <td>看單 request 平均 cost、設 alert</td>
      </tr>
      <tr>
          <td>Cached / total token ratio</td>
          <td><a href="/blog/llm/knowledge-cards/prompt-cache/" data-link-title="Prompt Cache" data-link-desc="重複出現的 prompt prefix 在推論伺服器或 LLM 服務端被 cache、後續 query 跳過 prefill、大幅降 cost 跟 TTFT">Prompt cache</a> 命中率</td>
      </tr>
      <tr>
          <td>Output / input token ratio</td>
          <td>輸出膨脹率、看 generation length 合理性</td>
      </tr>
  </tbody>
</table>
<h2 id="use-case-2latency--failure-debug">Use case 2：Latency / failure debug</h2>
<p>Trace 自然編碼 latency tree、能定位「哪個 span 卡」：</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">User query → response total: 5.2s
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── Agent iteration 1: 4.8s
</span></span><span class="line"><span class="ln">3</span><span class="cl">│   ├── LLM call (claude): 4.2s     ← 主要時間在這
</span></span><span class="line"><span class="ln">4</span><span class="cl">│   │   - prefill: 3.8s             ← prefill 太久、看 prompt 是否需要 cache
</span></span><span class="line"><span class="ln">5</span><span class="cl">│   │   - generation: 0.4s
</span></span><span class="line"><span class="ln">6</span><span class="cl">│   ├── tool: read_file: 0.5s
</span></span><span class="line"><span class="ln">7</span><span class="cl">│   └── memory: retrieval: 0.1s
</span></span><span class="line"><span class="ln">8</span><span class="cl">└── Agent iteration 2: 0.4s</span></span></code></pre></div><p>從這 trace 看出「90% 時間在 prefill、開 prompt cache 可以救」、不必猜。</p>
<p>Failure debug：</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">User query → response: ERROR
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── Agent iteration 1: success
</span></span><span class="line"><span class="ln">3</span><span class="cl">│   └── LLM call: tool_call(run_bash, cmd=&#34;rm -rf /&#34;)
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── Agent iteration 2: failure
</span></span><span class="line"><span class="ln">5</span><span class="cl">│   └── tool: run_bash: REJECTED by permission system
</span></span><span class="line"><span class="ln">6</span><span class="cl">└── Agent fallback: error response
</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">從 trace 看：tool call 被 permission 擋下、不是 LLM 自己亂、而是 user query 觸發危險 tool call、permission 正確擋下。</span></span></code></pre></div><p>對應 <a href="/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2 tool use 權限模型</a> 跟 <a href="/blog/llm/01-local-llm-services/hands-on/permission-boundary/" data-link-title="Hands-on：Ollama 改檔案 / 寫程式碼的權限邊界在哪" data-link-desc="四組對照實驗：Ollama 自己沒 FS / shell 權限、wrapper 才有；--dry-run / --confirm / --auto 三檔審查粒度的取捨">hands-on permission-boundary</a> 的判讀。</p>
<h2 id="use-case-3production-trace--eval-loop">Use case 3：Production trace → eval loop</h2>
<p>Production trace 是 <a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">LLM-as-judge</a> 的最佳資料來源：</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">Production users
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   ↓ 產生 trace
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">Trace storage（LangSmith / Phoenix / Langfuse）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   ↓ filter（e.g. user thumbs-down 的 trace）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   ↓ sample N 個
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">LLM-as-judge eval
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   ↓ rubric scoring
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">找出系統性問題（哪類 query 品質差）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">10</span><span class="cl">改 system prompt / tool / agent loop
</span></span><span class="line"><span class="ln">11</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">12</span><span class="cl">A/B test on production traces</span></span></code></pre></div><p>這是 <a href="/blog/llm/04-applications/benchmarking-and-evaluation/" data-link-title="4.14 Benchmarking 與評估方法論" data-link-desc="判讀 model card benchmark 數字、做自己工作流的 in-house benchmark、量測本地推論速度的完整方法論">4.14 benchmarking</a> 提的「in-house benchmark」的具體 implementation — production trace 是最真實的 benchmark dataset。</p>
<h2 id="主流平台選型">主流平台選型</h2>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>類型</th>
          <th>強項</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>LangSmith</td>
          <td>SaaS（LangChain 系）</td>
          <td>Auto-instrumentation 強、UI 完整</td>
          <td>LangChain / LangGraph user</td>
      </tr>
      <tr>
          <td>Phoenix</td>
          <td>OSS + SaaS（Arize 系）</td>
          <td>OpenInference 標準、可 self-host</td>
          <td>想 self-host + OTel native</td>
      </tr>
      <tr>
          <td>Langfuse</td>
          <td>OSS + SaaS</td>
          <td>開源強、cost 監控好</td>
          <td>Cost / eval 中心、可 self-host</td>
      </tr>
      <tr>
          <td>Braintrust</td>
          <td>SaaS</td>
          <td>Eval + tracing 一體</td>
          <td>重 eval workflow 的 team</td>
      </tr>
      <tr>
          <td>Datadog APM</td>
          <td>SaaS</td>
          <td>跟 traditional APM 整合</td>
          <td>已用 Datadog、想統一監控</td>
      </tr>
      <tr>
          <td>Logfire</td>
          <td>SaaS（Pydantic）</td>
          <td>簡潔、Python 為主</td>
          <td>Python 為主、輕量</td>
      </tr>
      <tr>
          <td>Self-host OTel + Jaeger</td>
          <td>OSS</td>
          <td>完全 self-host、最便宜</td>
          <td>隱私敏感、cost 敏感、技術強</td>
      </tr>
  </tbody>
</table>
<p>判讀：</p>
<ol>
<li><strong>個人 / 小流量</strong>：SaaS 免費 tier（LangSmith / Langfuse / Phoenix）夠用</li>
<li><strong>隱私敏感（user data 不能離本機）</strong>：Self-host（Langfuse / Phoenix self-hosted、或 OTel + Jaeger）</li>
<li><strong>已有 observability stack</strong>：用 OTel + 現有 Datadog / Grafana、別再加一層</li>
<li><strong>重 eval</strong>：Braintrust / Langfuse 的 eval feature 強</li>
</ol>
<h2 id="跟-49-production-resource-的關係">跟 <a href="/blog/llm/04-applications/production-resource-planning/" data-link-title="4.9 Production 部署的資源評估原理" data-link-desc="從本地單 user 到 production multi-tenant：concurrent users、cost model、observability、SLA、capacity planning 的設計取捨">4.9 production resource</a> 的關係</h2>
<p>4.5 寫 production resource 的 6 個 dimension（concurrency / latency / cost / storage / observability / reliability）、其中 observability 是 4.5 點到、本章展開。讀者讀完 4.5 知道「需要 observability」、本章補「具體怎麼做」。</p>
<h2 id="設計失敗模式">設計失敗模式</h2>
<ol>
<li><strong>過度 instrument</strong>：每個 internal function 都加 span、trace overhead 大、實際 production noise 多</li>
</ol>
<p><strong>緩解</strong>：聚焦 LLM-related 跟跨 service 邊界、internal logic 不必 trace</p>
<ol start="2">
<li><strong>PII / sensitive data 寫進 span attribute</strong>：user prompt、API key、會被 SaaS 平台看到</li>
</ol>
<p><strong>緩解</strong>：Span attribute 過 PII filter、敏感資料 hash / masking、跟 <a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端邊界</a> 結合</p>
<ol start="3">
<li><strong>不 sample</strong>：production 100% trace、storage / cost 爆</li>
</ol>
<p><strong>緩解</strong>：Production sample rate &lt; 10%、error / outlier 100% capture</p>
<ol start="4">
<li><strong>沒設 trace 保留期</strong>：trace 越累積越多、舊 trace 沒人看但仍付儲存</li>
</ol>
<p><strong>緩解</strong>：明確保留 policy（如 7-30 天 hot、之後 archive 或刪）</p>
<ol start="5">
<li><strong>Trace 不跟 metric 串</strong>：trace 是 sample、metric 是 aggregate、debug 要兩個一起看</li>
</ol>
<p><strong>緩解</strong>：cost / latency 也輸出 metric（Prometheus 等）、trace 補 specific instance debug</p>
<h2 id="何時不需要-tracing">何時不需要 tracing</h2>
<ol>
<li><strong>純 demo / 個人玩</strong>：log 字串夠用</li>
<li><strong>單一 LLM call、無 agent loop</strong>：簡單到 grep log 也能 debug</li>
<li><strong>隱私極敏感且不 self-host</strong>：trace 內容流向 SaaS 是邊界、評估 risk</li>
<li><strong>每 request 都 trace 的 overhead &gt; 收益</strong>：超低 latency 場景看是否 worth it</li>
</ol>
<h2 id="何時過時--何時不過時">何時過時 / 何時不過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>LLM tracing 跟 traditional logging 的根本差異</li>
<li>結構化 span + parent-child 關係的 framing</li>
<li>Cost monitoring / latency debug / failure debug 三大 use case</li>
<li>Trace → eval 的閉環概念</li>
<li>5 個設計失敗模式</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>OTel GenAI semconv 的具體 attribute 名稱（仍在 stabilizing）</li>
<li>主流 SaaS 平台（每年 1-2 個新進入者）</li>
<li>Auto-instrumentation 的支援度（持續擴展）</li>
<li>跟具體 framework 的整合方式</li>
</ul>
<h2 id="下一章">下一章</h2>
<p>下一章：<a href="/blog/llm/04-applications/llm-as-judge/" data-link-title="4.21 LLM-as-Judge 評估方法" data-link-desc="LLM 評估 LLM 的 production eval 方法：rubric design、pairwise / direct scoring、三大 bias 緩解、跟 trace 串接的閉環、calibration">4.21 LLM-as-judge 評估方法</a>、把 production trace 變成系統性 eval 的閉環。</p>
]]></content:encoded></item><item><title>4.20 Observability Evidence Package</title><link>https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>evidence package 的責任：把分散的 observability 資料包成可交給 reliability 與 incident response 的證據&lt;/li>
&lt;li>資料來源：log、metric、trace、audit log、dashboard、query、client-side signal、deployment event&lt;/li>
&lt;li>欄位：source、time range、owner、query link、data quality、confidence、known gap、retention&lt;/li>
&lt;li>跟 4.17 的關係：telemetry data quality 提供資料限制，evidence package 提供交接格式&lt;/li>
&lt;li>跟 6.23 的關係：可靠性驗證使用同一格式保存 experiment evidence&lt;/li>
&lt;li>跟 8.18 / 8.19 的關係：事故 intake 與 decision log 使用同一組 evidence link&lt;/li>
&lt;li>反模式：只貼 dashboard 截圖；query 沒有時間窗；evidence 沒標示 sampling / freshness 限制&lt;/li>
&lt;/ul>
&lt;p>Observability &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 的核心是把可觀測資料從「查詢結果」升級成「可交接證據」。事故與驗證需要一組能說明來源、時間窗、可信度、限制與 owner 的 evidence。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Observability evidence package 是可觀測性模組交給可靠性驗證與事故處理的證據包，責任是讓 log、metric、trace 與 audit log 能被重用、回放與復盤。&lt;/p>
&lt;p>這一頁處理的是交接格式。4.17 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality&lt;/a> 說明資料是否可信；evidence package 說明如何把可信度、查詢入口與限制一起交給下游流程。&lt;/p>
&lt;p>證據包的價值在於保存判讀上下文。只有截圖時，讀者看不到 query、時間窗、sampling、資料延遲與 owner；有 evidence package 時，後續 release gate、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 與 post-incident review 才能回放同一組事實。&lt;/p>
&lt;h2 id="evidence-欄位">Evidence 欄位&lt;/h2>
&lt;p>Evidence 欄位的責任是讓每個觀測證據都可查、可解釋、可追蹤。欄位不需要複雜，但要覆蓋事中判讀與事後復盤的最小需求。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>欄位&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Source&lt;/td>
 &lt;td>標示資料來源&lt;/td>
 &lt;td>區分 log、metric、trace、audit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range&lt;/a>&lt;/td>
 &lt;td>標示查詢時間窗&lt;/td>
 &lt;td>對齊 incident timeline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link&lt;/a>&lt;/td>
 &lt;td>保留可重跑查詢&lt;/td>
 &lt;td>支援 handoff 與復盤&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Owner&lt;/td>
 &lt;td>指定可解釋資料的人&lt;/td>
 &lt;td>避免 evidence 失去語意&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality&lt;/a>&lt;/td>
 &lt;td>標示 completeness / freshness&lt;/td>
 &lt;td>防止資料限制被誤讀&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence&lt;/a>&lt;/td>
 &lt;td>標示 confirmed / suspected&lt;/td>
 &lt;td>支援分級與決策&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap&lt;/a>&lt;/td>
 &lt;td>標示 missing signal 或 drift&lt;/td>
 &lt;td>回寫 04 readiness 與 data quality&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention&lt;/td>
 &lt;td>標示保存期限&lt;/td>
 &lt;td>支援 audit、PIR 與長事故&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Source 欄位讓讀者知道 evidence 的能力邊界。Metric 適合看趨勢，log 適合看事件細節，trace 適合看路徑，audit log 適合看責任鏈。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>evidence package 的責任：把分散的 observability 資料包成可交給 reliability 與 incident response 的證據</li>
<li>資料來源：log、metric、trace、audit log、dashboard、query、client-side signal、deployment event</li>
<li>欄位：source、time range、owner、query link、data quality、confidence、known gap、retention</li>
<li>跟 4.17 的關係：telemetry data quality 提供資料限制，evidence package 提供交接格式</li>
<li>跟 6.23 的關係：可靠性驗證使用同一格式保存 experiment evidence</li>
<li>跟 8.18 / 8.19 的關係：事故 intake 與 decision log 使用同一組 evidence link</li>
<li>反模式：只貼 dashboard 截圖；query 沒有時間窗；evidence 沒標示 sampling / freshness 限制</li>
</ul>
<p>Observability <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 的核心是把可觀測資料從「查詢結果」升級成「可交接證據」。事故與驗證需要一組能說明來源、時間窗、可信度、限制與 owner 的 evidence。</p>
<h2 id="概念定位">概念定位</h2>
<p>Observability evidence package 是可觀測性模組交給可靠性驗證與事故處理的證據包，責任是讓 log、metric、trace 與 audit log 能被重用、回放與復盤。</p>
<p>這一頁處理的是交接格式。4.17 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a> 說明資料是否可信；evidence package 說明如何把可信度、查詢入口與限制一起交給下游流程。</p>
<p>證據包的價值在於保存判讀上下文。只有截圖時，讀者看不到 query、時間窗、sampling、資料延遲與 owner；有 evidence package 時，後續 release gate、<a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 與 post-incident review 才能回放同一組事實。</p>
<h2 id="evidence-欄位">Evidence 欄位</h2>
<p>Evidence 欄位的責任是讓每個觀測證據都可查、可解釋、可追蹤。欄位不需要複雜，但要覆蓋事中判讀與事後復盤的最小需求。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>責任</th>
          <th>判讀用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>標示資料來源</td>
          <td>區分 log、metric、trace、audit</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>標示查詢時間窗</td>
          <td>對齊 incident timeline</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>保留可重跑查詢</td>
          <td>支援 handoff 與復盤</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>指定可解釋資料的人</td>
          <td>避免 evidence 失去語意</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>標示 completeness / freshness</td>
          <td>防止資料限制被誤讀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>標示 confirmed / suspected</td>
          <td>支援分級與決策</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>標示 missing signal 或 drift</td>
          <td>回寫 04 readiness 與 data quality</td>
      </tr>
      <tr>
          <td>Retention</td>
          <td>標示保存期限</td>
          <td>支援 audit、PIR 與長事故</td>
      </tr>
  </tbody>
</table>
<p>Source 欄位讓讀者知道 evidence 的能力邊界。Metric 適合看趨勢，log 適合看事件細節，trace 適合看路徑，audit log 適合看責任鏈。</p>
<p><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a> 是 evidence package 的基本欄位。事故前後 30 分鐘、部署期間、DR drill 時窗、<a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 短窗與長窗都需要明確，否則同一張圖可能被不同人解讀成不同結論。</p>
<p><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a> 比截圖更重要。截圖適合溝通當下狀態，query link 才能讓下一班 on-call、可靠性 owner 或 PIR reviewer 重跑同一個判讀。</p>
<p><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a> 欄位讓 evidence 保留限制。sampling ratio、ingest delay、schema drift、log drop、cardinality truncation 與 timestamp skew 都應直接出現在證據包中。</p>
<h2 id="資料來源">資料來源</h2>
<p>Evidence package 的資料來源要按判讀責任分層。每一層回答的問題不同，下游使用時也要保留這個差異。</p>
<table>
  <thead>
      <tr>
          <th>資料來源</th>
          <th>回答問題</th>
          <th>常見限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Log</td>
          <td>單一事件發生了什麼</td>
          <td>schema drift、drop、PII masking</td>
      </tr>
      <tr>
          <td>Metric</td>
          <td>趨勢是否偏離穩態</td>
          <td>聚合粒度、cardinality、延遲</td>
      </tr>
      <tr>
          <td>Trace</td>
          <td>失效卡在哪個服務或依賴邊界</td>
          <td>sampling、async 斷鏈</td>
      </tr>
      <tr>
          <td>Audit log</td>
          <td>高風險操作與責任鏈如何形成</td>
          <td>權限限制、retention、法規要求</td>
      </tr>
      <tr>
          <td>Dashboard</td>
          <td>操作視角如何快速判讀</td>
          <td>面板版本、查詢成本、owner</td>
      </tr>
      <tr>
          <td>Client-side signal</td>
          <td>使用者感知是否和 server 一致</td>
          <td>browser / region / device bias</td>
      </tr>
      <tr>
          <td>Deployment event</td>
          <td>近期變更是否與異常時間線重疊</td>
          <td>rollout 粒度、feature flag owner</td>
      </tr>
  </tbody>
</table>
<p>Log evidence 適合進入 incident intake。它要保留 request id、tenant、region、error class 與 trace id，讓事故候選能被查證。</p>
<p>Metric evidence 適合進入 SLO、release gate 與 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a> 判讀。它要保留時間窗、分母分子、聚合粒度與資料延遲，讓 burn rate 與容量判斷可回放。</p>
<p>Trace evidence 適合支援 dependency 與 async workflow 判讀。它要標示 sampling policy 與缺失 span，讓下游知道 trace 能支持到哪個邊界。</p>
<p>Audit log evidence 適合支援資安、資料修復與高風險操作。它要保留 access path、retention、masking 與 chain of custody 限制。</p>
<h2 id="打包流程">打包流程</h2>
<p>Evidence package 的打包流程是從問題開始。先問下游要做什麼決策，再選擇足以支援該決策的資料與工具入口。</p>
<ol>
<li>定義 evidence 要支援的決策：readiness、release gate、incident intake、decision log 或 PIR。</li>
<li>選擇最小資料集合：metric 看趨勢、log 看事件、trace 看路徑、audit 看責任。</li>
<li>補上 time range、query link、owner 與 data quality。</li>
<li>標示 confidence 與 known gap。</li>
<li>把缺口回寫到 4.16 readiness、4.17 data quality 或 4.18 operating model。</li>
</ol>
<p>Readiness 用的 evidence package 要回答「服務是否能被判讀」。它重視核心旅程、依賴、dashboard、alert、trace 與 owner。</p>
<p>Reliability 用的 evidence package 要回答「驗證是否有結果」。它重視 steady state、stop condition、experiment timeline、SLO burn 與回復訊號。</p>
<p>Incident 用的 evidence package 要回答「事故是否需要啟動、升級或回退」。它重視 source、impact scope、confidence、decision log 與 stakeholder update。</p>
<p>資料庫 migration 用的 evidence package 要回答「資料語意是否能進入下一階段」。它重視 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a>、row count、mismatch sample、replication lag、slow query 與資料限制；完整服務路徑可接到 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。</p>
<h2 id="案例中的證據包判讀">案例中的證據包判讀</h2>
<p>證據包的價值要放回真實事故才看得清楚。Cloudflare 2019 與 AWS S3 2017 都不是「缺資料」，而是「資料若沒被包成可交接證據，決策會慢、通訊會亂、回寫會斷」。</p>
<p>Cloudflare 2019 的第一波判讀來自跨區 CPU、5xx 與 latency 同步惡化。這組訊號如果只有圖表截圖，團隊只能知道「全網變慢」；把 query link、time range、rule rollout event 與 confidence 一起交接，才能快速形成「先回滾規則」的決策。</p>
<p>AWS S3 2017 的關鍵是恢復分層：GET/LIST/DELETE 與 PUT 回線時間不同，且狀態頁通訊入口也受依賴影響。證據包若保留 subsystem 狀態、操作類型影響範圍與已知限制，對外更新才不會把「部分恢復」誤寫成「全面恢復」。</p>
<p>兩個案例共同指向同一個判讀原則：證據包要保留「能支持當下決策」的最小閉環，蒐集越多越好的思路反而製造噪音，至少包含事件時間窗、跨訊號對位、資料限制與決策責任人。</p>
<h2 id="誤判風險與修正路徑">誤判風險與修正路徑</h2>
<p>事故中的誤判多半源自證據包缺少判讀上下文，演算法本身很少是問題。當 evidence 只有結論沒有限制，下游就會把暫時訊號當成穩定事實。</p>
<table>
  <thead>
      <tr>
          <th>誤判場景</th>
          <th>為何會誤判</th>
          <th>修正路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>圖表短暫回穩就宣告恢復</td>
          <td>缺少時間窗與回線連續性門檻</td>
          <td>在 evidence 補 recovery window 與 steady state 對位</td>
      </tr>
      <tr>
          <td>trace 看起來正常</td>
          <td>缺 sampling ratio 與 missing span</td>
          <td>在 evidence 補 data quality 與 known gap</td>
      </tr>
      <tr>
          <td>對外說法過度樂觀</td>
          <td>缺 subsystem 分層狀態與限制說明</td>
          <td>在 evidence 補 scope / limitation / next update</td>
      </tr>
      <tr>
          <td>回滾決策反覆</td>
          <td>缺 deployment event 與影響範圍對位</td>
          <td>在 evidence 補 rollout event、impact scope 與 owner</td>
      </tr>
      <tr>
          <td>復盤找不到依據</td>
          <td>只留截圖，沒有 query 與時間窗</td>
          <td>在 evidence 補 query link 與 retention</td>
      </tr>
  </tbody>
</table>
<p>修正路徑的核心是把 evidence package 當成事故中的工作物，而不是事故後整理物。當下有完整欄位，後續 8.19 決策紀錄才有可回放證據，8.22 回寫才有可追蹤缺口。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>Evidence package 的反模式通常來自把資料貼出來就當作證據交接。證據需要上下文，否則只是一段輸出。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只貼 dashboard 截圖</td>
          <td>事後缺少可重跑查詢</td>
          <td>保留 query link 與 time range</td>
      </tr>
      <tr>
          <td>Query 無時間窗</td>
          <td>同一查詢不同時間跑出不同結論</td>
          <td>標準化 time range</td>
      </tr>
      <tr>
          <td>缺資料品質限制</td>
          <td>sampling / drop / delay 被忽略</td>
          <td>引用 4.17 data quality 欄位</td>
      </tr>
      <tr>
          <td>Evidence 無 owner</td>
          <td>下游無人能解釋欄位語意</td>
          <td>指定 service / platform owner</td>
      </tr>
      <tr>
          <td>Retention 未標示</td>
          <td>PIR 或 audit 時證據已過期</td>
          <td>標示 retention 與保存責任</td>
      </tr>
  </tbody>
</table>
<p>只貼 dashboard 截圖會讓 evidence 失去可回放性。截圖可以當摘要，query、時間窗與資料限制則提供復盤與交接能力。</p>
<p>缺資料品質限制會讓下游高估證據。若 trace sampling 只保留 10%、log pipeline 有 drop、metric 有 ingest delay，這些限制要跟證據一起交接。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>4.16 observability readiness：補 evidence package 所需的訊號入口</li>
<li>4.17 telemetry data quality：標示 completeness、freshness、drift 與 sampling 限制</li>
<li>4.18 operating model：指定 evidence owner、retention 與 review cadence</li>
<li>1.7 Schema Migration Rollout 證據：把 validation query 與資料限制包成 migration gate 可用的證據</li>
<li>6.23 verification evidence handoff：把驗證結果包成同一格式</li>
<li>8.18 incident intake：把 evidence package 轉成事故候選</li>
<li>8.19 incident decision log：把 evidence package 連到事中決策</li>
</ul>
]]></content:encoded></item><item><title>4.21 Rule-level CPU Signal Governance</title><link>https://tarrragon.github.io/blog/backend/04-observability/rule-level-cpu-signal-governance/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/rule-level-cpu-signal-governance/</guid><description>&lt;p>Rule-level CPU signal governance 的核心責任是讓規則與策略執行成本可被提前判讀，避免高成本規則在全域 rollout 後才以 5xx 與 latency 形式被動暴露。&lt;/p>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Rule-level CPU signal governance 是把「哪一條規則在吃 CPU」變成可量測、可回退、可治理的觀測能力，責任是補上服務級 CPU 指標看不到的規則層風險。&lt;/p>
&lt;p>服務級 CPU 只告訴團隊「系統變慢了」，rule-level 訊號才告訴團隊「是哪個規則讓系統變慢」。兩者一起存在，事故才能從症狀快速收斂到可操作原因。&lt;/p>
&lt;h2 id="核心判讀">核心判讀&lt;/h2>
&lt;p>判讀順序是先看服務級異常，再下鑽到規則層成本分佈。若 CPU、latency、5xx 同步惡化，且 rule hit 分佈在短時間發生偏移，通常代表規則層出現新的成本熱點。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>代表意義&lt;/th>
 &lt;th>第一波決策價值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Rule hit rate 突增&lt;/td>
 &lt;td>某規則命中流量異常放大&lt;/td>
 &lt;td>先核對最近規則推送與 traffic pattern&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rule-level CPU p95 / p99 上升&lt;/td>
 &lt;td>規則執行成本惡化&lt;/td>
 &lt;td>先降級或回退高成本規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CPU hotspot 只集中在少數規則&lt;/td>
 &lt;td>問題可收斂到有限規則集合&lt;/td>
 &lt;td>優先處理 top-N 規則&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退後 rule-level 成本快速回穩&lt;/td>
 &lt;td>異常與新規則高度關聯&lt;/td>
 &lt;td>凍結同批 rollout，進入 replay 驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Rule trace 缺失&lt;/td>
 &lt;td>無法確認成本來自哪個分支與 payload&lt;/td>
 &lt;td>先補埋點再擴大 rollout&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="訊號模型">訊號模型&lt;/h2>
&lt;p>Rule-level CPU 訊號模型的重點是同時保留成本、命中與上下文。只有成本沒有命中，無法判斷影響面；只有命中沒有成本，無法判斷風險等級。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號欄位&lt;/th>
 &lt;th>用途&lt;/th>
 &lt;th>常見陷阱&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>rule_id / rule_version&lt;/td>
 &lt;td>對應具體規則版本&lt;/td>
 &lt;td>規則改版未更新版本標記&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>match_count&lt;/td>
 &lt;td>量測命中流量&lt;/td>
 &lt;td>未按 tenant / region 分層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>exec_cpu_ms&lt;/td>
 &lt;td>量測規則執行成本&lt;/td>
 &lt;td>只看平均值，忽略長尾&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>input_class&lt;/td>
 &lt;td>區分 payload 類型與風險來源&lt;/td>
 &lt;td>缺少分類導致 replay 不可重現&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollout_stage&lt;/td>
 &lt;td>對齊分批 rollout 狀態&lt;/td>
 &lt;td>觀測資料無法對應 rollout 階段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>fallback_action&lt;/td>
 &lt;td>記錄降級、旁路或阻擋策略是否觸發&lt;/td>
 &lt;td>事故後難以回放決策&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="控制面">控制面&lt;/h2>
&lt;p>Rule-level CPU signal governance 的控制面是把「測到異常後要怎麼停」直接接到 rollout 流程，而不是只做監控展示。&lt;/p>
&lt;ol>
&lt;li>對高風險規則建立 rule-level CPU baseline 與異常門檻。&lt;/li>
&lt;li>把 rule-level 訊號接到 staged rollout gate。&lt;/li>
&lt;li>對 top-N 高成本規則建立自動降級或回退條件。&lt;/li>
&lt;li>在 evidence package 記錄當次 rollout 的 rule-level 成本分佈與限制。&lt;/li>
&lt;li>在 post-incident review 回寫新 payload 類型與新風險樣式。&lt;/li>
&lt;/ol>
&lt;h2 id="常見反模式">常見反模式&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>反模式&lt;/th>
 &lt;th>表面現象&lt;/th>
 &lt;th>修正方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>只看服務級 CPU&lt;/td>
 &lt;td>知道有問題但找不到高成本規則&lt;/td>
 &lt;td>補 rule_id / version / cost 埋點&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>規則測試只跑功能正確&lt;/td>
 &lt;td>事故時才看見計算成本爆點&lt;/td>
 &lt;td>增加 representative payload replay&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>rollout 與觀測脫鉤&lt;/td>
 &lt;td>分批推送但缺乏階段判讀依據&lt;/td>
 &lt;td>把 rollout_stage 變成必填訊號欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回退無證據包&lt;/td>
 &lt;td>復盤只剩結論，缺成本時間線&lt;/td>
 &lt;td>接 4.20 evidence package&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="案例回扣">案例回扣&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 規則推送安全閘門&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>Cloudflare 2019 事故顯示高成本 regex 可以在全網同步推送下快速放大。Rule-level CPU 訊號治理的價值是把這類風險前移到 rollout 過程，而不是等到全球 5xx 才回頭排查。&lt;/p></description><content:encoded><![CDATA[<p>Rule-level CPU signal governance 的核心責任是讓規則與策略執行成本可被提前判讀，避免高成本規則在全域 rollout 後才以 5xx 與 latency 形式被動暴露。</p>
<h2 id="概念定位">概念定位</h2>
<p>Rule-level CPU signal governance 是把「哪一條規則在吃 CPU」變成可量測、可回退、可治理的觀測能力，責任是補上服務級 CPU 指標看不到的規則層風險。</p>
<p>服務級 CPU 只告訴團隊「系統變慢了」，rule-level 訊號才告訴團隊「是哪個規則讓系統變慢」。兩者一起存在，事故才能從症狀快速收斂到可操作原因。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀順序是先看服務級異常，再下鑽到規則層成本分佈。若 CPU、latency、5xx 同步惡化，且 rule hit 分佈在短時間發生偏移，通常代表規則層出現新的成本熱點。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>代表意義</th>
          <th>第一波決策價值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rule hit rate 突增</td>
          <td>某規則命中流量異常放大</td>
          <td>先核對最近規則推送與 traffic pattern</td>
      </tr>
      <tr>
          <td>Rule-level CPU p95 / p99 上升</td>
          <td>規則執行成本惡化</td>
          <td>先降級或回退高成本規則</td>
      </tr>
      <tr>
          <td>CPU hotspot 只集中在少數規則</td>
          <td>問題可收斂到有限規則集合</td>
          <td>優先處理 top-N 規則</td>
      </tr>
      <tr>
          <td>回退後 rule-level 成本快速回穩</td>
          <td>異常與新規則高度關聯</td>
          <td>凍結同批 rollout，進入 replay 驗證</td>
      </tr>
      <tr>
          <td>Rule trace 缺失</td>
          <td>無法確認成本來自哪個分支與 payload</td>
          <td>先補埋點再擴大 rollout</td>
      </tr>
  </tbody>
</table>
<h2 id="訊號模型">訊號模型</h2>
<p>Rule-level CPU 訊號模型的重點是同時保留成本、命中與上下文。只有成本沒有命中，無法判斷影響面；只有命中沒有成本，無法判斷風險等級。</p>
<table>
  <thead>
      <tr>
          <th>訊號欄位</th>
          <th>用途</th>
          <th>常見陷阱</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>rule_id / rule_version</td>
          <td>對應具體規則版本</td>
          <td>規則改版未更新版本標記</td>
      </tr>
      <tr>
          <td>match_count</td>
          <td>量測命中流量</td>
          <td>未按 tenant / region 分層</td>
      </tr>
      <tr>
          <td>exec_cpu_ms</td>
          <td>量測規則執行成本</td>
          <td>只看平均值，忽略長尾</td>
      </tr>
      <tr>
          <td>input_class</td>
          <td>區分 payload 類型與風險來源</td>
          <td>缺少分類導致 replay 不可重現</td>
      </tr>
      <tr>
          <td>rollout_stage</td>
          <td>對齊分批 rollout 狀態</td>
          <td>觀測資料無法對應 rollout 階段</td>
      </tr>
      <tr>
          <td>fallback_action</td>
          <td>記錄降級、旁路或阻擋策略是否觸發</td>
          <td>事故後難以回放決策</td>
      </tr>
  </tbody>
</table>
<h2 id="控制面">控制面</h2>
<p>Rule-level CPU signal governance 的控制面是把「測到異常後要怎麼停」直接接到 rollout 流程，而不是只做監控展示。</p>
<ol>
<li>對高風險規則建立 rule-level CPU baseline 與異常門檻。</li>
<li>把 rule-level 訊號接到 staged rollout gate。</li>
<li>對 top-N 高成本規則建立自動降級或回退條件。</li>
<li>在 evidence package 記錄當次 rollout 的 rule-level 成本分佈與限制。</li>
<li>在 post-incident review 回寫新 payload 類型與新風險樣式。</li>
</ol>
<h2 id="常見反模式">常見反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>只看服務級 CPU</td>
          <td>知道有問題但找不到高成本規則</td>
          <td>補 rule_id / version / cost 埋點</td>
      </tr>
      <tr>
          <td>規則測試只跑功能正確</td>
          <td>事故時才看見計算成本爆點</td>
          <td>增加 representative payload replay</td>
      </tr>
      <tr>
          <td>rollout 與觀測脫鉤</td>
          <td>分批推送但缺乏階段判讀依據</td>
          <td>把 rollout_stage 變成必填訊號欄位</td>
      </tr>
      <tr>
          <td>回退無證據包</td>
          <td>復盤只剩結論，缺成本時間線</td>
          <td>接 4.20 evidence package</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回扣">案例回扣</h2>
<ul>
<li><a href="/blog/backend/08-incident-response/cases/cloudflare/2019-regex-cpu-outage/" data-link-title="Cloudflare 2019 Regex CPU Outage" data-link-desc="2019-07-02 Cloudflare WAF 規則更新導致全球 CPU 飆升的事故解析：觸發條件、擴散機制、止血決策與可回寫控制面。">Cloudflare 2019 Regex CPU Outage</a></li>
<li><a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24 規則推送安全閘門</a></li>
</ul>
<p>Cloudflare 2019 事故顯示高成本 regex 可以在全網同步推送下快速放大。Rule-level CPU 訊號治理的價值是把這類風險前移到 rollout 過程，而不是等到全球 5xx 才回頭排查。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>04.17： <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a></li>
<li>04.20： <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a></li>
<li>06.24： <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">Rule Rollout Safety Gate</a></li>
<li>08.19： <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></li>
</ul>
]]></content:encoded></item><item><title>4.22 Checkout API Evidence Package 實作示範</title><link>https://tarrragon.github.io/blog/backend/04-observability/checkout-api-evidence-package/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/checkout-api-evidence-package/</guid><description>&lt;p>Checkout API evidence package 的核心責任是把同一條交易路徑的訊號整理成可交接證據，讓放行與事故判斷用到同一組事實。&lt;/p>
&lt;h2 id="服務路徑與邊界">服務路徑與邊界&lt;/h2>
&lt;p>本篇服務路徑是 &lt;code>client -&amp;gt; checkout-api -&amp;gt; payment-adapter -&amp;gt; order-db&lt;/code>。觀測邊界只處理「這條路徑目前是否可判讀」，不處理重試策略與回退決策本身；後者交給 06 與 08。&lt;/p>
&lt;p>要先定義 evidence package 的最小欄位：&lt;code>Source&lt;/code>、&lt;code>Time range&lt;/code>、&lt;code>Query link&lt;/code>、&lt;code>Owner&lt;/code>、&lt;code>Data quality&lt;/code>、&lt;code>Confidence&lt;/code>、&lt;code>Known gap&lt;/code>。這些欄位在事故期與放行期共用，避免兩套語言。&lt;/p>
&lt;h2 id="實作步驟">實作步驟&lt;/h2>
&lt;ol>
&lt;li>固定交易路徑的觀測主鍵：&lt;code>trace_id&lt;/code>、&lt;code>order_id&lt;/code>、&lt;code>tenant_id&lt;/code>、&lt;code>region&lt;/code>。&lt;/li>
&lt;li>建立三組查詢入口：延遲分布（p50/p95/p99）、錯誤率與錯誤類別、下游 payment dependency timeout。&lt;/li>
&lt;li>為每組查詢補欄位：時間窗、資料延遲、採樣比例、目前 owner。&lt;/li>
&lt;li>在 deploy 前把同一份 evidence package 連到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate&lt;/a>。&lt;/li>
&lt;li>事故期間把同一份 evidence package 連到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log&lt;/a>。&lt;/li>
&lt;/ol>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>判讀重點&lt;/th>
 &lt;th>對應動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>p95 latency 升高但 error rate 無明顯變化&lt;/td>
 &lt;td>可能是下游慢查詢或連線池飽和&lt;/td>
 &lt;td>先查 dependency span 與 DB wait&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>payment timeout 增加且 trace 斷在 adapter&lt;/td>
 &lt;td>下游依賴退化，不是本地 CPU 飽和&lt;/td>
 &lt;td>進 6.8 依賴風險 gate，限制放行&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>log 有錯誤但 metric 沒反映&lt;/td>
 &lt;td>訊號覆蓋不一致或聚合粒度不對&lt;/td>
 &lt;td>回寫 data quality，補 query 與聚合維度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>dashboard 正常但客訴增加&lt;/td>
 &lt;td>可觀測性盲區或取樣偏差&lt;/td>
 &lt;td>提升 client-side signal 權重並標示 known gap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>同版不同區域行為差異大&lt;/td>
 &lt;td>區域配置或依賴拓樸差異，非單點程式回歸&lt;/td>
 &lt;td>補 region 維度 evidence，進 8.18 分流 triage&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把 evidence package 寫成 dashboard 截圖集合，會失去可重跑性。沒有 query link 與時間窗，事故交班時很難重建判讀脈絡。&lt;/p>
&lt;p>把 confidence 省略也會導致誤判。事故前期資料常不完整，若不標示 &lt;code>suspected&lt;/code> 與 &lt;code>known gap&lt;/code>，下游決策容易把猜測當成結論。&lt;/p>
&lt;h2 id="案例回寫">案例回寫&lt;/h2>
&lt;p>這條路徑可用 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">GCP 2019 Network Incident&lt;/a> 回寫。先看跨服務訊號如何失真，再回到本章檢查欄位是否能支撐「先分流、再判斷」。&lt;/p>
&lt;p>這個案例主要支撐的是「證據欄位完整度」判讀，不直接支撐 release gate 停損門檻設計；停損規則要回到 6.8。&lt;/p>
&lt;h2 id="跨模組路由">跨模組路由&lt;/h2>
&lt;ol>
&lt;li>與 4.17 的交接：資料限制與偏差回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality&lt;/a>。&lt;/li>
&lt;li>與 6.8 的交接：放行判斷使用同一份 evidence package。&lt;/li>
&lt;li>與 6.23 的交接：驗證證據欄位對齊 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">Verification Evidence Handoff&lt;/a>。&lt;/li>
&lt;li>與 8.19 的交接：事故決策直接引用 evidence link 與 confidence。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>要把證據轉成放行條件，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate 實作示範&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Checkout API evidence package 的核心責任是把同一條交易路徑的訊號整理成可交接證據，讓放行與事故判斷用到同一組事實。</p>
<h2 id="服務路徑與邊界">服務路徑與邊界</h2>
<p>本篇服務路徑是 <code>client -&gt; checkout-api -&gt; payment-adapter -&gt; order-db</code>。觀測邊界只處理「這條路徑目前是否可判讀」，不處理重試策略與回退決策本身；後者交給 06 與 08。</p>
<p>要先定義 evidence package 的最小欄位：<code>Source</code>、<code>Time range</code>、<code>Query link</code>、<code>Owner</code>、<code>Data quality</code>、<code>Confidence</code>、<code>Known gap</code>。這些欄位在事故期與放行期共用，避免兩套語言。</p>
<h2 id="實作步驟">實作步驟</h2>
<ol>
<li>固定交易路徑的觀測主鍵：<code>trace_id</code>、<code>order_id</code>、<code>tenant_id</code>、<code>region</code>。</li>
<li>建立三組查詢入口：延遲分布（p50/p95/p99）、錯誤率與錯誤類別、下游 payment dependency timeout。</li>
<li>為每組查詢補欄位：時間窗、資料延遲、採樣比例、目前 owner。</li>
<li>在 deploy 前把同一份 evidence package 連到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a>。</li>
<li>事故期間把同一份 evidence package 連到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>p95 latency 升高但 error rate 無明顯變化</td>
          <td>可能是下游慢查詢或連線池飽和</td>
          <td>先查 dependency span 與 DB wait</td>
      </tr>
      <tr>
          <td>payment timeout 增加且 trace 斷在 adapter</td>
          <td>下游依賴退化，不是本地 CPU 飽和</td>
          <td>進 6.8 依賴風險 gate，限制放行</td>
      </tr>
      <tr>
          <td>log 有錯誤但 metric 沒反映</td>
          <td>訊號覆蓋不一致或聚合粒度不對</td>
          <td>回寫 data quality，補 query 與聚合維度</td>
      </tr>
      <tr>
          <td>dashboard 正常但客訴增加</td>
          <td>可觀測性盲區或取樣偏差</td>
          <td>提升 client-side signal 權重並標示 known gap</td>
      </tr>
      <tr>
          <td>同版不同區域行為差異大</td>
          <td>區域配置或依賴拓樸差異，非單點程式回歸</td>
          <td>補 region 維度 evidence，進 8.18 分流 triage</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 evidence package 寫成 dashboard 截圖集合，會失去可重跑性。沒有 query link 與時間窗，事故交班時很難重建判讀脈絡。</p>
<p>把 confidence 省略也會導致誤判。事故前期資料常不完整，若不標示 <code>suspected</code> 與 <code>known gap</code>，下游決策容易把猜測當成結論。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>這條路徑可用 <a href="/blog/backend/08-incident-response/cases/gcp/2019-us-network-congestion-multi-service-incident/" data-link-title="GCP 2019 US Network Congestion Multi-service Incident" data-link-desc="2019-06-02 Google Cloud 因美國區域網路壅塞造成多服務退化的事故解析：跨產品依賴、流量控制與區域隔離判讀。">GCP 2019 Network Incident</a> 回寫。先看跨服務訊號如何失真，再回到本章檢查欄位是否能支撐「先分流、再判斷」。</p>
<p>這個案例主要支撐的是「證據欄位完整度」判讀，不直接支撐 release gate 停損門檻設計；停損規則要回到 6.8。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 4.17 的交接：資料限制與偏差回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a>。</li>
<li>與 6.8 的交接：放行判斷使用同一份 evidence package。</li>
<li>與 6.23 的交接：驗證證據欄位對齊 <a href="/blog/backend/06-reliability/verification-evidence-handoff/" data-link-title="6.23 Verification Evidence Handoff" data-link-desc="把 SLO、load、chaos、DR 與 readiness 結果包成 release / incident 可用證據">Verification Evidence Handoff</a>。</li>
<li>與 8.19 的交接：事故決策直接引用 evidence link 與 confidence。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把證據轉成放行條件，接著讀 <a href="/blog/backend/06-reliability/provider-dependency-release-gate/" data-link-title="6.25 Provider Dependency Release Gate 實作示範" data-link-desc="以 payment provider 變更示範 release gate 如何結合 evidence、stop condition 與 rollback window。">6.25 Provider Dependency Release Gate 實作示範</a>。</p>
]]></content:encoded></item><item><title>4.23 觀測查詢設計</title><link>https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/</guid><description>&lt;h2 id="大綱">大綱&lt;/h2>
&lt;ul>
&lt;li>觀測資料的讀寫不對稱：一種寫入路徑對應多種讀取路徑&lt;/li>
&lt;li>三種查詢模式：即席診斷、聚合趨勢、鑑識回溯&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering&lt;/a> 與查詢路由：hot / warm / cold 不只是成本分層、是查詢能力分層&lt;/li>
&lt;li>Pre-aggregation 策略：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 的使用情境與維護成本&lt;/li>
&lt;li>Query 資源治理：priority、queue 分離、timeout 差異化、cost estimation&lt;/li>
&lt;li>觀測領域的讀寫分離：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS&lt;/a> 的特化應用&lt;/li>
&lt;li>反模式：把 raw log 當 OLAP 查、dashboard 查詢直打 raw storage 無 pre-aggregation、recording rule 跟 raw query 重複計算&lt;/li>
&lt;/ul>
&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>觀測查詢設計是把「產生訊號之後怎麼被讀取」當成獨立的系統設計問題。觀測資料的寫入路徑（agent → collector → ingest → storage）在 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline&lt;/a> 處理；本章處理的是讀取路徑 — 從 storage 經 query engine 到 dashboard、alert 與即席查詢的資料流。&lt;/p>
&lt;p>寫入路徑的設計目標是吞吐穩定、schema 一致、成本可控；讀取路徑的設計目標是在不同的時間壓力下，用對的精度取回對的切面。兩者的效能瓶頸不同、擴展方向不同、治理責任也不同。把讀取當寫入的附屬處理，會在流量成長後遇到「寫入正常但查詢崩潰」的局面。&lt;/p>
&lt;h2 id="觀測資料的讀寫不對稱">觀測資料的讀寫不對稱&lt;/h2>
&lt;p>觀測資料有一個 application data 不常見的特性：同一份資料被多種完全不同的查詢形狀讀取，每種查詢的時間壓力、精度需求、結果形狀差距可以到三個數量級。&lt;/p>
&lt;p>寫入面相對單純。不管是 log、metric 還是 trace，寫入都是 append-only、schema 由產生端定義、吞吐由流量決定。寫入路徑的設計問題集中在 cardinality 控制（&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7&lt;/a>）、pipeline 可靠性（&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11&lt;/a>）與 sampling 策略。&lt;/p>
&lt;p>讀取面則至少有三種模式，各自有獨立的 SLA、索引需求與資源消耗模型。把三種模式混在同一個未分化的 query engine 裡，會在任何一種模式的負載增長時拖累其他模式。&lt;/p>
&lt;h2 id="三種查詢模式">三種查詢模式&lt;/h2>
&lt;h3 id="即席診斷">即席診斷&lt;/h3>
&lt;p>事故中的查詢，責任是在秒級內定位問題。&lt;/p>
&lt;p>查詢形狀是精確 filter + 短時間範圍：拿一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 查關聯事件、拿一個 error code 加 time window 撈錯誤樣本、拿一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 展開完整 span tree。&lt;/p>
&lt;p>對儲存的要求：需要 hot tier 的完整索引、完整精度、毫秒到秒級回應。即席查詢幾乎不命中 warm 或 cold tier — 事故通常發生在「現在」或「剛才」。&lt;/p>
&lt;p>資源特性：低頻（事故時才有）、單次掃描量小、但延遲要求最嚴格。事故中的每一秒等待都在消耗 MTTR。&lt;/p>
&lt;h3 id="聚合趨勢">聚合趨勢&lt;/h3>
&lt;p>Dashboard 跟 alert rule 的查詢，責任是提供持續的服務健康視圖。&lt;/p></description><content:encoded><![CDATA[<h2 id="大綱">大綱</h2>
<ul>
<li>觀測資料的讀寫不對稱：一種寫入路徑對應多種讀取路徑</li>
<li>三種查詢模式：即席診斷、聚合趨勢、鑑識回溯</li>
<li><a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering</a> 與查詢路由：hot / warm / cold 不只是成本分層、是查詢能力分層</li>
<li>Pre-aggregation 策略：<a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a>、<a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a>、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 的使用情境與維護成本</li>
<li>Query 資源治理：priority、queue 分離、timeout 差異化、cost estimation</li>
<li>觀測領域的讀寫分離：<a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a> 的特化應用</li>
<li>反模式：把 raw log 當 OLAP 查、dashboard 查詢直打 raw storage 無 pre-aggregation、recording rule 跟 raw query 重複計算</li>
</ul>
<h2 id="概念定位">概念定位</h2>
<p>觀測查詢設計是把「產生訊號之後怎麼被讀取」當成獨立的系統設計問題。觀測資料的寫入路徑（agent → collector → ingest → storage）在 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 處理；本章處理的是讀取路徑 — 從 storage 經 query engine 到 dashboard、alert 與即席查詢的資料流。</p>
<p>寫入路徑的設計目標是吞吐穩定、schema 一致、成本可控；讀取路徑的設計目標是在不同的時間壓力下，用對的精度取回對的切面。兩者的效能瓶頸不同、擴展方向不同、治理責任也不同。把讀取當寫入的附屬處理，會在流量成長後遇到「寫入正常但查詢崩潰」的局面。</p>
<h2 id="觀測資料的讀寫不對稱">觀測資料的讀寫不對稱</h2>
<p>觀測資料有一個 application data 不常見的特性：同一份資料被多種完全不同的查詢形狀讀取，每種查詢的時間壓力、精度需求、結果形狀差距可以到三個數量級。</p>
<p>寫入面相對單純。不管是 log、metric 還是 trace，寫入都是 append-only、schema 由產生端定義、吞吐由流量決定。寫入路徑的設計問題集中在 cardinality 控制（<a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>）、pipeline 可靠性（<a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11</a>）與 sampling 策略。</p>
<p>讀取面則至少有三種模式，各自有獨立的 SLA、索引需求與資源消耗模型。把三種模式混在同一個未分化的 query engine 裡，會在任何一種模式的負載增長時拖累其他模式。</p>
<h2 id="三種查詢模式">三種查詢模式</h2>
<h3 id="即席診斷">即席診斷</h3>
<p>事故中的查詢，責任是在秒級內定位問題。</p>
<p>查詢形狀是精確 filter + 短時間範圍：拿一個 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 查關聯事件、拿一個 error code 加 time window 撈錯誤樣本、拿一個 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 展開完整 span tree。</p>
<p>對儲存的要求：需要 hot tier 的完整索引、完整精度、毫秒到秒級回應。即席查詢幾乎不命中 warm 或 cold tier — 事故通常發生在「現在」或「剛才」。</p>
<p>資源特性：低頻（事故時才有）、單次掃描量小、但延遲要求最嚴格。事故中的每一秒等待都在消耗 MTTR。</p>
<h3 id="聚合趨勢">聚合趨勢</h3>
<p>Dashboard 跟 alert rule 的查詢，責任是提供持續的服務健康視圖。</p>
<p>查詢形狀是 group by + aggregation + 中等時間範圍：過去 5 分鐘的 error rate by service、過去 1 小時的 latency p99 by endpoint、過去 24 小時的 log volume by level。Dashboard 每 30 秒到 1 分鐘刷新，alert rule 每 1 到 5 分鐘 evaluate。</p>
<p>對儲存的要求：可以讀 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 或 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 的預聚合資料，不需要完整精度。延遲容忍比即席查詢寬（秒級到十秒級），但查詢頻率比即席查詢高兩到三個數量級。</p>
<p>資源特性：高頻、穩定、佔 query engine 的常態負載大頭。一個 Grafana dashboard 有 20 個 panel、每 30 秒刷新一次 = 每分鐘 40 個查詢；十個團隊各自有 dashboard = 每分鐘 400 個背景查詢。</p>
<h3 id="鑑識回溯">鑑識回溯</h3>
<p>事後分析、合規稽核與根因調查的查詢，責任是在大時間範圍內還原完整脈絡。</p>
<p>查詢形狀是寬時間範圍 + 條件掃描：過去 30 天某 tenant 的所有 authentication failure、過去 90 天某 API 的 error 分布演變、某次事故前後 48 小時的完整 log 流。</p>
<p>對儲存的要求：會命中 warm 甚至 cold tier。完整性比延遲重要 — 漏掉一筆 audit log 比多等 30 秒更嚴重。可能需要 rehydrate（把 cold tier 歸檔資料暫時載回可查詢狀態）。</p>
<p>資源特性：低頻但單次掃描量極大。一個 cold tier 的全量掃描可能佔用 query engine 數分鐘的計算資源。</p>
<h3 id="三種模式的設計衝突">三種模式的設計衝突</h3>
<p>三種模式搶同一個 query engine 時，聚合趨勢的穩定高頻負載會佔滿常態資源、擠壓即席診斷的突發需求；鑑識回溯的大範圍掃描會吃掉臨時資源、拖慢同時進行的即席查詢。</p>
<p>事故中是衝突最嚴重的時刻：incident commander 在做即席診斷、dashboard 在高頻刷新聚合趨勢、事後調查團隊可能同時在做鑑識回溯。三種負載同時打在同一個 query engine 上，誰先退讓取決於 query 資源治理的設計。</p>
<h2 id="storage-tiering-與查詢路由">Storage tiering 與查詢路由</h2>
<p><a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering</a> 在讀取路徑上的責任不只是降低儲存成本，而是為不同時間範圍的查詢提供對應的查詢能力。每一層的儲存介質、索引密度、資料精度共同決定該層能回答什麼問題。</p>
<h3 id="每一層的查詢能力">每一層的查詢能力</h3>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>查詢延遲</th>
          <th>可用索引</th>
          <th>資料精度</th>
          <th>適合的查詢模式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hot</td>
          <td>毫秒到秒</td>
          <td>完整結構化索引 + 全文索引</td>
          <td>原始精度</td>
          <td>即席診斷</td>
      </tr>
      <tr>
          <td>Warm</td>
          <td>秒到十秒</td>
          <td>結構化索引（可能移除低價值欄位索引）</td>
          <td>原始或輕度 rollup</td>
          <td>聚合趨勢</td>
      </tr>
      <tr>
          <td>Cold</td>
          <td>十秒到分鐘</td>
          <td>最小索引（timestamp + service + tenant）</td>
          <td>rollup 或歸檔</td>
          <td>鑑識回溯</td>
      </tr>
  </tbody>
</table>
<p>查詢跨越 tier 邊界時，回應時間由最慢的 tier 決定。Dashboard 時間範圍從「最近 1 小時」（全部 hot）拉到「最近 30 天」（hot + warm + cold），查詢延遲可能從毫秒跳到分鐘。這個延遲跳變需要在 dashboard UI 上提示使用者。</p>
<h3 id="查詢路由的設計">查詢路由的設計</h3>
<p>查詢路由的責任是根據查詢的時間範圍跟精度需求，自動選擇最合適的 tier 跟資料精度。</p>
<ul>
<li>時間範圍在 hot tier 內：直接查 raw data，完整精度。</li>
<li>時間範圍跨越 hot 跟 warm：hot 部分查 raw data、warm 部分查 rollup series，query engine 負責拼接。</li>
<li>時間範圍延伸到 cold tier：cold 部分需要 rehydrate 或走 object storage 查詢路徑，延遲大幅增加。</li>
</ul>
<p>查詢路由的透明度影響使用者信任。使用者需要知道目前看到的資料是什麼精度、來自哪一層、是否有 freshness lag。Grafana 的 annotation 機制可以在 dashboard 上標示 tier 邊界跟精度切換點，避免使用者把精度變化誤讀成服務異常。</p>
<h3 id="rehydrate-的操作成本">Rehydrate 的操作成本</h3>
<p>Cold tier 的資料通常儲存在 object storage（S3、GCS、Azure Blob），查詢前需要 rehydrate — 把資料從歸檔格式解壓、重建索引、載入到可查詢狀態。這個操作有時間成本（分鐘到小時）、儲存成本（臨時佔用 hot/warm 空間）跟計算成本（CPU 用在解壓跟索引重建）。</p>
<p>Rehydrate 是事故事後分析跟合規稽核的常見操作。設計 tiering 時要把 rehydrate 的 SLA（多久可以完成）、容量（同時可以 rehydrate 多少資料）跟觸發方式（手動 / API / 自動 policy）納入規劃。</p>
<h2 id="pre-aggregation-策略">Pre-aggregation 策略</h2>
<p>Pre-aggregation 是把讀取時的計算成本轉移到寫入時的策略。觀測領域有三種常見的 pre-aggregation 機制，適用場景跟維護成本不同。</p>
<h3 id="recording-rule">Recording rule</h3>
<p><a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">Recording rule</a> 在 TSDB 層定期執行 query expression，把聚合結果寫成新 series。適合 metrics 的高頻聚合查詢（SLO burn rate、error ratio、跨服務 latency summary）。</p>
<p>Recording rule 的維護成本集中在規則增長後的管理。數百條 recording rule 需要命名慣例、版本控制、執行時間監控（rule evaluation duration）與定期審計（是否有 rule 不再被 dashboard 或 alert 引用）。</p>
<h3 id="log-to-metric-轉換">Log-to-metric 轉換</h3>
<p>在 collector 端把高頻 log pattern 轉成 metric。適合「從 log 衍生的聚合查詢」— 例如把 <code>level=error</code> 的 log 計數轉成 error_log_total counter，把 specific exception 的出現率轉成 gauge。</p>
<p>Log-to-metric 的好處是讓 dashboard 讀 metric 而非重掃 log volume。維護成本在於 collector 配置要跟 log schema 保持同步 — log 的 field name 改了，轉換規則沒跟著改，metric 會靜默歸零。</p>
<h3 id="rollup--downsampling">Rollup / downsampling</h3>
<p><a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">Rollup</a> 把高精度時間序列聚合成低精度版本。適合長時間範圍的趨勢查詢（90 天 error rate 趨勢、capacity planning 的年度成長曲線）。</p>
<p>Rollup 的設計關鍵是聚合函數必須按 metric type 選擇。Counter 用 sum、gauge 用 average（或 min/max 保留極端值）、histogram 需要保留 bucket boundary 而非做 average（否則 percentile 計算會失真）。混用聚合函數是 rollup 最常見的 silent data corruption。</p>
<h3 id="pre-aggregation-的維護成本">Pre-aggregation 的維護成本</h3>
<p>Pre-aggregation 不是免費的。每一條 recording rule、每一個 log-to-metric 轉換、每一層 rollup 都需要：</p>
<ul>
<li><strong>儲存空間</strong>：預聚合結果本身佔用 series 或 index 空間，增加 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a> 負擔。</li>
<li><strong>計算資源</strong>：定期執行聚合需要 CPU，rule evaluation lag 會讓 dashboard 看到過期資料。</li>
<li><strong>配置維護</strong>：規則需要跟 schema、label、service 保持同步，漂移會靜默產生錯誤資料。</li>
<li><strong>除錯成本</strong>：dashboard 讀的是 recording rule 輸出，事故時可能需要同時查 raw data 驗證 recording rule 是否正確。</li>
</ul>
<p>設計時的判準是：預聚合的讀取節省是否大於維護成本。高頻讀取（dashboard auto-refresh、alert evaluation）的聚合計算值得 pre-aggregation；低頻讀取（月度報表、偶發 ad-hoc query）直接查 raw data 更簡單。</p>
<h2 id="query-資源治理">Query 資源治理</h2>
<p>觀測平台的 query engine 是共用資源，需要顯式的治理機制避免單一查詢類型或單一使用者耗盡資源。</p>
<h3 id="query-priority-與排程">Query priority 與排程</h3>
<p>Query engine 需要知道每個查詢的優先級，在資源不足時讓高優先查詢先執行。</p>
<table>
  <thead>
      <tr>
          <th>查詢類型</th>
          <th>建議優先級</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Alert evaluate</td>
          <td>最高</td>
          <td>告警延遲直接影響 MTTD，不可因其他查詢排隊而漏發</td>
      </tr>
      <tr>
          <td>即席診斷</td>
          <td>高</td>
          <td>事故中的查詢，每秒延遲消耗 MTTR</td>
      </tr>
      <tr>
          <td>Dashboard 刷新</td>
          <td>中</td>
          <td>穩定背景負載，短暫延遲不影響決策品質</td>
      </tr>
      <tr>
          <td>鑑識回溯</td>
          <td>低</td>
          <td>延遲容忍高，可排程到低負載時段執行</td>
      </tr>
      <tr>
          <td>Ad-hoc 探索</td>
          <td>最低</td>
          <td>非事故的探索性查詢，可被其他類型搶佔</td>
      </tr>
  </tbody>
</table>
<h3 id="query-timeout-差異化">Query timeout 差異化</h3>
<p>不同查詢類型設不同的 timeout：alert evaluation 設短 timeout（30 秒到 1 分鐘，跑不完說明 query 有問題）、即席診斷設中等 timeout（1 到 5 分鐘）、鑑識回溯允許較長 timeout（10 到 30 分鐘）。統一 timeout 會讓鑑識查詢被過早截斷、或讓 alert evaluation 等太久。</p>
<h3 id="query-cost-estimation">Query cost estimation</h3>
<p>在查詢執行前估算掃描量（掃描的 series 數、time range、shard 數），超過閾值的查詢被拒絕或降級。避免單一 heavy query（例：跨所有 service 的 90 天 full-resolution 聚合）拖垮 query engine。</p>
<p>Query cost estimation 對使用者的回饋要足夠清楚。拒絕查詢時要說明「這個查詢預計掃描 N 條 series × M 天，超過單次查詢上限；請縮小時間範圍或增加 filter 條件」，而不是只回 timeout 或 500 error。</p>
<h3 id="query-cache">Query cache</h3>
<p>聚合趨勢查詢的特徵是高頻重複 — 同一個 dashboard panel 每 30 秒查一次，查詢的時間範圍大部分重疊。Query cache 在 query-frontend 層快取最近的聯合結果，下一次刷新只需要增量計算新進的資料區間。</p>
<p>Thanos Query Frontend、Mimir Query Frontend、Grafana Cloud 的 query splitting + caching 都實作這個模式。Cache 的命中率直接影響 query engine 負載 — 高命中率讓 query engine 的常態負載下降、留更多資源給即席查詢。</p>
<h2 id="觀測領域的讀寫分離cqrs-的特化應用">觀測領域的讀寫分離：CQRS 的特化應用</h2>
<p>觀測查詢設計的底層問題是讀寫不對稱 — 寫入跟讀取的形狀、頻率、SLA 都不同，單一模型無法同時服務。這個問題在 application data 層有成熟的設計框架：<a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS</a>。觀測領域面對的是同一類不對稱，但不對稱的程度更極端，實作層級也不同。</p>
<h3 id="觀測場景的不對稱比-application-更極端">觀測場景的不對稱比 application 更極端</h3>
<p><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS 知識卡</a>描述了讀寫不對稱的三個維度（形狀、頻率、SLA）。觀測場景在這三個維度上都比典型 application 更極端：</p>
<p><strong>形狀不對稱</strong>：application 的 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 通常是一到兩種（列表頁、報表）。觀測的讀取面至少三種：即席診斷要精確 filter + 完整精度、聚合趨勢要 group by + pre-aggregated、鑑識回溯要寬範圍 + 完整性優先。三種形狀對索引、精度、儲存層的需求互斥。</p>
<p><strong>頻率不對稱</strong>：application 的讀寫比通常在 10:1 到 100:1 之間。觀測的 dashboard 每 30 秒刷新一次、alert 每分鐘 evaluate、十個團隊各自有 dashboard — 讀取頻率可以到寫入的千倍以上，而且是持續穩定的背景負載而非突發。</p>
<p><strong>SLA 不對稱</strong>：application CQRS 的讀寫 SLA 差距通常在同一個數量級（毫秒 vs 數百毫秒）。觀測的三種讀取模式 SLA 跨三個數量級 — 即席診斷要求毫秒到秒級、聚合趨勢容忍秒到十秒級、鑑識回溯容忍分鐘級。</p>
<h3 id="觀測領域怎麼實作讀寫分離">觀測領域怎麼實作讀寫分離</h3>
<p>CQRS 在 application 層透過 event handler、projector、read store 實作。觀測領域用自己的 first-class 機制做同樣的事：</p>
<table>
  <thead>
      <tr>
          <th>CQRS 概念</th>
          <th>觀測領域的對應</th>
          <th>設計責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Write model</td>
          <td>Raw series / log / span — append-only 寫入</td>
          <td>Schema 穩定、吞吐</td>
      </tr>
      <tr>
          <td>Read model</td>
          <td><a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">Recording rule</a>、<a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a>、log-to-metric 轉換</td>
          <td>讀取最佳化</td>
      </tr>
      <tr>
          <td>Projection</td>
          <td>Collector 端的 aggregation / enrichment / routing</td>
          <td>寫入到讀取模型的轉換</td>
      </tr>
      <tr>
          <td>Event 同步延遲</td>
          <td>Recording rule evaluation lag、rollup delay、buffer freshness lag</td>
          <td>最終一致性的延遲窗口</td>
      </tr>
      <tr>
          <td>多 read store</td>
          <td><a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">Storage tiering</a>（hot / warm / cold 各自支援不同查詢模式）</td>
          <td>不同 SLA 的讀取走不同儲存層</td>
      </tr>
  </tbody>
</table>
<h3 id="cqrs-的代價在觀測領域同樣存在">CQRS 的代價在觀測領域同樣存在</h3>
<p><a href="/blog/backend/knowledge-cards/cqrs/" data-link-title="CQRS" data-link-desc="說明讀寫不對稱時為何需要分離查詢與寫入責任、分離的判準與代價">CQRS 知識卡</a>列出的三項代價（最終一致性、同步可靠性、多模型維護）在觀測場景都找得到對應：</p>
<p><strong>最終一致性</strong>：Recording rule 每 N 秒 evaluate 一次，dashboard 看到的聚合結果落後 raw data。Rollup 的延遲更長。事故中 incident commander 看 dashboard 做決策時，需要知道資料的 freshness — 這就是 CQRS 的 read model 延遲在觀測領域的具體表現。</p>
<p><strong>同步可靠性</strong>：Recording rule evaluation 本身可能失敗（expression 太重跑不完、TSDB 暫時不可用）。Log-to-metric 轉換可能因 schema 漂移而靜默歸零。這些同步失敗跟 application CQRS 的 projector 失敗是同一類問題 — read model 看起來有資料但其實是過期的。</p>
<p><strong>多模型維護</strong>：Metric schema 變更後，raw series、recording rule、rollup、dashboard query 都需要同步更新。Recording rule 引用的 label name 改了沒跟著改，aggregation 結果會靜默錯誤。這跟 application 的「schema migration 要同時更新 write model 跟所有 read model」是同一個維護負擔。</p>
<h3 id="術語邊界">術語邊界</h3>
<p>觀測領域的讀寫分離跟 CQRS 概念對應，但在業界溝通中直接說「log 的 CQRS」或「metrics 的 CQRS」會造成混淆。觀測領域有自己的 first-class 術語（recording rule、rollup、tiering、query routing），跟 application CQRS 的術語（command、query、projection、read model）平行但不互通。</p>
<p>理解 CQRS 的讀者可以把觀測查詢設計視為「infrastructure-level 的讀寫分離」，同樣的設計原則（分離的動機、最終一致性的代價、多模型維護的負擔）在不同層級重複出現。但設計決策時要用觀測領域的術語，把 recording rule 跟 rollup 當第一等公民，而非 CQRS 的衍生品。</p>
<h2 id="核心判讀">核心判讀</h2>
<p>判讀觀測查詢設計時，先看三種查詢模式是否有對應的資源與資料形狀，再看 pre-aggregation 跟 tiering 是否對齊實際查詢負載。</p>
<p>重點訊號包括：</p>
<ul>
<li>即席查詢在事故中的延遲是否在秒級以內</li>
<li>Dashboard 刷新是否佔用過多 query engine 資源</li>
<li>長時間範圍查詢是否有 rollup / recording rule 支撐</li>
<li>Storage tiering 的查詢路由是否對使用者透明</li>
<li>Alert evaluation 是否有最高 query priority</li>
<li>Pre-aggregation 規則是否跟 schema 保持同步</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<ul>
<li>Dashboard 載入時間持續退化、panel timeout 增加</li>
<li>Alert rule evaluation duration 成長、偶發 missed evaluation</li>
<li>事故中即席查詢被 dashboard 背景負載擠壓</li>
<li>長時間範圍的查詢精度突變但使用者不知道</li>
<li>Recording rule 輸出跟 raw query 結果不一致</li>
<li>Rehydrate 需求頻繁但沒有預設流程</li>
<li>Query engine CPU 被少數 heavy query 佔滿</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>表面現象</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Raw log 當 OLAP 查</td>
          <td>聚合查詢掃 TB 級 log、timeout</td>
          <td>用 log-to-metric 轉換把常用聚合推到 metric 層</td>
      </tr>
      <tr>
          <td>Dashboard 直打 raw storage</td>
          <td>Panel 載入慢、query engine 過載</td>
          <td>用 recording rule / rollup 支撐高頻 panel</td>
      </tr>
      <tr>
          <td>Recording rule 跟 raw query 重複</td>
          <td>同一個指標有兩條查詢路徑、數值不一致</td>
          <td>統一入口：dashboard 讀 recording rule、ad-hoc 讀 raw</td>
      </tr>
      <tr>
          <td>所有查詢同一個 priority</td>
          <td>Alert 被 dashboard 查詢排隊延遲</td>
          <td>Query priority 分級、alert evaluation 最高</td>
      </tr>
      <tr>
          <td>Tier 邊界對使用者不透明</td>
          <td>拉長時間範圍時數值突變但不知為何</td>
          <td>Dashboard 標示 tier 邊界跟精度切換</td>
      </tr>
      <tr>
          <td>Rollup 聚合函數混用</td>
          <td>Histogram percentile 在長時間視圖被壓平</td>
          <td>按 metric type 指定聚合函數、histogram 保留 bucket</td>
      </tr>
      <tr>
          <td>所有訊號同一個 tier 邊界</td>
          <td>高價值訊號過早退化、低價值訊號佔 hot</td>
          <td>依訊號優先級設差異化 tier 邊界</td>
      </tr>
  </tbody>
</table>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>：log 的即席 / 聚合 / 鑑識三種查詢模式細節</li>
<li><a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics</a>：metrics 的 recording rule 與 rollup 設計</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality / cost</a>：storage tiering 對查詢能力的影響</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a>：讀取路徑作為 pipeline 的延伸</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>：query 資源的成本歸屬</li>
<li><a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>：pre-aggregation 與 raw data 的一致性驗證</li>
<li><a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a>：query 資源治理的 ownership</li>
<li><a href="/blog/monitoring/04-collector/read-write-separation/" data-link-title="讀寫分離與查詢擴展" data-link-desc="Monitor 在 PostgreSQL 層之後的讀寫競爭問題、Read Replica 分離策略、CQRS 判讀訊號">Monitoring 讀寫分離</a>：Monitor 專案的讀寫分離具體應用</li>
</ul>
]]></content:encoded></item><item><title>4.24 Client-to-Server 端到端觀測串接</title><link>https://tarrragon.github.io/blog/backend/04-observability/client-server-trace-integration/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/client-server-trace-integration/</guid><description>&lt;p>Client-to-server 端到端觀測串接的核心責任是讓一次使用者操作的完整路徑 — 從 browser click 到 server 處理到 response rendering — 可以用同一個 trace ID 串起來。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side / Synthetic / RUM&lt;/a> 講的是概念和 vendor 定位；本篇走完一個具體場景的實作鏈路。&lt;a href="https://tarrragon.github.io/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">Monitoring 模組 03 SDK 設計&lt;/a> 講的是 client 端怎麼埋點；本篇講 server 端怎麼接收和整合。&lt;/p>
&lt;h2 id="完整鏈路">完整鏈路&lt;/h2>
&lt;p>以使用者在 web app 點擊「結帳」為例，一次操作產生的觀測鏈路：&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">Browser: user clicks &amp;#34;checkout&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> → RUM SDK 建立 client span（type: resource / xhr）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> → HTTP POST /api/checkout + W3C traceparent header
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> → Server middleware 提取 trace context
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> → Server 建立 child span（checkout-handler）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> → DB query span（order insert）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> → Cache span（inventory check）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> → Queue span（event publish）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> → Server 回 200 + response body
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> → Browser 收到 response → resource timing 結束
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> → RUM SDK 關閉 client span（記錄 duration + status）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> → 統一 trace waterfall：client span 是 root、server spans 是 children&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>鏈路的每一段都需要 trace context 正確傳遞。任何一段斷掉，trace waterfall 就會出現孤立的 span — server 端看到的 trace 跟 client 端看到的 trace 是兩條不相關的紀錄。&lt;/p>
&lt;h2 id="trace-context-propagation">Trace context propagation&lt;/h2>
&lt;h3 id="w3c-traceparent-header">W3C traceparent header&lt;/h3>
&lt;p>W3C Trace Context 是跨 vendor 的標準 propagation 格式。Header 長這樣：&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">traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> │ │ │ │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> │ trace-id (32 hex) parent-id (16 hex) flags
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> version&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>RUM SDK 在發起 XHR / fetch 時把 &lt;code>traceparent&lt;/code> 注入 request header。Server 的 trace SDK 從 header 提取 trace-id 和 parent-id，建立 child span。&lt;/p></description><content:encoded><![CDATA[<p>Client-to-server 端到端觀測串接的核心責任是讓一次使用者操作的完整路徑 — 從 browser click 到 server 處理到 response rendering — 可以用同一個 trace ID 串起來。<a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side / Synthetic / RUM</a> 講的是概念和 vendor 定位；本篇走完一個具體場景的實作鏈路。<a href="/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">Monitoring 模組 03 SDK 設計</a> 講的是 client 端怎麼埋點；本篇講 server 端怎麼接收和整合。</p>
<h2 id="完整鏈路">完整鏈路</h2>
<p>以使用者在 web app 點擊「結帳」為例，一次操作產生的觀測鏈路：</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">Browser: user clicks &#34;checkout&#34;
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  → RUM SDK 建立 client span（type: resource / xhr）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  → HTTP POST /api/checkout + W3C traceparent header
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    → Server middleware 提取 trace context
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    → Server 建立 child span（checkout-handler）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">      → DB query span（order insert）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">      → Cache span（inventory check）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">      → Queue span（event publish）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    → Server 回 200 + response body
</span></span><span class="line"><span class="ln">10</span><span class="cl">  → Browser 收到 response → resource timing 結束
</span></span><span class="line"><span class="ln">11</span><span class="cl">  → RUM SDK 關閉 client span（記錄 duration + status）
</span></span><span class="line"><span class="ln">12</span><span class="cl">  → 統一 trace waterfall：client span 是 root、server spans 是 children</span></span></code></pre></div><p>鏈路的每一段都需要 trace context 正確傳遞。任何一段斷掉，trace waterfall 就會出現孤立的 span — server 端看到的 trace 跟 client 端看到的 trace 是兩條不相關的紀錄。</p>
<h2 id="trace-context-propagation">Trace context propagation</h2>
<h3 id="w3c-traceparent-header">W3C traceparent header</h3>
<p>W3C Trace Context 是跨 vendor 的標準 propagation 格式。Header 長這樣：</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">traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
</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">              │  trace-id (32 hex)                 parent-id (16 hex) flags
</span></span><span class="line"><span class="ln">4</span><span class="cl">              version</span></span></code></pre></div><p>RUM SDK 在發起 XHR / fetch 時把 <code>traceparent</code> 注入 request header。Server 的 trace SDK 從 header 提取 trace-id 和 parent-id，建立 child span。</p>
<h3 id="client-端注入">Client 端注入</h3>
<p>各 RUM SDK 的注入方式：</p>
<table>
  <thead>
      <tr>
          <th>SDK</th>
          <th>注入機制</th>
          <th>配置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog RUM</td>
          <td>自動 patch XHR / fetch，注入 <code>x-datadog-*</code> + 可選 <code>traceparent</code></td>
          <td><code>allowedTracingUrls</code> 設定允許注入的 domain</td>
      </tr>
      <tr>
          <td>Sentry browser</td>
          <td>自動 patch fetch / XHR，注入 <code>sentry-trace</code> + <code>baggage</code> + 可選 <code>traceparent</code></td>
          <td><code>tracePropagationTargets</code> 設定目標 URL</td>
      </tr>
      <tr>
          <td>OTel browser SDK</td>
          <td>透過 <code>XMLHttpRequestInstrumentation</code> / <code>FetchInstrumentation</code> 注入 <code>traceparent</code></td>
          <td><code>propagateTraceHeaderCorsUrls</code> 設定 CORS 允許的 URL</td>
      </tr>
  </tbody>
</table>
<p>三者的共同模式：只對設定的 domain 注入 trace header。不設定白名單時，header 不會被注入到第三方 API（避免 information leakage）。</p>
<h3 id="server-端提取">Server 端提取</h3>
<p>Server 端的 trace SDK（OTel auto-instrumentation 或 vendor agent）從 incoming request 的 header 提取 trace context：</p>





<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="c1"># OTel Python 範例 — auto-instrumentation 自動處理</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 不需要手動提取，middleware 自動讀 traceparent header</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 建立的 span 會繼承 client 傳來的 trace-id 和 parent-id</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"># 手動提取（不用 auto-instrumentation 時）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.propagate</span> <span class="kn">import</span> <span class="n">extract</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">ctx</span> <span class="o">=</span> <span class="n">extract</span><span class="p">(</span><span class="n">carrier</span><span class="o">=</span><span class="n">request</span><span class="o">.</span><span class="n">headers</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;checkout-handler&#34;</span><span class="p">,</span> <span class="n">context</span><span class="o">=</span><span class="n">ctx</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1"># server logic</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">pass</span></span></span></code></pre></div><h3 id="cors-限制">CORS 限制</h3>
<p>跨域請求時，browser 的 CORS preflight 會阻止非標準 header。Server 需要明確允許 trace header：</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">Access-Control-Allow-Headers: traceparent, tracestate, sentry-trace, baggage</span></span></code></pre></div><p>CORS 是 client-server trace 串接最常見的斷裂原因。Server 沒有回 <code>Access-Control-Allow-Headers: traceparent</code> 時，browser 會 strip 掉 trace header，server 端收到的 request 沒有 trace context，建立的 span 成為新的 root — 跟 client span 斷裂。</p>
<h2 id="跨層-correlation-設計">跨層 correlation 設計</h2>
<h3 id="trace-id-串接">Trace ID 串接</h3>
<p>統一 trace-id 是最基本的 correlation。同一個 trace-id 下的所有 span（client + server）可以在 trace backend 的 waterfall view 裡按時間排列，看到完整的 request 路徑。</p>
<h3 id="session-跟-transaction-的-mapping">Session 跟 transaction 的 mapping</h3>
<p>RUM SDK 的 session（使用者的一次造訪）包含多個 user action，每個 action 可能觸發多個 HTTP request。Mapping 關係：</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">RUM session
</span></span><span class="line"><span class="ln">2</span><span class="cl">  └── user action (click &#34;checkout&#34;)
</span></span><span class="line"><span class="ln">3</span><span class="cl">        ├── HTTP request /api/checkout  →  server transaction (trace)
</span></span><span class="line"><span class="ln">4</span><span class="cl">        ├── HTTP request /api/inventory →  server transaction (trace)
</span></span><span class="line"><span class="ln">5</span><span class="cl">        └── client-side rendering time</span></span></code></pre></div><p>Datadog RUM 和 Sentry 都支援從 session replay 點進去看對應的 server trace。這個 mapping 靠的是 RUM event 裡記錄的 trace-id，跟 server trace backend 裡的同一個 trace-id 做 join。</p>
<h3 id="breadcrumbs-跟-server-log-的時間對齊">Breadcrumbs 跟 server log 的時間對齊</h3>
<p>RUM SDK 收集的 breadcrumbs（使用者操作序列：page view → button click → form submit）跟 server-side log 的 timestamp 需要可比對。時間對齊的前提是 client 和 server 的 clock 差距在可接受範圍（通常 &lt; 1s）。</p>
<p>NTP 同步的 server 端 clock 通常精準。Client 端（browser）依賴使用者裝置的系統時間，可能偏差數秒到數分鐘。RUM SDK 通常會記錄 relative timing（相對於 session 開始的 offset），而非絕對 timestamp，來降低 clock skew 的影響。</p>
<h3 id="error-correlation">Error correlation</h3>
<p>Client-side JS error 跟 server-side 5xx 可能是同一個問題的兩面。Correlation 方式：</p>
<ul>
<li><strong>同一 trace-id</strong>：client error 發生在某個 HTTP request 的 response 處理中，該 request 的 trace-id 跟 server-side 500 的 trace-id 相同 — 直接 correlation</li>
<li><strong>時間窗 + endpoint</strong>：client error 沒有 trace-id（例如 CORS block 導致 request 沒發出），用時間窗 + endpoint 模式做 fuzzy correlation</li>
<li><strong>Server 無異常但 client 報錯</strong>：client-side rendering error（JSON parse failure、type error），server 端看不到 — 需要 RUM 獨立分析</li>
</ul>
<h2 id="evidence-package-整合">Evidence package 整合</h2>
<p>把 client-side 訊號納入 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a> 時，需要額外記錄：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Client-side 補充</th>
          <th>為什麼需要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>標註 &ldquo;RUM&rdquo; 或 &ldquo;Synthetic&rdquo;</td>
          <td>區分 server-side metrics 和 client-side metrics</td>
      </tr>
      <tr>
          <td>Latency</td>
          <td>Client perceived latency（含 DNS + network + server + rendering）</td>
          <td>跟 server-side latency 差異是 network + rendering 時間</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>Trace sampling 不一致</td>
          <td>Client 和 server 可能各自取樣，同一個 request 不一定兩邊都有</td>
      </tr>
      <tr>
          <td>Confidence</td>
          <td>Client clock skew 可能影響 timestamp precision</td>
          <td>標注 client timestamp 的精確度限制</td>
      </tr>
  </tbody>
</table>
<p>Client perceived latency 跟 server-side latency 的差異本身就是一個觀測訊號。差異穩定在 50ms 是正常的 network overhead；差異突然從 50ms 跳到 500ms 代表網路或 CDN 出了問題 — 而這個問題 server-side dashboard 完全看不到。</p>
<h2 id="失敗場景判讀">失敗場景判讀</h2>
<table>
  <thead>
      <tr>
          <th>失敗訊號</th>
          <th>判讀</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Client span 存在但 server span 缺失</td>
          <td>Trace context header 沒被 propagate — 最常見原因是 CORS block</td>
          <td>檢查 <code>Access-Control-Allow-Headers</code> 是否包含 <code>traceparent</code>；檢查 RUM SDK 的 <code>allowedTracingUrls</code> 設定</td>
      </tr>
      <tr>
          <td>Server 正常但 client perceived latency 高</td>
          <td>網路延遲或 client rendering 慢</td>
          <td>看 RUM 的 resource timing breakdown（DNS / TCP / TLS / TTFB / download / render）</td>
      </tr>
      <tr>
          <td>Client error 但 server 無對應 request</td>
          <td>Request 沒發出 — client-side validation 擋掉或 network offline</td>
          <td>看 RUM breadcrumbs 確認 request 是否有送出；檢查 navigator.onLine 狀態</td>
      </tr>
      <tr>
          <td>Trace sampling 不一致</td>
          <td>Client 取樣到但 server 沒取樣到同一個 request</td>
          <td>統一 sampling decision — 用 head-based sampling（decision 在 trace 起點做、propagate 到下游）</td>
      </tr>
      <tr>
          <td>Client 和 server 的 error count 對不上</td>
          <td>Client 包含 JS rendering error（server 看不到）；server 包含非 user-facing 的背景 job error</td>
          <td>分開看：API error 用 trace correlation 比對、non-API error 各自歸類</td>
      </tr>
  </tbody>
</table>
<h2 id="vendor-整合模式">Vendor 整合模式</h2>
<table>
  <thead>
      <tr>
          <th>組合</th>
          <th>串接方式</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Datadog RUM + Datadog APM</td>
          <td>原生 — 同一個 Datadog org 裡 client 跟 server trace 自動關聯</td>
          <td>兩邊都要 Datadog plan</td>
      </tr>
      <tr>
          <td>Sentry browser + Sentry server</td>
          <td>原生 — <code>sentry-trace</code> header propagation</td>
          <td>Performance monitoring 需要 Sentry paid plan</td>
      </tr>
      <tr>
          <td>OTel browser SDK + OTel server SDK</td>
          <td>W3C <code>traceparent</code> — vendor-neutral 標準</td>
          <td>Browser SDK 較新、instrumentation 覆蓋度不如 server 端成熟</td>
      </tr>
      <tr>
          <td>混合（Sentry browser + Datadog server）</td>
          <td>手動橋接 — 確保雙方都支援 W3C <code>traceparent</code></td>
          <td>Trace context format 要一致；session-level correlation 需自建</td>
      </tr>
  </tbody>
</table>
<p>同 vendor 組合的串接最自然。跨 vendor 組合只要雙方都支援 W3C Trace Context，trace-level correlation 可以通；但 session-level 的功能（session replay → server trace）需要同 vendor 才有。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/client-side-monitoring/" data-link-title="4.10 Client-side / Synthetic / RUM" data-link-desc="補 server-side 看不到的 user perceived 訊號">4.10 Client-side / Synthetic / RUM</a>：概念定位和 vendor 選型</li>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 Tracing Context</a>：server-side trace context 設計</li>
<li><a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22 Checkout API Evidence Package</a>：evidence 整合到 release gate</li>
<li><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>：evidence 欄位標準</li>
<li><a href="/blog/monitoring/03-sdk-design/" data-link-title="模組三：SDK 設計模式" data-link-desc="跨平台 SDK 的自動攔截、手動上報、攢批送出、離線 buffer 設計">Monitoring 03 SDK 設計</a>：client-side SDK 埋點設計</li>
<li><a href="/blog/monitoring/06-commercial-comparison/" data-link-title="模組六：商業方案對照" data-link-desc="Sentry / Crashlytics / Datadog RUM / Mixpanel — 自架 vs 商業的功能和成本取捨">Monitoring 06 商業方案</a>：Sentry / Datadog RUM 的 client-side 能力比較</li>
<li><a href="/blog/monitoring/telemetry-data-dual-use/" data-link-title="監控資料的雙重用途：行為分析與訊號治理" data-link-desc="同一份 event data 如何同時服務行為分析（funnel / cohort / attribution）和訊號治理（cardinality / cost / signal governance）— 格式交叉、治理衝突與分流架構">監控資料的雙重用途</a>：同一份 event data 如何同時服務行為分析與訊號治理</li>
</ul>
]]></content:encoded></item><item><title>Log Schema</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/</guid><description>&lt;p>Log schema 的核心概念是「用穩定欄位描述 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 事件」。結構化 log 應包含時間、等級、服務名稱、事件名稱、錯誤類型、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id&lt;/a>、tenant、資源 ID 與處理結果。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Log schema 是可觀測性的事件明細層。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics&lt;/a> 提供趨勢，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 提供跨服務路徑，log 提供單一事件的上下文與細節。三者透過共享欄位（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>、correlation id）互相連結。&lt;/p>
&lt;p>Log schema 的穩定性決定了查詢的效率 — 跨服務使用不同的欄位名稱記錄同一概念時，查詢需要窮舉所有變體。見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema 與搜尋規劃&lt;/a>。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 log schema 的訊號是事故時查詢依賴全文搜尋或逐台機器翻查。Checkout 失敗時，穩定欄位讓團隊用 order_id、payment_id、request_id 在秒級內追出同一流程的所有紀錄。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Log schema 要控制欄位名稱（跨服務統一）、錯誤分類（error type / error code 有界而非 free-form message）、敏感資料遮罩（API key / token / PII 在寫入前 redact）與索引成本（高 cardinality 欄位不全部建索引）。高流量服務還要管理 log level、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 與查詢成本。&lt;/p></description><content:encoded><![CDATA[<p>Log schema 的核心概念是「用穩定欄位描述 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 事件」。結構化 log 應包含時間、等級、服務名稱、事件名稱、錯誤類型、<a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a>、<a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a>、tenant、資源 ID 與處理結果。</p>
<h2 id="概念位置">概念位置</h2>
<p>Log schema 是可觀測性的事件明細層。<a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">Metrics</a> 提供趨勢，<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 提供跨服務路徑，log 提供單一事件的上下文與細節。三者透過共享欄位（<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>、correlation id）互相連結。</p>
<p>Log schema 的穩定性決定了查詢的效率 — 跨服務使用不同的欄位名稱記錄同一概念時，查詢需要窮舉所有變體。見 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema 與搜尋規劃</a>。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 log schema 的訊號是事故時查詢依賴全文搜尋或逐台機器翻查。Checkout 失敗時，穩定欄位讓團隊用 order_id、payment_id、request_id 在秒級內追出同一流程的所有紀錄。</p>
<h2 id="設計責任">設計責任</h2>
<p>Log schema 要控制欄位名稱（跨服務統一）、錯誤分類（error type / error code 有界而非 free-form message）、敏感資料遮罩（API key / token / PII 在寫入前 redact）與索引成本（高 cardinality 欄位不全部建索引）。高流量服務還要管理 log level、<a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a>、<a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 與查詢成本。</p>
]]></content:encoded></item><item><title>Metrics</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/</guid><description>&lt;p>Metrics 的核心概念是「用可聚合數值描述系統行為的時間序列」。常見指標包括 request count、error rate、latency、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a>、CPU、memory、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a> 使用量與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache hit rate&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Metrics 是趨勢觀測跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 的基礎。跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>（事件明細）跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>（跨服務路徑）互補：log 適合查單一事件的細節，trace 適合看一次 request 的路徑，metrics 適合回答「服務是否在變慢、錯誤是否在增加、容量是否接近上限」。&lt;/p>
&lt;p>Metrics 有三種基本型別：counter（累積計數、只增不減）、gauge（瞬間值、可增可減）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a>（分布、支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a> 計算）。選錯型別會讓後面的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 跟 alert 建立在錯誤訊號上。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 metrics 的訊號是團隊需要在使用者回報前知道服務異常。Checkout p95 latency 上升、Redis &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 增加、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> lag 擴大，都應先從 metrics 看見。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Metrics 設計要選擇正確的型別（latency 用 histogram、request count 用 counter、connection pool size 用 gauge）跟有界的 label（service、method、status_code，排除 user_id / request_id）。重要指標要能對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>；高 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a> label 會推高儲存跟查詢成本。Metrics 的聚合查詢跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Metrics 的核心概念是「用可聚合數值描述系統行為的時間序列」。常見指標包括 request count、error rate、latency、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a>、CPU、memory、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a> 使用量與 <a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache hit rate</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Metrics 是趨勢觀測跟 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的基礎。跟 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>（事件明細）跟 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>（跨服務路徑）互補：log 適合查單一事件的細節，trace 適合看一次 request 的路徑，metrics 適合回答「服務是否在變慢、錯誤是否在增加、容量是否接近上限」。</p>
<p>Metrics 有三種基本型別：counter（累積計數、只增不減）、gauge（瞬間值、可增可減）、<a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a>（分布、支援 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a> 計算）。選錯型別會讓後面的 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a>、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 跟 alert 建立在錯誤訊號上。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 metrics 的訊號是團隊需要在使用者回報前知道服務異常。Checkout p95 latency 上升、Redis <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 增加、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> lag 擴大，都應先從 metrics 看見。</p>
<h2 id="設計責任">設計責任</h2>
<p>Metrics 設計要選擇正確的型別（latency 用 histogram、request count 用 counter、connection pool size 用 gauge）跟有界的 label（service、method、status_code，排除 user_id / request_id）。重要指標要能對應 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a> 跟 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>；高 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a> label 會推高儲存跟查詢成本。Metrics 的聚合查詢跟 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 設計見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a>。</p>
]]></content:encoded></item><item><title>SLI / SLO</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/</guid><description>&lt;p>SLI / SLO 的核心概念是「用可量測訊號表達服務承諾」。SLI（Service Level Indicator）是服務品質指標 — 成功率、延遲、可用性；SLO（Service Level Objective）是這些指標的目標 — 99.9% request 在 300ms 內成功回應。SLO 的執行力來自 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> — 預算耗盡就暫停發版。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>SLI / SLO 把觀測資料轉成決策語言。單純看到 error rate 上升只能說明症狀；對照 SLO 後，團隊才能判斷是否需要暫停發版、啟動 incident、擴容或降級。SLO 不是「越高越好」— 99.999% 的 SLO 意味著幾乎沒有 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 做變更，反而限制了功能交付速度。&lt;/p>
&lt;p>SLI 的設計起點是使用者旅程（checkout 是否成功、搜尋是否夠快），量測點選擇（edge / gateway / service）決定了 SLI 反映的是「使用者體驗」還是「基礎設施健康」。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 SLI / SLO 的訊號是服務重要性已經影響收入、合約或使用者信任。付款、登入、訂單建立與訊息送達通常需要不同 SLO，因為失敗代價不同。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>SLI 需要定義「什麼算 good request」的邊界（5xx 算 bad、4xx 通常不算）。SLO 需要定義目標值、量測窗口（30 天 rolling）跟 owner。SLO 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> alerting 搭配使用，讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 反映使用者影響而非基礎設施噪音。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>SLI / SLO 的核心概念是「用可量測訊號表達服務承諾」。SLI（Service Level Indicator）是服務品質指標 — 成功率、延遲、可用性；SLO（Service Level Objective）是這些指標的目標 — 99.9% request 在 300ms 內成功回應。SLO 的執行力來自 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> — 預算耗盡就暫停發版。</p>
<h2 id="概念位置">概念位置</h2>
<p>SLI / SLO 把觀測資料轉成決策語言。單純看到 error rate 上升只能說明症狀；對照 SLO 後，團隊才能判斷是否需要暫停發版、啟動 incident、擴容或降級。SLO 不是「越高越好」— 99.999% 的 SLO 意味著幾乎沒有 <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 做變更，反而限制了功能交付速度。</p>
<p>SLI 的設計起點是使用者旅程（checkout 是否成功、搜尋是否夠快），量測點選擇（edge / gateway / service）決定了 SLI 反映的是「使用者體驗」還是「基礎設施健康」。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 SLI / SLO 的訊號是服務重要性已經影響收入、合約或使用者信任。付款、登入、訂單建立與訊息送達通常需要不同 SLO，因為失敗代價不同。</p>
<h2 id="設計責任">設計責任</h2>
<p>SLI 需要定義「什麼算 good request」的邊界（5xx 算 bad、4xx 通常不算）。SLO 需要定義目標值、量測窗口（30 天 rolling）跟 owner。SLO 跟 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> alerting 搭配使用，讓 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 反映使用者影響而非基礎設施噪音。完整設計見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>。</p>
]]></content:encoded></item><item><title>Trace Context</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/</guid><description>&lt;p>Trace context 的核心概念是「讓同一個 request 在跨服務呼叫中保持同一條追蹤線」。它包含 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a>（標識整條 trace）、span id（標識上游 span）與 trace flags（sampling 決策），讓下游服務建立的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 能歸屬同一條 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Trace context 是跨服務診斷的關聯層，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id&lt;/a> 互補 — correlation id 關聯業務流程、trace context 關聯技術呼叫路徑。它的傳遞機制決定 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 能不能完整串起 — context 斷掉的地方，trace 就從「完整路徑」退化成需要人工拼接的局部紀錄。&lt;/p>
&lt;p>W3C Trace Context 標準定義了 HTTP 的傳遞格式：&lt;code>traceparent&lt;/code> header 帶 version + trace id + parent span id + trace flags，&lt;code>tracestate&lt;/code> header 帶 vendor-specific 附加資訊。OpenTelemetry SDK 預設使用 W3C 格式。部分 vendor 有自己的 header（Datadog 用 &lt;code>x-datadog-trace-id&lt;/code>、AWS X-Ray 用 &lt;code>X-Amzn-Trace-Id&lt;/code>），跨 vendor 時需要在 collector 層轉換。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 trace context 的訊號是延遲或錯誤跨越多個服務。Checkout 變慢時，trace context 讓 tracing 系統把 API gateway、order service、payment service、database query 的 span 串成一條路徑，在 waterfall view 中直接看到時間花在哪。&lt;/p>
&lt;p>Context 在 HTTP call、gRPC metadata、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue&lt;/a> message header 上傳遞。Queue 邊界的 propagation 比 HTTP 複雜 — consumer 可能在 producer 之後很久才消費，context 的時間跨度從毫秒擴大到分鐘。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Trace context 設計要處理四個邊界的傳遞：HTTP / gRPC（SDK auto-instrumentation 自動處理）、queue（需要 instrumented client 注入 message header）、thread pool（需要語言級的 context 傳播機制）、background job（需要在 job 啟動時建立 root span）。&lt;/p>
&lt;p>斷鏈的常見原因和修復策略見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing 與 context link&lt;/a>。Sampling 決策跟 trace context 的關係見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 sampling 策略&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Trace context 的核心概念是「讓同一個 request 在跨服務呼叫中保持同一條追蹤線」。它包含 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a>（標識整條 trace）、span id（標識上游 span）與 trace flags（sampling 決策），讓下游服務建立的 <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 能歸屬同一條 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Trace context 是跨服務診斷的關聯層，跟 <a href="/blog/backend/knowledge-cards/correlation-id/" data-link-title="Correlation ID" data-link-desc="說明跨事件或跨服務的關聯識別碼如何支援排障">correlation id</a> 互補 — correlation id 關聯業務流程、trace context 關聯技術呼叫路徑。它的傳遞機制決定 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 能不能完整串起 — context 斷掉的地方，trace 就從「完整路徑」退化成需要人工拼接的局部紀錄。</p>
<p>W3C Trace Context 標準定義了 HTTP 的傳遞格式：<code>traceparent</code> header 帶 version + trace id + parent span id + trace flags，<code>tracestate</code> header 帶 vendor-specific 附加資訊。OpenTelemetry SDK 預設使用 W3C 格式。部分 vendor 有自己的 header（Datadog 用 <code>x-datadog-trace-id</code>、AWS X-Ray 用 <code>X-Amzn-Trace-Id</code>），跨 vendor 時需要在 collector 層轉換。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 trace context 的訊號是延遲或錯誤跨越多個服務。Checkout 變慢時，trace context 讓 tracing 系統把 API gateway、order service、payment service、database query 的 span 串成一條路徑，在 waterfall view 中直接看到時間花在哪。</p>
<p>Context 在 HTTP call、gRPC metadata、<a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> message header 上傳遞。Queue 邊界的 propagation 比 HTTP 複雜 — consumer 可能在 producer 之後很久才消費，context 的時間跨度從毫秒擴大到分鐘。</p>
<h2 id="設計責任">設計責任</h2>
<p>Trace context 設計要處理四個邊界的傳遞：HTTP / gRPC（SDK auto-instrumentation 自動處理）、queue（需要 instrumented client 注入 message header）、thread pool（需要語言級的 context 傳播機制）、background job（需要在 job 啟動時建立 root span）。</p>
<p>斷鏈的常見原因和修復策略見 <a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 tracing 與 context link</a>。Sampling 決策跟 trace context 的關係見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 sampling 策略</a>。</p>
]]></content:encoded></item><item><title>監控資料的雙重用途：行為分析與訊號治理</title><link>https://tarrragon.github.io/blog/monitoring/telemetry-data-dual-use/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/telemetry-data-dual-use/</guid><description>&lt;p>SDK 埋的每一筆 event 有兩個下游消費者：產品團隊用它做行為分析（轉換率、留存、歸因），工程團隊用它做訊號治理（cardinality 控制、成本歸因、事故判讀）。兩邊各自有教學章節（&lt;a href="https://tarrragon.github.io/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">Monitoring 08 Business Analytics&lt;/a> 和 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Backend 04 可觀測性&lt;/a>），但讀者常不知道這是同一份資料的兩種消費方式。本文是橋。&lt;/p>
&lt;h2 id="同一份資料兩種消費路徑">同一份資料、兩種消費路徑&lt;/h2>





&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">SDK 埋點（event / error / metric / lifecycle）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> ├── 行為分析路徑 → Monitoring 08
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> │ 消費者：PM / 行銷 / 產品
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> │ 方法：funnel / cohort / attribution / A-B test
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> │ 決策：改 UI、調定價、投廣告
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> └── 訊號治理路徑 → Backend 04
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> 消費者：SRE / platform team / on-call
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> 方法：cardinality budget / cost attribution / signal governance
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> 決策：降 cardinality、調 sampling、改 alert、產出 evidence&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這不是兩套埋點。同一個 &lt;code>button.click&lt;/code> event，產品團隊看的是「哪個步驟流失最多使用者」，工程團隊看的是「這個 event 的 cardinality 是否在預算內、ingestion cost 是否合理」。event 相同，切入角度不同。&lt;/p>
&lt;h2 id="資料格式的交叉點">資料格式的交叉點&lt;/h2>
&lt;p>Monitoring SDK 送出的事件格式（&lt;a href="https://tarrragon.github.io/blog/monitoring/02-log-schema/" data-link-title="模組二：Log Schema 設計" data-link-desc="跨平台統一事件格式、欄位設計、版本演進策略">02 Log Schema&lt;/a>）和 Backend 04 的 log schema / OTel event format 有共通欄位：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>欄位&lt;/th>
 &lt;th>Monitoring SDK 格式&lt;/th>
 &lt;th>Backend 04 / OTel 格式&lt;/th>
 &lt;th>交叉用途&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>timestamp&lt;/td>
 &lt;td>&lt;code>timestamp&lt;/code>（ISO 8601）&lt;/td>
 &lt;td>&lt;code>TimeUnixNano&lt;/code>&lt;/td>
 &lt;td>兩邊都需要精確時間做時序查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>event type&lt;/td>
 &lt;td>&lt;code>type&lt;/code>（event/error/metric/lifecycle）&lt;/td>
 &lt;td>&lt;code>SeverityText&lt;/code> / &lt;code>SpanKind&lt;/code>&lt;/td>
 &lt;td>行為分析按 type 做 funnel；訊號治理按 type 做 cardinality budget&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>source&lt;/td>
 &lt;td>&lt;code>source.sdk&lt;/code> / &lt;code>source.platform&lt;/code> / &lt;code>source.app&lt;/code>&lt;/td>
 &lt;td>&lt;code>Resource&lt;/code> attributes&lt;/td>
 &lt;td>行為分析按 platform 切分；訊號治理按 service 做 cost attribution&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>trace context&lt;/td>
 &lt;td>手動注入（若有）&lt;/td>
 &lt;td>&lt;code>TraceId&lt;/code> / &lt;code>SpanId&lt;/code>&lt;/td>
 &lt;td>client-to-server 端到端追蹤的串接欄位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>payload&lt;/td>
 &lt;td>&lt;code>data&lt;/code>（自由 JSON）&lt;/td>
 &lt;td>&lt;code>Attributes&lt;/code> / &lt;code>Body&lt;/code>&lt;/td>
 &lt;td>行為分析讀 business fields；訊號治理讀 operational fields&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>格式一致性的價值是&lt;strong>一份 event 同時餵 BigQuery（行為分析）和 Grafana Loki（訊號查詢）不需要格式轉換&lt;/strong>。如果兩邊各自定義 schema，同一個 event 要寫兩次 adapter，schema drift 的風險倍增。&lt;/p></description><content:encoded><![CDATA[<p>SDK 埋的每一筆 event 有兩個下游消費者：產品團隊用它做行為分析（轉換率、留存、歸因），工程團隊用它做訊號治理（cardinality 控制、成本歸因、事故判讀）。兩邊各自有教學章節（<a href="/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">Monitoring 08 Business Analytics</a> 和 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">Backend 04 可觀測性</a>），但讀者常不知道這是同一份資料的兩種消費方式。本文是橋。</p>
<h2 id="同一份資料兩種消費路徑">同一份資料、兩種消費路徑</h2>





<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">SDK 埋點（event / error / metric / lifecycle）
</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">  ├── 行為分析路徑 → Monitoring 08
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  │     消費者：PM / 行銷 / 產品
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  │     方法：funnel / cohort / attribution / A-B test
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  │     決策：改 UI、調定價、投廣告
</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">  └── 訊號治理路徑 → Backend 04
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        消費者：SRE / platform team / on-call
</span></span><span class="line"><span class="ln">10</span><span class="cl">        方法：cardinality budget / cost attribution / signal governance
</span></span><span class="line"><span class="ln">11</span><span class="cl">        決策：降 cardinality、調 sampling、改 alert、產出 evidence</span></span></code></pre></div><p>這不是兩套埋點。同一個 <code>button.click</code> event，產品團隊看的是「哪個步驟流失最多使用者」，工程團隊看的是「這個 event 的 cardinality 是否在預算內、ingestion cost 是否合理」。event 相同，切入角度不同。</p>
<h2 id="資料格式的交叉點">資料格式的交叉點</h2>
<p>Monitoring SDK 送出的事件格式（<a href="/blog/monitoring/02-log-schema/" data-link-title="模組二：Log Schema 設計" data-link-desc="跨平台統一事件格式、欄位設計、版本演進策略">02 Log Schema</a>）和 Backend 04 的 log schema / OTel event format 有共通欄位：</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>Monitoring SDK 格式</th>
          <th>Backend 04 / OTel 格式</th>
          <th>交叉用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>timestamp</td>
          <td><code>timestamp</code>（ISO 8601）</td>
          <td><code>TimeUnixNano</code></td>
          <td>兩邊都需要精確時間做時序查詢</td>
      </tr>
      <tr>
          <td>event type</td>
          <td><code>type</code>（event/error/metric/lifecycle）</td>
          <td><code>SeverityText</code> / <code>SpanKind</code></td>
          <td>行為分析按 type 做 funnel；訊號治理按 type 做 cardinality budget</td>
      </tr>
      <tr>
          <td>source</td>
          <td><code>source.sdk</code> / <code>source.platform</code> / <code>source.app</code></td>
          <td><code>Resource</code> attributes</td>
          <td>行為分析按 platform 切分；訊號治理按 service 做 cost attribution</td>
      </tr>
      <tr>
          <td>trace context</td>
          <td>手動注入（若有）</td>
          <td><code>TraceId</code> / <code>SpanId</code></td>
          <td>client-to-server 端到端追蹤的串接欄位</td>
      </tr>
      <tr>
          <td>payload</td>
          <td><code>data</code>（自由 JSON）</td>
          <td><code>Attributes</code> / <code>Body</code></td>
          <td>行為分析讀 business fields；訊號治理讀 operational fields</td>
      </tr>
  </tbody>
</table>
<p>格式一致性的價值是<strong>一份 event 同時餵 BigQuery（行為分析）和 Grafana Loki（訊號查詢）不需要格式轉換</strong>。如果兩邊各自定義 schema，同一個 event 要寫兩次 adapter，schema drift 的風險倍增。</p>
<h2 id="資料治理的衝突">資料治理的衝突</h2>
<p>同一份資料被兩邊消費時，治理需求會衝突：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>行為分析需要</th>
          <th>訊號治理需要</th>
          <th>衝突點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>保留期</td>
          <td>長期保留（年級，趨勢與 cohort 需要歷史資料）</td>
          <td>短期保留（30-90 天，debug 用完即丟）</td>
          <td>成本 vs 分析完整度</td>
      </tr>
      <tr>
          <td>粒度</td>
          <td>高粒度（per-user、per-session、per-action）</td>
          <td>低粒度（聚合到 service / endpoint 維度）</td>
          <td>cardinality 爆炸 vs 分析精度</td>
      </tr>
      <tr>
          <td>PII 處理</td>
          <td>去識別但需保留 user segment（國家、裝置、方案）</td>
          <td>完全匿名或 redacted</td>
          <td>分析需求 vs 合規要求</td>
      </tr>
      <tr>
          <td>取樣</td>
          <td>低取樣或全量（行為趨勢需要完整分布）</td>
          <td>可以高取樣（error 全收，正常 request 取樣即可）</td>
          <td>成本 vs 覆蓋度</td>
      </tr>
      <tr>
          <td>查詢延遲</td>
          <td>可接受分鐘級（batch analytics）</td>
          <td>需要秒級（incident debug 不能等）</td>
          <td>儲存分層與查詢 backend 選擇</td>
      </tr>
  </tbody>
</table>
<p>這些衝突無法靠「選一邊」解決。行為分析少了歷史資料就看不到趨勢；訊號治理存太多高粒度資料就 cardinality 爆炸。解法是分流。</p>
<h2 id="解法在-transport-層分流">解法：在 transport 層分流</h2>
<p>把 SDK 送出的 event 在 collector 或 pipeline 層分流到不同 backend，各自按需求治理：</p>
<h3 id="hot-path即時訊號">Hot path：即時訊號</h3>
<p>error 和 metric 類事件即時進入 <a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">04 telemetry pipeline</a>（Loki / Prometheus / Tempo），短期 retention（30-90 天），服務 on-call debug 和 incident triage。這條路徑要求秒級延遲、低 cardinality（聚合維度）。</p>
<h3 id="warm-path行為分析">Warm path：行為分析</h3>
<p>全部四類事件進入 data warehouse（BigQuery / ClickHouse / Snowflake），長期 retention（年級），服務 funnel、cohort、attribution 和 A/B test。這條路徑接受分鐘級延遲、高粒度（per-user / per-session）。</p>
<h3 id="cold-path合規留存">Cold path：合規留存</h3>
<p>audit-level event 進入 archive storage（Cloud Storage / S3 / Glacier），法規要求的年級保留（GDPR 刪除請求、HIPAA 6 年、金融業更長）。這條路徑寫入後幾乎不查詢，查詢時接受小時級延遲。</p>
<h3 id="分流的關鍵設計">分流的關鍵設計</h3>
<p>分流在 transport 層做，不在 SDK 層做。SDK 統一送出全部 event 到同一個 endpoint，pipeline 按 event type / source / tag 路由到不同 backend。</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">SDK → Collector / OTel Collector / Cloud Logging
</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">         ├─ [type=error OR type=metric] → Hot path (Loki / Prometheus)
</span></span><span class="line"><span class="ln">4</span><span class="cl">         ├─ [all events]                → Warm path (BigQuery)
</span></span><span class="line"><span class="ln">5</span><span class="cl">         └─ [audit=true]               → Cold path (Cloud Storage)</span></span></code></pre></div><p>SDK 不需要知道下游有幾個消費者。新增一個消費者（例如新的分析平台）只要在 pipeline 加一條路由，不用改 SDK。</p>
<h2 id="實作考量">實作考量</h2>
<p>分流的實作方式取決於 pipeline 架構：</p>
<table>
  <thead>
      <tr>
          <th>架構</th>
          <th>分流機制</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>自架 collector（<a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">Monitoring 04</a>）</td>
          <td>Rule engine 按 event type 寫不同 output file / HTTP endpoint</td>
          <td>小規模、自用場景</td>
      </tr>
      <tr>
          <td>OTel Collector</td>
          <td>Processor + 多個 Exporter 組成 pipeline fan-out</td>
          <td>中規模、已採用 OTel</td>
      </tr>
      <tr>
          <td>Cloud Logging（GCP）</td>
          <td>Subscription filter + Sink（BigQuery / Cloud Storage / Pub/Sub）</td>
          <td>GCP 生態</td>
      </tr>
      <tr>
          <td>Kinesis / Firehose（AWS）</td>
          <td>Firehose delivery stream + Lambda transform</td>
          <td>AWS 生態</td>
      </tr>
  </tbody>
</table>
<p>不論哪種架構，分流後的每條 path 要各自設定 retention、sampling、PII handling 和 cost budget。Hot path 的 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">cardinality 治理</a> 規則不該影響 warm path 的分析粒度；warm path 的長期保留成本不該擠壓 hot path 的 freshness。</p>
<h2 id="常見誤區">常見誤區</h2>
<h3 id="用兩套-sdk-替代分流">用兩套 SDK 替代分流</h3>
<p>在 client 端同時整合行為分析 SDK（Mixpanel）和 error tracking SDK（Sentry），看似分工清楚，實際是兩套 schema、兩份 ingestion cost、兩組 PII 風險面、兩套 consent 管理。同一個 user action 在兩個平台各記一次，但欄位名、timestamp 精度、user identifier 可能不同，跨平台 correlation 困難。</p>
<p>統一 SDK + pipeline 分流的成本通常低於雙 SDK 的整合與治理成本。</p>
<h3 id="hot-path-存全量高粒度">Hot path 存全量高粒度</h3>
<p>把 per-user / per-session 的完整事件直接灌進 Prometheus 或 Loki，會導致 cardinality 爆炸（<a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理</a>）。Hot path 的正確做法是在 pipeline 層做 aggregation 或 relabeling，只保留 service / endpoint / status 等低 cardinality 維度。高粒度資料走 warm path。</p>
<h3 id="warm-path-不做-pii-處理">Warm path 不做 PII 處理</h3>
<p>行為分析需要 user segment，但不需要 PII 原文。warm path 的 ingestion pipeline 應該在寫入 warehouse 前做 PII redaction（hash user_id、truncate IP、strip email）。<a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">Monitoring 07 去識別化</a> 的策略同時適用於 hot 和 warm path。</p>
<h2 id="讀者路由">讀者路由</h2>
<table>
  <thead>
      <tr>
          <th>如果你想</th>
          <th>先讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>理解 event 格式設計</td>
          <td><a href="/blog/monitoring/02-log-schema/" data-link-title="模組二：Log Schema 設計" data-link-desc="跨平台統一事件格式、欄位設計、版本演進策略">Monitoring 02 Log Schema</a></td>
      </tr>
      <tr>
          <td>理解行為分析方法</td>
          <td><a href="/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">Monitoring 08 Business Analytics</a></td>
      </tr>
      <tr>
          <td>理解訊號治理和成本控制</td>
          <td><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">Backend 04 Cardinality 治理</a>、<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a></td>
      </tr>
      <tr>
          <td>理解 pipeline 分流架構</td>
          <td><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">Backend 04 Telemetry Pipeline</a></td>
      </tr>
      <tr>
          <td>理解 PII 去識別化</td>
          <td><a href="/blog/monitoring/07-security-privacy/" data-link-title="模組七：資安與隱私" data-link-desc="SDK redaction / transport 加密 / collector access control / 去識別化 — 蒐集的資料本身就是風險資產">Monitoring 07 Security Privacy</a></td>
      </tr>
      <tr>
          <td>理解 client-to-server 端到端觀測串接</td>
          <td><a href="/blog/backend/04-observability/client-server-trace-integration/" data-link-title="4.24 Client-to-Server 端到端觀測串接" data-link-desc="用一個結帳場景走完 browser click → trace context → server span → 統一 waterfall 的完整實作鏈路">Backend 04 Client-to-Server 觀測串接</a></td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>Retention</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/retention/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/retention/</guid><description>&lt;p>Retention 的核心概念是「資料或事件在系統中保留多久」。它影響 storage cost、audit 能力、replay 能力、debug 時間窗口、合規義務與資料刪除責任，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 共同構成資料生命週期管理。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Retention 連接資料生命週期跟查詢能力。不同類型的資料需要不同保留期限 — log 的 debug 用途可能只需要 7 天、audit log 因合規要求可能需要 1 年以上、metrics 的 raw data 可能保留 15 天但 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 保留 90 天。&lt;/p>
&lt;p>Retention 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 搭配運作 — hot tier 保留最近的高精度資料、warm / cold tier 保留較舊的低精度或歸檔資料。保留期限的設定見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality 與成本邊界&lt;/a> 的保留階梯段。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 retention 設計的訊號是事故排查或資料修復需要回看歷史。若 event stream 只保留 24 小時，三天前的錯誤就無法靠 replay 重建。反過來，無限保留會讓儲存成本持續成長。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Retention 要同時考慮成本（儲存 × 時間）、法規（合規要求的最短保留期跟 GDPR 要求的最長保留期可能衝突）、資安（高敏感資料保留越久風險越高）、replay 需求（MQ 的 retention 影響 consumer 的 catchup 能力）跟 debug 能力（retention 太短讓事後分析無資料可用）。不同訊號類型用不同 retention 是基本做法 — error log 保留比 debug log 長、audit log 保留比 operational log 長。&lt;/p></description><content:encoded><![CDATA[<p>Retention 的核心概念是「資料或事件在系統中保留多久」。它影響 storage cost、audit 能力、replay 能力、debug 時間窗口、合規義務與資料刪除責任，跟 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 與 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 共同構成資料生命週期管理。</p>
<h2 id="概念位置">概念位置</h2>
<p>Retention 連接資料生命週期跟查詢能力。不同類型的資料需要不同保留期限 — log 的 debug 用途可能只需要 7 天、audit log 因合規要求可能需要 1 年以上、metrics 的 raw data 可能保留 15 天但 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 保留 90 天。</p>
<p>Retention 跟 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 搭配運作 — hot tier 保留最近的高精度資料、warm / cold tier 保留較舊的低精度或歸檔資料。保留期限的設定見 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality 與成本邊界</a> 的保留階梯段。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 retention 設計的訊號是事故排查或資料修復需要回看歷史。若 event stream 只保留 24 小時，三天前的錯誤就無法靠 replay 重建。反過來，無限保留會讓儲存成本持續成長。</p>
<h2 id="設計責任">設計責任</h2>
<p>Retention 要同時考慮成本（儲存 × 時間）、法規（合規要求的最短保留期跟 GDPR 要求的最長保留期可能衝突）、資安（高敏感資料保留越久風險越高）、replay 需求（MQ 的 retention 影響 consumer 的 catchup 能力）跟 debug 能力（retention 太短讓事後分析無資料可用）。不同訊號類型用不同 retention 是基本做法 — error log 保留比 debug log 長、audit log 保留比 operational log 長。</p>
]]></content:encoded></item><item><title>可觀測性案例正文</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/</guid><description>&lt;p>這個資料夾的核心責任是把觀測案例變成可回寫章節。案例表格提供線索，正文負責輸出訊號邊界與路由。&lt;/p>
&lt;h2 id="章節列表">章節列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1&lt;/a>&lt;/td>
 &lt;td>FinTech 審計證據觀測&lt;/td>
 &lt;td>把審計與證據鏈變成可觀測訊號&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2&lt;/a>&lt;/td>
 &lt;td>Gaming 高峰訊號治理&lt;/td>
 &lt;td>把高峰流量下訊號失真風險前移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3&lt;/a>&lt;/td>
 &lt;td>Healthcare 存取可追溯性&lt;/td>
 &lt;td>把資料主權場景的存取證據做成治理閉環&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4&lt;/a>&lt;/td>
 &lt;td>X-Ray 到 OTel 轉換&lt;/td>
 &lt;td>把觀測遷移標準化成可分段執行流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5&lt;/a>&lt;/td>
 &lt;td>Cloud Trace OTLP 導入&lt;/td>
 &lt;td>把資料通道標準化納入觀測平台治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6&lt;/a>&lt;/td>
 &lt;td>ADOT on EKS 遷移&lt;/td>
 &lt;td>把 collector/agent 管線轉換成集中治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7&lt;/a>&lt;/td>
 &lt;td>Datadog OTel 遷移實務&lt;/td>
 &lt;td>把 APM 採集轉成 OTel-compatible 流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8&lt;/a>&lt;/td>
 &lt;td>Airbnb K8s 規模化訊號&lt;/td>
 &lt;td>把叢集擴縮行為接回觀測與容量治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9&lt;/a>&lt;/td>
 &lt;td>反例：OTel 遷移訊號漂移&lt;/td>
 &lt;td>雙軌採集未對齊導致告警與 SLO 判讀失真&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10&lt;/a>&lt;/td>
 &lt;td>對照：規模差異下觀測遷移&lt;/td>
 &lt;td>不同規模團隊在觀測遷移的風險與流程差異&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">4.C11&lt;/a>&lt;/td>
 &lt;td>Uber M3 大規模 Metrics&lt;/td>
 &lt;td>從散落的 Prometheus 到統一 metrics 平台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/" data-link-title="4.C12 Cloudflare：內部觀測平台的三層能力" data-link-desc="全球 300&amp;#43; edge 節點的觀測架構，把 monitoring、analytics 與 forensics 拆成三個獨立能力層。">4.C12&lt;/a>&lt;/td>
 &lt;td>Cloudflare 觀測三層能力&lt;/td>
 &lt;td>monitoring / analytics / forensics 拆分&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">4.C13&lt;/a>&lt;/td>
 &lt;td>Discord 儲存→觀測缺口&lt;/td>
 &lt;td>每次遷移暴露觀測盲區的共同結構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">4.C14&lt;/a>&lt;/td>
 &lt;td>觀測成本治理&lt;/td>
 &lt;td>attribution + cardinality budget + tiering&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把觀測案例變成可回寫章節。案例表格提供線索，正文負責輸出訊號邊界與路由。</p>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/fintech-audit-evidence-observability/" data-link-title="FinTech：審計證據鏈的可觀測性設計" data-link-desc="把交易與存取事件轉成可回查證據，降低合規審核與事故判讀落差。">4.C1</a></td>
          <td>FinTech 審計證據觀測</td>
          <td>把審計與證據鏈變成可觀測訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/gaming-peak-signal-freshness-and-cardinality/" data-link-title="Gaming：高峰流量下的訊號新鮮度與 Cardinality" data-link-desc="在高峰事件中控制訊號延遲與維度爆炸，維持告警與定位可信度。">4.C2</a></td>
          <td>Gaming 高峰訊號治理</td>
          <td>把高峰流量下訊號失真風險前移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/healthcare-access-traceability-and-retention/" data-link-title="Healthcare：存取可追溯性與保留邊界" data-link-desc="在資料主權限制下，建立可追溯存取證據與分層保留策略。">4.C3</a></td>
          <td>Healthcare 存取可追溯性</td>
          <td>把資料主權場景的存取證據做成治理閉環</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/xray-to-opentelemetry-migration/" data-link-title="4.C4 AWS：X-Ray 到 OpenTelemetry 轉換" data-link-desc="觀測儀表從 vendor-specific SDK 轉向 OpenTelemetry 的治理重點。">4.C4</a></td>
          <td>X-Ray 到 OTel 轉換</td>
          <td>把觀測遷移標準化成可分段執行流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/cloud-trace-otlp-adoption/" data-link-title="4.C5 Google Cloud：Cloud Trace 導入 OTLP 入口" data-link-desc="觀測平台從專有入口擴展到 OTLP 標準通道的案例。">4.C5</a></td>
          <td>Cloud Trace OTLP 導入</td>
          <td>把資料通道標準化納入觀測平台治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/adot-eks-observability-pipeline-migration/" data-link-title="4.C6 AWS：ADOT on EKS 管線遷移" data-link-desc="從分散式 agent 組合轉成 OpenTelemetry collector 管線治理。">4.C6</a></td>
          <td>ADOT on EKS 遷移</td>
          <td>把 collector/agent 管線轉換成集中治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7</a></td>
          <td>Datadog OTel 遷移實務</td>
          <td>把 APM 採集轉成 OTel-compatible 流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8</a></td>
          <td>Airbnb K8s 規模化訊號</td>
          <td>把叢集擴縮行為接回觀測與容量治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/failure-otel-migration-signal-drift/" data-link-title="4.C9 反例：OTel 遷移後訊號漂移" data-link-desc="雙軌採集未對齊導致告警與 SLO 判讀失真。">4.C9</a></td>
          <td>反例：OTel 遷移訊號漂移</td>
          <td>雙軌採集未對齊導致告警與 SLO 判讀失真</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/contrast-observability-rollout-by-scale/" data-link-title="4.C10 對照：規模差異下的觀測遷移" data-link-desc="觀測遷移在不同規模團隊下的流程與風險差異。">4.C10</a></td>
          <td>對照：規模差異下觀測遷移</td>
          <td>不同規模團隊在觀測遷移的風險與流程差異</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/uber-m3-metrics-platform-scale/" data-link-title="4.C11 Uber：M3 大規模 Metrics 平台" data-link-desc="從散落的 Prometheus 實例到統一 metrics 平台，處理 cardinality 爆炸、長期 retention 與跨叢集查詢的規模化挑戰。">4.C11</a></td>
          <td>Uber M3 大規模 Metrics</td>
          <td>從散落的 Prometheus 到統一 metrics 平台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/" data-link-title="4.C12 Cloudflare：內部觀測平台的三層能力" data-link-desc="全球 300&#43; edge 節點的觀測架構，把 monitoring、analytics 與 forensics 拆成三個獨立能力層。">4.C12</a></td>
          <td>Cloudflare 觀測三層能力</td>
          <td>monitoring / analytics / forensics 拆分</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">4.C13</a></td>
          <td>Discord 儲存→觀測缺口</td>
          <td>每次遷移暴露觀測盲區的共同結構</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">4.C14</a></td>
          <td>觀測成本治理</td>
          <td>attribution + cardinality budget + tiering</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>可觀測性 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/</guid><description>&lt;p>可觀測性 Vendor 清單的核心責任是把工具名稱放回 telemetry contract、signal ownership、data quality、cardinality 與成本治理的判斷。每個服務頁先回答它承擔 metrics、logs、traces、errors、APM 或平台原生觀測的哪一段，再討論資料模型、查詢能力、成本與案例回寫。觀測這塊能力的買 vs 建特別現實：自建 telemetry stack（Prometheus、Grafana、Loki）、買 observability SaaS（Datadog、New Relic、Grafana Cloud），還是用雲端原生（CloudWatch、Cloud Monitoring）— 取捨與遷出代價見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建&lt;/a>。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>可觀測性服務要從訊號責任進入。讀者如果要建立 metrics baseline，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics Basics&lt;/a>；如果要處理資料品質，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality&lt;/a>；如果要交付 evidence，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>可觀測性服務頁的教學順序是先建立 OpenTelemetry 標準入口，再比較 metrics / logs / traces backend、SaaS observability 與 cloud-native 工具。這個順序服務 E1-E7 所有 checkout episode：每個服務變更都要把訊號整理成 evidence package，讀者要先理解 signal quality，再進入 vendor 能力與成本模型。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>服務&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;th>頁面要回答的核心問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry&lt;/a>&lt;/td>
 &lt;td>Standard / SDK&lt;/td>
 &lt;td>instrumentation、collector、semantic convention 如何降低 vendor lock-in&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus&lt;/a>&lt;/td>
 &lt;td>Metrics&lt;/td>
 &lt;td>pull model、PromQL、cardinality 與 retention 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack&lt;/a>&lt;/td>
 &lt;td>OSS / Cloud stack&lt;/td>
 &lt;td>Grafana、Loki、Tempo、Mimir 如何組成可觀測性平台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog&lt;/a>&lt;/td>
 &lt;td>SaaS APM&lt;/td>
 &lt;td>all-in-one APM、logs、traces、profiling 與成本治理如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &amp;#43; Beats / APM">Elastic Stack&lt;/a>&lt;/td>
 &lt;td>Search / logs&lt;/td>
 &lt;td>log search、index lifecycle、APM 與資料量成本如何治理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb&lt;/a>&lt;/td>
 &lt;td>High-cardinality&lt;/td>
 &lt;td>event-based observability 與 high-cardinality 查詢如何支援除錯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch&lt;/a>&lt;/td>
 &lt;td>AWS-native&lt;/td>
 &lt;td>AWS metrics、logs、alarms 與 account / region 邊界如何管理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations&lt;/a>&lt;/td>
 &lt;td>GCP-native&lt;/td>
 &lt;td>Cloud Monitoring、Logging、Trace 與 GCP resource model 如何整合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry&lt;/a>&lt;/td>
 &lt;td>Error tracking&lt;/td>
 &lt;td>error event、release、trace、session replay 如何連到 owner action&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入、其他形式代表 same-vendor 的 topology / version / config 變動。&lt;/p></description><content:encoded><![CDATA[<p>可觀測性 Vendor 清單的核心責任是把工具名稱放回 telemetry contract、signal ownership、data quality、cardinality 與成本治理的判斷。每個服務頁先回答它承擔 metrics、logs、traces、errors、APM 或平台原生觀測的哪一段，再討論資料模型、查詢能力、成本與案例回寫。觀測這塊能力的買 vs 建特別現實：自建 telemetry stack（Prometheus、Grafana、Loki）、買 observability SaaS（Datadog、New Relic、Grafana Cloud），還是用雲端原生（CloudWatch、Cloud Monitoring）— 取捨與遷出代價見 <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建</a>。</p>
<h2 id="讀法">讀法</h2>
<p>可觀測性服務要從訊號責任進入。讀者如果要建立 metrics baseline，先回到 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">Metrics Basics</a>；如果要處理資料品質，先回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>；如果要交付 evidence，先回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>可觀測性服務頁的教學順序是先建立 OpenTelemetry 標準入口，再比較 metrics / logs / traces backend、SaaS observability 與 cloud-native 工具。這個順序服務 E1-E7 所有 checkout episode：每個服務變更都要把訊號整理成 evidence package，讀者要先理解 signal quality，再進入 vendor 能力與成本模型。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a></td>
          <td>Standard / SDK</td>
          <td>instrumentation、collector、semantic convention 如何降低 vendor lock-in</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/prometheus/" data-link-title="Prometheus" data-link-desc="Pull-based metrics 主流 OSS、PromQL 與 alerting">Prometheus</a></td>
          <td>Metrics</td>
          <td>pull model、PromQL、cardinality 與 retention 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/grafana-stack/" data-link-title="Grafana Stack" data-link-desc="Grafana / Loki / Tempo / Mimir / Pyroscope 全棧">Grafana Stack</a></td>
          <td>OSS / Cloud stack</td>
          <td>Grafana、Loki、Tempo、Mimir 如何組成可觀測性平台</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a></td>
          <td>SaaS APM</td>
          <td>all-in-one APM、logs、traces、profiling 與成本治理如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/elastic-stack/" data-link-title="Elastic Stack" data-link-desc="ELK：Elasticsearch / Logstash / Kibana &#43; Beats / APM">Elastic Stack</a></td>
          <td>Search / logs</td>
          <td>log search、index lifecycle、APM 與資料量成本如何治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/honeycomb/" data-link-title="Honeycomb" data-link-desc="High-cardinality observability 平台、events-based 模型">Honeycomb</a></td>
          <td>High-cardinality</td>
          <td>event-based observability 與 high-cardinality 查詢如何支援除錯</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch</a></td>
          <td>AWS-native</td>
          <td>AWS metrics、logs、alarms 與 account / region 邊界如何管理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/gcp-cloud-operations/" data-link-title="GCP Cloud Operations" data-link-desc="GCP 原生觀測性套件（前 Stackdriver）：Logging / Monitoring / Trace / Profiler">GCP Cloud Operations</a></td>
          <td>GCP-native</td>
          <td>Cloud Monitoring、Logging、Trace 與 GCP resource model 如何整合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/04-observability/vendors/sentry/" data-link-title="Sentry" data-link-desc="Error tracking 主流、APM / Profiling / Session Replay 擴展">Sentry</a></td>
          <td>Error tracking</td>
          <td>error event、release、trace、session replay 如何連到 owner action</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、「← X」代表從 X 遷入、其他形式代表 same-vendor 的 topology / version / config 變動。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="aws-cloudwatch/">AWS CloudWatch</a></td>
          <td><a href="aws-cloudwatch/logs-insights-governance/">Logs Insights 治理</a> / <a href="aws-cloudwatch/alarms-composite-operations/">Alarms 與 Composite</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="datadog/">Datadog</a></td>
          <td><a href="datadog/cost-governance-agent-config/">成本治理與 Agent 配置</a> / <a href="datadog/otlp-ingestion-otel-integration/">OTLP Ingestion 與 OTel 整合</a></td>
          <td><a href="datadog/migrate-from-new-relic/">← New Relic</a> / <a href="datadog/migrate-to-grafana-stack/">→ Grafana Stack</a></td>
      </tr>
      <tr>
          <td><a href="elastic-stack/">Elastic Stack</a></td>
          <td><a href="elastic-stack/ilm-log-pipeline/">ILM 與 Log Pipeline</a></td>
          <td><a href="elastic-stack/migrate-to-elastic-cloud/">→ Elastic Cloud</a></td>
      </tr>
      <tr>
          <td><a href="gcp-cloud-operations/">GCP Cloud Ops</a></td>
          <td><a href="gcp-cloud-operations/cloud-monitoring-mql/">Monitoring MQL</a> / <a href="gcp-cloud-operations/cloud-logging-export-compliance/">Logging 匯出合規</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="grafana-stack/">Grafana Stack</a></td>
          <td><a href="grafana-stack/lgtm-stack-operations/">LGTM Stack Operations</a> / <a href="grafana-stack/loki-design-operational-limits/">Loki 設計與操作限制</a></td>
          <td><a href="grafana-stack/migrate-prometheus-to-cloud-metrics/">Prometheus → Cloud Metrics</a></td>
      </tr>
      <tr>
          <td><a href="honeycomb/">Honeycomb</a></td>
          <td><a href="honeycomb/high-cardinality-query-bubbleup/">High-Cardinality BubbleUp</a></td>
          <td><a href="honeycomb/migrate-from-sentry/">← Sentry</a></td>
      </tr>
      <tr>
          <td><a href="opentelemetry/">OpenTelemetry</a></td>
          <td><a href="opentelemetry/collector-deployment-patterns/">Collector 部署模式</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="prometheus/">Prometheus</a></td>
          <td><a href="prometheus/capacity-failure-modes/">容量規劃與故障模式</a> / <a href="prometheus/promql-recording-rules/">PromQL 與 Recording Rules</a> / <a href="prometheus/remote-write-long-term-storage/">Remote Write 與長期儲存</a></td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="sentry/">Sentry</a></td>
          <td><a href="sentry/error-grouping-fingerprinting/">Error Grouping Fingerprinting</a> / <a href="sentry/release-tracking-session-replay/">Release Tracking Session Replay</a></td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<p>進度（2026-06-23）：9 個 T1 vendor 全部有 deep article（共 21 篇）。OpenTelemetry 後續候選：Sampling 策略 / Auto-instrumentation。各 vendor 進階主題的更多 deep article 見各自 <code>_index.md</code> 的「預計實作話題」段。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>可觀測性服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 signal standard、metrics、logs、traces、error tracking 還是 APM platform</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>cardinality、retention、debug speed、multi-cloud、compliance、成本哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>OSS stack、cloud-native、SaaS APM、specialized error tracking 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>instrumentation、agent、collector、index、retention、query cost、PII governance</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>dashboard、query link、trace sample、log sample、alert rule、data quality note</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>事故、capacity、release gate 與 cost attribution 如何回寫成 evidence package</td>
      </tr>
  </tbody>
</table>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>可觀測性服務頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是 standard、metrics backend、log search、trace backend、APM 還是 error tracking</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷 signal ownership、data quality、cardinality、retention 與 cost</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「現在缺哪個訊號會阻止決策」快速判斷該看 metrics、logs、traces 或 errors</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>instrumentation、collector、agent、dashboard、alert、retention</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>OSS stack、SaaS APM、cloud-native、specialized tool 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>high-cardinality、sampling、multi-cloud、PII redaction、cost attribution</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>missing signal、label explosion、trace gap、log index cost、alert noise</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>標準化先用 OpenTelemetry、規模化 metrics 轉 managed backend、事故協作轉 08</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>每種語言 SDK 完整教學、dashboard 美術、所有 query cookbook</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 4.20 evidence package、9.8 performance observability、8 incident cases</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>橫向議題在不同 vendor 用不同 mechanism 達成。本表列同一議題在 9 個 vendor 的對應位置、確保大綱不缺漏、讀者跨 vendor 查找時有索引。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>OTel</th>
          <th>Prometheus</th>
          <th>Grafana Stack</th>
          <th>Datadog</th>
          <th>Elastic Stack</th>
          <th>Honeycomb</th>
          <th>CloudWatch</th>
          <th>Cloud Ops</th>
          <th>Sentry</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訊號類型</td>
          <td>全（標準）</td>
          <td>metrics</td>
          <td>全 stack</td>
          <td>全 + Security</td>
          <td>logs + APM</td>
          <td>events / traces</td>
          <td>全 AWS-native</td>
          <td>全 GCP-native</td>
          <td>errors + APM</td>
      </tr>
      <tr>
          <td>採集模式</td>
          <td>SDK + Collector</td>
          <td>Pull scrape</td>
          <td>mixed</td>
          <td>Agent push</td>
          <td>Beats / Agent</td>
          <td>SDK / OTLP</td>
          <td>Agent / native</td>
          <td>Agent / native</td>
          <td>SDK push</td>
      </tr>
      <tr>
          <td>查詢語言</td>
          <td>N/A</td>
          <td>PromQL</td>
          <td>PromQL/LogQL/TraceQL</td>
          <td>Datadog query</td>
          <td>KQL / ES DSL</td>
          <td>Honeycomb query</td>
          <td>Logs Insights</td>
          <td>Logs query</td>
          <td>Issue filter</td>
      </tr>
      <tr>
          <td>Cardinality</td>
          <td>由 backend 決定</td>
          <td>受限（series）</td>
          <td>Mimir / Loki 各自</td>
          <td>計費 per dim</td>
          <td>Mapping limit</td>
          <td>設計目標 (high)</td>
          <td>計費 per metric</td>
          <td>計費 per metric</td>
          <td>issue grouping</td>
      </tr>
      <tr>
          <td>部署模式</td>
          <td>OSS standard</td>
          <td>OSS self-host</td>
          <td>OSS / Cloud</td>
          <td>SaaS only</td>
          <td>OSS / Cloud</td>
          <td>SaaS only</td>
          <td>AWS managed</td>
          <td>GCP managed</td>
          <td>OSS / SaaS</td>
      </tr>
      <tr>
          <td>成本模型</td>
          <td>取決 backend</td>
          <td>self-host CapEx</td>
          <td>self-host / Cloud</td>
          <td>hosts + signals</td>
          <td>self-host</td>
          <td>events volume</td>
          <td>ingestion + API</td>
          <td>ingestion + API</td>
          <td>events volume</td>
      </tr>
      <tr>
          <td>多雲 / 跨平台</td>
          <td>是（標準）</td>
          <td>是 (OSS)</td>
          <td>是</td>
          <td>是</td>
          <td>是</td>
          <td>是</td>
          <td>AWS-only</td>
          <td>GCP-only</td>
          <td>是</td>
      </tr>
      <tr>
          <td>OTel 相容度</td>
          <td>原生</td>
          <td>exporter</td>
          <td>OTLP receiver</td>
          <td>OTLP ingestion</td>
          <td>OTLP ES 7.16+</td>
          <td>OTLP 原生</td>
          <td>ADOT</td>
          <td>OTLP Trace 2.0+</td>
          <td>OTel context</td>
      </tr>
      <tr>
          <td>主討論案例</td>
          <td>C2/C3/C4/C5/C8</td>
          <td>C1/C6/C7</td>
          <td>C6/C11</td>
          <td>C5</td>
          <td>C5/C6</td>
          <td>C7</td>
          <td>C1/C8</td>
          <td>C3</td>
          <td>待補</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、檢查橫向議題是否有對應的進階主題子段</li>
<li>讀者選型時、知道對應 mechanism 在不同 vendor 的形態</li>
<li>評估遷移風險：訊號類型 + 部署模式 + OTel 相容度三維度合併判讀</li>
</ul>
<p>下面 8 段把對照表的每行展開、避免裸表格成為終點。</p>
<h3 id="訊號類型">訊號類型</h3>
<p>訊號類型決定 vendor 解決哪一段觀測問題。<strong>OpenTelemetry</strong> 是 standard、覆蓋 traces / metrics / logs；<strong>Prometheus</strong> 純 metrics；<strong>Grafana Stack</strong> 全 stack（各 backend 各司其職、Loki + Tempo + Mimir + Pyroscope）；<strong>Datadog</strong> 全 + Security + RUM + CI；<strong>Elastic Stack</strong> logs 為主 + APM；<strong>Honeycomb</strong> events-based（不是 metrics aggregation）；<strong>CloudWatch / Cloud Operations</strong> 雲原生全 stack（含 traces / profiler）；<strong>Sentry</strong> 專精 error tracking + 簡易 APM。</p>
<p>選型判讀：缺哪個訊號 → 補對應 vendor；想 turnkey 全棧 → Datadog / cloud-native；想 OSS 全棧 → Grafana Stack；error tracking 已有 → Sentry / Bugsnag 補強。</p>
<h3 id="採集模式">採集模式</h3>
<p>採集模式影響部署複雜度跟 instrumentation 工作量。<strong>OTel</strong> 是 SDK + Collector 兩層；<strong>Prometheus</strong> 是 pull scrape（service discovery）；<strong>Grafana Stack</strong> 各 backend 模式不同（Loki push / Tempo OTLP / Mimir remote write）；<strong>Datadog</strong> Agent push；<strong>Elastic</strong> Beats / Logstash / Agent；<strong>Honeycomb</strong> SDK push 或 OTLP；<strong>CloudWatch / Cloud Ops</strong> 雲服務內建 + Agent；<strong>Sentry</strong> SDK push。</p>
<p>選型判讀：服務在 K8s + 想自管 → Prometheus pull + Operator；應用層 push → OTel SDK + Collector；不想配 instrumentation → Datadog / cloud-native 自動。</p>
<h3 id="查詢語言">查詢語言</h3>
<p>查詢語言差異影響 dashboard / alert 設計成本。<strong>Prometheus PromQL</strong>（業界 metrics query 標準）；<strong>Grafana</strong> 支援 PromQL（Mimir）/ LogQL（Loki）/ TraceQL（Tempo）；<strong>Datadog</strong> 自家 query syntax；<strong>Elastic</strong> KQL / Lucene / ES DSL / ES|QL；<strong>Honeycomb</strong> point-and-click + 簡單 query；<strong>CloudWatch</strong> Logs Insights syntax；<strong>Cloud Ops</strong> 類似但 GCP-specific；<strong>Sentry</strong> 是 issue filter、不算 query language。</p>
<p>選型判讀：跨 vendor 統一 → 學 PromQL + LogQL（Grafana 通用）；vendor-specific → 依該 vendor 學；OTel 不解決 query 問題（純 instrumentation 標準）。</p>
<h3 id="cardinality-處理">Cardinality 處理</h3>
<p>Cardinality 是 observability 成本跟可用性的關鍵。<strong>Prometheus</strong> 受限（series 爆炸會 OOM）；<strong>Datadog</strong> custom metrics 計費 per dimension；<strong>CloudWatch / Cloud Ops</strong> metrics 計費 per metric；<strong>Elastic</strong> mapping field limit；<strong>Honeycomb</strong> 設計目標就是 high-cardinality（events-based）；<strong>Grafana Stack</strong> Mimir 多 tenant 各自 cardinality budget；<strong>Sentry</strong> 用 issue grouping 替代 cardinality 概念。</p>
<p>選型判讀：high-cardinality 是核心需求（per-user / per-request debug）→ Honeycomb；中等 cardinality + 成本敏感 → Prometheus + 設計謹慎；任意 cardinality + 計費承擔 → Datadog。</p>
<h3 id="部署模式">部署模式</h3>
<p>部署模式決定運維責任歸屬。<strong>OTel</strong> 是 standard、各 backend 各自部署；<strong>Prometheus</strong> OSS self-host；<strong>Grafana Stack</strong> OSS self-host / Grafana Cloud；<strong>Datadog / Honeycomb / Sentry</strong> SaaS（Sentry 有 self-host OSS）；<strong>Elastic</strong> OSS / Elastic Cloud / OpenSearch fork；<strong>CloudWatch / Cloud Ops</strong> 雲原生 managed。</p>
<p>選型判讀：要極致控制 → self-host OSS；不想運維 → SaaS（Datadog / Honeycomb / Sentry）；已在 AWS / GCP → 雲原生 + 補強；混合模式 → OTel 抽象層 + 多 backend。</p>
<h3 id="成本模型">成本模型</h3>
<p>成本模型差異大、容易誤判。<strong>OTel</strong> 本身無成本、取決下游 backend；<strong>Prometheus</strong> self-host CapEx（compute + storage）；<strong>Grafana Stack</strong> self-host CapEx 或 Grafana Cloud OpEx；<strong>Datadog</strong> hosts + signal 各自計費（容易堆疊）；<strong>Elastic</strong> self-host CapEx 或 Elastic Cloud；<strong>Honeycomb</strong> events volume；<strong>CloudWatch / Cloud Ops</strong> ingestion + API call；<strong>Sentry</strong> events / users / replays 計費。</p>
<p>選型判讀：可預期固定成本 → self-host（CapEx）；流量不穩 → SaaS（OpEx + 預警）；多訊號類型 → Datadog 容易爆、Honeycomb 計費單純；AWS / GCP-only 場景 → 雲原生通常 cheaper than 第三方 SaaS。</p>
<h3 id="多雲--跨平台">多雲 / 跨平台</h3>
<p>多雲決定 vendor 鎖定風險。<strong>OTel</strong> 是抽象層、最不 lock-in；<strong>Prometheus / Grafana Stack / Elastic / Datadog / Honeycomb / Sentry</strong> 都支援多雲；<strong>CloudWatch</strong> AWS-only；<strong>Cloud Ops</strong> GCP-only；<strong>Azure Monitor</strong> Azure-only（T2 候選）。</p>
<p>選型判讀：多雲 → 避免 AWS / GCP-only vendor、用 Datadog / Grafana Stack / OTel + multi-backend；單一雲 → 雲原生通常成本最低；既有混合 → OTel 標準化 + 漸進遷移。</p>
<h3 id="otel-相容度">OTel 相容度</h3>
<p>OTel 相容度影響 vendor 切換成本。各 vendor 接受程度：</p>
<ul>
<li>完全相容（drop-in）：Honeycomb / Grafana Tempo / Cloud Trace（2.0+）</li>
<li>接受但 feature 落後 vendor SDK：Datadog / CloudWatch（X-Ray 整合）/ Elastic APM</li>
<li>跟 OTel 互補但設計不同：Prometheus（exporter pattern）/ Sentry（OTel context）</li>
</ul>
<p>選型判讀：未來想換 vendor → 從 day 1 用 OTel SDK；不換 vendor → vendor SDK 較深；多 backend dual ship → OTel 幾乎是唯一可行路徑。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>O1</td>
          <td>OpenTelemetry</td>
          <td>建立 instrumentation standard、collector 與 vendor portability</td>
      </tr>
      <tr>
          <td>O2</td>
          <td>Prometheus / Grafana Stack</td>
          <td>建立 metrics baseline、cardinality 與 OSS platform 判準</td>
      </tr>
      <tr>
          <td>O3</td>
          <td>Elastic Stack / Datadog / Honeycomb / Sentry</td>
          <td>建立 logs / APM / high-cardinality / error tracking 對照</td>
      </tr>
      <tr>
          <td>O4</td>
          <td>AWS CloudWatch / GCP Cloud Operations</td>
          <td>建立 cloud-native observability 與 account / project 邊界</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Enterprise APM</td>
          <td>New Relic、Dynatrace、Splunk Observability</td>
          <td>SaaS APM、enterprise workflow、成本治理</td>
      </tr>
      <tr>
          <td>OSS / Hybrid</td>
          <td>SigNoz、Chronosphere、VictoriaMetrics、Thanos、Cortex</td>
          <td>Prometheus scale、managed metrics、OpenTelemetry ingestion</td>
      </tr>
      <tr>
          <td>Tracing</td>
          <td>Jaeger、OpenSearch Observability</td>
          <td>trace backend、OpenTelemetry-native ingestion、log correlation</td>
      </tr>
      <tr>
          <td>Logs / pipeline</td>
          <td>Fluent Bit、Fluentd、Vector、OpenSearch</td>
          <td>log shipping、filtering、index lifecycle、cost</td>
      </tr>
      <tr>
          <td>Error tracking</td>
          <td>Bugsnag、Rollbar、Raygun</td>
          <td>release health、frontend / backend error ownership</td>
      </tr>
      <tr>
          <td>Cloud-native</td>
          <td>Azure Monitor</td>
          <td>Azure resource model、Log Analytics、cost boundary</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是分開 instrumentation、metrics、logs、traces、APM 與 error tracking。OpenTelemetry 是標準入口，Prometheus / Thanos / Cortex / VictoriaMetrics 是 metrics 路線，Loki / OpenSearch / Elastic 是 logs / search 路線，Jaeger / Tempo 是 tracing 路線，Datadog / New Relic / Dynatrace / Splunk 是 SaaS APM 路線。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a></li>
<li>上游：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a></li>
<li>服務路徑：<a href="/blog/backend/04-observability/checkout-api-evidence-package/" data-link-title="4.22 Checkout API Evidence Package 實作示範" data-link-desc="用 checkout 路徑示範 evidence package 如何交接給 release gate 與 incident decision。">4.22 Checkout API Evidence Package 實作示範</a></li>
<li>跨模組：<a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a></li>
</ul>
]]></content:encoded></item><item><title>Histogram</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/</guid><description>&lt;p>Histogram 的核心概念是「把觀測值分到多個 bucket，記錄每個範圍的累積數量」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 中描述分布的工具，常用來觀察 latency、request size、payload size、queue wait time 與處理耗時，支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a> 計算。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Histogram 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 中描述分布的工具，跟 counter（計數）跟 gauge（瞬間值）互補。Average 只能說明中心趨勢；histogram 可以支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile&lt;/a>（p95 / p99）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a> 計算跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 判斷。&lt;/p>
&lt;p>Prometheus 的 histogram 用累積 bucket（&lt;code>le&lt;/code> label）實作 — 每個 bucket 記錄「值 &amp;lt;= le 的觀測次數」。PromQL 的 &lt;code>histogram_quantile()&lt;/code> 從 bucket 資料估算 percentile。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 histogram 的訊號是少數慢 request 會影響使用者體驗但 average 看不出來。Checkout 平均延遲 100ms 看起來良好，但 p99 若超過 3 秒，1% 的使用者體驗極差。Histogram 讓這個長尾可見。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Histogram bucket boundary 要依 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a> 閾值跟實際延遲範圍設計。Bucket 太粗（只有 100ms / 500ms / 1s）會讓 percentile 估計跳躍式變化；太細會增加 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a>（每個 bucket 是一條 time series）。常見做法是在 SLO 閾值附近密集、在兩端稀疏。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Histogram 的核心概念是「把觀測值分到多個 bucket，記錄每個範圍的累積數量」。它是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 中描述分布的工具，常用來觀察 latency、request size、payload size、queue wait time 與處理耗時，支援 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a> 計算。</p>
<h2 id="概念位置">概念位置</h2>
<p>Histogram 是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 中描述分布的工具，跟 counter（計數）跟 gauge（瞬間值）互補。Average 只能說明中心趨勢；histogram 可以支援 <a href="/blog/backend/knowledge-cards/percentile/" data-link-title="Percentile" data-link-desc="說明 p95 與 p99 如何描述長尾延遲與使用者體驗">percentile</a>（p95 / p99）、<a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a> 計算跟 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 判斷。</p>
<p>Prometheus 的 histogram 用累積 bucket（<code>le</code> label）實作 — 每個 bucket 記錄「值 &lt;= le 的觀測次數」。PromQL 的 <code>histogram_quantile()</code> 從 bucket 資料估算 percentile。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 histogram 的訊號是少數慢 request 會影響使用者體驗但 average 看不出來。Checkout 平均延遲 100ms 看起來良好，但 p99 若超過 3 秒，1% 的使用者體驗極差。Histogram 讓這個長尾可見。</p>
<h2 id="設計責任">設計責任</h2>
<p>Histogram bucket boundary 要依 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a> 閾值跟實際延遲範圍設計。Bucket 太粗（只有 100ms / 500ms / 1s）會讓 percentile 估計跳躍式變化；太細會增加 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a>（每個 bucket 是一條 time series）。常見做法是在 SLO 閾值附近密集、在兩端稀疏。詳見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a>。</p>
]]></content:encoded></item><item><title>Percentile</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/percentile/</guid><description>&lt;p>Percentile 的核心概念是「某比例的觀測值低於某個門檻」。p95 latency 表示 95% 的 request 延遲低於該值；p99 觀察更長尾的慢請求。Percentile 描述的是分布的尾端，從 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a> 資料計算而來，用來捕捉 average 掩蓋的使用者體驗問題。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Percentile 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram&lt;/a> 搭配使用。Histogram 記錄延遲分布（哪些 bucket 收到多少 request），percentile 從 histogram 資料計算（&lt;code>histogram_quantile&lt;/code> in PromQL）。Average latency 看不到長尾 — 平均 80ms 但 p99 是 2 秒，代表 1% 的使用者體驗極差。&lt;/p>
&lt;p>Percentile 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a> 的常見型別 — latency SLI 用「p99 &amp;lt; 500ms 的 request 佔比」量化使用者體驗。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 percentile 的訊號是 average latency 穩定但使用者仍回報卡頓。搜尋 API 平均 80ms、p99 2 秒，表示少數 request 走到慢查詢或下游 timeout。高流量服務的 1%（p99 以外）可能代表數千個使用者。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Percentile 要搭配 histogram bucket 設計 — bucket boundary 決定 percentile 計算的精度。Bucket 太少（只有 100ms / 500ms / 1s）會讓 p99 的估計跳躍式變化。Bucket 太多會增加 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a>。低流量服務的高 percentile 容易受少量樣本影響，alert 閾值要考慮統計穩定性。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Percentile 的核心概念是「某比例的觀測值低於某個門檻」。p95 latency 表示 95% 的 request 延遲低於該值；p99 觀察更長尾的慢請求。Percentile 描述的是分布的尾端，從 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 資料計算而來，用來捕捉 average 掩蓋的使用者體驗問題。</p>
<h2 id="概念位置">概念位置</h2>
<p>Percentile 跟 <a href="/blog/backend/knowledge-cards/histogram/" data-link-title="Histogram" data-link-desc="說明 histogram 如何用分桶統計延遲、大小與分布">histogram</a> 搭配使用。Histogram 記錄延遲分布（哪些 bucket 收到多少 request），percentile 從 histogram 資料計算（<code>histogram_quantile</code> in PromQL）。Average latency 看不到長尾 — 平均 80ms 但 p99 是 2 秒，代表 1% 的使用者體驗極差。</p>
<p>Percentile 是 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a> 的常見型別 — latency SLI 用「p99 &lt; 500ms 的 request 佔比」量化使用者體驗。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 percentile 的訊號是 average latency 穩定但使用者仍回報卡頓。搜尋 API 平均 80ms、p99 2 秒，表示少數 request 走到慢查詢或下游 timeout。高流量服務的 1%（p99 以外）可能代表數千個使用者。</p>
<h2 id="設計責任">設計責任</h2>
<p>Percentile 要搭配 histogram bucket 設計 — bucket boundary 決定 percentile 計算的精度。Bucket 太少（只有 100ms / 500ms / 1s）會讓 p99 的估計跳躍式變化。Bucket 太多會增加 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a>。低流量服務的高 percentile 容易受少量樣本影響，alert 閾值要考慮統計穩定性。詳見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics basics</a> 跟 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>。</p>
]]></content:encoded></item><item><title>Error Budget</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/</guid><description>&lt;p>Error budget 的核心概念是「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a> 允許的失敗額度」。SLO = 99.9% 代表 30 天內允許 0.1% 的 request 失敗；這 0.1% 就是 error budget，用來平衡功能交付速度與可靠性改善投入。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Error budget 把可靠性討論轉成可量化的決策語言。Budget 消耗過快時，團隊應暫停高風險變更、優先修可靠性；budget 充足時，可以承擔更多變更風險跟 experiment。&lt;/p>
&lt;p>Error budget 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> alerting 的基礎 — burn rate 量化的是 error budget 被消耗的速度。Error budget 接近耗盡時，進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate&lt;/a> 的 freeze 條件。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 error budget 的訊號是發版速度與事故風險需要共同管理。Checkout 服務本月多次 timeout，若 error budget 已接近耗盡，團隊應暫停高風險變更直到 budget 恢復。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Error budget 的 metric 結構需要 rolling window 的 total requests 跟 failed requests（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>）。Budget remaining 作為 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> panel 跟 release gate 的輸入 — 用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 維護 rolling window 計算，避免每次查詢掃描 30 天的 raw data。&lt;/p></description><content:encoded><![CDATA[<p>Error budget 的核心概念是「<a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a> 允許的失敗額度」。SLO = 99.9% 代表 30 天內允許 0.1% 的 request 失敗；這 0.1% 就是 error budget，用來平衡功能交付速度與可靠性改善投入。</p>
<h2 id="概念位置">概念位置</h2>
<p>Error budget 把可靠性討論轉成可量化的決策語言。Budget 消耗過快時，團隊應暫停高風險變更、優先修可靠性；budget 充足時，可以承擔更多變更風險跟 experiment。</p>
<p>Error budget 是 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> alerting 的基礎 — burn rate 量化的是 error budget 被消耗的速度。Error budget 接近耗盡時，進入 <a href="/blog/backend/knowledge-cards/release-gate/" data-link-title="Release Gate" data-link-desc="說明變更在正式釋出前如何通過或阻擋">release gate</a> 的 freeze 條件。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 error budget 的訊號是發版速度與事故風險需要共同管理。Checkout 服務本月多次 timeout，若 error budget 已接近耗盡，團隊應暫停高風險變更直到 budget 恢復。</p>
<h2 id="設計責任">設計責任</h2>
<p>Error budget 的 metric 結構需要 rolling window 的 total requests 跟 failed requests（見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>）。Budget remaining 作為 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> panel 跟 release gate 的輸入 — 用 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 維護 rolling window 計算，避免每次查詢掃描 30 天的 raw data。</p>
]]></content:encoded></item><item><title>Burn Rate</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/</guid><description>&lt;p>Burn rate 的核心概念是「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget&lt;/a> 被消耗的速度」。Burn rate = 1 代表按 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a> 允許的速度正常消耗；burn rate = 10 代表消耗速度是允許值的 10 倍 — 如果持續下去，error budget 會在 SLO 週期的 1/10 內耗盡。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Burn rate 是 SLO alerting 的核心機制，把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI&lt;/a> 的 error ratio 轉成可行動的嚴重度判斷。短時間高 burn rate（14x、5 分鐘窗口）代表急性事故；長時間中等 burn rate（1x、數小時窗口）代表慢性可靠性退化。&lt;/p>
&lt;p>Burn rate alerting 比固定閾值 alert 更能反映使用者影響 — 低流量時段的幾筆 error 可能 burn rate 很低（對 error budget 影響小），高流量時段的相同 error rate 可能 burn rate 很高（影響大量使用者）。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 burn rate 的訊號是固定閾值 alert（error rate &amp;gt; 1%）在不同流量時段的表現不穩定 — 低流量時 false alarm、高流量時漏報。Burn rate 自動適應流量基線。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Burn rate alerting 用 multi-window 策略：短窗口（5min）抓急性 + 長窗口（1hr）做確認，兩個窗口都超過閾值才觸發。Recording rule 預計算各窗口的 error ratio，讓 alert evaluate 讀預計算結果而非重算 raw series。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Burn rate 的核心概念是「<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 被消耗的速度」。Burn rate = 1 代表按 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a> 允許的速度正常消耗；burn rate = 10 代表消耗速度是允許值的 10 倍 — 如果持續下去，error budget 會在 SLO 週期的 1/10 內耗盡。</p>
<h2 id="概念位置">概念位置</h2>
<p>Burn rate 是 SLO alerting 的核心機制，把 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI</a> 的 error ratio 轉成可行動的嚴重度判斷。短時間高 burn rate（14x、5 分鐘窗口）代表急性事故；長時間中等 burn rate（1x、數小時窗口）代表慢性可靠性退化。</p>
<p>Burn rate alerting 比固定閾值 alert 更能反映使用者影響 — 低流量時段的幾筆 error 可能 burn rate 很低（對 error budget 影響小），高流量時段的相同 error rate 可能 burn rate 很高（影響大量使用者）。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 burn rate 的訊號是固定閾值 alert（error rate &gt; 1%）在不同流量時段的表現不穩定 — 低流量時 false alarm、高流量時漏報。Burn rate 自動適應流量基線。</p>
<h2 id="設計責任">設計責任</h2>
<p>Burn rate alerting 用 multi-window 策略：短窗口（5min）抓急性 + 長窗口（1hr）做確認，兩個窗口都超過閾值才觸發。Recording rule 預計算各窗口的 error ratio，讓 alert evaluate 讀預計算結果而非重算 raw series。完整設計見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>。</p>
]]></content:encoded></item><item><title>Correlation ID</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/correlation-id/</guid><description>&lt;p>Correlation ID 的核心概念是「把同一個業務流程中的多筆紀錄關聯起來的識別碼」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a> 的核心欄位，可以跨 request、queue message、background job、log、trace 與外部 API 呼叫。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Correlation ID 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 的定位不同。Trace id 偏向一次技術呼叫路徑（一個 HTTP request 經過多個服務）；correlation ID 可以代表更長的業務流程（一筆訂單從建立到付款到出貨，跨越多個獨立 request）。&lt;/p>
&lt;p>Correlation ID 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema&lt;/a> 的核心欄位。Log 帶 correlation ID 時，跨服務跟跨 async 邊界的事件可以用同一個 ID 查出完整業務流程。見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a>。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 correlation ID 的訊號是事故排查需要跨同步與非同步邊界。訂單建立 request、付款事件、寄信 job 與出貨事件共享同一 correlation ID，讓客服跟工程師追到完整流程。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Correlation ID 要在入口（API gateway 或 first service）建立或從 upstream 接收，並傳遞到 log、message header、trace context 與外部呼叫。欄位名稱要穩定（跨服務一致，避免 &lt;code>request_id&lt;/code> vs &lt;code>req_id&lt;/code> vs &lt;code>requestId&lt;/code> 的漂移），避免把敏感資料當成 ID。&lt;/p></description><content:encoded><![CDATA[<p>Correlation ID 的核心概念是「把同一個業務流程中的多筆紀錄關聯起來的識別碼」。它是 <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a> 的核心欄位，可以跨 request、queue message、background job、log、trace 與外部 API 呼叫。</p>
<h2 id="概念位置">概念位置</h2>
<p>Correlation ID 跟 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 的定位不同。Trace id 偏向一次技術呼叫路徑（一個 HTTP request 經過多個服務）；correlation ID 可以代表更長的業務流程（一筆訂單從建立到付款到出貨，跨越多個獨立 request）。</p>
<p>Correlation ID 是 <a href="/blog/backend/knowledge-cards/log-schema/" data-link-title="Log Schema" data-link-desc="說明結構化 log 欄位如何支援搜尋、關聯與事故排查">log schema</a> 的核心欄位。Log 帶 correlation ID 時，跨服務跟跨 async 邊界的事件可以用同一個 ID 查出完整業務流程。見 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 correlation ID 的訊號是事故排查需要跨同步與非同步邊界。訂單建立 request、付款事件、寄信 job 與出貨事件共享同一 correlation ID，讓客服跟工程師追到完整流程。</p>
<h2 id="設計責任">設計責任</h2>
<p>Correlation ID 要在入口（API gateway 或 first service）建立或從 upstream 接收，並傳遞到 log、message header、trace context 與外部呼叫。欄位名稱要穩定（跨服務一致，避免 <code>request_id</code> vs <code>req_id</code> vs <code>requestId</code> 的漂移），避免把敏感資料當成 ID。</p>
]]></content:encoded></item><item><title>Trace ID</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/</guid><description>&lt;p>Trace ID 的核心概念是「分散式追蹤中同一條呼叫路徑的全域識別碼」。一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 由多個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 組成，trace ID 讓 tracing 系統把散落在不同服務的 span 聚合成同一次操作的完整路徑。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Trace ID 是 tracing 的頂層關聯欄位。W3C Trace Context 標準使用 128-bit 隨機值（32 hex chars）；部分 vendor 使用 64-bit（Datadog 舊版、Zipkin v1）。混用不同長度時需要在 collector 層做 ID 轉換或 padding。&lt;/p>
&lt;p>Trace ID 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id&lt;/a> 的定位不同：request id 是單一服務內的請求識別碼（通常由 API gateway 或 load balancer 產生），trace id 是跨服務的追蹤識別碼（由第一個 instrumented service 產生）。兩者可以共存在同一筆 log 的不同欄位，各自服務不同的查詢需求。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>Trace ID 的診斷價值是「拿到一個 ID 就能看到整條 request 路徑」。事故中從 error log 拿到 trace ID，貼進 tracing UI（Jaeger、Grafana Tempo、Datadog APM），直接看 waterfall view 定位瓶頸。&lt;/p>
&lt;p>Trace ID 也是 log / metric / trace 三者的關聯樞紐。Log 的結構化欄位帶 trace ID 時，debug 工作流可以從 log → trace 或 trace → log 雙向跳轉。Metric 的 exemplar 帶 trace ID 時，可以從 dashboard 的 latency spike 跳到具體的高延遲 trace。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Trace ID 要透過 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 在 HTTP header、queue message header、thread context 上傳遞。Log 層面，trace ID 應作為必要欄位寫入 structured log（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a>）。Sampling 策略要確保錯誤與高延遲 trace 有足夠保留率，避免事故時 trace ID 存在於 log 但對應的 trace 資料已被 sampling 丟棄。&lt;/p></description><content:encoded><![CDATA[<p>Trace ID 的核心概念是「分散式追蹤中同一條呼叫路徑的全域識別碼」。一個 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 由多個 <a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 組成，trace ID 讓 tracing 系統把散落在不同服務的 span 聚合成同一次操作的完整路徑。</p>
<h2 id="概念位置">概念位置</h2>
<p>Trace ID 是 tracing 的頂層關聯欄位。W3C Trace Context 標準使用 128-bit 隨機值（32 hex chars）；部分 vendor 使用 64-bit（Datadog 舊版、Zipkin v1）。混用不同長度時需要在 collector 層做 ID 轉換或 padding。</p>
<p>Trace ID 跟 <a href="/blog/backend/knowledge-cards/request-id/" data-link-title="Request ID" data-link-desc="說明單次 request 的識別碼如何支援 log 搜尋與問題定位">request id</a> 的定位不同：request id 是單一服務內的請求識別碼（通常由 API gateway 或 load balancer 產生），trace id 是跨服務的追蹤識別碼（由第一個 instrumented service 產生）。兩者可以共存在同一筆 log 的不同欄位，各自服務不同的查詢需求。</p>
<h2 id="使用情境">使用情境</h2>
<p>Trace ID 的診斷價值是「拿到一個 ID 就能看到整條 request 路徑」。事故中從 error log 拿到 trace ID，貼進 tracing UI（Jaeger、Grafana Tempo、Datadog APM），直接看 waterfall view 定位瓶頸。</p>
<p>Trace ID 也是 log / metric / trace 三者的關聯樞紐。Log 的結構化欄位帶 trace ID 時，debug 工作流可以從 log → trace 或 trace → log 雙向跳轉。Metric 的 exemplar 帶 trace ID 時，可以從 dashboard 的 latency spike 跳到具體的高延遲 trace。</p>
<h2 id="設計責任">設計責任</h2>
<p>Trace ID 要透過 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 在 HTTP header、queue message header、thread context 上傳遞。Log 層面，trace ID 應作為必要欄位寫入 structured log（見 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>）。Sampling 策略要確保錯誤與高延遲 trace 有足夠保留率，避免事故時 trace ID 存在於 log 但對應的 trace 資料已被 sampling 丟棄。</p>
]]></content:encoded></item><item><title>Span</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/span/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/span/</guid><description>&lt;p>Span 的核心概念是「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 中的一段有起止時間的工作」。每個 span 記錄操作名稱、開始與結束時間、狀態（OK / Error）、屬性（service name、http.status_code、db.statement）與事件（exception message）。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Span 是 tracing 的基本單位。HTTP handler、database query、cache call、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker&lt;/a> publish、consumer handle 與外部 API 呼叫都可以形成 span。Span 之間透過 parent-child 關係組成 tree — 共享同一個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 的所有 span 構成一條完整的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a>。&lt;/p>
&lt;p>Span 有四種 kind：&lt;code>CLIENT&lt;/code>（發起呼叫）、&lt;code>SERVER&lt;/code>（接收呼叫）、&lt;code>PRODUCER&lt;/code>（投遞訊息）、&lt;code>CONSUMER&lt;/code>（消費訊息）。Kind 影響 trace backend 怎麼計算 service-to-service 的延遲跟依賴方向。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 span 的訊號是單一 request 裡有多個步驟，需要知道哪一步變慢或出錯。Checkout trace 中 payment span 佔 80% 時間，問題焦點就落在付款依賴或其網路路徑。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Span 設計要控制名稱粒度、屬性選擇、錯誤狀態與敏感資料。Span 名稱太粗（所有 HTTP call 都叫 &lt;code>HTTP&lt;/code>）會看不出瓶頸；太細（每個 URL path parameter 都獨立命名）會讓 span 名稱成為無界維度、影響 trace backend 的聚合效能。&lt;/p>
&lt;p>屬性要帶足夠的診斷資訊但避免敏感資料。&lt;code>http.url&lt;/code> 帶完整 URL 可能含 query parameter 裡的 token；&lt;code>db.statement&lt;/code> 帶完整 SQL 可能含使用者資料。需要在 SDK 或 collector 層做 redaction。&lt;/p></description><content:encoded><![CDATA[<p>Span 的核心概念是「<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 中的一段有起止時間的工作」。每個 span 記錄操作名稱、開始與結束時間、狀態（OK / Error）、屬性（service name、http.status_code、db.statement）與事件（exception message）。</p>
<h2 id="概念位置">概念位置</h2>
<p>Span 是 tracing 的基本單位。HTTP handler、database query、cache call、<a href="/blog/backend/knowledge-cards/broker/" data-link-title="Broker" data-link-desc="說明 broker 在訊息傳遞系統中負責保存、路由與交付訊息">broker</a> publish、consumer handle 與外部 API 呼叫都可以形成 span。Span 之間透過 parent-child 關係組成 tree — 共享同一個 <a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 的所有 span 構成一條完整的 <a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a>。</p>
<p>Span 有四種 kind：<code>CLIENT</code>（發起呼叫）、<code>SERVER</code>（接收呼叫）、<code>PRODUCER</code>（投遞訊息）、<code>CONSUMER</code>（消費訊息）。Kind 影響 trace backend 怎麼計算 service-to-service 的延遲跟依賴方向。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 span 的訊號是單一 request 裡有多個步驟，需要知道哪一步變慢或出錯。Checkout trace 中 payment span 佔 80% 時間，問題焦點就落在付款依賴或其網路路徑。</p>
<h2 id="設計責任">設計責任</h2>
<p>Span 設計要控制名稱粒度、屬性選擇、錯誤狀態與敏感資料。Span 名稱太粗（所有 HTTP call 都叫 <code>HTTP</code>）會看不出瓶頸；太細（每個 URL path parameter 都獨立命名）會讓 span 名稱成為無界維度、影響 trace backend 的聚合效能。</p>
<p>屬性要帶足夠的診斷資訊但避免敏感資料。<code>http.url</code> 帶完整 URL 可能含 query parameter 裡的 token；<code>db.statement</code> 帶完整 SQL 可能含使用者資料。需要在 SDK 或 collector 層做 redaction。</p>
]]></content:encoded></item><item><title>Symptom-Based Alert</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/</guid><description>&lt;p>Symptom-based alert 的核心概念是「&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 優先偵測使用者或產品可感知的症狀」。症狀包括錯誤率、延遲、可用性、資料延遲、付款失敗與訊息未送達。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Symptom-based alert 跟 cause-based alert 分工不同。CPU 高、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a> 高、GC 頻繁是可能的原因；checkout 失敗率升高才是直接的產品症狀。Symptom-based 適合 critical severity（page &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a>），cause-based 適合 warning severity（工作時間排入 task）。&lt;/p>
&lt;p>Symptom-based alert 是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert&lt;/a> 建議的 alert 設計起點 — 先確認使用者是否受影響、再看系統原因。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 symptom-based alert 的訊號是 on-call 被大量低層訊號吵醒，但無法判斷使用者是否受影響。付款成功率下降應立即告警；單台 instance CPU 高則可先進 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 觀察或走自動修復流程。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Symptom-based alert 要連到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與影響判斷。SLO-based alerting 用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 量化症狀嚴重度 — 「error budget 消耗速度是允許值的 14 倍」比「error rate &amp;gt; 1%」更能反映使用者影響規模。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Symptom-based alert 的核心概念是「<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 優先偵測使用者或產品可感知的症狀」。症狀包括錯誤率、延遲、可用性、資料延遲、付款失敗與訊息未送達。</p>
<h2 id="概念位置">概念位置</h2>
<p>Symptom-based alert 跟 cause-based alert 分工不同。CPU 高、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a> 高、GC 頻繁是可能的原因；checkout 失敗率升高才是直接的產品症狀。Symptom-based 適合 critical severity（page <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a>），cause-based 適合 warning severity（工作時間排入 task）。</p>
<p>Symptom-based alert 是 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 dashboard-alert</a> 建議的 alert 設計起點 — 先確認使用者是否受影響、再看系統原因。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 symptom-based alert 的訊號是 on-call 被大量低層訊號吵醒，但無法判斷使用者是否受影響。付款成功率下降應立即告警；單台 instance CPU 高則可先進 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 觀察或走自動修復流程。</p>
<h2 id="設計責任">設計責任</h2>
<p>Symptom-based alert 要連到 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a>、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與影響判斷。SLO-based alerting 用 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 量化症狀嚴重度 — 「error budget 消耗速度是允許值的 14 倍」比「error rate &gt; 1%」更能反映使用者影響規模。完整設計見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI/SLO 訊號設計</a>。</p>
]]></content:encoded></item><item><title>Alert Fatigue</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/</guid><description>&lt;p>Alert fatigue 的核心概念是「過多低品質告警讓處理者對告警失去敏感度」。當告警常常沒有使用者影響、沒有行動步驟或頻繁自動恢復，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 會開始忽略訊號 — 包括真正需要處理的那些。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Alert fatigue 是可觀測性設計的失敗模式，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 的品質治理直接相關。告警應代表需要人介入的產品風險；其他訊號可以進 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、ticket、報表或自動修復流程。&lt;/p>
&lt;p>常見的 fatigue 來源：false positive（條件觸發但實際沒問題）、redundant alert（同一問題觸發多個 alert）、stale alert（條件已不適用但 rule 沒更新）。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要治理 alert fatigue 的訊號是 noise rate &amp;gt; 30%（超過三成的 alert 不需要行動），或 on-call 工程師反應「收到 alert 先 ack 再看、有時直接 resolve 不看」。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Alert fatigue 的治理包括：追蹤 noise rate（on-call ack 時標記 actionable / noise）、定期審視高 noise 的 alert rule（調整閾值、改 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based&lt;/a>、加 inhibition、或刪除）、用 grouping 跟 inhibition 減少同一問題的重複通知。治理節奏跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環&lt;/a> 整合。&lt;/p></description><content:encoded><![CDATA[<p>Alert fatigue 的核心概念是「過多低品質告警讓處理者對告警失去敏感度」。當告警常常沒有使用者影響、沒有行動步驟或頻繁自動恢復，<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 會開始忽略訊號 — 包括真正需要處理的那些。</p>
<h2 id="概念位置">概念位置</h2>
<p>Alert fatigue 是可觀測性設計的失敗模式，跟 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的品質治理直接相關。告警應代表需要人介入的產品風險；其他訊號可以進 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、ticket、報表或自動修復流程。</p>
<p>常見的 fatigue 來源：false positive（條件觸發但實際沒問題）、redundant alert（同一問題觸發多個 alert）、stale alert（條件已不適用但 rule 沒更新）。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要治理 alert fatigue 的訊號是 noise rate &gt; 30%（超過三成的 alert 不需要行動），或 on-call 工程師反應「收到 alert 先 ack 再看、有時直接 resolve 不看」。</p>
<h2 id="設計責任">設計責任</h2>
<p>Alert fatigue 的治理包括：追蹤 noise rate（on-call ack 時標記 actionable / noise）、定期審視高 noise 的 alert rule（調整閾值、改 <a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">symptom-based</a>、加 inhibition、或刪除）、用 grouping 跟 inhibition 減少同一問題的重複通知。治理節奏跟 <a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a> 整合。</p>
]]></content:encoded></item><item><title>Trace</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/trace/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/trace/</guid><description>&lt;p>Trace 的核心概念是「把一次 request 或工作流程拆成可關聯的多段執行紀錄」。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">Trace context&lt;/a> 串起整條路徑，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span&lt;/a> 記錄每一段工作，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id&lt;/a> 讓 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 能回到同一條流程。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Trace 是跨服務診斷的路徑層，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>（事件層）和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>（趨勢層）互補。Log 回答「某個服務發生了什麼」；metrics 回答「服務的健康趨勢」；trace 回答「一次 request 跨服務時，時間花在哪、錯誤發生在哪一段」。&lt;/p>
&lt;p>Trace 在 waterfall view 中呈現為時間軸上的巢狀條狀圖，root span 在最上面、child span 依序往下。診斷價值是一眼看出延遲瓶頸 — checkout 總延遲 800ms 中 payment span 佔 600ms，問題定位立刻縮小範圍。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 trace 的訊號是單一服務的 log 只呈現局部。Checkout 變慢時，trace 可以顯示時間主要花在庫存查詢、付款 API、database lock 或通知 worker。跨服務錯誤（upstream 回 500 但不知道是哪個 downstream 引起的）也依賴 trace 定位。&lt;/p>
&lt;p>Trace 聚合後可以自動生成 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">service topology&lt;/a> — 哪些服務在呼叫哪些服務、call 頻率、延遲分布、錯誤率。這個 graph 反映實際流量而非設計文件。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Trace 設計要處理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context&lt;/a> 傳遞（HTTP header、queue message header、thread context）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> 策略（head / tail / adaptive）、span 命名慣例、敏感資料 redaction、跨語言 SDK 相容性與 log correlation（trace id 寫進 log 欄位）。&lt;/p>
&lt;p>高流量服務需要控制採樣成本，同時保留錯誤與高延遲樣本。Sampling 策略的完整討論見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7&lt;/a>。Context propagation 在不同邊界（HTTP / queue / thread pool / background job）的斷鏈風險與修復見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Trace 的核心概念是「把一次 request 或工作流程拆成可關聯的多段執行紀錄」。<a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">Trace context</a> 串起整條路徑，<a href="/blog/backend/knowledge-cards/span/" data-link-title="Span" data-link-desc="說明 trace 中一段工作如何記錄耗時、狀態與關聯">span</a> 記錄每一段工作，<a href="/blog/backend/knowledge-cards/trace-id/" data-link-title="Trace ID" data-link-desc="說明分散式追蹤中同一條呼叫路徑的識別碼">trace id</a> 讓 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a> 與 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 能回到同一條流程。</p>
<h2 id="概念位置">概念位置</h2>
<p>Trace 是跨服務診斷的路徑層，跟 <a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>（事件層）和 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>（趨勢層）互補。Log 回答「某個服務發生了什麼」；metrics 回答「服務的健康趨勢」；trace 回答「一次 request 跨服務時，時間花在哪、錯誤發生在哪一段」。</p>
<p>Trace 在 waterfall view 中呈現為時間軸上的巢狀條狀圖，root span 在最上面、child span 依序往下。診斷價值是一眼看出延遲瓶頸 — checkout 總延遲 800ms 中 payment span 佔 600ms，問題定位立刻縮小範圍。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 trace 的訊號是單一服務的 log 只呈現局部。Checkout 變慢時，trace 可以顯示時間主要花在庫存查詢、付款 API、database lock 或通知 worker。跨服務錯誤（upstream 回 500 但不知道是哪個 downstream 引起的）也依賴 trace 定位。</p>
<p>Trace 聚合後可以自動生成 <a href="/blog/backend/04-observability/service-topology/" data-link-title="4.13 Service Topology 與 Dependency Map" data-link-desc="把跨服務依賴從文件變成自動發現的觀測訊號">service topology</a> — 哪些服務在呼叫哪些服務、call 頻率、延遲分布、錯誤率。這個 graph 反映實際流量而非設計文件。</p>
<h2 id="設計責任">設計責任</h2>
<p>Trace 設計要處理 <a href="/blog/backend/knowledge-cards/trace-context/" data-link-title="Trace Context" data-link-desc="說明跨服務 request 如何用 trace context 串起路徑與耗時">trace context</a> 傳遞（HTTP header、queue message header、thread context）、<a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 策略（head / tail / adaptive）、span 命名慣例、敏感資料 redaction、跨語言 SDK 相容性與 log correlation（trace id 寫進 log 欄位）。</p>
<p>高流量服務需要控制採樣成本，同時保留錯誤與高延遲樣本。Sampling 策略的完整討論見 <a href="/blog/backend/04-observability/cardinality-cost-governance/#sampling-%e7%ad%96%e7%95%a5" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7</a>。Context propagation 在不同邊界（HTTP / queue / thread pool / background job）的斷鏈風險與修復見 <a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3</a>。</p>
]]></content:encoded></item><item><title>Dashboard</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/</guid><description>&lt;p>Dashboard 的核心概念是「把多個觀測訊號組成可判讀的服務狀態畫面」。它讓團隊用同一個視角查看 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO&lt;/a>、latency、error rate、traffic、saturation、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a> 與下游依賴狀態。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Dashboard 是告警與排障之間的判讀層。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert&lt;/a> 告訴團隊需要注意，dashboard 幫團隊判斷影響範圍、變化趨勢與可能原因，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 則把判讀結果轉成處理步驟。&lt;/p>
&lt;p>Dashboard 分層服務不同使用者：service overview 給 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 工程師、debug dashboard 給事故中的深入診斷、capacity dashboard 給容量規劃。把所有資訊擠在同一個 dashboard 會讓每個角色都找不到自己要的。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 dashboard 的訊號是事故中需要快速回答「影響多大、從何時開始、哪個依賴異常」。Dashboard 也是日常巡檢的入口 — on-call 工程師每天先看 service overview 確認服務健康，再處理 alert queue。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Dashboard 設計要服務具體決策。每個面板應對應一個可回答的問題（「服務現在健康嗎」「延遲瓶頸在哪」「容量還夠嗎」）。高 cardinality、缺少單位或只呈現低層資源的圖表會增加判讀成本而非降低。&lt;/p>
&lt;p>Dashboard panel 的查詢效能影響使用體驗 — 長時間趨勢 panel 應讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 資料，避免每次刷新都掃描 raw series。Dashboard / alert 的完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Dashboard 的核心概念是「把多個觀測訊號組成可判讀的服務狀態畫面」。它讓團隊用同一個視角查看 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a>、latency、error rate、traffic、saturation、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 與下游依賴狀態。</p>
<h2 id="概念位置">概念位置</h2>
<p>Dashboard 是告警與排障之間的判讀層。<a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">Alert</a> 告訴團隊需要注意，dashboard 幫團隊判斷影響範圍、變化趨勢與可能原因，<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 則把判讀結果轉成處理步驟。</p>
<p>Dashboard 分層服務不同使用者：service overview 給 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 工程師、debug dashboard 給事故中的深入診斷、capacity dashboard 給容量規劃。把所有資訊擠在同一個 dashboard 會讓每個角色都找不到自己要的。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 dashboard 的訊號是事故中需要快速回答「影響多大、從何時開始、哪個依賴異常」。Dashboard 也是日常巡檢的入口 — on-call 工程師每天先看 service overview 確認服務健康，再處理 alert queue。</p>
<h2 id="設計責任">設計責任</h2>
<p>Dashboard 設計要服務具體決策。每個面板應對應一個可回答的問題（「服務現在健康嗎」「延遲瓶頸在哪」「容量還夠嗎」）。高 cardinality、缺少單位或只呈現低層資源的圖表會增加判讀成本而非降低。</p>
<p>Dashboard panel 的查詢效能影響使用體驗 — 長時間趨勢 panel 應讀 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 或 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 資料，避免每次刷新都掃描 raw series。Dashboard / alert 的完整設計見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>。</p>
]]></content:encoded></item><item><title>Alert</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/alert/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/alert/</guid><description>&lt;p>Alert 的核心概念是「把需要人或自動流程處理的服務症狀轉成通知」。好的 alert 連到產品影響、判斷條件、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 與升級流程。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Alert 是可觀測性進入操作流程的入口。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">Symptom-based alert&lt;/a> 優先偵測使用者可感知結果（error rate、latency p99）；cause-based alert 偵測內部原因（CPU、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool&lt;/a>）。Symptom-based 用於 page on-call、cause-based 用於 warning 級通知。&lt;/p>
&lt;p>Alert 觸發後由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 工程師承接，按 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 的步驟診斷跟處理。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 alert 設計的訊號是服務異常需要在使用者大量回報前被發現跟處理。付款成功率下降、API availability 低於 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag&lt;/a> 持續擴大或 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ&lt;/a> 快速增加，都應觸發可行動通知。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Alert 設計要定義門檻、持續時間（&lt;code>for&lt;/code> duration）、severity、通知對象、抑制規則、runbook link 與回復條件。每個 alert rule 帶 owner metadata — 沒有 owner 的 alert 會在服務演進後退化成 noise 來源，形成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a>。&lt;/p>
&lt;p>SLO-based alerting 用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate&lt;/a> 取代固定閾值，自動適應流量變化。完整的 alert 設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4&lt;/a>、SLO-based alerting 見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Alert 的核心概念是「把需要人或自動流程處理的服務症狀轉成通知」。好的 alert 連到產品影響、判斷條件、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 與升級流程。</p>
<h2 id="概念位置">概念位置</h2>
<p>Alert 是可觀測性進入操作流程的入口。<a href="/blog/backend/knowledge-cards/symptom-based-alert/" data-link-title="Symptom-Based Alert" data-link-desc="說明告警應優先偵測使用者可感知症狀">Symptom-based alert</a> 優先偵測使用者可感知結果（error rate、latency p99）；cause-based alert 偵測內部原因（CPU、<a href="/blog/backend/knowledge-cards/queue-depth/" data-link-title="Queue Depth" data-link-desc="說明 queue 中等待處理的訊息數如何反映 backlog 與容量壓力">queue depth</a>、<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection pool</a>）。Symptom-based 用於 page on-call、cause-based 用於 warning 級通知。</p>
<p>Alert 觸發後由 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 工程師承接，按 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 的步驟診斷跟處理。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 alert 設計的訊號是服務異常需要在使用者大量回報前被發現跟處理。付款成功率下降、API availability 低於 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLO</a>、<a href="/blog/backend/knowledge-cards/consumer-lag/" data-link-title="Consumer Lag" data-link-desc="說明 consumer lag 如何反映訊息堆積、處理能力與容量風險">consumer lag</a> 持續擴大或 <a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a> 快速增加，都應觸發可行動通知。</p>
<h2 id="設計責任">設計責任</h2>
<p>Alert 設計要定義門檻、持續時間（<code>for</code> duration）、severity、通知對象、抑制規則、runbook link 與回復條件。每個 alert rule 帶 owner metadata — 沒有 owner 的 alert 會在服務演進後退化成 noise 來源，形成 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a>。</p>
<p>SLO-based alerting 用 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> 取代固定閾值，自動適應流量變化。完整的 alert 設計見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>、SLO-based alerting 見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6</a>。</p>
]]></content:encoded></item><item><title>Runbook</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/</guid><description>&lt;p>Runbook 的核心概念是「把事故判斷與操作步驟標準化」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 的行動指南，描述 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 工程師看到特定訊號時如何確認影響、查哪些資料、採取哪些緩解、何時升級，以及如何驗證恢復。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Runbook 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 的行動指南。Alert 告訴 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 工程師有問題，runbook 告訴他們「收到這個 alert 時該做什麼」。每個 critical alert 應該連到一份 runbook — 缺少 runbook link 的 alert 等於「通知了但不告訴你做什麼」，是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a> 的起點。&lt;/p>
&lt;p>Runbook 也服務於 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> — 事故中實際執行的步驟跟 runbook 預設的步驟比較，差異就是 runbook 需要更新的地方。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 runbook 的訊號是同一類事故每次都靠個人經驗處理。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ&lt;/a> 快速增加時，runbook 應引導處理者查看錯誤分類、payload 範圍、最近部署、replay 條件與暫停 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer&lt;/a> 的判斷。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Runbook 的有效結構：症狀描述、影響評估、診斷步驟（先看哪個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、查哪些 log）、可能的修復動作（restart / scale / rollback / failover）、升級路徑（15 分鐘內無法解決時通知誰）。維護責任跟 alert 的 owner 一致 — alert rule 改了但 runbook 沒更新是常見的退化。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Runbook 的核心概念是「把事故判斷與操作步驟標準化」。它是 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的行動指南，描述 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 工程師看到特定訊號時如何確認影響、查哪些資料、採取哪些緩解、何時升級，以及如何驗證恢復。</p>
<h2 id="概念位置">概念位置</h2>
<p>Runbook 是 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 的行動指南。Alert 告訴 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 工程師有問題，runbook 告訴他們「收到這個 alert 時該做什麼」。每個 critical alert 應該連到一份 runbook — 缺少 runbook link 的 alert 等於「通知了但不告訴你做什麼」，是 <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 的起點。</p>
<p>Runbook 也服務於 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> — 事故中實際執行的步驟跟 runbook 預設的步驟比較，差異就是 runbook 需要更新的地方。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 runbook 的訊號是同一類事故每次都靠個人經驗處理。<a href="/blog/backend/knowledge-cards/dead-letter-queue/" data-link-title="Dead-Letter Queue" data-link-desc="說明 dead-letter queue 如何隔離多次處理失敗的訊息">DLQ</a> 快速增加時，runbook 應引導處理者查看錯誤分類、payload 範圍、最近部署、replay 條件與暫停 <a href="/blog/backend/knowledge-cards/consumer/" data-link-title="Consumer" data-link-desc="說明 consumer 如何取得等待處理的工作並產生業務結果">consumer</a> 的判斷。</p>
<h2 id="設計責任">設計責任</h2>
<p>Runbook 的有效結構：症狀描述、影響評估、診斷步驟（先看哪個 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、查哪些 log）、可能的修復動作（restart / scale / rollback / failover）、升級路徑（15 分鐘內無法解決時通知誰）。維護責任跟 alert 的 owner 一致 — alert rule 改了但 runbook 沒更新是常見的退化。完整設計見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4</a>。</p>
]]></content:encoded></item><item><title>Search Index</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/search-index/</guid><description>&lt;p>Search index 的核心概念是「為查詢體驗建立專用的讀取模型」。它擅長全文搜尋、排序、filter 與 facet，通常是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">derived state&lt;/a>、從正式資料源同步而來。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Search index 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 的一種實作。正式狀態仍由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 管理（relational DB、document DB），search index 透過 CDC、event subscription 或 ETL 同步更新。概念上跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view&lt;/a> 類似 — 都是為特定查詢需求預先準備的資料形狀。&lt;/p>
&lt;p>在觀測領域，log storage 的 search index（Elasticsearch / Loki 的 label index）承擔 log 查詢的效能。Index 的欄位選擇跟 cardinality 影響查詢延遲跟儲存成本，見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema&lt;/a>。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>商品搜尋、文件站搜尋、客服多條件檢索、log 查詢通常都需要 search index 提供低延遲查詢體驗。Elasticsearch、Algolia、Meilisearch、Typesense 是常見實作。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計時要定義索引更新延遲（source 變更到 index 可查的時間）、重建流程（index 損壞或 schema 改版時的 full reindex）、查詢語意（全文 vs 結構化 filter）與權限過濾（search 結果是否要按使用者權限過濾）。Index 是 derived state — 修復方式是 rebuild 而非直接修改。&lt;/p></description><content:encoded><![CDATA[<p>Search index 的核心概念是「為查詢體驗建立專用的讀取模型」。它擅長全文搜尋、排序、filter 與 facet，通常是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">derived state</a>、從正式資料源同步而來。</p>
<h2 id="概念位置">概念位置</h2>
<p>Search index 是 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 的一種實作。正式狀態仍由 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 管理（relational DB、document DB），search index 透過 CDC、event subscription 或 ETL 同步更新。概念上跟 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a> 類似 — 都是為特定查詢需求預先準備的資料形狀。</p>
<p>在觀測領域，log storage 的 search index（Elasticsearch / Loki 的 label index）承擔 log 查詢的效能。Index 的欄位選擇跟 cardinality 影響查詢延遲跟儲存成本，見 <a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 log schema</a>。</p>
<h2 id="使用情境">使用情境</h2>
<p>商品搜尋、文件站搜尋、客服多條件檢索、log 查詢通常都需要 search index 提供低延遲查詢體驗。Elasticsearch、Algolia、Meilisearch、Typesense 是常見實作。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計時要定義索引更新延遲（source 變更到 index 可查的時間）、重建流程（index 損壞或 schema 改版時的 full reindex）、查詢語意（全文 vs 結構化 filter）與權限過濾（search 結果是否要按使用者權限過濾）。Index 是 derived state — 修復方式是 rebuild 而非直接修改。</p>
]]></content:encoded></item><item><title>Incident Timeline</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/</guid><description>&lt;p>Incident timeline 的核心概念是「按時間順序記錄事故中的觀測、決策與操作」。時間線是事故的共同事實來源，連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 觸發到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 復盤，讓團隊可以對齊發生順序與影響變化。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Timeline 連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 觸發（事故何時被偵測到）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 回應（何時開始處理）、操作紀錄（做了什麼）、影響變化（使用者影響何時改善 / 惡化）跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>（復盤時重建因果鏈）。&lt;/p>
&lt;p>Timeline 也是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 的時間軸基礎 — decision log 記錄「在這個時間點、基於這個觀測、做了這個決策」，timeline 提供「這個時間點」的上下文。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 incident timeline 的訊號是事故後大家對「先發生什麼」說法不同。若沒有一致時間軸，復盤時很難判斷哪個操作真正帶來改善、哪個決策在當時是合理的。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Timeline 要包含時間戳（UTC、精確到分鐘）、訊號來源（哪個 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> / 人為觀察）、操作內容（restart / rollback / scale）、決策理由與結果驗證。記錄方式應簡潔且可在高壓下維持更新 — 事故中寫 timeline 的成本太高會導致沒人寫。Slack channel pinned message 或事故管理工具的自動 timeline 是常見實作。&lt;/p></description><content:encoded><![CDATA[<p>Incident timeline 的核心概念是「按時間順序記錄事故中的觀測、決策與操作」。時間線是事故的共同事實來源，連接 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 觸發到 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 復盤，讓團隊可以對齊發生順序與影響變化。</p>
<h2 id="概念位置">概念位置</h2>
<p>Timeline 連接 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 觸發（事故何時被偵測到）、<a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 回應（何時開始處理）、操作紀錄（做了什麼）、影響變化（使用者影響何時改善 / 惡化）跟 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>（復盤時重建因果鏈）。</p>
<p>Timeline 也是 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 的時間軸基礎 — decision log 記錄「在這個時間點、基於這個觀測、做了這個決策」，timeline 提供「這個時間點」的上下文。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 incident timeline 的訊號是事故後大家對「先發生什麼」說法不同。若沒有一致時間軸，復盤時很難判斷哪個操作真正帶來改善、哪個決策在當時是合理的。</p>
<h2 id="設計責任">設計責任</h2>
<p>Timeline 要包含時間戳（UTC、精確到分鐘）、訊號來源（哪個 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> / <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> / 人為觀察）、操作內容（restart / rollback / scale）、決策理由與結果驗證。記錄方式應簡潔且可在高壓下維持更新 — 事故中寫 timeline 的成本太高會導致沒人寫。Slack channel pinned message 或事故管理工具的自動 timeline 是常見實作。</p>
]]></content:encoded></item><item><title>On-Call</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/</guid><description>&lt;p>On-call 的核心概念是「在指定時段由明確責任角色承接運行事件」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a> 的執行入口，把告警回應、事故分級、升級決策與交接責任固定化，讓事故處理不依賴臨時找人。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>On-call 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a> 的執行入口。值班工程師是 alert 的第一個接收者，負責判斷「這個 alert 需要什麼等級的回應」。&lt;/p>
&lt;p>On-call 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a> 搭配運作 — runbook 提供行動指南、on-call 工程師執行。制度需要跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a> 跟演練一起維護，避免值班只剩 pager 通知而沒有可執行流程。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 on-call 制度的訊號是事故常在非上班時間發生、或跨區團隊需要連續處理。付款 API 夜間故障時，若沒有清楚值班安排，回復時間通常被人員定位延遲拉長。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>On-call 設計要定義排班週期、回應時限（critical alert 需要 N 分鐘內 ack）、交接格式（交班時把當前狀態跟未關閉事項傳給下一位）、升級路徑（on-call 解不了時升級到 tech lead / manager）與支援角色（secondary on-call 或 subject matter expert）。Alert noise 治理跟 on-call 品質直接相關 — &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue&lt;/a> 會讓值班品質退化。&lt;/p></description><content:encoded><![CDATA[<p>On-call 的核心概念是「在指定時段由明確責任角色承接運行事件」。它是 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a> 與 <a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a> 的執行入口，把告警回應、事故分級、升級決策與交接責任固定化，讓事故處理不依賴臨時找人。</p>
<h2 id="概念位置">概念位置</h2>
<p>On-call 是 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>、<a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> 與 <a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a> 的執行入口。值班工程師是 alert 的第一個接收者，負責判斷「這個 alert 需要什麼等級的回應」。</p>
<p>On-call 跟 <a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a> 搭配運作 — runbook 提供行動指南、on-call 工程師執行。制度需要跟 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a> 跟演練一起維護，避免值班只剩 pager 通知而沒有可執行流程。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 on-call 制度的訊號是事故常在非上班時間發生、或跨區團隊需要連續處理。付款 API 夜間故障時，若沒有清楚值班安排，回復時間通常被人員定位延遲拉長。</p>
<h2 id="設計責任">設計責任</h2>
<p>On-call 設計要定義排班週期、回應時限（critical alert 需要 N 分鐘內 ack）、交接格式（交班時把當前狀態跟未關閉事項傳給下一位）、升級路徑（on-call 解不了時升級到 tech lead / manager）與支援角色（secondary on-call 或 subject matter expert）。Alert noise 治理跟 on-call 品質直接相關 — <a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">alert fatigue</a> 會讓值班品質退化。</p>
]]></content:encoded></item><item><title>Ownership</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/ownership/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/ownership/</guid><description>&lt;p>Ownership 的核心概念是「把責任固定到可執行角色」。它讓團隊在事件、變更與回寫流程中能快速判斷誰主責、誰協作、誰做決策，是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a> 運作的前提。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Ownership 連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert&lt;/a>（每個 alert rule 需要 owner）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>（每個 dashboard 需要維護者）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>（runbook 的更新責任跟服務 owner 一致）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy&lt;/a>。&lt;/p>
&lt;p>在觀測系統中，沒有 owner 的 alert 跟 dashboard 會隨服務演進退化 — alert 變成 noise、dashboard 變成裝飾。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環&lt;/a> 的定期審視需要每個訊號都有明確 owner。&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model&lt;/a> 定義 ownership 矩陣。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 ownership 的訊號是同一事件在不同角色之間反覆轉手、或 alert 觸發後沒人知道該誰處理。Owner 離職但 alert / dashboard / runbook 沒有交接是常見的退化模式。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Ownership 需要定義主責角色、協作角色、升級路由與關閉責任。Owner 變動時（離職、轉組）需要交接流程 — orphan alert / dashboard 的定期掃描是治理的一部分。每次服務邊界調整（新服務上線、服務合併）都應同步檢查 ownership 是否仍對齊。&lt;/p></description><content:encoded><![CDATA[<p>Ownership 的核心概念是「把責任固定到可執行角色」。它讓團隊在事件、變更與回寫流程中能快速判斷誰主責、誰協作、誰做決策，是 <a href="/blog/backend/knowledge-cards/on-call/" data-link-title="On-Call" data-link-desc="說明值班制度如何承接告警、事故分級與升級流程">on-call</a> 與 <a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a> 運作的前提。</p>
<h2 id="概念位置">概念位置</h2>
<p>Ownership 連接 <a href="/blog/backend/knowledge-cards/alert/" data-link-title="Alert" data-link-desc="說明 alert 如何把需要處理的服務症狀轉成可行動通知">alert</a>（每個 alert rule 需要 owner）、<a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>（每個 dashboard 需要維護者）、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>（runbook 的更新責任跟服務 owner 一致）、<a href="/blog/backend/knowledge-cards/incident-severity/" data-link-title="Incident Severity" data-link-desc="說明事故分級如何把產品影響轉成對應處置節奏">incident severity</a> 跟 <a href="/blog/backend/knowledge-cards/escalation-policy/" data-link-title="Escalation Policy" data-link-desc="說明事故升級鏈與值班轉接規則">escalation policy</a>。</p>
<p>在觀測系統中，沒有 owner 的 alert 跟 dashboard 會隨服務演進退化 — alert 變成 noise、dashboard 變成裝飾。<a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a> 的定期審視需要每個訊號都有明確 owner。<a href="/blog/backend/04-observability/observability-operating-model/" data-link-title="4.18 Observability Operating Model" data-link-desc="定義 platform / service team / on-call 對訊號、dashboard、alert 與成本的 ownership">4.18 operating model</a> 定義 ownership 矩陣。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 ownership 的訊號是同一事件在不同角色之間反覆轉手、或 alert 觸發後沒人知道該誰處理。Owner 離職但 alert / dashboard / runbook 沒有交接是常見的退化模式。</p>
<h2 id="設計責任">設計責任</h2>
<p>Ownership 需要定義主責角色、協作角色、升級路由與關閉責任。Owner 變動時（離職、轉組）需要交接流程 — orphan alert / dashboard 的定期掃描是治理的一部分。每次服務邊界調整（新服務上線、服務合併）都應同步檢查 ownership 是否仍對齊。</p>
]]></content:encoded></item><item><title>Continuous Profiling</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/continuous-profiling/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/continuous-profiling/</guid><description>&lt;p>Continuous profiling 的核心概念是「在 production 持續以低 overhead 採集 CPU / heap / lock profile，讓 baseline 隨時可用、不需要等事故才開 profiler」。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 之外能精確到 callstack level 的觀測訊號。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Continuous profiling 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 之外的第四角觀測訊號。Metrics 告訴你「CPU usage 上升了」，trace 告訴你「某條 request 變慢」，profile 告訴你「變慢的那段程式碼是哪幾個 function call」。Profile 是唯一能精確到 callstack level 的觀測訊號。&lt;/p>
&lt;p>Always-on 的核心價值是 baseline — 事故時跟 baseline 做 diff（flame graph diff），看「哪些 function 的 CPU 消耗跟平時不同」。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 continuous profiling 的訊號是「latency 退化常找不到原因、靠事後重現很慢」或「同一段 hot path 反覆出現在事故 RCA 中但缺 baseline 資料」。版本升級後 latency 退化時，profile diff 能直接定位是哪個 function 變慢。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Overhead 控制是 continuous profiling 可行性的前提 — CPU overhead &amp;lt; 1%、memory overhead &amp;lt; 10MB。eBPF-based profiler（Parca、Pyroscope eBPF）在 kernel 層採集、overhead 最低；language runtime 內建（Go pprof、Java JFR）居中。Profile 資料要帶 service / version / region label，讓跨版本 diff 跟 canary 對照可行。完整設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 continuous profiling&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Continuous profiling 的核心概念是「在 production 持續以低 overhead 採集 CPU / heap / lock profile，讓 baseline 隨時可用、不需要等事故才開 profiler」。它是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 之外能精確到 callstack level 的觀測訊號。</p>
<h2 id="概念位置">概念位置</h2>
<p>Continuous profiling 是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/log/" data-link-title="Log" data-link-desc="說明 log 如何記錄單一事件的上下文並支援事故排查">log</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 之外的第四角觀測訊號。Metrics 告訴你「CPU usage 上升了」，trace 告訴你「某條 request 變慢」，profile 告訴你「變慢的那段程式碼是哪幾個 function call」。Profile 是唯一能精確到 callstack level 的觀測訊號。</p>
<p>Always-on 的核心價值是 baseline — 事故時跟 baseline 做 diff（flame graph diff），看「哪些 function 的 CPU 消耗跟平時不同」。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 continuous profiling 的訊號是「latency 退化常找不到原因、靠事後重現很慢」或「同一段 hot path 反覆出現在事故 RCA 中但缺 baseline 資料」。版本升級後 latency 退化時，profile diff 能直接定位是哪個 function 變慢。</p>
<h2 id="設計責任">設計責任</h2>
<p>Overhead 控制是 continuous profiling 可行性的前提 — CPU overhead &lt; 1%、memory overhead &lt; 10MB。eBPF-based profiler（Parca、Pyroscope eBPF）在 kernel 層採集、overhead 最低；language runtime 內建（Go pprof、Java JFR）居中。Profile 資料要帶 service / version / region label，讓跨版本 diff 跟 canary 對照可行。完整設計見 <a href="/blog/backend/04-observability/continuous-profiling/" data-link-title="4.9 Continuous Profiling" data-link-desc="把 CPU / memory / lock profile 從一次性除錯升級為持續訊號">4.9 continuous profiling</a>。</p>
]]></content:encoded></item><item><title>Action Item Closure</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/</guid><description>&lt;p>Action item closure 的核心概念是「把復盤行動項變成可驗證完成的工程責任」。它承接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 的產出，關心的是每一項是否有 owner、完成標準、驗證方式與截止時間，而非列出多少待辦。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Action item closure 連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a>（產出行動項）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook&lt;/a>（行動項可能是更新 runbook）、&lt;a href="https://tarrragon.github.io/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環&lt;/a>（行動項可能是新增 alert / metric / dashboard）。&lt;/p>
&lt;p>Detection gap 類的行動項（「事故中缺少某個 alert / metric」）應指派給觀測系統的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">owner&lt;/a>，帶明確的變更規格（新增哪個 metric、alert 閾值多少、連到哪個 runbook）。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>系統需要 action item closure 流程的訊號是事故復盤後大量 open items 超過 90 天仍未關閉，或同類事故重複發生但上次復盤的改善項還沒完成。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>每個 action item 定義：owner（誰負責完成）、完成標準（什麼狀態算 done — 不是「已開始」而是「已部署、已驗證」）、驗證方式（怎麼確認完成 — 跑一次演練、查 dashboard 確認 metric 存在）、截止時間（兩週內 close）。逾期的 action item 自動升級到管理層 — 這個升級機制是 closure 流程的背壓。&lt;/p></description><content:encoded><![CDATA[<p>Action item closure 的核心概念是「把復盤行動項變成可驗證完成的工程責任」。它承接 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 的產出，關心的是每一項是否有 owner、完成標準、驗證方式與截止時間，而非列出多少待辦。</p>
<h2 id="概念位置">概念位置</h2>
<p>Action item closure 連接 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a>（產出行動項）、<a href="/blog/backend/knowledge-cards/runbook/" data-link-title="Runbook" data-link-desc="說明 runbook 如何把事故判斷與操作步驟標準化">runbook</a>（行動項可能是更新 runbook）、<a href="/blog/backend/04-observability/signal-governance-loop/" data-link-title="4.8 訊號治理閉環" data-link-desc="把 postmortem 揭露的偵測缺口回寫成新訊號、讓觀測能力隨事故學習成長">4.8 訊號治理閉環</a>（行動項可能是新增 alert / metric / dashboard）。</p>
<p>Detection gap 類的行動項（「事故中缺少某個 alert / metric」）應指派給觀測系統的 <a href="/blog/backend/knowledge-cards/ownership/" data-link-title="Ownership" data-link-desc="說明 ownership 如何把問題、決策與交接責任固定到可執行角色">owner</a>，帶明確的變更規格（新增哪個 metric、alert 閾值多少、連到哪個 runbook）。</p>
<h2 id="使用情境">使用情境</h2>
<p>系統需要 action item closure 流程的訊號是事故復盤後大量 open items 超過 90 天仍未關閉，或同類事故重複發生但上次復盤的改善項還沒完成。</p>
<h2 id="設計責任">設計責任</h2>
<p>每個 action item 定義：owner（誰負責完成）、完成標準（什麼狀態算 done — 不是「已開始」而是「已部署、已驗證」）、驗證方式（怎麼確認完成 — 跑一次演練、查 dashboard 確認 metric 存在）、截止時間（兩週內 close）。逾期的 action item 自動升級到管理層 — 這個升級機制是 closure 流程的背壓。</p>
]]></content:encoded></item><item><title>Time Range</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/</guid><description>&lt;p>Time range 的核心概念是「證據或查詢對應的明確時間窗」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state&lt;/a>，讓同一組資料能被事中交班、release gate 與事後復盤一致解讀。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Time range 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。Dashboard 顯示狀態，query link 保留查詢入口，time range 則定義這次判讀看的時間範圍。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 time range 的訊號是：&lt;/p>
&lt;ul>
&lt;li>同一張圖在不同時間重跑會得到不同結果&lt;/li>
&lt;li>release gate 要判斷某批 rollout 是否已穩定&lt;/li>
&lt;li>事故交班需要知道某個 evidence 觀察的是哪段時間&lt;/li>
&lt;li>復盤要對齊 deploy、alert、customer report 與 rollback 的先後&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 的 validation query 若標示 &lt;code>2026-05-11T02:10:00Z/2026-05-11T02:20:00Z&lt;/code>，下一班 on-call 就能把 mismatch、replication lag 與 slow query 放回同一個 backfill batch 判讀。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Time range 要定義開始時間、結束時間、時區、資料延遲限制與關聯事件。它應進入 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition&lt;/a>，避免團隊用不同時間窗比較同一個決策。&lt;/p></description><content:encoded><![CDATA[<p>Time range 的核心概念是「證據或查詢對應的明確時間窗」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/incident-timeline/" data-link-title="Incident Timeline" data-link-desc="說明事故時間線如何支援判斷、溝通與復盤">incident timeline</a> 與 <a href="/blog/backend/knowledge-cards/steady-state/" data-link-title="Steady State" data-link-desc="說明可靠性實驗與事故恢復如何定義系統應維持的可接受狀態">steady state</a>，讓同一組資料能被事中交班、release gate 與事後復盤一致解讀。</p>
<h2 id="概念位置">概念位置</h2>
<p>Time range 位在 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。Dashboard 顯示狀態，query link 保留查詢入口，time range 則定義這次判讀看的時間範圍。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 time range 的訊號是：</p>
<ul>
<li>同一張圖在不同時間重跑會得到不同結果</li>
<li>release gate 要判斷某批 rollout 是否已穩定</li>
<li>事故交班需要知道某個 evidence 觀察的是哪段時間</li>
<li>復盤要對齊 deploy、alert、customer report 與 rollback 的先後</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 的 validation query 若標示 <code>2026-05-11T02:10:00Z/2026-05-11T02:20:00Z</code>，下一班 on-call 就能把 mismatch、replication lag 與 slow query 放回同一個 backfill batch 判讀。</p>
<h2 id="設計責任">設計責任</h2>
<p>Time range 要定義開始時間、結束時間、時區、資料延遲限制與關聯事件。它應進入 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a> 與 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a>，避免團隊用不同時間窗比較同一個決策。</p>
]]></content:encoded></item><item><title>Query Link</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/</guid><description>&lt;p>Query link 的核心概念是「保存可重跑的查詢入口」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality&lt;/a>，讓後續接手者能重新驗證同一個判讀。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Query link 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。截圖適合溝通當下狀態，query link 則保留可回放、可調整、可驗證的入口。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 query link 的訊號是：&lt;/p>
&lt;ul>
&lt;li>事故交班時下一班需要重跑同一個判讀&lt;/li>
&lt;li>release gate 要引用具體查詢結果，而不是貼圖表摘要&lt;/li>
&lt;li>PIR reviewer 需要查證當時資料限制&lt;/li>
&lt;li>dashboard panel 版本變動可能改變圖表語意&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>Checkout API evidence package 可以保存錯誤率 query、p95 latency query 與 provider timeout query 的連結。資料庫 migration evidence package 則可以保存 row count、mismatch sample 與 replication lag query link。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Query link 要保留查詢版本、參數、time range、資料來源與 owner。它要搭配 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap&lt;/a> 記錄查詢未覆蓋的資料範圍，避免截圖或 dashboard 名稱被誤當成完整證據。&lt;/p></description><content:encoded><![CDATA[<p>Query link 的核心概念是「保存可重跑的查詢入口」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">time range</a> 與 <a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality</a>，讓後續接手者能重新驗證同一個判讀。</p>
<h2 id="概念位置">概念位置</h2>
<p>Query link 位在 <a href="/blog/backend/knowledge-cards/dashboard/" data-link-title="Dashboard" data-link-desc="說明 dashboard 如何把關鍵訊號組成可判讀的服務狀態畫面">dashboard</a>、<a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。截圖適合溝通當下狀態，query link 則保留可回放、可調整、可驗證的入口。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 query link 的訊號是：</p>
<ul>
<li>事故交班時下一班需要重跑同一個判讀</li>
<li>release gate 要引用具體查詢結果，而不是貼圖表摘要</li>
<li>PIR reviewer 需要查證當時資料限制</li>
<li>dashboard panel 版本變動可能改變圖表語意</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>Checkout API evidence package 可以保存錯誤率 query、p95 latency query 與 provider timeout query 的連結。資料庫 migration evidence package 則可以保存 row count、mismatch sample 與 replication lag query link。</p>
<h2 id="設計責任">設計責任</h2>
<p>Query link 要保留查詢版本、參數、time range、資料來源與 owner。它要搭配 <a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap</a> 記錄查詢未覆蓋的資料範圍，避免截圖或 dashboard 名稱被誤當成完整證據。</p>
]]></content:encoded></item><item><title>Data Quality</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/</guid><description>&lt;p>Data quality 的核心概念是「證據資料本身的完整度、新鮮度與限制」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap&lt;/a>，讓下游知道這份 evidence 能支持到哪個判斷範圍。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Data quality 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。Metric、log、trace、audit log 都可能有延遲、抽樣、drop、masking 或 schema drift，這些限制要跟證據一起交接。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 data quality 的訊號是：&lt;/p>
&lt;ul>
&lt;li>trace sampling 讓某些 request path 無法完整重建&lt;/li>
&lt;li>log pipeline 有 ingest delay 或 drop&lt;/li>
&lt;li>query 只跑 primary、replica 或部分 tenant&lt;/li>
&lt;li>dashboard 結論需要標示 freshness 或 completeness 限制&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 的 evidence package 可以標示 &lt;code>primary only; replica lag still recovering&lt;/code>，表示 validation query 可信，但 replica 讀取路徑還不能用同一份 evidence 直接放行。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Data quality 要標示 completeness、freshness、sampling、masking、retention 與 owner。它要支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence&lt;/a> 判讀，避免 release gate 或 incident decision log 把有限資料誤當成完整事實。&lt;/p></description><content:encoded><![CDATA[<p>Data quality 的核心概念是「證據資料本身的完整度、新鮮度與限制」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/sampling/" data-link-title="Sampling" data-link-desc="說明觀測資料如何抽樣以控制成本並保留診斷能力">sampling</a> 與 <a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap</a>，讓下游知道這份 evidence 能支持到哪個判斷範圍。</p>
<h2 id="概念位置">概念位置</h2>
<p>Data quality 位在 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a>、<a href="/blog/backend/knowledge-cards/trace/" data-link-title="Trace" data-link-desc="說明 trace 如何重建跨服務請求的路徑、耗時與依賴關係">trace</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。Metric、log、trace、audit log 都可能有延遲、抽樣、drop、masking 或 schema drift，這些限制要跟證據一起交接。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 data quality 的訊號是：</p>
<ul>
<li>trace sampling 讓某些 request path 無法完整重建</li>
<li>log pipeline 有 ingest delay 或 drop</li>
<li>query 只跑 primary、replica 或部分 tenant</li>
<li>dashboard 結論需要標示 freshness 或 completeness 限制</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 的 evidence package 可以標示 <code>primary only; replica lag still recovering</code>，表示 validation query 可信，但 replica 讀取路徑還不能用同一份 evidence 直接放行。</p>
<h2 id="設計責任">設計責任</h2>
<p>Data quality 要標示 completeness、freshness、sampling、masking、retention 與 owner。它要支援 <a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence</a> 判讀，避免 release gate 或 incident decision log 把有限資料誤當成完整事實。</p>
]]></content:encoded></item><item><title>Confidence</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/</guid><description>&lt;p>Confidence 的核心概念是「標示目前證據能支持決策的信心等級」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">gate decision&lt;/a>，讓團隊能區分 confirmed、suspected 與 needs follow-up。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Confidence 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log&lt;/a> 之間。它不是情緒性的「我覺得」，而是基於證據完整度、資料限制與反向驗證狀態的判讀欄位。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 confidence 的訊號是：&lt;/p>
&lt;ul>
&lt;li>evidence 足以支持繼續 backfill，但不足以支持使用者可見 cutover&lt;/li>
&lt;li>事故中某個根因還在 suspected 狀態&lt;/li>
&lt;li>release gate 需要分辨可以放行、暫停或補證據&lt;/li>
&lt;li>stakeholder update 需要避免把未確認資訊說成事實&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration 的 evidence package 可以把 &lt;code>confidence&lt;/code> 標成 &lt;code>suspected&lt;/code>：validation query 顯示 mismatch 低於門檻，但 manual refund repair path 尚未被抽樣，因此只放行下一批 backfill，不放行使用者可見讀取 cutover。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Confidence 要定義等級、證據依據、限制與下一步。它要與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap&lt;/a> 和 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition&lt;/a> 一起保存，避免團隊把暫時結論當成穩定事實。&lt;/p></description><content:encoded><![CDATA[<p>Confidence 的核心概念是「標示目前證據能支持決策的信心等級」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality</a> 與 <a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">gate decision</a>，讓團隊能區分 confirmed、suspected 與 needs follow-up。</p>
<h2 id="概念位置">概念位置</h2>
<p>Confidence 位在 <a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a>、<a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap</a> 與 <a href="/blog/backend/knowledge-cards/incident-decision-log/" data-link-title="Incident Decision Log" data-link-desc="說明事故期間如何保留決策、證據、owner 與回退條件">incident decision log</a> 之間。它不是情緒性的「我覺得」，而是基於證據完整度、資料限制與反向驗證狀態的判讀欄位。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 confidence 的訊號是：</p>
<ul>
<li>evidence 足以支持繼續 backfill，但不足以支持使用者可見 cutover</li>
<li>事故中某個根因還在 suspected 狀態</li>
<li>release gate 需要分辨可以放行、暫停或補證據</li>
<li>stakeholder update 需要避免把未確認資訊說成事實</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration 的 evidence package 可以把 <code>confidence</code> 標成 <code>suspected</code>：validation query 顯示 mismatch 低於門檻，但 manual refund repair path 尚未被抽樣，因此只放行下一批 backfill，不放行使用者可見讀取 cutover。</p>
<h2 id="設計責任">設計責任</h2>
<p>Confidence 要定義等級、證據依據、限制與下一步。它要與 <a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">known gap</a> 和 <a href="/blog/backend/knowledge-cards/rollback-condition/" data-link-title="Rollback Condition" data-link-desc="說明決策執行後出現哪些訊號時要撤回、回退或改路線">rollback condition</a> 一起保存，避免團隊把暫時結論當成穩定事實。</p>
]]></content:encoded></item><item><title>Known Gap</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/known-gap/</guid><description>&lt;p>Known gap 的核心概念是「把已知但尚未覆蓋的證據缺口寫進 artifact」。它連接 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure&lt;/a>，讓缺口能被追蹤、交班與回寫。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Known gap 位在 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review&lt;/a> 之間。Data quality 說明資料限制，known gap 則列出目前尚未被證據覆蓋的具體範圍。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 known gap 的訊號是：&lt;/p>
&lt;ul>
&lt;li>某些 tenant、region、callback path 或 manual repair path 未被抽樣&lt;/li>
&lt;li>trace 或 log 缺少關鍵 span / field&lt;/li>
&lt;li>release gate 放行時仍有需要 follow-up 的證據缺口&lt;/li>
&lt;li>PIR 需要把缺口回寫成 readiness 或 observability 改善項&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>資料庫 migration evidence package 可以記錄 &lt;code>manual refund repair path not yet sampled&lt;/code>。這個 known gap 會限制 cutover decision，並回寫成後續 validation query 或 audit log coverage 的改善項。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Known gap 要描述缺口內容、影響範圍、目前風險、owner 與 follow-up。它要支援 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence&lt;/a> 分級，避免 evidence package 看起來完整，但實際漏掉高風險路徑。&lt;/p></description><content:encoded><![CDATA[<p>Known gap 的核心概念是「把已知但尚未覆蓋的證據缺口寫進 artifact」。它連接 <a href="/blog/backend/knowledge-cards/evidence-package/" data-link-title="Evidence Package" data-link-desc="說明觀測、驗證與事故流程如何把證據包成可交接、可回放的 artifact">evidence package</a>、<a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">data quality</a> 與 <a href="/blog/backend/knowledge-cards/action-item-closure/" data-link-title="Action Item Closure" data-link-desc="說明事故行動項如何被驗證完成，而不是只停留在待辦清單">action item closure</a>，讓缺口能被追蹤、交班與回寫。</p>
<h2 id="概念位置">概念位置</h2>
<p>Known gap 位在 <a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence</a>、<a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">query link</a> 與 <a href="/blog/backend/knowledge-cards/post-incident-review/" data-link-title="Post-Incident Review" data-link-desc="說明事故後如何完成復盤、學習與改進閉環">post-incident review</a> 之間。Data quality 說明資料限制，known gap 則列出目前尚未被證據覆蓋的具體範圍。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 known gap 的訊號是：</p>
<ul>
<li>某些 tenant、region、callback path 或 manual repair path 未被抽樣</li>
<li>trace 或 log 缺少關鍵 span / field</li>
<li>release gate 放行時仍有需要 follow-up 的證據缺口</li>
<li>PIR 需要把缺口回寫成 readiness 或 observability 改善項</li>
</ul>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>資料庫 migration evidence package 可以記錄 <code>manual refund repair path not yet sampled</code>。這個 known gap 會限制 cutover decision，並回寫成後續 validation query 或 audit log coverage 的改善項。</p>
<h2 id="設計責任">設計責任</h2>
<p>Known gap 要描述缺口內容、影響範圍、目前風險、owner 與 follow-up。它要支援 <a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">confidence</a> 分級，避免 evidence package 看起來完整，但實際漏掉高風險路徑。</p>
]]></content:encoded></item><item><title>Recording Rule</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/</guid><description>&lt;p>Recording rule 把重複的聚合計算從查詢時推到寫入時。當 dashboard 或 alert 反覆對同一組 raw &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 做 rate / sum / histogram_quantile，每次查詢都重新掃描原始資料；recording rule 把計算結果預先寫成新的 time series，查詢時直接讀取結果。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Recording rule 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics&lt;/a> 讀取路徑的效能工具。它在 TSDB 層（如 Prometheus、Thanos、Mimir）定期執行 query expression，把結果作為新 series 寫入儲存。概念上類似 OLAP 的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view&lt;/a>，但作用在時間序列而非關聯式資料。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 recording rule 時要定義計算表達式、執行間隔、命名慣例與維護責任。命名慣例通常遵循 &lt;code>level:metric:operations&lt;/code> 格式（如 &lt;code>job:http_requests_total:rate5m&lt;/code>），讓讀者從名稱判斷來源、粒度與計算方式。&lt;/p>
&lt;p>Recording rule 產生的 series 本身也佔儲存空間與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality&lt;/a>。規則數量增長時，要監控 rule evaluation duration 跟 rule group lag，避免 rule 跑不完的情況讓 dashboard 看到過期資料。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 recording rule 的訊號是 dashboard panel 載入時間持續退化、或 alert rule 因為 query timeout 而漏發。把 SLO burn rate 計算、高流量 endpoint 的 rate 與 error ratio 預先聚合成 recording rule，是最常見的起點。&lt;/p>
&lt;p>Recording rule 與 raw query 的分工：高頻讀取（dashboard 自動刷新、alert 每分鐘 evaluate）適合 recording rule；低頻即席查詢（事故時的 ad-hoc 切片）直接查 raw series，保留完整維度。&lt;/p>
&lt;p>在觀測領域的應用見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics 聚合查詢&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Recording rule 把重複的聚合計算從查詢時推到寫入時。當 dashboard 或 alert 反覆對同一組 raw <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 做 rate / sum / histogram_quantile，每次查詢都重新掃描原始資料；recording rule 把計算結果預先寫成新的 time series，查詢時直接讀取結果。</p>
<h2 id="概念位置">概念位置</h2>
<p>Recording rule 是 <a href="/blog/backend/knowledge-cards/metrics/" data-link-title="Metrics" data-link-desc="說明指標如何描述服務趨勢、容量與健康狀態">metrics</a> 讀取路徑的效能工具。它在 TSDB 層（如 Prometheus、Thanos、Mimir）定期執行 query expression，把結果作為新 series 寫入儲存。概念上類似 OLAP 的 <a href="/blog/backend/knowledge-cards/materialized-view/" data-link-title="Materialized View" data-link-desc="說明預先計算並儲存查詢結果以加速讀取的資料結構">materialized view</a>，但作用在時間序列而非關聯式資料。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 recording rule 時要定義計算表達式、執行間隔、命名慣例與維護責任。命名慣例通常遵循 <code>level:metric:operations</code> 格式（如 <code>job:http_requests_total:rate5m</code>），讓讀者從名稱判斷來源、粒度與計算方式。</p>
<p>Recording rule 產生的 series 本身也佔儲存空間與 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">cardinality</a>。規則數量增長時，要監控 rule evaluation duration 跟 rule group lag，避免 rule 跑不完的情況讓 dashboard 看到過期資料。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 recording rule 的訊號是 dashboard panel 載入時間持續退化、或 alert rule 因為 query timeout 而漏發。把 SLO burn rate 計算、高流量 endpoint 的 rate 與 error ratio 預先聚合成 recording rule，是最常見的起點。</p>
<p>Recording rule 與 raw query 的分工：高頻讀取（dashboard 自動刷新、alert 每分鐘 evaluate）適合 recording rule；低頻即席查詢（事故時的 ad-hoc 切片）直接查 raw series，保留完整維度。</p>
<p>在觀測領域的應用見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics 聚合查詢</a> 跟 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
]]></content:encoded></item><item><title>Rollup / Downsampling</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/</guid><description>&lt;p>Rollup 用降低時間精度換取儲存成本與查詢效能。原始資料以秒級或分鐘級採集，隨時間推移被聚合成更粗的粒度（5 分鐘、1 小時、1 天），舊的高精度資料可以刪除或歸檔。它是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 在時間維度的具體實作，跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 分工互補。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Rollup 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering&lt;/a> 在時間維度的具體實作。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 的差別在於：recording rule 是降維度（把多個 label 聚合成一條 series），rollup 是降時間精度（把 15 秒的點變成 5 分鐘的點）。兩者經常搭配使用。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 rollup 時要定義每一層的精度、保留期、聚合函數與查詢路由規則。聚合函數的選擇影響查詢語意：對 counter 做 sum 跟對 gauge 做 average 是合理的；但對 histogram 做 average 會失去分布資訊。&lt;/p>
&lt;p>查詢路由是 rollup 設計的關鍵配套。使用者查詢 7 天範圍時系統自動路由到 5 分鐘粒度、查詢 90 天範圍時路由到 1 小時粒度。若路由不透明，使用者會對精度差異產生困惑。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 rollup 的訊號是 TSDB 儲存成本持續成長、長時間範圍的 dashboard panel 查詢逾時、或保留政策因為儲存限制被迫縮短。Thanos compactor、Cortex/Mimir compactor、VictoriaMetrics downsampling 都是常見實作。&lt;/p>
&lt;p>在觀測領域的查詢設計見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics 聚合查詢&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Rollup 用降低時間精度換取儲存成本與查詢效能。原始資料以秒級或分鐘級採集，隨時間推移被聚合成更粗的粒度（5 分鐘、1 小時、1 天），舊的高精度資料可以刪除或歸檔。它是 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 在時間維度的具體實作，跟 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 分工互補。</p>
<h2 id="概念位置">概念位置</h2>
<p>Rollup 是 <a href="/blog/backend/knowledge-cards/storage-tiering/" data-link-title="Storage Tiering" data-link-desc="說明按資料熱度分層儲存以平衡查詢速度、儲存成本與保留完整性的機制">storage tiering</a> 在時間維度的具體實作。它跟 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 的差別在於：recording rule 是降維度（把多個 label 聚合成一條 series），rollup 是降時間精度（把 15 秒的點變成 5 分鐘的點）。兩者經常搭配使用。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 rollup 時要定義每一層的精度、保留期、聚合函數與查詢路由規則。聚合函數的選擇影響查詢語意：對 counter 做 sum 跟對 gauge 做 average 是合理的；但對 histogram 做 average 會失去分布資訊。</p>
<p>查詢路由是 rollup 設計的關鍵配套。使用者查詢 7 天範圍時系統自動路由到 5 分鐘粒度、查詢 90 天範圍時路由到 1 小時粒度。若路由不透明，使用者會對精度差異產生困惑。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 rollup 的訊號是 TSDB 儲存成本持續成長、長時間範圍的 dashboard panel 查詢逾時、或保留政策因為儲存限制被迫縮短。Thanos compactor、Cortex/Mimir compactor、VictoriaMetrics downsampling 都是常見實作。</p>
<p>在觀測領域的查詢設計見 <a href="/blog/backend/04-observability/metrics-basics/" data-link-title="4.2 metrics 與 SLI/SLO" data-link-desc="整理 counter、gauge、histogram 與服務健康指標">4.2 metrics 聚合查詢</a> 跟 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
]]></content:encoded></item><item><title>Storage Tiering</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/storage-tiering/</guid><description>&lt;p>Storage tiering 按資料被查詢的頻率與時間壓力，把資料放在不同速度與成本的儲存層。最近的資料放在快速儲存（hot tier），較舊的資料依序移到較慢但便宜的儲存（warm tier、cold tier），最終可歸檔到 object storage 或離線備份。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 共同構成觀測資料的生命週期管理，受 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention&lt;/a> 期限驅動。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Storage tiering 是觀測資料管理的基礎設施層決策，影響查詢能力、成本結構與保留政策。它跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup&lt;/a> 的分工是：tiering 決定資料放在哪種儲存、rollup 決定資料以什麼精度存放。兩者共同構成觀測資料的生命週期管理。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 tiering 時要定義每一層的查詢 SLA、儲存成本、資料轉移觸發條件與跨層查詢行為。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>典型儲存&lt;/th>
 &lt;th>查詢延遲&lt;/th>
 &lt;th>資料精度&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Hot&lt;/td>
 &lt;td>SSD / in-memory TSDB&lt;/td>
 &lt;td>毫秒到秒&lt;/td>
 &lt;td>原始精度&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Warm&lt;/td>
 &lt;td>HDD / 分散式儲存&lt;/td>
 &lt;td>秒到十秒&lt;/td>
 &lt;td>原始或輕度 rollup&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cold&lt;/td>
 &lt;td>Object storage / S3&lt;/td>
 &lt;td>十秒到分鐘&lt;/td>
 &lt;td>rollup 或歸檔&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>跨層查詢是 tiering 設計的關鍵問題。當查詢範圍橫跨 hot 跟 warm 兩層時，回應時間由最慢的那層決定。使用者在 dashboard 把時間範圍從「最近 1 小時」拉到「最近 7 天」時，查詢延遲可能從毫秒跳到秒級，體驗落差需要在 UI 或文件中說明。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 tiering 的訊號是觀測儲存成本持續成長但大部分查詢只命中最近的資料、或保留期因為成本壓力被迫縮短導致鑑識與稽核需求無法滿足。Elasticsearch ILM、Loki 的 chunk storage 分層、Thanos / Cortex 的 object storage backend 都是常見實作。&lt;/p>
&lt;p>Tiering 對查詢能力的影響見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality 治理&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Storage tiering 按資料被查詢的頻率與時間壓力，把資料放在不同速度與成本的儲存層。最近的資料放在快速儲存（hot tier），較舊的資料依序移到較慢但便宜的儲存（warm tier、cold tier），最終可歸檔到 object storage 或離線備份。它跟 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 共同構成觀測資料的生命週期管理，受 <a href="/blog/backend/knowledge-cards/retention/" data-link-title="Retention" data-link-desc="說明資料或事件保留多久，以及保留期限如何影響重放與成本">retention</a> 期限驅動。</p>
<h2 id="概念位置">概念位置</h2>
<p>Storage tiering 是觀測資料管理的基礎設施層決策，影響查詢能力、成本結構與保留政策。它跟 <a href="/blog/backend/knowledge-cards/rollup/" data-link-title="Rollup / Downsampling" data-link-desc="說明時間序列資料隨時間降低精度以控制儲存成本與查詢效能的機制">rollup</a> 的分工是：tiering 決定資料放在哪種儲存、rollup 決定資料以什麼精度存放。兩者共同構成觀測資料的生命週期管理。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 tiering 時要定義每一層的查詢 SLA、儲存成本、資料轉移觸發條件與跨層查詢行為。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>典型儲存</th>
          <th>查詢延遲</th>
          <th>資料精度</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Hot</td>
          <td>SSD / in-memory TSDB</td>
          <td>毫秒到秒</td>
          <td>原始精度</td>
      </tr>
      <tr>
          <td>Warm</td>
          <td>HDD / 分散式儲存</td>
          <td>秒到十秒</td>
          <td>原始或輕度 rollup</td>
      </tr>
      <tr>
          <td>Cold</td>
          <td>Object storage / S3</td>
          <td>十秒到分鐘</td>
          <td>rollup 或歸檔</td>
      </tr>
  </tbody>
</table>
<p>跨層查詢是 tiering 設計的關鍵問題。當查詢範圍橫跨 hot 跟 warm 兩層時，回應時間由最慢的那層決定。使用者在 dashboard 把時間範圍從「最近 1 小時」拉到「最近 7 天」時，查詢延遲可能從毫秒跳到秒級，體驗落差需要在 UI 或文件中說明。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 tiering 的訊號是觀測儲存成本持續成長但大部分查詢只命中最近的資料、或保留期因為成本壓力被迫縮短導致鑑識與稽核需求無法滿足。Elasticsearch ILM、Loki 的 chunk storage 分層、Thanos / Cortex 的 object storage backend 都是常見實作。</p>
<p>Tiering 對查詢能力的影響見 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality 治理</a> 跟 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
]]></content:encoded></item><item><title>Materialized View</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/materialized-view/</guid><description>&lt;p>Materialized view 把查詢結果預先計算並持久儲存，是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 的一種實作方式。它跟一般 view 的差別在於 materialized view 有實體儲存，查詢時讀取的是快照而非即時計算。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Materialized view 是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model&lt;/a> 的一種實作方式。在關聯式資料庫中它是 SQL-level 的物化查詢；在觀測領域，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule&lt;/a> 扮演類似角色 — 把聚合計算的結果寫成新的 time series。兩者的共同設計問題是更新頻率、一致性延遲與維護成本。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計 materialized view 時要定義刷新策略（定時 / 觸發 / 手動）、資料新鮮度容忍上限、儲存成本與失效重建流程。刷新頻率決定讀取的 freshness — 每分鐘刷新的 materialized view 最多落後一分鐘，對 dashboard 場景通常足夠，對即席事故診斷可能不夠。&lt;/p>
&lt;h2 id="使用情境">使用情境&lt;/h2>
&lt;p>需要 materialized view 的訊號是同一個複雜查詢被多個消費者反覆執行（dashboard panel、定期報表、alert rule），而且每次查詢的計算成本高到影響原始資料源的效能。在觀測場景中，SLO burn rate、跨服務 error ratio、多維度 latency percentile 是常見的 materialization 候選。&lt;/p>
&lt;p>在資料庫的應用見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership&lt;/a>。在觀測領域的應用見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Materialized view 把查詢結果預先計算並持久儲存，是 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 的一種實作方式。它跟一般 view 的差別在於 materialized view 有實體儲存，查詢時讀取的是快照而非即時計算。</p>
<h2 id="概念位置">概念位置</h2>
<p>Materialized view 是 <a href="/blog/backend/knowledge-cards/read-model/" data-link-title="Read Model" data-link-desc="說明為查詢場景建立的讀取模型，與正式狀態的責任分離">read model</a> 的一種實作方式。在關聯式資料庫中它是 SQL-level 的物化查詢；在觀測領域，<a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 扮演類似角色 — 把聚合計算的結果寫成新的 time series。兩者的共同設計問題是更新頻率、一致性延遲與維護成本。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計 materialized view 時要定義刷新策略（定時 / 觸發 / 手動）、資料新鮮度容忍上限、儲存成本與失效重建流程。刷新頻率決定讀取的 freshness — 每分鐘刷新的 materialized view 最多落後一分鐘，對 dashboard 場景通常足夠，對即席事故診斷可能不夠。</p>
<h2 id="使用情境">使用情境</h2>
<p>需要 materialized view 的訊號是同一個複雜查詢被多個消費者反覆執行（dashboard panel、定期報表、alert rule），而且每次查詢的計算成本高到影響原始資料源的效能。在觀測場景中，SLO burn rate、跨服務 error ratio、多維度 latency percentile 是常見的 materialization 候選。</p>
<p>在資料庫的應用見 <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">1.8 State Ownership</a>。在觀測領域的應用見 <a href="/blog/backend/04-observability/observability-query-design/" data-link-title="4.23 觀測查詢設計" data-link-desc="把觀測資料的讀取路徑當系統設計問題處理：三種查詢模式、storage tiering、pre-aggregation 與資源治理">4.23 觀測查詢設計</a>。</p>
]]></content:encoded></item><item><title>終端機看 nginx 請求：GoAccess、ngxtop 與何時該用 pipeline 而非 TUI</title><link>https://tarrragon.github.io/blog/linux/tools/cli/web-server-log-monitoring/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/tools/cli/web-server-log-monitoring/</guid><description>&lt;p>Web 伺服器日誌監控工具把 nginx／Apache 的 access log 解析成終端機可讀的請求統計，讓遠端 SSH 進去的那台機器上，能即時看到現在誰在打、打哪些路徑、回什麼狀態碼、吃多少頻寬。它跟系統監控（&lt;code>btop&lt;/code> 看 CPU／記憶體）的差別在於觀測對象：系統監控看主機資源，這類看的是 HTTP 請求流。&lt;/p>
&lt;p>本文承接 &lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽&lt;/a> 的 TUI 工具脈絡，屬監控的 web 請求子題。但比起工具本身，更該先分清的是「什麼時候用終端機看請求、什麼時候不該」，這放在最後一節。&lt;/p>
&lt;h2 id="goaccess即時請求儀表板">GoAccess：即時請求儀表板&lt;/h2>
&lt;p>&lt;code>GoAccess&lt;/code> 把 access log 解析成全螢幕的即時儀表板，責任是把一份 log 變成可讀的請求分析：狀態碼分布、top 請求路徑、不重複訪客、頻寬、回應時間、訪客的 OS 與瀏覽器。它既能開互動 TUI，也能輸出 HTML／CSV／JSON 報表。&lt;/p>
&lt;p>驗證它解析的正確性可以走非互動模式 — 餵一份 nginx access log、指定格式、輸出報表：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">goaccess access.log --log-format&lt;span class="o">=&lt;/span>COMBINED -o report.html&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--log-format=COMBINED&lt;/code> 是對應 nginx 標準 combined 格式的預設。實測對一份 13 筆請求的 log，GoAccess 正確分出 9 筆 2xx、4 筆 4xx，並列出 top 路徑（&lt;code>/&lt;/code> 佔多數、&lt;code>/missing&lt;/code> 等 404）、訪客 host、user-agent 與頻寬。互動模式（不加 &lt;code>-o&lt;/code>）則是同一份資料的全螢幕即時版，連線中持續更新。&lt;/p>
&lt;h2 id="ngxtoptop-風格的請求即時表">ngxtop：top 風格的請求即時表&lt;/h2>
&lt;p>&lt;code>ngxtop&lt;/code> 把 access log 做成 &lt;code>top&lt;/code> 風格的即時表，責任是用最精簡的版面看「現在最熱的請求路徑與其狀態碼分布」。它比 GoAccess 輕、聚焦在請求路徑與狀態碼，適合快速掃一眼。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ngxtop -l access.log --no-follow&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--no-follow&lt;/code> 處理現有 log 後就退出（預設會持續跟隨新進的 log）。&lt;/p>
&lt;p>這裡有一個實測會撞到的 gotcha：&lt;strong>ngxtop 的 log 格式要跟實際的 nginx log_format 完全對上，否則它靜默回 0 records&lt;/strong>。nginx 官方 image 的預設 log_format 在標準 combined 之後多了一個 &lt;code>&amp;quot;$http_x_forwarded_for&amp;quot;&lt;/code> 欄位，ngxtop 的預設格式不含它，結果就是「跑得起來、但一筆都沒解析到」。對策是用 &lt;code>-f&lt;/code> 餵實際的格式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ngxtop -l access.log --no-follow &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -f &lt;span class="s1">&amp;#39;$remote_addr - $remote_user [$time_local] &amp;#34;$request&amp;#34; $status $body_bytes_sent &amp;#34;$http_referer&amp;#34; &amp;#34;$http_user_agent&amp;#34; &amp;#34;$http_x_forwarded_for&amp;#34;&amp;#39;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>格式對上後，ngxtop 正確處理 13 筆、分出 9 筆 2xx 與 4 筆 4xx，跟 GoAccess 的結果一致。相較之下 GoAccess 的 &lt;code>--log-format=COMBINED&lt;/code> 對尾端多出的欄位較寬容。判讀訊號很明確：ngxtop 顯示 0 records 時，先懷疑的是格式沒對上，而非沒有流量。&lt;/p>
&lt;h2 id="何時用終端機看請求何時不該">何時用終端機看請求、何時不該&lt;/h2>
&lt;p>工具會用之後，真正該分清的是使用時機。監控 nginx 請求依目的走兩條完全不同的路。&lt;/p>
&lt;p>當下排查與 ad-hoc 觀測，用終端機。情境是「伺服器現在很忙，進去看誰在打」「某個 endpoint 的 5xx 突然變多，即時看是哪一條」。這時 GoAccess／ngxtop／&lt;code>tail -f access.log&lt;/code> 直接在那台機器上看當下狀況，是遠端 SSH 除錯的日常，也是這類 TUI 工具的主場。&lt;/p>
&lt;p>持續的生產監控，不用終端機。沒有人 24 小時盯著 GoAccess。生產環境的請求監控走 pipeline：指標面用 nginx 的 &lt;code>stub_status&lt;/code>（基礎）或 VTS 模組／&lt;code>nginx-prometheus-exporter&lt;/code>（細到 per-status、per-upstream 的請求率），由 Prometheus 抓、Grafana 畫儀表板並設告警；日誌面把 access log 送到 Loki／ELK／Datadog 之類做查詢與長期保存。&lt;/p>
&lt;p>分界濃縮成一句：終端機 TUI 答「這台機器現在怎樣」，pipeline 答「趨勢如何、超標叫我」。所以請求一直都有被監控，只是持續監控的那份在 Prometheus 與日誌平台、不在終端機。生產 pipeline 的設計（metrics、dashboard、SLO、告警與 vendor 選型）屬後端觀測性的範圍，見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台&lt;/a>；當排查升級成事故、需要止血與復盤的協作流程時，見 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Web 伺服器日誌監控工具把 nginx／Apache 的 access log 解析成終端機可讀的請求統計，讓遠端 SSH 進去的那台機器上，能即時看到現在誰在打、打哪些路徑、回什麼狀態碼、吃多少頻寬。它跟系統監控（<code>btop</code> 看 CPU／記憶體）的差別在於觀測對象：系統監控看主機資源，這類看的是 HTTP 請求流。</p>
<p>本文承接 <a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a> 的 TUI 工具脈絡，屬監控的 web 請求子題。但比起工具本身，更該先分清的是「什麼時候用終端機看請求、什麼時候不該」，這放在最後一節。</p>
<h2 id="goaccess即時請求儀表板">GoAccess：即時請求儀表板</h2>
<p><code>GoAccess</code> 把 access log 解析成全螢幕的即時儀表板，責任是把一份 log 變成可讀的請求分析：狀態碼分布、top 請求路徑、不重複訪客、頻寬、回應時間、訪客的 OS 與瀏覽器。它既能開互動 TUI，也能輸出 HTML／CSV／JSON 報表。</p>
<p>驗證它解析的正確性可以走非互動模式 — 餵一份 nginx access log、指定格式、輸出報表：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">goaccess access.log --log-format<span class="o">=</span>COMBINED -o report.html</span></span></code></pre></div><p><code>--log-format=COMBINED</code> 是對應 nginx 標準 combined 格式的預設。實測對一份 13 筆請求的 log，GoAccess 正確分出 9 筆 2xx、4 筆 4xx，並列出 top 路徑（<code>/</code> 佔多數、<code>/missing</code> 等 404）、訪客 host、user-agent 與頻寬。互動模式（不加 <code>-o</code>）則是同一份資料的全螢幕即時版，連線中持續更新。</p>
<h2 id="ngxtoptop-風格的請求即時表">ngxtop：top 風格的請求即時表</h2>
<p><code>ngxtop</code> 把 access log 做成 <code>top</code> 風格的即時表，責任是用最精簡的版面看「現在最熱的請求路徑與其狀態碼分布」。它比 GoAccess 輕、聚焦在請求路徑與狀態碼，適合快速掃一眼。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ngxtop -l access.log --no-follow</span></span></code></pre></div><p><code>--no-follow</code> 處理現有 log 後就退出（預設會持續跟隨新進的 log）。</p>
<p>這裡有一個實測會撞到的 gotcha：<strong>ngxtop 的 log 格式要跟實際的 nginx log_format 完全對上，否則它靜默回 0 records</strong>。nginx 官方 image 的預設 log_format 在標準 combined 之後多了一個 <code>&quot;$http_x_forwarded_for&quot;</code> 欄位，ngxtop 的預設格式不含它，結果就是「跑得起來、但一筆都沒解析到」。對策是用 <code>-f</code> 餵實際的格式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ngxtop -l access.log --no-follow <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -f <span class="s1">&#39;$remote_addr - $remote_user [$time_local] &#34;$request&#34; $status $body_bytes_sent &#34;$http_referer&#34; &#34;$http_user_agent&#34; &#34;$http_x_forwarded_for&#34;&#39;</span></span></span></code></pre></div><p>格式對上後，ngxtop 正確處理 13 筆、分出 9 筆 2xx 與 4 筆 4xx，跟 GoAccess 的結果一致。相較之下 GoAccess 的 <code>--log-format=COMBINED</code> 對尾端多出的欄位較寬容。判讀訊號很明確：ngxtop 顯示 0 records 時，先懷疑的是格式沒對上，而非沒有流量。</p>
<h2 id="何時用終端機看請求何時不該">何時用終端機看請求、何時不該</h2>
<p>工具會用之後，真正該分清的是使用時機。監控 nginx 請求依目的走兩條完全不同的路。</p>
<p>當下排查與 ad-hoc 觀測，用終端機。情境是「伺服器現在很忙，進去看誰在打」「某個 endpoint 的 5xx 突然變多，即時看是哪一條」。這時 GoAccess／ngxtop／<code>tail -f access.log</code> 直接在那台機器上看當下狀況，是遠端 SSH 除錯的日常，也是這類 TUI 工具的主場。</p>
<p>持續的生產監控，不用終端機。沒有人 24 小時盯著 GoAccess。生產環境的請求監控走 pipeline：指標面用 nginx 的 <code>stub_status</code>（基礎）或 VTS 模組／<code>nginx-prometheus-exporter</code>（細到 per-status、per-upstream 的請求率），由 Prometheus 抓、Grafana 畫儀表板並設告警；日誌面把 access log 送到 Loki／ELK／Datadog 之類做查詢與長期保存。</p>
<p>分界濃縮成一句：終端機 TUI 答「這台機器現在怎樣」，pipeline 答「趨勢如何、超標叫我」。所以請求一直都有被監控，只是持續監控的那份在 Prometheus 與日誌平台、不在終端機。生產 pipeline 的設計（metrics、dashboard、SLO、告警與 vendor 選型）屬後端觀測性的範圍，見 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">可觀測性平台</a>；當排查升級成事故、需要止血與復盤的協作流程時，見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">事故處理與復盤</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>系統資源（CPU／記憶體／磁碟）的即時監控：<a href="/blog/linux/tools/cli/tui-monitoring-tools/" data-link-title="TUI 監控工具：btop、htop、k9s 的遠端使用與刷新率調校" data-link-desc="全螢幕 TUI 監控工具在遠端 SSH 情境的使用：htop 進程操作、btop 多資源儀表板、k9s 管 Kubernetes，以及慢速連線下刷新率與頻寬的取捨。">TUI 監控工具</a>。</li>
<li>把即時觀測擺進可持久化的多工器 pane：<a href="/blog/linux/tools/cli/tmux-persistence-and-basics/" data-link-title="tmux 基礎：遠端 session 持久化與基本操作" data-link-desc="tmux 終端機多工器的遠端使用核心：detach/reattach 讓 session 脫離連線生命週期、prefix key 與 window/pane 操作、手機友善的快捷鍵調校，以及 tmux 與 zellij 的選型對照。">tmux 基礎</a>。</li>
<li>這類工具在遠端工具分類中的定位：<a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a>。</li>
</ul>
]]></content:encoded></item><item><title>SQLite Observability and Runbook</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/</guid><description>&lt;p>SQLite observability and runbook 的核心責任是把低操作成本服務補成可交接的 production evidence。SQLite 的元件少，但正式服務仍需要觀測 busy errors、WAL growth、backup freshness、restore drill、disk usage、migration result、file permission 與 application-level query health。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite 的 observability 要貼近 file、process、filesystem 與 application。它通常沒有 server DB 那種長駐監控平面，因此 runbook 要把 signal 從 app metrics、log、scheduled job、file metadata 與 restore evidence 裡組出來。&lt;/p>
&lt;h2 id="signal-inventory">Signal Inventory&lt;/h2>
&lt;p>Signal inventory 的核心責任是列出 SQLite production 化後最能預告事故的訊號。這些訊號要放進 dashboard、log search 或 scheduled report，讓事故前後都能直接查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>代表風險&lt;/th>
 &lt;th>建議反應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>SQLITE_BUSY&lt;/code> count&lt;/td>
 &lt;td>app log / metric&lt;/td>
 &lt;td>writer contention、long reader&lt;/td>
 &lt;td>查 transaction duration、busy timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WAL file size&lt;/td>
 &lt;td>filesystem metric&lt;/td>
 &lt;td>checkpoint lag、long reader&lt;/td>
 &lt;td>查 checkpoint result、reader age&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup age&lt;/td>
 &lt;td>scheduled job metric&lt;/td>
 &lt;td>RPO 擴大&lt;/td>
 &lt;td>重跑 backup、檢查 storage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Restore drill age&lt;/td>
 &lt;td>release evidence&lt;/td>
 &lt;td>RTO 信心下降&lt;/td>
 &lt;td>排程 restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Disk free&lt;/td>
 &lt;td>host / platform metric&lt;/td>
 &lt;td>write failure、checkpoint failure&lt;/td>
 &lt;td>清理、擴容、降級寫入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration version&lt;/td>
 &lt;td>app startup / metadata&lt;/td>
 &lt;td>schema drift&lt;/td>
 &lt;td>block release、跑 validation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Integrity check result&lt;/td>
 &lt;td>maintenance job&lt;/td>
 &lt;td>corruption / storage issue&lt;/td>
 &lt;td>進入 restore decision&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>SQLITE_BUSY&lt;/code> 是 writer boundary 的最直接訊號。它可能代表長交易、read cursor 未關、parallel test 共用 DB、checkpoint 壓力或 write burst；runbook 要先查 query duration 與 transaction boundary，再調 busy timeout。&lt;/p>
&lt;p>WAL size 是 checkpoint 與 reader 壓力的綜合訊號。WAL 持續成長時，先確認是否有長 reader、backup process、未完成 transaction 或 checkpoint 失敗；接著才考慮手動 checkpoint。&lt;/p>
&lt;p>Backup age 是 RPO 的可觀測版本。若目標 RPO 是 5 分鐘，dashboard 就要顯示 last successful backup / replica time 與警戒線。&lt;/p>
&lt;h2 id="backup-evidence">Backup Evidence&lt;/h2>
&lt;p>Backup evidence 的核心責任是證明資料可被拿回來。SQLite backup 的完成標準包含成功建立備份、保存 sidecar 語意、恢復到新路徑、通過 integrity check、跑 application smoke test。&lt;/p></description><content:encoded><![CDATA[<p>SQLite observability and runbook 的核心責任是把低操作成本服務補成可交接的 production evidence。SQLite 的元件少，但正式服務仍需要觀測 busy errors、WAL growth、backup freshness、restore drill、disk usage、migration result、file permission 與 application-level query health。</p>
<p>本文的判讀錨點是：SQLite 的 observability 要貼近 file、process、filesystem 與 application。它通常沒有 server DB 那種長駐監控平面，因此 runbook 要把 signal 從 app metrics、log、scheduled job、file metadata 與 restore evidence 裡組出來。</p>
<h2 id="signal-inventory">Signal Inventory</h2>
<p>Signal inventory 的核心責任是列出 SQLite production 化後最能預告事故的訊號。這些訊號要放進 dashboard、log search 或 scheduled report，讓事故前後都能直接查。</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>來源</th>
          <th>代表風險</th>
          <th>建議反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SQLITE_BUSY</code> count</td>
          <td>app log / metric</td>
          <td>writer contention、long reader</td>
          <td>查 transaction duration、busy timeout</td>
      </tr>
      <tr>
          <td>WAL file size</td>
          <td>filesystem metric</td>
          <td>checkpoint lag、long reader</td>
          <td>查 checkpoint result、reader age</td>
      </tr>
      <tr>
          <td>Backup age</td>
          <td>scheduled job metric</td>
          <td>RPO 擴大</td>
          <td>重跑 backup、檢查 storage</td>
      </tr>
      <tr>
          <td>Restore drill age</td>
          <td>release evidence</td>
          <td>RTO 信心下降</td>
          <td>排程 restore drill</td>
      </tr>
      <tr>
          <td>Disk free</td>
          <td>host / platform metric</td>
          <td>write failure、checkpoint failure</td>
          <td>清理、擴容、降級寫入</td>
      </tr>
      <tr>
          <td>Migration version</td>
          <td>app startup / metadata</td>
          <td>schema drift</td>
          <td>block release、跑 validation</td>
      </tr>
      <tr>
          <td>Integrity check result</td>
          <td>maintenance job</td>
          <td>corruption / storage issue</td>
          <td>進入 restore decision</td>
      </tr>
  </tbody>
</table>
<p><code>SQLITE_BUSY</code> 是 writer boundary 的最直接訊號。它可能代表長交易、read cursor 未關、parallel test 共用 DB、checkpoint 壓力或 write burst；runbook 要先查 query duration 與 transaction boundary，再調 busy timeout。</p>
<p>WAL size 是 checkpoint 與 reader 壓力的綜合訊號。WAL 持續成長時，先確認是否有長 reader、backup process、未完成 transaction 或 checkpoint 失敗；接著才考慮手動 checkpoint。</p>
<p>Backup age 是 RPO 的可觀測版本。若目標 RPO 是 5 分鐘，dashboard 就要顯示 last successful backup / replica time 與警戒線。</p>
<h2 id="backup-evidence">Backup Evidence</h2>
<p>Backup evidence 的核心責任是證明資料可被拿回來。SQLite backup 的完成標準包含成功建立備份、保存 sidecar 語意、恢復到新路徑、通過 integrity check、跑 application smoke test。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>最小內容</th>
          <th>失敗時路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup job result</td>
          <td>timestamp、duration、file size、target</td>
          <td>重跑 job、檢查 credential / disk</td>
      </tr>
      <tr>
          <td>Restore artifact</td>
          <td>restored path、checksum、row count</td>
          <td>回前一份 backup、檢查 WAL / snapshot</td>
      </tr>
      <tr>
          <td>Integrity result</td>
          <td><code>PRAGMA integrity_check;</code></td>
          <td>停止寫入、進入 corruption triage</td>
      </tr>
      <tr>
          <td>Application smoke test</td>
          <td>啟動、讀核心頁、寫測試資料</td>
          <td>rollback、保留 evidence</td>
      </tr>
      <tr>
          <td>Retention note</td>
          <td>保存天數、刪除策略、legal hold</td>
          <td>更新 data protection policy</td>
      </tr>
  </tbody>
</table>
<p>SQLite 官方 <a href="https://www.sqlite.org/backup.html">backup API</a> 與 CLI <code>.backup</code> 是備份設計的基礎路由。WAL mode 下，直接複製單一 <code>.db</code> 檔容易漏掉 sidecar file 的時序；runbook 應使用 SQLite-aware backup 或經過 checkpoint / stop-the-world 的 snapshot。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.backup &#39;backup/app-2026-05-21.db&#39;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 backup/app-2026-05-21.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這段命令提供最小 restore evidence 的起點。正式演練要把備份檔複製到隔離路徑，使用相同 application version 啟動，跑核心 read/write smoke test，再記錄耗時與失敗條件。</p>
<h2 id="migration-evidence">Migration Evidence</h2>
<p>Migration evidence 的核心責任是讓 SQLite schema change 可回退、可審查、可交接。單檔 DB 在使用者裝置或服務節點上升級時，migration 失敗會直接影響啟動、資料讀取與同步。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>內容</th>
          <th>Release gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema version</td>
          <td><code>PRAGMA user_version</code> 或 migration table</td>
          <td>app startup 比對 expected version</td>
      </tr>
      <tr>
          <td>Pre-migration snapshot</td>
          <td>backup path、size、checksum</td>
          <td>migration 前完成</td>
      </tr>
      <tr>
          <td>Validation query</td>
          <td>row count、FK check、domain invariant</td>
          <td>migration 後立即執行</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>核心 read/write workflow</td>
          <td>app release gate</td>
      </tr>
      <tr>
          <td>Rollback route</td>
          <td>restore snapshot 或 block startup</td>
          <td>migration 失敗時啟動</td>
      </tr>
  </tbody>
</table>
<p>Migration log 要包含版本、耗時、row count、錯誤、validation result 與 rollback decision。若 SQLite file 位於 end-user device，log 還要能被使用者支援流程收集，避免事故只停在「app 開不起來」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_key_check</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span></span></span></code></pre></div><p>這些 query 是 migration 後的最小 evidence。正式服務要再補 domain-specific invariant，例如「所有 active subscription 都有 owner」、「所有 pending mutation 都有 idempotency key」。</p>
<h2 id="incident-runbook">Incident Runbook</h2>
<p>Incident runbook 的核心責任是把 SQLite 事故分流到正確處置。SQLite 常見事故包含 disk full、busy storm、WAL growth、bad migration、corruption suspicion、backup failure 與 permission error。</p>
<table>
  <thead>
      <tr>
          <th>Incident</th>
          <th>第一個判讀問題</th>
          <th>立即處置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Busy storm</td>
          <td>有長 transaction 或 write burst 嗎</td>
          <td>暫停非必要寫入、查 transaction duration</td>
      </tr>
      <tr>
          <td>Disk full</td>
          <td>DB / WAL / backup 哪個吃掉空間</td>
          <td>停止寫入、清理 backup、擴容</td>
      </tr>
      <tr>
          <td>WAL growth</td>
          <td>checkpoint 被誰阻擋</td>
          <td>查 reader、跑 checkpoint evidence</td>
      </tr>
      <tr>
          <td>Bad migration</td>
          <td>schema version 與 app version 是否一致</td>
          <td>停止 rollout、restore snapshot、保留 failed DB</td>
      </tr>
      <tr>
          <td>Corruption signal</td>
          <td>integrity check 是否失敗</td>
          <td>進入 read-only、restore last good backup</td>
      </tr>
      <tr>
          <td>Backup failure</td>
          <td>credential、network、destination 是否可用</td>
          <td>切換 destination、補跑 restore drill</td>
      </tr>
  </tbody>
</table>
<p>Busy storm 要先保護使用者操作。可以降低 write endpoint、停用背景 job、延長 retry backoff，然後用 log 查最長 transaction 與最多重試的 query。</p>
<p>Disk full 要先停止寫入。SQLite 在 disk full 時可能讓 write / checkpoint / backup 同時失敗；runbook 要保留剩餘空間、DB file、WAL file、backup directory 與 tmp directory 的大小。</p>
<p>Bad migration 要保留 failed artifact。先複製 failed DB 到 evidence path，記錄 schema version、app version、migration id、validation error，再執行 rollback。</p>
<h2 id="dashboard-and-alert-route">Dashboard and Alert Route</h2>
<p>Dashboard and alert route 的核心責任是讓 SQLite 被納入正式服務的可觀測系統。SQLite signal 常來自 application，因此 metric 命名要接近操作問題。</p>
<table>
  <thead>
      <tr>
          <th>Metric name example</th>
          <th>類型</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sqlite_busy_total</code></td>
          <td>counter</td>
          <td>writer contention</td>
      </tr>
      <tr>
          <td><code>sqlite_query_duration_ms</code></td>
          <td>histogram</td>
          <td>slow query / long transaction</td>
      </tr>
      <tr>
          <td><code>sqlite_wal_size_bytes</code></td>
          <td>gauge</td>
          <td>checkpoint pressure</td>
      </tr>
      <tr>
          <td><code>sqlite_backup_age_seconds</code></td>
          <td>gauge</td>
          <td>RPO evidence</td>
      </tr>
      <tr>
          <td><code>sqlite_restore_drill_age_days</code></td>
          <td>gauge</td>
          <td>RTO confidence</td>
      </tr>
      <tr>
          <td><code>sqlite_disk_free_bytes</code></td>
          <td>gauge</td>
          <td>disk full prevention</td>
      </tr>
      <tr>
          <td><code>sqlite_migration_version</code></td>
          <td>gauge</td>
          <td>schema drift</td>
      </tr>
  </tbody>
</table>
<p>Alert 要連到 runbook，並提供可執行的第一步。每個 alert 至少要有 owner、severity、first query、rollback condition 與 escalation route。</p>
<p>Log schema 要保留 query category，而非只記原始 SQL。正式服務通常應避免把完整 SQL 與 PII 直接寫入 log；可以記 operation name、duration、row count、error code、busy retry count 與 correlation id。</p>
<h2 id="handoff">Handoff</h2>
<p>Handoff 的核心責任是讓下一個維護者知道 SQLite service 的邊界。交接文件要把「誰負責檔案」、「誰負責備份」、「誰能執行 restore」、「何時升級資料庫」寫清楚。</p>
<p>最小 handoff 包含：</p>
<ol>
<li>Database file path、sidecar file policy、journal mode 與 PRAGMA baseline。</li>
<li>Backup command、destination、retention、last restore drill。</li>
<li>Migration command、schema version、rollback route。</li>
<li>Alert list、dashboard link、incident owner。</li>
<li>Known limits：writer concurrency、file size、edge / sync boundary。</li>
<li>Next route：PostgreSQL、D1 / Turso、Litestream / LiteFS 的評估條件。</li>
</ol>
<p>Handoff 的重點是把低操作成本保留下來。SQLite 的好處來自少元件；可交接文件讓少元件不等於少 evidence。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Observability / runbook 完成後，下一步要接到具體演練。Backup 與 restore 讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/backup-restore-drill/" data-link-title="SQLite Backup Restore Drill" data-link-desc="SQLite .backup、VACUUM INTO、restore validation、sidecar file handling 與 RPO / RTO note 的操作說明">SQLite backup restore drill</a>；WAL 與 busy 讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a>；正式服務的 evidence 可對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
]]></content:encoded></item></channel></rss>