2026-08-25 需求討論拍板(user+協調者,對話中定案,未走 HTML 討論稿——範圍單純、決策點僅三個)。 Notion 母卡:見 FR 登記表。agent log 轉發本案不做(user 裁示,未來另案)。
這節不需要任何技術背景就讀得懂。工程細節從下一節起。
這案在做什麼:讓管理者在系統設定頁上填幾個欄位,就把我們系統產生的 log(系統運作 紀錄)自動送到客戶自己的 log 平台去。
為什麼客戶要這個:客戶(特別是把系統裝在自己機房的落地版客戶)機房裡通常已經有一套 集中收 log 的平台——ELK、Graylog、rsyslog 這類,所有系統的 log 都往那裡送,統一在一個 地方查。合規客戶更進一步:他們的資安監控平台(SIEM)需要收到「誰登入了、誰改了什麼」 這種稽核事件,這是稽核與事故調查的依據。我們的系統如果不送過去,就成了客戶監控網裡 的一個黑洞。
送的內容分兩條,各自獨立勾選:
| 條 | 內容 | 誰要看 |
|---|---|---|
| 應用 log | 系統運作紀錄、錯誤訊息 | 工程師排錯用 |
| 稽核事件 | 誰登入、誰改了什麼設定 | 合規客戶接 SIEM 真正要的就是這條 |
影響哪些畫面/操作:系統管理下新增一頁「Log 轉發設定」——填對方主機位址、埠號、選 用哪種格式送、勾要送哪兩條流,存檔就開始送。不必改設定檔、不必重啟服務。旁邊有一顆 「發送測試 log」按鈕,可以當場確認連不連得通。
拍板了什麼(決策 D1–D6 的白話版,完整理由見 §2):
| 題目 | 定案 |
|---|---|
| 用什麼格式送 | Syslog 與 GELF 兩種——這兩種是 rsyslog/Graylog/ELK 三家都吃的最大公約數。不做各家專屬的對接(每家的認證方式和版本相容性都是坑,維護成本不成比例) |
| 送哪些內容 | 應用 log 與稽核事件兩條都做,畫面上分開勾。只做應用 log 會做完才發現不是合規客戶要的東西 |
| 設定放哪一層 | 系統全域一份(目標客群是一個客戶一套系統)。資料結構預留了「每個租戶各自設定」的空間,未來要開只動畫面和一個小函式 |
| 送不出去怎麼辦 | 絕不能拖垮主系統——網路斷、對方主機掛了,本機 log 照常寫、系統照常跑,不重試轟炸 |
| 敏感內容要遮嗎 | 第一版不遮(log 是送到客戶自己機房內、信任邊界在客戶內網,手冊會註明)。架構有預留遮罩的掛點 |
| 設定改了要重啟嗎 | 不用,改完數十秒內自動生效(實作查證後的精確說法見 §5.2,畫面上會如實顯示這個延遲,不假裝是瞬時) |
現在到哪了:母卡 CM-1389 已收案,隨 v1.17.0 出貨,188 STG 與 189 POC 都已升級 到位。出貨後四筆修正(含一筆「Graylog 收得到卻讀不懂」的封包格式問題)見 README.md 的「實作與設計的差異」節。
本案不含:檢測 Agent 那側的 log 轉發(決策者裁示未來另案處理)。
| 日期 | 變更 | 對應 |
|---|---|---|
| 2026-08-25 | 初版設計定案(D1–D6) | 母案 |
客戶(尤其落地版)有集中 log 管理需求:系統 log 要能轉發到客戶自有的 log server(ELK/Graylog/rsyslog 等),在系統 UI 上設定即可生效。合規客群接 SIEM 的核心訴求是稽核事件(誰登入、誰改了什麼)進他們的集中平台;應用 log 轉發則服務工程排錯集中化。
| # | 決策 | 定案內容 | 理由/被排除方案 |
|---|---|---|---|
| D1 | 協定 | Syslog(RFC 5424,UDP/TCP 可選)+ GELF(UDP/TCP)兩種 | Syslog 是三家(rsyslog/Graylog/ELK via Logstash syslog input)最大公約數,Python stdlib SysLogHandler 現成;GELF 給 Graylog 原生結構化欄位保真。排除各家專屬 API 對接(Elastic API 認證/版本相容坑多;syslog 通吃零依賴)。ELK 對接以手冊附 Logstash 配置範例達成 |
| D2 | 轉發範圍 | 應用 log + 稽核事件兩者都轉,UI 分開勾選 | GRC 客戶接 SIEM 十之八九要的是稽核事件;只做 app log 會做完發現不是客戶要的。兩流各自獨立開關 |
| D3 | 設定層級 | 系統層(全域一份),資料結構預留租戶層擴充 | 目標客群偏落地版(一客戶一套系統),全域一份最簡。預留:設定表帶 tenant_id 欄可空(NULL=全域),讀取邏輯「租戶層有值優先、fallback 全域」的形狀先做成 helper 單點,未來開租戶層只動 UI 與該 helper |
| D4 | 失敗語意 | 轉發失敗絕不拖垮主服務:UDP fire-and-forget;TCP 非阻塞+斷線靜默降級(本地 log 照寫+本地記一條轉發失敗告警,不重試風暴)。佇列緩衝:handler 掛在 QueueHandler/QueueListener 後面(stdlib),主執行緒零阻塞 |
log 轉發是旁路,任何網路抖動都不可影響 API 延遲 |
| D5 | 敏感內容 | 第一版不過濾(客戶自有 log server,信任邊界在客戶內網,手冊註明)。架構預留 pipeline 掛點:轉發 handler 前的 filter 鏈天然支援;UI 預留「敏感資料遮罩」開關位置(disabled 標示即將推出)。實作時內建預設規則組(IPv4/email/token 形狀 regex 遮罩),不做自訂規則編輯器(過度設計) | |
| D6 | 生效方式 | 設定變更熱生效(重讀設定、卸舊掛新 handler,不重啟服務)+ UI「發送測試 log」按鈕驗連通 | 改個 log 位址要重啟服務不可接受 |
新表 config.log_forwarding_settings(系統層單列起步):
| 欄位 | 說明 |
|---|---|
tenant_id BIGINT NULL |
NULL=全域(D3 預留;第一版只有 NULL 一列) |
enabled BOOLEAN |
總開關 |
protocol VARCHAR |
syslog / gelf |
transport VARCHAR |
udp / tcp |
host / port |
log server 座標 |
forward_app_log BOOLEAN |
D2 流一:應用 log |
forward_audit_events BOOLEAN |
D2 流二:稽核事件 |
masking_enabled BOOLEAN DEFAULT FALSE |
D5 預留(第一版 UI disabled) |
| 審計四欄 | 慣例 |
QueueHandler → QueueListener → SysLogHandler(或 GELF handler,GELF UDP 百行內自寫或極輕依賴,實作棒查證後定)。reference_audit_event_logging_pattern 既有埋點鏈),以 structured 欄位出(GELF 時 event_type/actor/target 成獨立欄位,syslog 時 JSON in message)。POST .../log-forwarding/test 發一條固定測試訊息,回傳送出成功與否(UDP 只能驗送出不驗到達,UI 文案如實說)。系統管理群新設定頁(或併入既有整合設定頁——實作棒看現有選單結構定):表單(開關/協定/傳輸/host/port/兩流勾選)+測試按鈕+遮罩開關(disabled)。權限比照同群其他系統管理頁。
三家對接範例各一段:rsyslog(收 UDP 514 範例 conf)、Graylog(GELF input 建法)、ELK(Logstash syslog input pipeline 範例)。
| # | 子任務 | Repo | 規模 |
|---|---|---|---|
| T-1 | BE:設定表 migration+CRUD API+handler 鏈掛載(syslog+gelf、queue、熱生效、測試端點)+稽核事件流接點 | BE | M |
| T-2 | FE:設定頁+測試按鈕+i18n | FE | S~M |
| T-3 | 手冊三家對接範例+SPEC 頁 | docs | S |
依賴:T-1 → T-2;T-3 收尾做。agent log 轉發不在本案(未來另案,屆時 agent 端沿同一設定下發形狀)。
2026-08-29(CM-1429)補記。§3 有三處寫「實作棒查證後定」,實作當時都查證並做了 決定,但結論只留在
common/log_forwarding/的檔頭註解裡,沒回到設計稿。本節把它們 收進來,內容取自那兩支檔頭(實作當下寫的一手判斷,非事後追述)。§1–§4 維持 2026-08-25 拍板當下的原樣不動——定案稿是快照,差異另記,與
README.md的「實作與設計的差異」節同一原則。出貨後的四筆行為修正記在 README 那節,本節只管「設計時懸而未決、實作查證後定案」的三處。
候選 pygelf 與 graypy 都不在本專案依賴內(pyproject.toml 無、venv 內 find_spec 皆 None),引入等於新增一個第三方依賴。而 GELF 1.1 的線上格式極小——固定 8 個欄位的 JSON,UDP 走 gzip、TCP 走 NUL 結尾,規格全文一頁,自寫約百行。
換掉的成本是:
順帶定案:不做 GELF UDP chunking。規格允許把大訊息切成多個 chunk,但本專案的 log 是單行文字加少量欄位,gzip 後遠小於單一 datagram 上限;真超長時寧可截斷(8 KiB,尾端 標 …[truncated])也不引入 chunk 重組——收不齊的 chunk 在對端是碎片垃圾,比截斷更難查。
落點:common/log_forwarding/gelf_handler.py。
§3.2 原本假設「走既有的跨 worker 訊號機制」。查證結果是本專案根本沒有這種機制:全 codebase grep 不到 SIGHUP handler、不到 Redis pub/sub 的訂閱端(redis 只被當 KV 用)、 不到 worker 登記表(register_gunicorn_master 只記 master pid 供 tamper 終止,是單向 的)。gunicorn 有 --reload,但那是重啟整個 worker,代價遠大於重掛一個 handler。
照 §3.2「沒有就 fallback,不為此蓋新輪子」——但 fallback 不必差到「下次 worker 重生才 生效」。定案:每個 worker 各起一條 daemon watcher 執行緒,週期(預設 30s, LOG_FORWARDING_WATCH_INTERVAL)重讀設定,指紋有變才重掛。
applies_within_seconds 明示這個延遲,不假裝瞬時(D6 說「熱生 效」,實際是「數十秒內生效」,UI 必須如實說)。SELECT × worker 數 ÷ 30s,以預設 4 workers 計約每秒 0.13 次查詢。落點:common/log_forwarding/forwarder.py。
定案 新開頁 /system/log-forwarding(src/views/log-forwarding/LogForwardingForm.vue), 選單掛在系統管理群下(ui_routes sort 35,migration scripts/sql/2026-08-27-fr068-2-log-forwarding-menu-route.sql),權限走 log-forwarding.{read,update} 兩個能力點,比照同群其他系統管理頁。