<?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>Alerting on Tarragon</title><link>https://tarrragon.github.io/blog/tags/alerting/</link><description>Recent content in Alerting on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 02 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/alerting/index.xml" rel="self" type="application/rss+xml"/><item><title>Rule engine 設計</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/rule-engine/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/rule-engine/</guid><description>&lt;p>Rule engine 是 collector 的主動處理層。事件寫入儲存後，rule engine 檢查事件是否匹配預定義的規則，匹配時執行對應的動作。沒有 rule engine 的 collector 是被動的資料倉庫 — 開發者需要主動查詢才能發現問題。Rule engine 讓 collector 能在問題發生時主動通知。&lt;/p>
&lt;h2 id="三段式規則結構">三段式規則結構&lt;/h2>
&lt;p>每條規則由三部分組成：條件（什麼事件觸發）、動作（觸發後做什麼）、模板（動作的內容格式）。&lt;/p>
&lt;h3 id="條件">條件&lt;/h3>
&lt;p>條件定義「哪些事件匹配這條規則」。條件是事件欄位的過濾器 — 事件類型、事件名稱、屬性值的比較。&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;condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;error&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;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;terminal.connect.*&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;severity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;fatal&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;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;/p>
&lt;ul>
&lt;li>&lt;strong>精確匹配&lt;/strong>：&lt;code>&amp;quot;type&amp;quot;: &amp;quot;error&amp;quot;&lt;/code> — 事件類型必須是 error&lt;/li>
&lt;li>&lt;strong>前綴匹配&lt;/strong>：&lt;code>&amp;quot;name&amp;quot;: &amp;quot;terminal.connect.*&amp;quot;&lt;/code> — 事件名稱以 &lt;code>terminal.connect.&lt;/code> 開頭&lt;/li>
&lt;li>&lt;strong>數值比較&lt;/strong>：&lt;code>&amp;quot;data.duration_ms&amp;quot;: { &amp;quot;gt&amp;quot;: 5000 }&lt;/code> — 持續時間超過 5 秒&lt;/li>
&lt;li>&lt;strong>組合條件&lt;/strong>：多個欄位條件同時滿足（AND 邏輯）&lt;/li>
&lt;/ul>
&lt;h3 id="動作">動作&lt;/h3>
&lt;p>動作定義「條件匹配後做什麼」。常見的動作類型：&lt;/p>
&lt;p>&lt;strong>通知&lt;/strong>：發送訊息到指定管道（email、Slack webhook、Telegram bot、桌面通知）。&lt;/p>
&lt;p>&lt;strong>寫 summary&lt;/strong>：把匹配的事件摘要寫入 summary 檔案，供定期 review。和逐筆事件不同，summary 是聚合後的結果（例如「過去一小時有 15 個 terminal.connect.failed」）。&lt;/p>
&lt;p>&lt;strong>觸發 webhook&lt;/strong>：向外部 URL 發送 HTTP POST，讓其他系統可以接收事件並做進一步處理。&lt;/p>
&lt;p>&lt;strong>執行腳本&lt;/strong>：在 collector server 上執行預定義的 shell script。適合自動化回應（重啟服務、清理暫存檔、輪替 log）。執行腳本的安全風險需要控制 — 只允許白名單內的腳本。&lt;/p>
&lt;h3 id="模板">模板&lt;/h3>
&lt;p>模板定義動作的內容格式。通知的訊息內容、webhook 的 request body — 用模板語法（Go template 或 mustache）把事件欄位填入。&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">{{ .name }} 發生於 {{ .ts }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">嚴重度：{{ .data.severity }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">訊息：{{ .data.message }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>模板讓同一個動作類型適用不同的事件 — 不需要為每種事件寫不同的通知函式。&lt;/p>
&lt;h2 id="規則評估時機">規則評估時機&lt;/h2>
&lt;h3 id="即時評估">即時評估&lt;/h3>
&lt;p>每個事件寫入後立即評估所有規則。適合需要即時回應的規則（fatal error 通知）。&lt;/p>
&lt;p>即時評估的成本和規則數量成正比 — 100 條規則代表每個事件寫入後做 100 次條件匹配。規則數量在數十條以內時，評估時間可以忽略。&lt;/p>
&lt;h3 id="批次評估">批次評估&lt;/h3>
&lt;p>定期（每分鐘、每小時）掃描一段時間內的事件，評估聚合類規則。適合基於統計的規則（「過去 5 分鐘 error 數量超過 10」「過去 1 小時某 endpoint 的 P95 回應時間超過 2 秒」）。&lt;/p>
&lt;p>批次評估需要時間窗口的概念 — 規則條件中包含時間範圍和聚合函式（count、avg、max、percentile）。&lt;/p>
&lt;h3 id="混合策略">混合策略&lt;/h3>
&lt;p>即時評估用於單一事件觸發的規則（fatal error → 立即通知），批次評估用於聚合觸發的規則（error rate 異常 → 定期檢查）。兩者可以共存。&lt;/p>
&lt;h2 id="規則管理">規則管理&lt;/h2>
&lt;p>規則以 JSON 或 YAML 檔案儲存在 collector 的設定目錄中。新增、修改、刪除規則是編輯檔案 + 重新載入 collector（signal 或 API call）。&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="nt">rules&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">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fatal-error-notify&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">condition&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">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">error&lt;/span>&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">data.severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fatal&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">action&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">7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">slack&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">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://hooks.slack.com/...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;FATAL: {{ .name }} at {{ .ts }}&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>規則檔案版本控制在 git 中，和 collector 的其他設定一起管理。規則變更歷史可追溯。&lt;/p>
&lt;h2 id="shell-執行的安全邊界">Shell 執行的安全邊界&lt;/h2>
&lt;p>Rule engine 的「執行腳本」動作在 collector 主機上執行 shell command。這個能力和 collector 的認證狀態組合後產生不同的風險等級。&lt;/p></description><content:encoded><![CDATA[<p>Rule engine 是 collector 的主動處理層。事件寫入儲存後，rule engine 檢查事件是否匹配預定義的規則，匹配時執行對應的動作。沒有 rule engine 的 collector 是被動的資料倉庫 — 開發者需要主動查詢才能發現問題。Rule engine 讓 collector 能在問題發生時主動通知。</p>
<h2 id="三段式規則結構">三段式規則結構</h2>
<p>每條規則由三部分組成：條件（什麼事件觸發）、動作（觸發後做什麼）、模板（動作的內容格式）。</p>
<h3 id="條件">條件</h3>
<p>條件定義「哪些事件匹配這條規則」。條件是事件欄位的過濾器 — 事件類型、事件名稱、屬性值的比較。</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;condition&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;error&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;terminal.connect.*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nt">&#34;severity&#34;</span><span class="p">:</span> <span class="s2">&#34;fatal&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>條件支援的匹配方式：</p>
<ul>
<li><strong>精確匹配</strong>：<code>&quot;type&quot;: &quot;error&quot;</code> — 事件類型必須是 error</li>
<li><strong>前綴匹配</strong>：<code>&quot;name&quot;: &quot;terminal.connect.*&quot;</code> — 事件名稱以 <code>terminal.connect.</code> 開頭</li>
<li><strong>數值比較</strong>：<code>&quot;data.duration_ms&quot;: { &quot;gt&quot;: 5000 }</code> — 持續時間超過 5 秒</li>
<li><strong>組合條件</strong>：多個欄位條件同時滿足（AND 邏輯）</li>
</ul>
<h3 id="動作">動作</h3>
<p>動作定義「條件匹配後做什麼」。常見的動作類型：</p>
<p><strong>通知</strong>：發送訊息到指定管道（email、Slack webhook、Telegram bot、桌面通知）。</p>
<p><strong>寫 summary</strong>：把匹配的事件摘要寫入 summary 檔案，供定期 review。和逐筆事件不同，summary 是聚合後的結果（例如「過去一小時有 15 個 terminal.connect.failed」）。</p>
<p><strong>觸發 webhook</strong>：向外部 URL 發送 HTTP POST，讓其他系統可以接收事件並做進一步處理。</p>
<p><strong>執行腳本</strong>：在 collector server 上執行預定義的 shell script。適合自動化回應（重啟服務、清理暫存檔、輪替 log）。執行腳本的安全風險需要控制 — 只允許白名單內的腳本。</p>
<h3 id="模板">模板</h3>
<p>模板定義動作的內容格式。通知的訊息內容、webhook 的 request body — 用模板語法（Go template 或 mustache）把事件欄位填入。</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">{{ .name }} 發生於 {{ .ts }}
</span></span><span class="line"><span class="ln">2</span><span class="cl">嚴重度：{{ .data.severity }}
</span></span><span class="line"><span class="ln">3</span><span class="cl">訊息：{{ .data.message }}</span></span></code></pre></div><p>模板讓同一個動作類型適用不同的事件 — 不需要為每種事件寫不同的通知函式。</p>
<h2 id="規則評估時機">規則評估時機</h2>
<h3 id="即時評估">即時評估</h3>
<p>每個事件寫入後立即評估所有規則。適合需要即時回應的規則（fatal error 通知）。</p>
<p>即時評估的成本和規則數量成正比 — 100 條規則代表每個事件寫入後做 100 次條件匹配。規則數量在數十條以內時，評估時間可以忽略。</p>
<h3 id="批次評估">批次評估</h3>
<p>定期（每分鐘、每小時）掃描一段時間內的事件，評估聚合類規則。適合基於統計的規則（「過去 5 分鐘 error 數量超過 10」「過去 1 小時某 endpoint 的 P95 回應時間超過 2 秒」）。</p>
<p>批次評估需要時間窗口的概念 — 規則條件中包含時間範圍和聚合函式（count、avg、max、percentile）。</p>
<h3 id="混合策略">混合策略</h3>
<p>即時評估用於單一事件觸發的規則（fatal error → 立即通知），批次評估用於聚合觸發的規則（error rate 異常 → 定期檢查）。兩者可以共存。</p>
<h2 id="規則管理">規則管理</h2>
<p>規則以 JSON 或 YAML 檔案儲存在 collector 的設定目錄中。新增、修改、刪除規則是編輯檔案 + 重新載入 collector（signal 或 API call）。</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">rules</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">fatal-error-notify</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">condition</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">type</span><span class="p">:</span><span class="w"> </span><span class="l">error</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">data.severity</span><span class="p">:</span><span class="w"> </span><span class="l">fatal</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">action</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">type</span><span class="p">:</span><span class="w"> </span><span class="l">slack</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">webhook</span><span class="p">:</span><span class="w"> </span><span class="l">https://hooks.slack.com/...</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">template</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;FATAL: {{ .name }} at {{ .ts }}&#34;</span></span></span></code></pre></div><p>規則檔案版本控制在 git 中，和 collector 的其他設定一起管理。規則變更歷史可追溯。</p>
<h2 id="shell-執行的安全邊界">Shell 執行的安全邊界</h2>
<p>Rule engine 的「執行腳本」動作在 collector 主機上執行 shell command。這個能力和 collector 的認證狀態組合後產生不同的風險等級。</p>
<h3 id="攻擊鏈">攻擊鏈</h3>
<p>無認證模式下，攻擊者可以向 collector 的 <code>/v1/events</code> endpoint 注入偽造事件。如果偽造事件匹配了一條規則、且規則的動作是執行 free-form shell command，攻擊者等於取得了 collector 主機的命令執行權（RCE — Remote Code Execution）。</p>
<p>攻擊路徑：注入假事件 → 匹配 rule → 執行 shell → RCE。</p>
<h3 id="防護措施">防護措施</h3>
<p><strong>Rule 定義不可透過 API 新增</strong>。Rule 只能由管理員透過配置檔或 CLI 設定，collector 的 HTTP API 不提供 rule CRUD endpoint。攻擊者即使能注入事件也無法新增 rule — 但現有 rule 的條件如果太寬（例如 <code>type: error</code> 沒有進一步限定 name），偽造的 error 事件仍可能匹配。</p>
<p><strong>Shell command 使用 allowlist</strong>。Rule 的 action 指定 command name（如 <code>restart-ttyd</code>），command 的實際路徑在配置檔的 allowlist 中定義。Rule 不接受 free-form shell string（如 <code>sh -c &quot;rm -rf /&quot;</code>）。</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"># 配置檔</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">allowed_commands</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">restart-ttyd</span><span class="p">:</span><span class="w"> </span><span class="l">/usr/local/bin/restart-ttyd.sh</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">notify-slack</span><span class="p">:</span><span class="w"> </span><span class="l">/usr/local/bin/notify-slack.sh</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</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"> 7</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">fatal-error-response</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">condition</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">type</span><span class="p">:</span><span class="w"> </span><span class="l">error</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">data.severity</span><span class="p">:</span><span class="w"> </span><span class="l">fatal</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></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">command</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">command</span><span class="p">:</span><span class="w"> </span><span class="l">restart-ttyd </span><span class="w"> </span><span class="c"># 只接受 allowlist 中的 name</span></span></span></code></pre></div><p><strong>無認證模式下的額外限制</strong>。Collector 無認證時（同區網信任），建議禁用 command 類型的動作、只允許通知和 webhook。認證啟用後才解鎖 command 動作 — 認證確保只有授權的 SDK 實例能送事件，降低偽造事件觸發 rule 的風險。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Collector 的完整架構 → <a href="/blog/monitoring/04-collector/architecture/" data-link-title="Collector 架構" data-link-desc="HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine 的五段式處理鏈路">Collector 架構</a></li>
<li>規模成長後的演進路徑 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>事件的分類和命名 → <a href="/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">監控心智模型 四類事件</a></li>
<li>Rule engine 在偽造流量偵測的應用 → <a href="/blog/monitoring/07-security-privacy/client-sdk-authentication/" data-link-title="Client-side SDK 認證的根本限制" data-link-desc="嵌在 client 端的 credential 必然可被提取 — 認清 architecture 天花板後的多層緩解策略，從 origin 驗證到 device attestation">Client-side SDK 認證</a></li>
</ul>
]]></content:encoded></item><item><title>服務掛了怎麼自動知道：從肉眼盯到主動告警</title><link>https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/</guid><description>&lt;p>服務掛了不需要用肉眼盯——systemd 本來就在追蹤每個 unit 的狀態，你要做的是把「讀權威狀態」這件事自動化，並在狀態變成失敗時主動推播給自己。這篇跟本系列其他篇的差別在時機：診斷是出事後回頭找根因，監控是讓系統在出事的當下就告訴你。兩者共用同一個地基——權威狀態。診斷是手動讀一次權威狀態，監控是訂閱權威狀態的變化、變壞就推播。&lt;/p>
&lt;p>理解這個框架後，監控就不是「裝一套很重的東西」，而是分層選擇：從 systemd 內建的失敗鉤子（不裝任何額外服務），到推播管道，到「整台機器死掉」的體外心跳，到完整的指標儀表板。多數人只需要前一兩層。&lt;/p>
&lt;h2 id="你現在手動在做的事要被取代的基線">你現在手動在做的事（要被取代的基線）&lt;/h2>
&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">systemctl --failed &lt;span class="c1"># 現在有哪些 unit 處於 failed（開機後系統怪怪的先掃這個）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">systemctl is-failed &amp;lt;unit&amp;gt; &lt;span class="c1"># 單一 unit 明確判失敗（比 is-active 直接）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">journalctl -u &amp;lt;unit&amp;gt; -f &lt;span class="c1"># 即時跟一個 unit 的 log&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>systemctl --failed&lt;/code> 就是「服務死活」的權威清單。手動版的問題不是不準，是你得記得去看。下面每一層都是把「記得去看」換成「壞了它來找你」。&lt;/p>
&lt;h2 id="第一層systemd-原生-onfailure-鉤子不裝額外服務">第一層：systemd 原生 &lt;code>OnFailure&lt;/code> 鉤子（不裝額外服務）&lt;/h2>
&lt;p>systemd 每個 unit 進入 failed 狀態時，可以自動觸發另一個 unit。這是最正統、零額外依賴的做法——告警邏輯就寫成一個普通的 systemd service。它由三塊組成：一個負責送通知的處理器 unit、一個實際送出的腳本、以及在你要監控的 unit 上掛一行 &lt;code>OnFailure=&lt;/code>。&lt;/p>
&lt;p>&lt;strong>通知處理器&lt;/strong>是一個 template unit（&lt;code>@&lt;/code> 表示可帶參數），參數 &lt;code>%i&lt;/code> 會是失敗的那個 unit 名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># /etc/systemd/system/alert@.service&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">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">Alert on failure of %i&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="na">Type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">oneshot&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="na">ExecStart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">/usr/local/bin/notify-failure %i&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>送出腳本&lt;/strong>負責把「哪個 unit、在哪台機、什麼時候」推出去。這裡有個實測踩到的坑：在 systemd service 的執行環境下，&lt;code>hostname&lt;/code> 指令可能回傳空字串，要改用 &lt;code>uname -n&lt;/code> 或讀 &lt;code>/etc/hostname&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="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1"># /usr/local/bin/notify-failure （記得 chmod +x）&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">unit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&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">&lt;span class="c1"># 只在「真正放棄」時告警：OnFailure 每次失敗都觸發（含 auto-restart 中途，見下節實測），&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"># auto-restart 中途 ActiveState 是 activating、撞重試上限才進 failed。gate 掉中途避免洗告警。&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nv">state&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>systemctl show &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -p ActiveState --value&lt;span class="k">)&lt;/span>&lt;span class="s2">&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="o">[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$state&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> failed &lt;span class="o">]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">exit&lt;/span> &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nv">host&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -n&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="c1"># 不要用 hostname，systemd 環境下可能回空&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nv">ts&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date -Is&lt;span class="k">)&lt;/span>&lt;span class="s2">&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="nv">topic&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;你的私密topic&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">curl -fsS &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Title: &lt;/span>&lt;span class="nv">$host&lt;/span>&lt;span class="s2">: &lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2"> failed&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2"> 於 &lt;/span>&lt;span class="nv">$ts&lt;/span>&lt;span class="s2"> 進入 failed&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;https://ntfy.sh/&lt;/span>&lt;span class="nv">$topic&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>在要監控的 unit 掛上鉤子&lt;/strong>。針對單一 unit，加一行：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="na">OnFailure&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">alert@%n.service # %n 是本 unit 的全名，會展開成 alert@&amp;lt;本unit&amp;gt;.service&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>要&lt;strong>一次套用到所有 service&lt;/strong>，用 top-level drop-in（放在 &lt;code>service.d/&lt;/code> 這個型別目錄下的設定會套用到每個 &lt;code>.service&lt;/code>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># /etc/systemd/system/service.d/onfailure.conf&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">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">OnFailure&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">alert@%n.service&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>改完 &lt;code>sudo systemctl daemon-reload&lt;/code>。&lt;strong>一個必須注意的遞迴陷阱&lt;/strong>：全域 drop-in 也會套到 &lt;code>alert@&lt;/code> 自己，它若失敗會觸發自己。給 &lt;code>alert@.service&lt;/code> 一個清空 &lt;code>OnFailure=&lt;/code> 的 override（&lt;code>[Unit]&lt;/code> 段寫 &lt;code>OnFailure=&lt;/code>）擋掉。&lt;/p>
&lt;p>這條鏈是實測驗證過的：故意讓一個 &lt;code>ExecStart=/bin/false&lt;/code> 的測試 service 失敗，systemd log 出現 &lt;code>Triggering OnFailure= dependencies&lt;/code>、&lt;code>alert@&lt;/code> 處理器被觸發跑完、&lt;code>curl&lt;/code> 推到 ntfy 回 HTTP 200——通知確實送出，全程沒有肉眼介入。&lt;/p>
&lt;h3 id="先自動重啟放棄了才吵你">先自動重啟、放棄了才吵你&lt;/h3>
&lt;p>多數暫時性失敗（一次連線抖動、一個 race）自己重試就好，不值得半夜叫醒你。把「自動復原」跟「告警」分兩段：讓 systemd 先重啟幾次，撐過重試上限才真的算放棄。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="na">Restart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">on-failure&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">RestartSec&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="na">StartLimitBurst&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">3 # 重試 3 次&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="na">StartLimitIntervalSec&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">60 # 60 秒內都失敗才進 failed（start-limit-hit）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>這裡有個實測踩到、跟直覺相反的坑&lt;/strong>：&lt;code>OnFailure&lt;/code> 不是「放棄才觸發」，而是&lt;strong>每一次失敗都觸發&lt;/strong>——包含 &lt;code>Restart=on-failure&lt;/code> 的每次 auto-restart 中途。實測一個反覆 crash 的服務（重試 3 次後放棄）觸發了 &lt;strong>4 次&lt;/strong> &lt;code>OnFailure&lt;/code>（3 次 auto-restart + 1 次最終 &lt;code>start-limit-hit&lt;/code>）。所以只靠 &lt;code>Restart=&lt;/code> + &lt;code>StartLimit=&lt;/code> 這段 config，你會被每次瞬斷洗告警。&lt;/p></description><content:encoded><![CDATA[<p>服務掛了不需要用肉眼盯——systemd 本來就在追蹤每個 unit 的狀態，你要做的是把「讀權威狀態」這件事自動化，並在狀態變成失敗時主動推播給自己。這篇跟本系列其他篇的差別在時機：診斷是出事後回頭找根因，監控是讓系統在出事的當下就告訴你。兩者共用同一個地基——權威狀態。診斷是手動讀一次權威狀態，監控是訂閱權威狀態的變化、變壞就推播。</p>
<p>理解這個框架後，監控就不是「裝一套很重的東西」，而是分層選擇：從 systemd 內建的失敗鉤子（不裝任何額外服務），到推播管道，到「整台機器死掉」的體外心跳，到完整的指標儀表板。多數人只需要前一兩層。</p>
<h2 id="你現在手動在做的事要被取代的基線">你現在手動在做的事（要被取代的基線）</h2>
<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">systemctl --failed          <span class="c1"># 現在有哪些 unit 處於 failed（開機後系統怪怪的先掃這個）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl is-failed &lt;unit&gt;  <span class="c1"># 單一 unit 明確判失敗（比 is-active 直接）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">journalctl -u &lt;unit&gt; -f     <span class="c1"># 即時跟一個 unit 的 log</span></span></span></code></pre></div><p><code>systemctl --failed</code> 就是「服務死活」的權威清單。手動版的問題不是不準，是你得記得去看。下面每一層都是把「記得去看」換成「壞了它來找你」。</p>
<h2 id="第一層systemd-原生-onfailure-鉤子不裝額外服務">第一層：systemd 原生 <code>OnFailure</code> 鉤子（不裝額外服務）</h2>
<p>systemd 每個 unit 進入 failed 狀態時，可以自動觸發另一個 unit。這是最正統、零額外依賴的做法——告警邏輯就寫成一個普通的 systemd service。它由三塊組成：一個負責送通知的處理器 unit、一個實際送出的腳本、以及在你要監控的 unit 上掛一行 <code>OnFailure=</code>。</p>
<p><strong>通知處理器</strong>是一個 template unit（<code>@</code> 表示可帶參數），參數 <code>%i</code> 會是失敗的那個 unit 名：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/alert@.service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Alert on failure of %i</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/bin/notify-failure %i</span></span></span></code></pre></div><p><strong>送出腳本</strong>負責把「哪個 unit、在哪台機、什麼時候」推出去。這裡有個實測踩到的坑：在 systemd service 的執行環境下，<code>hostname</code> 指令可能回傳空字串，要改用 <code>uname -n</code> 或讀 <code>/etc/hostname</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span><span class="c1"># /usr/local/bin/notify-failure   （記得 chmod +x）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">unit</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 只在「真正放棄」時告警：OnFailure 每次失敗都觸發（含 auto-restart 中途，見下節實測），</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># auto-restart 中途 ActiveState 是 activating、撞重試上限才進 failed。gate 掉中途避免洗告警。</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nv">state</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>systemctl show <span class="s2">&#34;</span><span class="nv">$unit</span><span class="s2">&#34;</span> -p ActiveState --value<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$state</span><span class="s2">&#34;</span> <span class="o">=</span> failed <span class="o">]</span> <span class="o">||</span> <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">host</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>uname -n<span class="k">)</span><span class="s2">&#34;</span>                     <span class="c1"># 不要用 hostname，systemd 環境下可能回空</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nv">ts</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date -Is<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">topic</span><span class="o">=</span><span class="s2">&#34;你的私密topic&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">curl -fsS <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  -H <span class="s2">&#34;Title: </span><span class="nv">$host</span><span class="s2">: </span><span class="nv">$unit</span><span class="s2"> failed&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  -d <span class="s2">&#34;</span><span class="nv">$unit</span><span class="s2"> 於 </span><span class="nv">$ts</span><span class="s2"> 進入 failed&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;https://ntfy.sh/</span><span class="nv">$topic</span><span class="s2">&#34;</span></span></span></code></pre></div><p><strong>在要監控的 unit 掛上鉤子</strong>。針對單一 unit，加一行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">OnFailure</span><span class="o">=</span><span class="s">alert@%n.service    # %n 是本 unit 的全名，會展開成 alert@&lt;本unit&gt;.service</span></span></span></code></pre></div><p>要<strong>一次套用到所有 service</strong>，用 top-level drop-in（放在 <code>service.d/</code> 這個型別目錄下的設定會套用到每個 <code>.service</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/service.d/onfailure.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">OnFailure</span><span class="o">=</span><span class="s">alert@%n.service</span></span></span></code></pre></div><p>改完 <code>sudo systemctl daemon-reload</code>。<strong>一個必須注意的遞迴陷阱</strong>：全域 drop-in 也會套到 <code>alert@</code> 自己，它若失敗會觸發自己。給 <code>alert@.service</code> 一個清空 <code>OnFailure=</code> 的 override（<code>[Unit]</code> 段寫 <code>OnFailure=</code>）擋掉。</p>
<p>這條鏈是實測驗證過的：故意讓一個 <code>ExecStart=/bin/false</code> 的測試 service 失敗，systemd log 出現 <code>Triggering OnFailure= dependencies</code>、<code>alert@</code> 處理器被觸發跑完、<code>curl</code> 推到 ntfy 回 HTTP 200——通知確實送出，全程沒有肉眼介入。</p>
<h3 id="先自動重啟放棄了才吵你">先自動重啟、放棄了才吵你</h3>
<p>多數暫時性失敗（一次連線抖動、一個 race）自己重試就好，不值得半夜叫醒你。把「自動復原」跟「告警」分兩段：讓 systemd 先重啟幾次，撐過重試上限才真的算放棄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">StartLimitBurst</span><span class="o">=</span><span class="s">3          # 重試 3 次</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">StartLimitIntervalSec</span><span class="o">=</span><span class="s">60   # 60 秒內都失敗才進 failed（start-limit-hit）</span></span></span></code></pre></div><p><strong>這裡有個實測踩到、跟直覺相反的坑</strong>：<code>OnFailure</code> 不是「放棄才觸發」，而是<strong>每一次失敗都觸發</strong>——包含 <code>Restart=on-failure</code> 的每次 auto-restart 中途。實測一個反覆 crash 的服務（重試 3 次後放棄）觸發了 <strong>4 次</strong> <code>OnFailure</code>（3 次 auto-restart + 1 次最終 <code>start-limit-hit</code>）。所以只靠 <code>Restart=</code> + <code>StartLimit=</code> 這段 config，你會被每次瞬斷洗告警。</p>
<p>真正做到「只在放棄才吵」，靠的是上面送出腳本開頭那道 gate：<code>systemctl show &lt;unit&gt; -p ActiveState</code> 在 auto-restart 中途是 <code>activating</code>、撞上限進 failed 才是 <code>failed</code>，腳本只在 <code>failed</code> 才送。加上 gate 後同一個 crash 測試從 4 次告警降到 1 次（只剩最終放棄那次）。config 負責「重試幾次」，handler 的 gate 負責「只在終局告警」——兩段合起來才是完整的「先重啟、放棄才吵」。</p>
<h3 id="抓進程活著但沒在做事外部健康探針">抓「進程活著但沒在做事」：外部健康探針</h3>
<p><code>OnFailure</code> 抓的是「進程狀態變了」——crash、exit、被 kill。但服務可能<strong>進程還在、卻沒在做事</strong>：hung、deadlock、內部子系統壞掉。這種 systemd 看它還 <code>active</code>、不會觸發任何告警——正是<a href="../process-service-state-diagnosis/">「進程活著 ≠ 在運作」</a>那條，搬到監控場景。</p>
<p>要抓這種，得從外面<strong>主動戳它、看它回不回應</strong>：一個 timer 定時對服務發一個健康請求（HTTP 服務就 curl 它的 <code>/health</code>）並設逾時；戳不動、逾時失敗，就讓「那個檢查」自己 failed，一樣走 <code>OnFailure</code> 告警。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># health-check.service（oneshot）+ 一個每 2 分鐘跑的 .timer</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/curl -fsS --max-time 5 http://127.0.0.1:8899/health</span></span></span></code></pre></div><p>實測對照最清楚：讓一個健康服務卡在 <code>sleep</code>（進程還在、單執行緒不再回應），<code>systemctl is-active</code> 仍顯示 <code>active</code>——systemd 沒察覺；但這個外部探針 curl <code>/health</code> 5 秒逾時、check 失敗、告警發出。<strong>systemd 抓進程死、外部探針抓進程活著但 hung，兩層互補、缺一漏一種。</strong></p>
<h3 id="canary先證明告警管線本身是好的">canary：先證明告警管線本身是好的</h3>
<p>監控最怕的失效模式是「出事時才發現它早就不會叫了」。防這個的辦法是養一隻 <strong>canary</strong>——一個你可控的假服務，專門用來確認整條管線是活的。它一物兩用：</p>
<ul>
<li><strong>驗證管線</strong>：故意弄掛它，看「失敗 → OnFailure → 推送」真的一路通到你手機，不必拿 sshd 這種真服務去冒險。</li>
<li><strong>當活性訊號</strong>：它自己若無故失敗告警，等於告訴你告警系統本身還在運作。</li>
</ul>
<p>做法是一個極簡 HTTP 服務（stdlib 就夠、不必框架），留幾個測試入口：<code>/health</code> 正常回、<code>/crash</code> 故意退出（測 <code>OnFailure</code>）、<code>/hang</code> 進程活著但不回應（測外部探針）。這樣任何時候都能一鍵重驗監控沒有默默失效。</p>
<h2 id="第二層推去哪裡關鍵是能離開這台機器">第二層：推去哪裡（關鍵是能離開這台機器）</h2>
<p>處理器腳本裡那一段 <code>curl</code> 可以換成任何管道：</p>
<ul>
<li><strong>ntfy</strong>（<code>ntfy.sh</code> 或自架）：一行 <code>curl</code> 推到手機，最省事，上面的例子就是。它怎麼運作、公共站 vs 自架、以及「topic 名稱就是唯一的密碼」這個安全模型，見 <a href="../ntfy-push-notification-service/">ntfy：推送通知服務</a>。</li>
<li><strong>email</strong>：要先設好一個 MTA（如 <code>msmtp</code>），腳本改成 <code>mail</code> / <code>sendmail</code>。</li>
<li><strong>Telegram bot、Apprise</strong>（一個工具打多個目標）等。</li>
</ul>
<p>判準只有一條：<strong>告警要送到機器外</strong>。送桌面 <code>notify-send</code> 只有你正盯著螢幕時才有用；送手機或 email，離開座位、人在外面也收得到。一台跑正事的機器，告警管道應該落在它之外。</p>
<h2 id="第三層整台機器死掉怎麼辦監控自己的盲點">第三層：整台機器死掉怎麼辦（監控自己的盲點）</h2>
<p><code>OnFailure</code> 有個根本限制：<strong>它靠 systemd 觸發，機器整台掛了（當機、斷電、kernel panic），systemd 自己都沒了，發不出任何告警。</strong> 這是所有「機器自己監控自己」方案的共同盲點——它報得了服務的死，報不了自己這台的死。</p>
<p>覆蓋這一層要反過來做：讓機器定時對一個<strong>體外</strong>的服務「報平安」，平安訊號一停，由那個體外服務替你告警。這叫 dead-man&rsquo;s switch（心跳監控）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/heartbeat.service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">curl -fsS https://hc-ping.com/&lt;你的-uuid&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 搭配一個 heartbeat.timer，OnUnitActiveSec=5min 定時打</span></span></span></code></pre></div><p>心跳超過設定時間沒到，healthchecks.io（或自架的 Uptime Kuma）就通知你。<strong>體內的監控管不了自己這台的死亡，一定要有體外的一隻眼睛</strong>——這跟本系列 <a href="../machine-unreachable/">機器連不到或起不來</a> 是同一個問題的兩面：那篇是機器已經不回應時從外面怎麼查，心跳是讓「不回應」這件事本身自動觸發告警。</p>
<h2 id="第四層要指標趨勢門檻不只是-updown">第四層：要指標、趨勢、門檻（不只是 up/down）</h2>
<p>當你要的不只是「掛了沒」，而是 CPU、記憶體、磁碟、延遲的趨勢與門檻告警（例如磁碟用量超過 80% 就先警告，接上本系列反覆出現的「磁碟滿連鎖」），就進到完整監控堆疊：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>定位</th>
          <th>什麼時候選它</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Netdata</td>
          <td>開箱即用、自帶大量預設告警</td>
          <td>單機、想要圖表 + 門檻告警、最不想設定</td>
      </tr>
      <tr>
          <td>Monit</td>
          <td>輕量、每服務健康檢查 + 自動動作</td>
          <td>要「掛了自動跑一段修復腳本」、超出 systemd <code>Restart=</code> 能表達的邏輯</td>
      </tr>
      <tr>
          <td>Prometheus + Alertmanager</td>
          <td>指標抓取 + 告警規則引擎</td>
          <td>多台機器、要歷史數據與可擴展的告警規則</td>
      </tr>
      <tr>
          <td>Uptime Kuma</td>
          <td>自架的 up/down + 心跳面板</td>
          <td>想要一個面板統一看多台/多服務、也能當第三層的心跳接收端</td>
      </tr>
  </tbody>
</table>
<p>這一層不是每個人都需要。單機、只想知道某個服務死活，第一層就夠；要看趨勢、跨機、設門檻，才值得付這層的設定與維運成本。</p>
<h2 id="先確認有沒有沒有就從最簡單開始">先確認有沒有，沒有就從最簡單開始</h2>
<p>監控最好在出事之前就建好，不是等第一次沒人發現的當機才想到。有兩個時機該主動確認這台機器有沒有在監控自己：<strong>裝好一台新機器時</strong>，跟<strong>發現自己反覆在除同一個服務的失敗時</strong>。確認的方式就是讀權威狀態：</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">systemctl --failed                      <span class="c1"># 現在有沒有 failed 的</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl show sshd -p OnFailure        <span class="c1"># 關鍵服務有沒有掛告警鉤子</span></span></span></code></pre></div><p>沒有任何監控的話，<strong>從最簡單那層開始建，別一開始就上重的</strong>：第一層的 <code>OnFailure</code> + ntfy 就能讓「服務掛了」主動找上你，零額外 daemon、幾個檔案就設好。遠端機器至少把 sshd 掛上——它掛了你就失聯，是最該先監控的一個。等你真的需要趨勢圖、跨機、或告警內容不能經過第三方時，再往自架 ntfy（帳號 + ACL）跟完整監控堆疊爬。多數單機、個人用的情境，停在第一層就夠。</p>
<h2 id="依情境選">依情境選</h2>
<p>把上面四層對回你實際要監控的東西：</p>
<ul>
<li><strong>某個 service 掛了想被通知</strong> → 第一層 <code>OnFailure</code> drop-in + ntfy。不裝額外 daemon，最貼近 systemd。</li>
<li><strong>希望先自動重啟、救不回來才告警</strong> → 第一層再加 <code>Restart=on-failure</code> + <code>StartLimit*</code>。</li>
<li><strong>怕整台機器當掉沒人知道</strong> → 第三層心跳 / dead-man switch。這層體內方案覆蓋不到，必須體外。</li>
<li><strong>要看資源趨勢、跨多台、設門檻告警</strong> → 第四層，單機用 Netdata、多機用 Prometheus 堆疊。</li>
</ul>
<p>判準是先分清你要監控的層級：<strong>單一 service 的死活、整台機器的死活、還是資源的趨勢</strong>——三種對應不同層，別拿其中一種去蓋另一種。最常見的誤區是以為體內的 <code>OnFailure</code> 能報自己這台的當機，那正是它的盲點。</p>
<h2 id="下一步">下一步</h2>
<ul>
<li>告警把你叫來之後，怎麼判那個服務到底是什麼狀態（failed、restart loop、還是活著但子系統 wedged）→ <a href="../process-service-state-diagnosis/">程序、服務與狀態怎麼判</a>。</li>
<li>機器完全不回應、心跳斷掉之後從外面怎麼查 → <a href="../machine-unreachable/">機器連不到或起不來</a>。</li>
<li>底層那套「讀權威狀態、不靠肉眼猜」的判讀紀律 → <a href="../diagnosis-read-authoritative-state/">診斷心法</a>。</li>
</ul>
]]></content:encoded></item><item><title>查詢消費模式</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/query-consumption-patterns/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/query-consumption-patterns/</guid><description>&lt;p>事件的價值在於被查詢消費。設計事件時反過來想：查詢需要什麼欄位 → 事件需要帶什麼 data → 感測器需要在什麼時機觸發。從消費端反推設計，避免「收了一堆事件但查不到想要的答案」。&lt;/p>
&lt;p>五種查詢場景各自需要不同的事件類型、欄位和查詢模式。每種場景的查詢模式也決定了需要 SQLite 層還是 PostgreSQL 層（見 &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇&lt;/a>）。&lt;/p>
&lt;h2 id="debug-查詢">Debug 查詢&lt;/h2>
&lt;p>Debug 查詢回答「問題出在哪」。觸發時機是使用者回報問題或 error alert 觸發後，開發者需要還原問題的 context。&lt;/p>
&lt;h3 id="查詢場景">查詢場景&lt;/h3>
&lt;h4 id="剛才使用者回報的問題">剛才使用者回報的問題&lt;/h4>
&lt;p>查詢模式：用 session_id 過濾，拉出該 session 的全部事件，按時間排序。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- SQLite
&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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">data&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="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&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 class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">session_id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;abc-123&amp;#39;&lt;/span>&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="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>需要的事件欄位：session_id（關聯同次使用的事件）、ts（排序）、error 的 stack trace 和 step（定位失敗點）。&lt;/p>
&lt;h4 id="這個-error-多常發生">這個 error 多常發生&lt;/h4>
&lt;p>查詢模式：按 error name 分群計數，看時間趨勢。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- SQLite
&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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">as&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">count&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="n">strftime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;%Y-%m-%d&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">as&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">day&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 class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&lt;/span>&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="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;now&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;-7 days&amp;#39;&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">7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">day&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="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">day&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">count&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">DESC&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>需要的事件欄位：type=&amp;lsquo;error&amp;rsquo;、name（分群鍵）、ts（時間分桶）。&lt;/p>
&lt;h3 id="需要的事件">需要的事件&lt;/h3>
&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>error&lt;/td>
 &lt;td>stack_trace, step, session_id&lt;/td>
 &lt;td>定位失敗點 + 關聯 session&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>event&lt;/td>
 &lt;td>name, session_id&lt;/td>
 &lt;td>還原使用者操作路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>lifecycle&lt;/td>
 &lt;td>name, session_id&lt;/td>
 &lt;td>還原系統狀態轉換&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="alerting-查詢">Alerting 查詢&lt;/h2>
&lt;p>Alerting 查詢回答「需要注意嗎」。分兩種機制：rule engine 的即時評估（事件到達時逐筆比對規則）和事後查詢的趨勢分析。&lt;/p>
&lt;h3 id="查詢場景-1">查詢場景&lt;/h3>
&lt;h4 id="error-數量突然上升">Error 數量突然上升&lt;/h4>
&lt;p>查詢模式：最近 1 小時的 error 計數 vs 前一天同時段，偏差超過閾值則告警。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- SQLite
&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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">as&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">recent_count&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="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&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 class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&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="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">datetime&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;now&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;-1 hour&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Rule engine 的即時版：每收到一筆 error 事件，遞增計數器，計數器超過閾值觸發動作。&lt;/p>
&lt;h4 id="特定-error-首次出現">特定 error 首次出現&lt;/h4>
&lt;p>查詢模式：收到 error 時查是否有歷史記錄。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">-- SQLite
&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">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">events&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="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;error&amp;#39;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">?&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 class="k">AND&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ts&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &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>結果為 0 代表首次出現 — 觸發「新 error 類型」告警。Sentry 的核心功能之一就是這個查詢。&lt;/p>
&lt;h3 id="rule-engine-vs-事後查詢">Rule engine vs 事後查詢&lt;/h3>
&lt;p>Rule engine 逐筆評估，延遲在毫秒級，適合「error 出現就通知」。事後查詢用 SQL 聚合，延遲在秒到分鐘級，適合「過去一小時的 error 趨勢」。兩者互補 — rule engine 做即時告警、SQL 查詢做事後分析。&lt;/p></description><content:encoded><![CDATA[<p>事件的價值在於被查詢消費。設計事件時反過來想：查詢需要什麼欄位 → 事件需要帶什麼 data → 感測器需要在什麼時機觸發。從消費端反推設計，避免「收了一堆事件但查不到想要的答案」。</p>
<p>五種查詢場景各自需要不同的事件類型、欄位和查詢模式。每種場景的查詢模式也決定了需要 SQLite 層還是 PostgreSQL 層（見 <a href="/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇</a>）。</p>
<h2 id="debug-查詢">Debug 查詢</h2>
<p>Debug 查詢回答「問題出在哪」。觸發時機是使用者回報問題或 error alert 觸發後，開發者需要還原問題的 context。</p>
<h3 id="查詢場景">查詢場景</h3>
<h4 id="剛才使用者回報的問題">剛才使用者回報的問題</h4>
<p>查詢模式：用 session_id 過濾，拉出該 session 的全部事件，按時間排序。</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="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">ts</span><span class="p">,</span><span class="w"> </span><span class="k">data</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">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">session_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;abc-123&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">ts</span><span class="p">;</span></span></span></code></pre></div><p>需要的事件欄位：session_id（關聯同次使用的事件）、ts（排序）、error 的 stack trace 和 step（定位失敗點）。</p>
<h4 id="這個-error-多常發生">這個 error 多常發生</h4>
<p>查詢模式：按 error name 分群計數，看時間趨勢。</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="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">name</span><span class="p">,</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">as</span><span class="w"> </span><span class="k">count</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="n">strftime</span><span class="p">(</span><span class="s1">&#39;%Y-%m-%d&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">day</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;error&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-7 days&#39;</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="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">day</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">day</span><span class="p">,</span><span class="w"> </span><span class="k">count</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span></span></span></code></pre></div><p>需要的事件欄位：type=&lsquo;error&rsquo;、name（分群鍵）、ts（時間分桶）。</p>
<h3 id="需要的事件">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>error</td>
          <td>stack_trace, step, session_id</td>
          <td>定位失敗點 + 關聯 session</td>
      </tr>
      <tr>
          <td>event</td>
          <td>name, session_id</td>
          <td>還原使用者操作路徑</td>
      </tr>
      <tr>
          <td>lifecycle</td>
          <td>name, session_id</td>
          <td>還原系統狀態轉換</td>
      </tr>
  </tbody>
</table>
<h2 id="alerting-查詢">Alerting 查詢</h2>
<p>Alerting 查詢回答「需要注意嗎」。分兩種機制：rule engine 的即時評估（事件到達時逐筆比對規則）和事後查詢的趨勢分析。</p>
<h3 id="查詢場景-1">查詢場景</h3>
<h4 id="error-數量突然上升">Error 數量突然上升</h4>
<p>查詢模式：最近 1 小時的 error 計數 vs 前一天同時段，偏差超過閾值則告警。</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="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></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">as</span><span class="w"> </span><span class="n">recent_count</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">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;error&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-1 hour&#39;</span><span class="p">);</span></span></span></code></pre></div><p>Rule engine 的即時版：每收到一筆 error 事件，遞增計數器，計數器超過閾值觸發動作。</p>
<h4 id="特定-error-首次出現">特定 error 首次出現</h4>
<p>查詢模式：收到 error 時查是否有歷史記錄。</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="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></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">events</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">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;error&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">?</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="o">?</span><span class="p">;</span></span></span></code></pre></div><p>結果為 0 代表首次出現 — 觸發「新 error 類型」告警。Sentry 的核心功能之一就是這個查詢。</p>
<h3 id="rule-engine-vs-事後查詢">Rule engine vs 事後查詢</h3>
<p>Rule engine 逐筆評估，延遲在毫秒級，適合「error 出現就通知」。事後查詢用 SQL 聚合，延遲在秒到分鐘級，適合「過去一小時的 error 趨勢」。兩者互補 — rule engine 做即時告警、SQL 查詢做事後分析。</p>
<h3 id="需要的事件-1">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>error</td>
          <td>name, ts</td>
          <td>計數 + 時間趨勢</td>
      </tr>
      <tr>
          <td>error</td>
          <td>source.version</td>
          <td>按版本分群看是否新版本引入</td>
      </tr>
  </tbody>
</table>
<h2 id="產品決策查詢">產品決策查詢</h2>
<p>產品決策查詢回答「使用者怎麼用產品」。從簡單的功能使用率到複雜的 funnel 分析。</p>
<h3 id="查詢場景-2">查詢場景</h3>
<h4 id="新功能有多少人用">新功能有多少人用</h4>
<p>查詢模式：按 event name 計數。SQLite 層即可。</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="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">name</span><span class="p">,</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">as</span><span class="w"> </span><span class="k">count</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">COUNT</span><span class="p">(</span><span class="k">DISTINCT</span><span class="w"> </span><span class="n">session_id</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">unique_sessions</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;event&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;new_feature.%&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-7 days&#39;</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="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">name</span><span class="p">;</span></span></span></code></pre></div><h4 id="註冊流程在哪流失">註冊流程在哪流失</h4>
<p>查詢模式：session 級 funnel JOIN。需要 PostgreSQL 層。</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="c1">-- PostgreSQL
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">WITH</span><span class="w"> </span><span class="n">session_steps</span><span class="w"> </span><span class="k">AS</span><span class="w"> </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="n">session_id</span><span class="p">,</span><span class="w"> </span><span class="n">name</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="n">ROW_NUMBER</span><span class="p">()</span><span class="w"> </span><span class="n">OVER</span><span class="w"> </span><span class="p">(</span><span class="n">PARTITION</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">session_id</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">step_order</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="k">WHERE</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;signup.start&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;signup.email&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;signup.verify&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;signup.complete&#39;</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="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;30 days&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></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="k">SELECT</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="k">DISTINCT</span><span class="w"> </span><span class="n">session_id</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">sessions</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">session_steps</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">name</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">MIN</span><span class="p">(</span><span class="n">step_order</span><span class="p">);</span></span></span></code></pre></div><p>完整的 funnel 分析方法論見 <a href="/blog/monitoring/08-business-analytics/self-hosted-funnel/" data-link-title="從 collector 資料做基礎 funnel 分析" data-link-desc="SQLite 層能做什麼程度的 funnel、PostgreSQL 層提供什麼進階能力、JSONL 匯出後的臨時分析">從 collector 資料做基礎 funnel 分析</a>。</p>
<h3 id="需要的事件-2">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>event</td>
          <td>name, session_id, ts</td>
          <td>漏斗步驟計數和排序</td>
      </tr>
      <tr>
          <td>lifecycle</td>
          <td>session.start, ts</td>
          <td>session 邊界定義</td>
      </tr>
  </tbody>
</table>
<h2 id="安全審計查詢">安全審計查詢</h2>
<p>安全審計查詢回答「有沒有非預期的存取」。重點是偵測異常模式而非單筆事件。</p>
<h3 id="查詢場景-3">查詢場景</h3>
<h4 id="有沒有異常登入">有沒有異常登入</h4>
<p>查詢模式：auth 失敗事件按 session 分群計數，短時間內大量失敗 = 暴力破解嘗試。</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="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">session_id</span><span class="p">,</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">as</span><span class="w"> </span><span class="n">fail_count</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">MIN</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">first_attempt</span><span class="p">,</span><span class="w"> </span><span class="k">MAX</span><span class="p">(</span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">last_attempt</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;error&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;auth.login.failed&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-1 hour&#39;</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="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">session_id</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">HAVING</span><span class="w"> </span><span class="n">fail_count</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">5</span><span class="p">;</span></span></span></code></pre></div><h4 id="誰存取了什麼敏感資料">誰存取了什麼敏感資料</h4>
<p>查詢模式：敏感操作的 audit trail — 按時間列出所有敏感操作事件。</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="c1">-- SQLite
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">ts</span><span class="p">,</span><span class="w"> </span><span class="n">session_id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">data</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">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;event&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;data.export&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;admin.user_lookup&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;config.secret_read&#39;</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="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="k">DESC</span><span class="p">;</span></span></span></code></pre></div><h3 id="需要的事件-3">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>error</td>
          <td>name=&lsquo;auth.*.failed&rsquo;, session_id</td>
          <td>偵測暴力破解</td>
      </tr>
      <tr>
          <td>event</td>
          <td>敏感操作的 name, session_id</td>
          <td>audit trail</td>
      </tr>
      <tr>
          <td>event</td>
          <td>data 中的操作目標（哪筆資料）</td>
          <td>存取範圍追溯</td>
      </tr>
  </tbody>
</table>
<p>安全事件的取樣率必須是 1.0（全收）— 取樣會讓攻擊嘗試在統計上隱形。見 <a href="/blog/monitoring/03-sdk-design/sensor-lifecycle-management/" data-link-title="感測器生命週期管理" data-link-desc="產品生命週期的五個階段各啟用什麼感測器 — feature flag 整合、取樣率動態調整、感測器開關的可觀察性">感測器生命週期管理</a> 的取樣率設計段。</p>
<h2 id="效能查詢">效能查詢</h2>
<p>效能查詢回答「系統有多快」和「哪裡變慢了」。</p>
<h3 id="查詢場景-4">查詢場景</h3>
<h4 id="p95-回應時間趨勢">P95 回應時間趨勢</h4>
<p>查詢模式：時間分桶 + percentile 聚合。需要 PostgreSQL 層。</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="c1">-- PostgreSQL
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">date_trunc</span><span class="p">(</span><span class="s1">&#39;hour&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">ts</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">hour</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="n">percentile_cont</span><span class="p">(</span><span class="mi">0</span><span class="p">.</span><span class="mi">95</span><span class="p">)</span><span class="w"> </span><span class="n">WITHIN</span><span class="w"> </span><span class="k">GROUP</span><span class="w"> </span><span class="p">(</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="p">(</span><span class="k">data</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;duration_ms&#39;</span><span class="p">)::</span><span class="nb">int</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">p95</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;metric&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;api.response.duration&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">NOW</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nb">INTERVAL</span><span class="w"> </span><span class="s1">&#39;7 days&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">hour</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">hour</span><span class="p">;</span></span></span></code></pre></div><p>SQLite 沒有內建 percentile 函數。SQLite 層的替代方案是排序後取第 95% 位置的值，但在大資料量時效能差。</p>
<h4 id="哪個版本變慢了">哪個版本變慢了</h4>
<p>查詢模式：按 source.version 分群比較效能。</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="c1">-- SQLite / PostgreSQL
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">source_version</span><span class="p">,</span><span class="w"> </span><span class="k">AVG</span><span class="p">((</span><span class="k">data</span><span class="o">-&gt;&gt;</span><span class="s1">&#39;duration_ms&#39;</span><span class="p">)::</span><span class="nb">int</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">avg_ms</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">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">sample_count</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">events</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;metric&#39;</span><span class="w"> </span><span class="k">AND</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;api.response.duration&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">ts</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">datetime</span><span class="p">(</span><span class="s1">&#39;now&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;-7 days&#39;</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="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">source_version</span><span class="p">;</span></span></span></code></pre></div><h3 id="需要的事件-4">需要的事件</h3>
<table>
  <thead>
      <tr>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>metric</td>
          <td>name, data.duration_ms, ts</td>
          <td>延遲趨勢</td>
      </tr>
      <tr>
          <td>metric</td>
          <td>source.version</td>
          <td>按版本比較</td>
      </tr>
      <tr>
          <td>metric</td>
          <td>data.memory_mb, data.cpu_percent</td>
          <td>資源使用趨勢</td>
      </tr>
  </tbody>
</table>
<h2 id="查詢--事件反推表">查詢 → 事件反推表</h2>
<p>設計事件時用這張表反向確認：每種查詢場景需要什麼事件、什麼欄位、什麼 storage 層級。</p>
<table>
  <thead>
      <tr>
          <th>查詢場景</th>
          <th>事件類型</th>
          <th>必要欄位</th>
          <th>Storage 層級</th>
          <th>保留需求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Session 回放</td>
          <td>全部</td>
          <td>session_id, ts</td>
          <td>SQLite</td>
          <td>原始 7d</td>
      </tr>
      <tr>
          <td>Error 計數趨勢</td>
          <td>error</td>
          <td>name, ts</td>
          <td>SQLite</td>
          <td>小時聚合 90d</td>
      </tr>
      <tr>
          <td>功能使用率</td>
          <td>event</td>
          <td>name</td>
          <td>SQLite</td>
          <td>天聚合 365d</td>
      </tr>
      <tr>
          <td>Funnel 分析</td>
          <td>event</td>
          <td>name, session_id, ts</td>
          <td>PostgreSQL</td>
          <td>原始 30d</td>
      </tr>
      <tr>
          <td>暴力破解偵測</td>
          <td>error</td>
          <td>auth name, session_id</td>
          <td>SQLite</td>
          <td>原始 30d</td>
      </tr>
      <tr>
          <td>Audit trail</td>
          <td>event</td>
          <td>敏感操作 name, session_id</td>
          <td>SQLite</td>
          <td>原始 365d</td>
      </tr>
      <tr>
          <td>P95 趨勢</td>
          <td>metric</td>
          <td>duration_ms, ts</td>
          <td>PostgreSQL</td>
          <td>小時聚合 90d</td>
      </tr>
      <tr>
          <td>版本比較</td>
          <td>metric</td>
          <td>duration_ms, version</td>
          <td>SQLite</td>
          <td>天聚合 365d</td>
      </tr>
  </tbody>
</table>
<p>這張表和 <a href="/blog/monitoring/01-mental-model/event-enumeration-method/" data-link-title="事件枚舉與補齊檢查" data-link-desc="從操作盤點系統性地推導出完整的事件清單 — 四類補齊檢查確保沒有遺漏、粒度判準確保每個事件只記一個事實">事件枚舉與補齊檢查</a> 的事件表互補 — 事件枚舉從操作端正向推導「要收什麼」，本表從查詢端反向確認「收的夠不夠」。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>從操作端正向推導事件 → <a href="/blog/monitoring/01-mental-model/event-enumeration-method/" data-link-title="事件枚舉與補齊檢查" data-link-desc="從操作盤點系統性地推導出完整的事件清單 — 四類補齊檢查確保沒有遺漏、粒度判準確保每個事件只記一個事實">事件枚舉與補齊檢查</a></li>
<li>動機和事件的對應關係 → <a href="/blog/monitoring/01-mental-model/motivation-to-event-mapping/" data-link-title="動機驅動的事件設計" data-link-desc="Debug / 商業 / 資安 / 效能四個動機各自需要什麼事件 — 從「為什麼收」反推「收什麼」和「什麼階段啟用」">動機驅動的事件設計</a></li>
<li>SQLite vs PostgreSQL 的查詢能力分界 → <a href="/blog/monitoring/04-collector/feature-tier-boundary/" data-link-title="功能分層與 Backend 選擇" data-link-desc="SQLite 層和 PostgreSQL 層各自承載哪些功能 — 分界線是查詢模式而非資料量、觸發升級的是功能需求而非規模成長">功能分層與 Backend 選擇</a></li>
<li>Rule engine 的即時評估 → <a href="/blog/monitoring/04-collector/rule-engine/" data-link-title="Rule engine 設計" data-link-desc="條件 → 動作 → 模板的三段式規則結構 — 讓 collector 從被動儲存變成主動回應">Rule engine 設計</a></li>
</ul>
]]></content:encoded></item><item><title>DevOps Dashboard 設計</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/dashboard-devops/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/dashboard-devops/</guid><description>&lt;p>DevOps dashboard 的消費者是維護 collector 的人 — 可能是開發者自己、可能是開源使用者的運維人員。這個 dashboard 不看被監控 app 的業務邏輯，只看 collector 這個基礎設施本身是否健康、各 SDK 實例是否正常回報。&lt;/p>
&lt;p>使用模式是混合型：平時靠告警被動通知，收到通知後到 dashboard 查看細節。日常監控視圖提供「一眼確認系統正常」的能力，告警觸發視圖提供「出事了去哪裡查」的排障路徑。&lt;/p>
&lt;h2 id="日常監控視圖">日常監控視圖&lt;/h2>
&lt;h3 id="服務狀態卡">服務狀態卡&lt;/h3>
&lt;p>一個狀態卡顯示 collector 的存活狀態和各 SDK 實例的最後心跳時間。狀態卡的設計是「綠色代表正常、紅色代表異常」的二元判斷 — 不需要使用者解讀數字。&lt;/p>
&lt;p>Collector 存活的判斷依據是 health endpoint 回應。各 SDK 實例的狀態依據是最後一次 &lt;code>sdk.heartbeat&lt;/code> 事件的時間 — 超過設定的逾時閾值（預設 10 分鐘）標為離線。&lt;/p>
&lt;p>需要的事件：&lt;code>collector.health.check&lt;/code>（collector 自身定期產生）、&lt;code>sdk.heartbeat&lt;/code>（各 SDK 定期送出）、&lt;code>sdk.init&lt;/code>（SDK 啟動時送出、標記上線）。&lt;/p>
&lt;h3 id="吞吐量曲線">吞吐量曲線&lt;/h3>
&lt;p>折線圖顯示過去 24 小時每分鐘收到的事件數量。多個 SDK 實例用不同顏色區分。吞吐量的正常範圍由歷史資料建立基線 — 突然下降代表某個 SDK 停止送資料，突然上升代表 error storm 或重複送出。&lt;/p>
&lt;p>需要的事件：&lt;code>collector.ingestion.count&lt;/code>（collector 每分鐘記錄收到的事件數，按 source.app 分群）。&lt;/p>
&lt;h3 id="儲存用量">儲存用量&lt;/h3>
&lt;p>磁碟使用率的趨勢圖 + 保留策略的執行狀態。開發者需要知道「磁碟什麼時候會滿」和「purge 有沒有正常跑」。&lt;/p>
&lt;p>需要的事件：&lt;code>collector.storage.disk_usage&lt;/code>（定期取樣、metric 類型）、&lt;code>collector.storage.purge.completed&lt;/code>（每次 purge 完成時記錄清了多少空間）。&lt;/p>
&lt;h3 id="sdk-連線列表">SDK 連線列表&lt;/h3>
&lt;p>表格列出所有已知的 SDK 實例，每行顯示：app 名稱、版本、平台、最後回報時間、最後一次 init 時間。表格按「最後回報時間」排序 — 最久沒回報的在最上面，方便發現異常。&lt;/p>
&lt;p>需要的事件：&lt;code>sdk.init&lt;/code>（帶 source 完整資訊）、&lt;code>sdk.heartbeat&lt;/code>（定期更新最後回報時間）。&lt;/p>
&lt;p>Heartbeat 的觸發機制是 flush timer 的副作用 — SDK 的 flush timer 觸發時，如果 buffer 為空且距上次 heartbeat 超過設定間隔（預設 5 分鐘），自動注入一筆 &lt;code>sdk.heartbeat&lt;/code> 事件後送出。不需要獨立的 heartbeat timer。App idle 時 heartbeat 仍會送出，dashboard 的 SDK 連線列表因此能偵測 SDK 是否仍存活。&lt;/p>
&lt;h2 id="告警觸發視圖">告警觸發視圖&lt;/h2>
&lt;p>告警由 rule engine 觸發，觸發後開發者進入 dashboard 查看細節。每種告警條件對應一個排障路徑。&lt;/p>
&lt;h3 id="health-check-失敗">Health check 失敗&lt;/h3>
&lt;p>Collector 的 health endpoint 連續 N 次回應失敗（由外部 uptime check 偵測、如 cron + curl）。&lt;/p>
&lt;p>進入 dashboard 後看：最後一次 &lt;code>collector.health.check&lt;/code> 的時間和結果、collector 的 stderr log（systemd journal）、process 是否存活。如果 collector 已經掛了，dashboard 本身也不可達 — 這時的排障路徑是 SSH 到主機查 systemd 狀態。&lt;/p>
&lt;h3 id="sdk-停止回報">SDK 停止回報&lt;/h3>
&lt;p>某個 SDK 實例超過逾時閾值沒有送 &lt;code>sdk.heartbeat&lt;/code>。可能原因：被監控 app 當掉、網路斷開、SDK 初始化失敗。&lt;/p>
&lt;p>進入 dashboard 後看：該 SDK 的最後事件（什麼類型、什麼時間）、最後 &lt;code>sdk.init&lt;/code> 的 source 資訊（版本、平台）、同時段其他 SDK 是否正常（區分「單一 SDK 問題」和「collector 端問題」）。&lt;/p>
&lt;h3 id="磁碟用量超過閾值">磁碟用量超過閾值&lt;/h3>
&lt;p>&lt;code>collector.storage.disk_usage&lt;/code> 超過 80%。&lt;/p>
&lt;p>進入 dashboard 後看：各 backend 的空間佔比（SQLite DB 大小 + 匯出檔大小）、最近一次 purge 的執行時間和清理量、保留策略的設定值。如果 purge 正常執行但空間仍不足，代表事件產生速度超過清理速度 — 需要調整保留策略或擴容磁碟。&lt;/p>
&lt;h3 id="事件吞吐量異常下降">事件吞吐量異常下降&lt;/h3>
&lt;p>每分鐘事件數從正常基線突然下降超過 50%。&lt;/p></description><content:encoded><![CDATA[<p>DevOps dashboard 的消費者是維護 collector 的人 — 可能是開發者自己、可能是開源使用者的運維人員。這個 dashboard 不看被監控 app 的業務邏輯，只看 collector 這個基礎設施本身是否健康、各 SDK 實例是否正常回報。</p>
<p>使用模式是混合型：平時靠告警被動通知，收到通知後到 dashboard 查看細節。日常監控視圖提供「一眼確認系統正常」的能力，告警觸發視圖提供「出事了去哪裡查」的排障路徑。</p>
<h2 id="日常監控視圖">日常監控視圖</h2>
<h3 id="服務狀態卡">服務狀態卡</h3>
<p>一個狀態卡顯示 collector 的存活狀態和各 SDK 實例的最後心跳時間。狀態卡的設計是「綠色代表正常、紅色代表異常」的二元判斷 — 不需要使用者解讀數字。</p>
<p>Collector 存活的判斷依據是 health endpoint 回應。各 SDK 實例的狀態依據是最後一次 <code>sdk.heartbeat</code> 事件的時間 — 超過設定的逾時閾值（預設 10 分鐘）標為離線。</p>
<p>需要的事件：<code>collector.health.check</code>（collector 自身定期產生）、<code>sdk.heartbeat</code>（各 SDK 定期送出）、<code>sdk.init</code>（SDK 啟動時送出、標記上線）。</p>
<h3 id="吞吐量曲線">吞吐量曲線</h3>
<p>折線圖顯示過去 24 小時每分鐘收到的事件數量。多個 SDK 實例用不同顏色區分。吞吐量的正常範圍由歷史資料建立基線 — 突然下降代表某個 SDK 停止送資料，突然上升代表 error storm 或重複送出。</p>
<p>需要的事件：<code>collector.ingestion.count</code>（collector 每分鐘記錄收到的事件數，按 source.app 分群）。</p>
<h3 id="儲存用量">儲存用量</h3>
<p>磁碟使用率的趨勢圖 + 保留策略的執行狀態。開發者需要知道「磁碟什麼時候會滿」和「purge 有沒有正常跑」。</p>
<p>需要的事件：<code>collector.storage.disk_usage</code>（定期取樣、metric 類型）、<code>collector.storage.purge.completed</code>（每次 purge 完成時記錄清了多少空間）。</p>
<h3 id="sdk-連線列表">SDK 連線列表</h3>
<p>表格列出所有已知的 SDK 實例，每行顯示：app 名稱、版本、平台、最後回報時間、最後一次 init 時間。表格按「最後回報時間」排序 — 最久沒回報的在最上面，方便發現異常。</p>
<p>需要的事件：<code>sdk.init</code>（帶 source 完整資訊）、<code>sdk.heartbeat</code>（定期更新最後回報時間）。</p>
<p>Heartbeat 的觸發機制是 flush timer 的副作用 — SDK 的 flush timer 觸發時，如果 buffer 為空且距上次 heartbeat 超過設定間隔（預設 5 分鐘），自動注入一筆 <code>sdk.heartbeat</code> 事件後送出。不需要獨立的 heartbeat timer。App idle 時 heartbeat 仍會送出，dashboard 的 SDK 連線列表因此能偵測 SDK 是否仍存活。</p>
<h2 id="告警觸發視圖">告警觸發視圖</h2>
<p>告警由 rule engine 觸發，觸發後開發者進入 dashboard 查看細節。每種告警條件對應一個排障路徑。</p>
<h3 id="health-check-失敗">Health check 失敗</h3>
<p>Collector 的 health endpoint 連續 N 次回應失敗（由外部 uptime check 偵測、如 cron + curl）。</p>
<p>進入 dashboard 後看：最後一次 <code>collector.health.check</code> 的時間和結果、collector 的 stderr log（systemd journal）、process 是否存活。如果 collector 已經掛了，dashboard 本身也不可達 — 這時的排障路徑是 SSH 到主機查 systemd 狀態。</p>
<h3 id="sdk-停止回報">SDK 停止回報</h3>
<p>某個 SDK 實例超過逾時閾值沒有送 <code>sdk.heartbeat</code>。可能原因：被監控 app 當掉、網路斷開、SDK 初始化失敗。</p>
<p>進入 dashboard 後看：該 SDK 的最後事件（什麼類型、什麼時間）、最後 <code>sdk.init</code> 的 source 資訊（版本、平台）、同時段其他 SDK 是否正常（區分「單一 SDK 問題」和「collector 端問題」）。</p>
<h3 id="磁碟用量超過閾值">磁碟用量超過閾值</h3>
<p><code>collector.storage.disk_usage</code> 超過 80%。</p>
<p>進入 dashboard 後看：各 backend 的空間佔比（SQLite DB 大小 + 匯出檔大小）、最近一次 purge 的執行時間和清理量、保留策略的設定值。如果 purge 正常執行但空間仍不足，代表事件產生速度超過清理速度 — 需要調整保留策略或擴容磁碟。</p>
<h3 id="事件吞吐量異常下降">事件吞吐量異常下降</h3>
<p>每分鐘事件數從正常基線突然下降超過 50%。</p>
<p>進入 dashboard 後看：吞吐量曲線標注「下降起始時間」、SDK 連線列表確認哪些 SDK 在該時間點後停止回報、collector 的 ingestion error log。</p>
<h2 id="需要的事件總表">需要的事件總表</h2>
<table>
  <thead>
      <tr>
          <th>事件名稱</th>
          <th>類型</th>
          <th>產生者</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>collector.health.check</td>
          <td>lifecycle</td>
          <td>Collector</td>
          <td>服務狀態卡</td>
      </tr>
      <tr>
          <td>collector.started</td>
          <td>lifecycle</td>
          <td>Collector</td>
          <td>部署追蹤</td>
      </tr>
      <tr>
          <td>collector.shutdown</td>
          <td>lifecycle</td>
          <td>Collector</td>
          <td>異常關閉偵測</td>
      </tr>
      <tr>
          <td>collector.ingestion.count</td>
          <td>metric</td>
          <td>Collector</td>
          <td>吞吐量曲線</td>
      </tr>
      <tr>
          <td>collector.storage.disk_usage</td>
          <td>metric</td>
          <td>Collector</td>
          <td>儲存用量圖</td>
      </tr>
      <tr>
          <td>collector.storage.purge.completed</td>
          <td>lifecycle</td>
          <td>Collector</td>
          <td>purge 執行記錄</td>
      </tr>
      <tr>
          <td>sdk.heartbeat</td>
          <td>lifecycle</td>
          <td>SDK</td>
          <td>連線列表、存活判斷</td>
      </tr>
      <tr>
          <td>sdk.init</td>
          <td>lifecycle</td>
          <td>SDK</td>
          <td>版本/平台資訊、上線記錄</td>
      </tr>
      <tr>
          <td>deployment.started</td>
          <td>lifecycle</td>
          <td>CI/CD hook</td>
          <td>部署追蹤</td>
      </tr>
      <tr>
          <td>deployment.completed</td>
          <td>lifecycle</td>
          <td>CI/CD hook</td>
          <td>部署追蹤</td>
      </tr>
      <tr>
          <td>rule.matched</td>
          <td>event</td>
          <td>Collector</td>
          <td>alert 歷史</td>
      </tr>
  </tbody>
</table>
<p>這些事件是 collector 自身的營運事件，和被監控 app 的事件走同一個 Storage interface 儲存。Collector 同時是事件的生產者和消費者 — <code>collector.ingestion.count</code> 由 collector 自己產生、自己儲存、自己在 dashboard 顯示。</p>
<p><code>deployment.started</code> / <code>deployment.completed</code> 這兩個 lifecycle event 在 server-side 部署流程中對應 <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">Backend 5.8 Deployment Rollout</a> 的 evidence package——rollout 的每一批切換需要可判讀的部署事件作為證據。自架 collector 場景的部署追蹤規模遠小於 production server-side rollout，但 event schema 設計（timestamp / version / environment / result）可以跟 server-side 的 evidence 欄位對齊，讓未來規模成長時 event 格式不用重新設計。</p>
<h2 id="自動恢復設計">自動恢復設計</h2>
<p>自用工具場景下「凌晨三點 collector 掛了」的處理策略是自動恢復，不需要人介入。</p>
<table>
  <thead>
      <tr>
          <th>機制</th>
          <th>做法</th>
          <th>恢復時間</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>systemd watchdog</td>
          <td><code>WatchdogSec=30s</code>，collector 定期寫 watchdog notify</td>
          <td>30 秒內重啟</td>
      </tr>
      <tr>
          <td>Restart policy</td>
          <td><code>Restart=on-failure</code>、<code>RestartSec=5s</code></td>
          <td>5 秒後自動重啟</td>
      </tr>
      <tr>
          <td>Health endpoint</td>
          <td><code>/health</code> 回應 200 + 最後寫入時間</td>
          <td>外部 check 偵測</td>
      </tr>
      <tr>
          <td>啟動自檢</td>
          <td>collector 啟動時檢查 storage 完整性、重建索引</td>
          <td>啟動時自動修復</td>
      </tr>
  </tbody>
</table>
<p>自動恢復後 collector 送出 <code>collector.started</code> 事件，dashboard 的服務狀態卡從紅轉綠。如果連續重啟（10 分鐘內重啟 3 次以上），systemd 的 <code>StartLimitBurst</code> 阻止無限重啟、改為發送告警通知人工介入。</p>
<h2 id="存取控制">存取控制</h2>
<p>Day-one 的 dashboard 預設無認證 — 同區網內的任何裝置都能打開 dashboard URL。這是同區網信任模型的設計選擇，和 collector 的 HTTP endpoint 無認證一致。</p>
<h3 id="風險告知">風險告知</h3>
<p>無認證的 dashboard 暴露以下資訊給同區網的所有裝置：</p>
<ul>
<li><strong>DevOps dashboard</strong>：SDK 版本、平台、IP、collector 的磁碟用量</li>
<li><strong>Developer dashboard</strong>：error stack trace（可能包含檔案路徑和程式碼片段）、session 回放（使用者操作序列）</li>
<li><strong>中台 dashboard</strong>：行為事件明細、funnel 轉換率</li>
</ul>
<p>家用 LAN 的場景下，家裡的其他裝置（IoT、家人的電腦）也能存取這些資訊。</p>
<h3 id="最小防護">最小防護</h3>
<p>Go 的 <code>net/http</code> middleware 可以用幾行程式碼加 basic auth：</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">basicAuth</span><span class="p">(</span><span class="nx">next</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span><span class="p">,</span> <span class="nx">user</span><span class="p">,</span> <span class="nx">pass</span> <span class="kt">string</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">return</span> <span class="nx">http</span><span class="p">.</span><span class="nf">HandlerFunc</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="nx">u</span><span class="p">,</span> <span class="nx">p</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nf">BasicAuth</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="o">||</span> <span class="nx">u</span> <span class="o">!=</span> <span class="nx">user</span> <span class="o">||</span> <span class="nx">p</span> <span class="o">!=</span> <span class="nx">pass</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;WWW-Authenticate&#34;</span><span class="p">,</span> <span class="s">`Basic realm=&#34;monitor&#34;`</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;Unauthorized&#34;</span><span class="p">,</span> <span class="mi">401</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="k">return</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="nx">next</span><span class="p">.</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>帳密在 collector 的配置檔設定。Day-one 可選（不設就不啟用），但配置檔中應有 commented-out 的範例讓使用者知道這個選項存在。</p>
<h3 id="tripwire">Tripwire</h3>
<p>Collector 暴露到公網或跨網路存取時，dashboard 的認證從可選變成必要。公網上的無認證 dashboard 等於公開了 error stack trace 和行為資料。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Developer dashboard 設計 → <a href="/blog/monitoring/04-collector/dashboard-developer/" data-link-title="Developer Dashboard 設計" data-link-desc="Bug 在哪、多嚴重、怎麼重現 — Error 列表和趨勢的日常監控、Session 回放和 Stack trace 的深入 debug">Developer Dashboard 設計</a></li>
<li>中台 dashboard 設計 → <a href="/blog/monitoring/04-collector/dashboard-business/" data-link-title="中台 Dashboard 設計" data-link-desc="使用者怎麼用、在哪流失、怎麼讓他們回來 — 營運和行銷的日常指標監控與深入分析視圖，全部需要 PostgreSQL 層">中台 Dashboard 設計</a></li>
<li>Rule engine 的告警設計 → <a href="/blog/monitoring/04-collector/rule-engine/" data-link-title="Rule engine 設計" data-link-desc="條件 → 動作 → 模板的三段式規則結構 — 讓 collector 從被動儲存變成主動回應">Rule engine 設計</a></li>
<li>Collector 自我監控的 bootstrapping 問題 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>服務探活與自動恢復 → <a href="/blog/devops/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">DevOps 服務探活</a></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></channel></rss>