Log 轉發設定(Log Forwarding)¶
功能群:系統管理|三層 RBAC 模型與角色權限基調先讀 功能群總覽 §2 / §5
事實基準:2026-08-27 從 FE
src/views/log-forwarding/LogForwardingForm.vue+ BEapi/log_forwarding//app/log_forwarding//domain/log_forwarding//common/log_forwarding/+ DEV 實庫config.log_forwarding_settings、public.ui_routes掃出(FR-068 為新表新頁,db_schema.json/routes.jsondump 早於本功能,欄位與選單位置改以 DEV 實庫核實)
變更紀錄¶
| 日期 | FR | 變更 |
|---|---|---|
| 2026-08-27 | FR-068 | 首版:系統 log 轉發設定頁(Syslog RFC 5424 / GELF 1.1 × UDP / TCP、應用 log 與稽核事件兩流獨立開關、測試送出、30 秒熱生效) |
1. 功能描述¶
Log 轉發設定頁是把系統紀錄送一份到客戶自有 log 平台(rsyslog / Graylog / ELK)的單一設定頁。它是系統層全域一份設定——config.log_forwarding_settings 的 tenant_id IS NULL 那一列,因此頁面沒有列表、沒有 uid、只有一張表單。設定存檔後不需重啟服務:處理該請求的進程立即重掛 handler 鏈,其餘 gunicorn worker 由各自的 watcher 執行緒在 30 秒內追上。
轉發是旁路:轉發目標不可達時 API 延遲無感、本機 log 與 system_logs 照常寫入,被丟棄的只有「送出去的那一份」(設計決策 D4)。
主要使用者:具 log-forwarding.* 能力的系統管理員(能力點授予對象比照 SMTP 伺服器設定,兩者同屬「系統設定」選單群)。
角色速覽
| 角色 | 一句話 |
|---|---|
具 log-forwarding.read 者 |
看得到本選單並讀得到設定 |
具 log-forwarding.update 者 |
可儲存設定與發送測試訊息(測試被歸為寫入類,見 §3) |
| 其他登入者 | 無此選單;直打 API 亦被 require_capability 擋下 403 |
1.1 功能總覽(本頁全部功能)¶
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 載入既有設定 | 進頁 GET /log-forwarding 回填;非 404 的讀取失敗會鎖住儲存(見 §4) |
整頁(onMounted) |
§6.2 |
| 2 | 啟用 log 轉發開關 | 總開關(InputSwitch)。關閉時完全不對外送出 | 表單 | §4 |
| 3 | 協定下拉 | syslog(RFC 5424)/gelf(GELF 1.1) |
表單 | §11 |
| 4 | 傳輸方式下拉 | udp(fire-and-forget)/tcp(可確認連線) |
表單 | §11 |
| 5 | Log 伺服器位址 | 啟用時必填;FE 送出前 trim,空字串轉 null |
表單 | §6.2 |
| 6 | 連接埠 | 啟用時必填;1–65535(FE InputNumber min/max、domain 與 DB CHECK 三層) | 表單 | §4 |
| 7 | 轉發應用程式 log 勾選 | 流一:無 event_code 的紀錄 |
表單 | §11 |
| 8 | 轉發稽核事件勾選 | 流二:帶 event_code 的紀錄([AUDIT:*]),SIEM 客戶主要訴求 |
表單 | §11 |
| 9 | 敏感資料遮罩開關 | 不渲染(CM-1410)——v-if="maskingAvailable",BE 的 masking_available 恆 false 故整塊不出現;未上的功能不在 UI 曝光 |
(目前不顯示) | §12 坑 3 |
| 10 | 儲存設定 | PUT,部分更新;成功後 toast 明示「其餘程序最多 N 秒跟上」 |
表單底部 | UC-LF-01 |
| 11 | 發送測試 log | POST .../test,用已儲存設定送一筆;表單有未存變更時按鈕反灰 |
表單底部 | UC-LF-02 |
| 12 | 離開頁面確認 | onBeforeRouteLeave + 快照比對(改回原值不算髒) |
全域 | §10 |
| 13 | 授權唯讀降級 | useLicenseReadonly 命中時兩顆按鈕反灰並提示原因(不隱藏) |
表單底部 | §3 |
展開成 UC:UC-LF-01(儲存設定)、UC-LF-02(發送測試 log)。載入、離開確認等為常規表單行為,併入流程圖不另展開。
2. Use Case¶
角色速覽
| 角色 | 一句話 |
|---|---|
具 log-forwarding.read 者 |
看得到本選單並讀得到設定 |
具 log-forwarding.update 者 |
可儲存設定與發送測試訊息(測試被歸為寫入類,見 §3) |
| 其他登入者 | 無此選單;直打 API 亦被 require_capability 擋下 403 |
視覺慣例:判斷框(黃菱形)=分支條件;橙框=擋下;紅框=錯誤終點(標 error code);綠框=成功終點;虛線=可選路徑。
UC-LF-01 儲存 log 轉發設定¶
| 項目 | 內容 |
|---|---|
| 角色 | 具 log-forwarding.update 能力者 |
| 前置條件 | 已登入;進頁讀取未失敗(configLoadFailed=false);總開關開啟時位址與連接埠已填 |
| 產出 / 後置條件 | config.log_forwarding_settings(tenant_id IS NULL)該列更新;發稽核事件 6100;本進程立即重掛 handler 鏈,其餘 worker 於 applies_within_seconds(預設 30)內跟上 |
UC-LF-02 發送測試 log¶
| 項目 | 內容 |
|---|---|
| 角色 | 具 log-forwarding.update 能力者(測試會實際送出網路封包,屬副作用非查詢) |
| 前置條件 | 已登入;表單無未儲存變更(否則 FE 反灰);DB 已存有 host + port |
| 產出 / 後置條件 | 不寫設定表;對外送出一筆測試訊息;發稽核事件 6101;回傳 sent 與 delivery_confirmed(UDP 恆 false) |
3. 權限矩陣¶
系統管理群整體基調見 _overview §5。本頁是該群少數三個端點都有能力點守門的頁(對照 SMTP 伺服器設定 與 LDAP 設定 的寫入端點僅 @jwt_required)。
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 進入本頁 | ui_routes 可見性(log-forwarding.read,requirement=ALL) |
— | public.route_capabilities |
| 讀取設定 | 有選單即可 | ✅ require_capability("log-forwarding.read") |
api/log_forwarding/routes/log_forwarding_route.py(LogForwardingSettingRoute.get) |
| 儲存設定 | hasCap('log-forwarding.update') → 否則全欄位 disabled |
✅ require_capability("log-forwarding.update") |
同上(LogForwardingSettingRoute.put) |
| 發送測試 log | 同上 + isDirty 反灰 |
✅ require_capability("log-forwarding.update")(非 read——會實際送出封包) |
同上(LogForwardingTestRoute.post) |
| 授權唯讀期間 | useLicenseReadonly().writeDisabled() 把儲存與測試兩顆按鈕反灰並顯示原因 |
由授權層另行守門 | src/composables/useLicenseReadonly.js |
守門走 FR-048 統一授權守門 的軸④ capability,route decorator 形式——主體域守門只看「你是誰」,不必先 resolve 資源,故允許放在 route 層(decorator 內部委派 DI 注入的 guard service,route 本身不碰 session)。
兩個能力點皆
is_platform = false:這是租戶級系統設定頁,落地部署版的客戶系統管理員必須自己改得動 log server 位址;設成平台層會被trg_role_capabilities_platform_guard擋住而下放不了(同 CM-1283 SMTP/LDAP 下放的理由)。
4. 狀態機與前置條件¶
本頁無業務狀態機(單一設定列覆蓋式更新),前置條件集中在啟用門檻與三層驗證分工:
| 條件 | 效果 | 出處 |
|---|---|---|
enabled=true 但 host 或 port 為空 |
400 GRC_400115;FE 亦先擋(標紅 + toast);DB CHECK chk_lfs_enabled_needs_target 為最後防線 |
domain/log_forwarding/service/log_forwarding_setting_domain_service.py::validate |
enabled=true 但兩條流都沒勾 |
400 GRC_400116(掛了鏈卻什麼都不送=靜默無效狀態) |
同上 |
enabled=false |
只驗值域,不強制 host/port——使用者可先關掉總開關再慢慢改設定 | 同上(if not entity.enabled: return) |
protocol ∉ {syslog,gelf} |
400 GRC_400117(marshmallow OneOf 先擋 → domain → DB CHECK) |
同上 |
transport ∉ {udp,tcp} |
400 GRC_400118 |
同上 |
port 不在 1–65535 |
400 GRC_400119 |
同上 |
masking_enabled=true |
400 GRC_400120「敏感資料遮罩功能尚未推出」——欄位存在但第一版無行為,允許開啟會給出錯誤的安全感 |
同上 |
| 設定列不存在 | 404 GRC_404048(migration 已預建全域列,此為防守路徑) |
LogForwardingSettingDomainService.get_global_required |
| 進頁 GET 失敗且非 404 | FE configLoadFailed=true,之後按儲存直接 toast 擋下——此刻表單顯示的是預設值而非 DB 實值,存下去等於用預設值覆蓋正確設定 |
LogForwardingForm.vue::getConfig |
表單有未儲存變更(isDirty) |
測試按鈕反灰+提示「請先儲存再測試」 | LogForwardingForm.vue::testDisabledReason |
| 設定變更後 | 本進程立即重掛;其餘 worker 最長 LOG_FORWARDING_WATCH_INTERVAL(預設 30s)後跟上 |
common/log_forwarding/forwarder.py::apply_settings / start |
驗證為什麼有三層不是重複:marshmallow 擋型別與值域(回可讀的 400)、domain service 擋業務規則(回帶語意的 error code)、DB CHECK 擋繞過 API 的直接寫入(最後防線)。三者分工不同層的攻擊面。
5. UI 設計¶
版面骨架(單卡片表單,無 tab、無列表):
📸 實機截圖待補(亮色模式)。待補清單:初次進頁(總開關關閉、位址空白)、啟用後填妥設定、測試成功(UDP 版 info toast)、測試失敗(帶原因的 error toast)、必填欄位紅框。
狀態呈現:
loading期間以LoadingState(size="page")取代整張表單。- 頁首固定一則 PrimeVue
Message(severity=info,不可關閉)說明「轉發為旁路、log 伺服器異常不影響系統運作」。 - 位址/連接埠的
required-label樣式與紅框隨總開關動態切換(:class="{ 'required-label': config.enabled }")——關閉時它們不是必填。 - 儲存與測試按鈕在送出期間各自顯示
loadingspinner;反灰時以v-tooltip.top顯示反灰原因(無權限/請先儲存/授權唯讀),不隱藏按鈕。 - 無 socket;儲存後以 BE 回傳的實際存檔結果回填(不是用送出的 payload),確保畫面與 DB 一致。
6. API 規格¶
Envelope:成功 {"code": 1, "data": …}、失敗 {"code": 0, "msg": "…"}(見專案 CLAUDE.md Response Format)。
6.1 總清單(本頁呼叫的全部 endpoint)¶
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 讀取 | GET /api/1.0/log-forwarding |
讀取系統層轉發設定 | §6.2 |
| 儲存 | PUT /api/1.0/log-forwarding |
部分更新(未帶的欄位沿用現值)+本進程立即重掛 | §6.2 |
| 測試 | POST /api/1.0/log-forwarding/test |
依已存設定送一筆測試訊息 | §6.3 |
無 uid、無列表端點:系統層單一設定語意,路徑不帶識別碼。三支端點皆註冊於
api/log_forwarding/,模組已登記在config/app_modules.py("log_forwarding")。
6.2 讀取 / 儲存¶
[GET] /log-forwarding
Response data(LogForwardingSettingResponse,api/log_forwarding/serializers/log_forwarding.py):
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | string | 設定列 UUID |
| enabled | boolean | 總開關 |
| protocol | string | syslog / gelf |
| transport | string | udp / tcp |
| host | string(nullable) | log 伺服器位址 |
| port | integer(nullable) | 連接埠 |
| forward_app_log | boolean | 流一:應用 log |
| forward_audit_events | boolean | 流二:稽核事件 |
| masking_enabled | boolean | D5 預留欄,第一版恆 false |
| masking_available | boolean | 恆 false;FE 依此決定遮罩區塊渲染與否(false=整塊不出現,CM-1410),不在前端寫死 |
| created_user / updated_user | string(nullable) | 操作者 login_name |
| created_user_name / updated_user_name | string(nullable) | 對應 nickname(審計欄位規範,app service 層批次查 jedi-auth,不在 infra JOIN) |
| created_at / updated_at | datetime(nullable) | 建立 / 更新時間 |
[PUT] /log-forwarding(部分更新)
Request(LogForwardingSettingUpdateRequest,unknown = EXCLUDE):
{
"enabled": true,
"protocol": "syslog",
"transport": "udp",
"host": "10.0.0.60",
"port": 5514,
"forward_app_log": true,
"forward_audit_events": true,
"masking_enabled": false
}
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| enabled | boolean | 否 | 未帶則沿用現值(下同) |
| protocol | string | 否 | OneOf(["syslog","gelf"]) |
| transport | string | 否 | OneOf(["udp","tcp"]) |
| host | string(allow_none) | 否 | FE 送出前 trim,空字串轉 null |
| port | integer(allow_none) | 否 | Range(1, 65535) |
| forward_app_log | boolean | 否 | |
| forward_audit_events | boolean | 否 | |
| masking_enabled | boolean | 否 | 送 true 一律 400 GRC_400120 |
schema 走
load而非use_kwargs注入:本端點是部分更新,必須區分「沒帶這個 key」與「帶了None」,kwargs 展開後兩者都是None(log_forwarding_route.py::put)。
unknown = EXCLUDE是刻意的:FE 常把 GET 回來的整份物件(含 uid/審計欄/唯讀旗標)原樣回送,嚴格模式會整個 400 掉。
Response data:同 GET 的全部欄位,另加兩個僅 PUT 回傳的旗標:
| 欄位 | 型別 | 說明 |
|---|---|---|
| forwarding_active | boolean | 本進程是否正在轉發(重掛結果) |
| applies_within_seconds | integer | 其餘 gunicorn worker 最長多久後跟上(預設 30,取自 LOG_FORWARDING_WATCH_INTERVAL)。如實告知非瞬時生效,FE 據此組 toast 文案 |
6.3 發送測試訊息¶
[POST] /log-forwarding/test — 無 request body。
Response data(LogForwardingTestResponse):
| 欄位 | 型別 | 說明 |
|---|---|---|
| sent | boolean | 是否成功送出 |
| delivery_confirmed | boolean | UDP 恆 false——送出成功不等於對端收到;TCP 且送出成功才為 true(連線層有回饋) |
| protocol / transport / host / port | string / integer | 本次實際使用的(已存)設定 |
| sent_at | string | ISO 8601 UTC |
| message | string | 三種措辭之一:TCP 成功「已送出,且與 log 伺服器連線成功」/UDP 成功「已送出。UDP 無法確認對方是否收到,請至 log 伺服器確認」/失敗「送出失敗:\<例外類型: 訊息>」 |
BE 鏈路(app/log_forwarding/service/log_forwarding_app_service.py::send_test_message):① 讀 DB 已存設定;② host/port 未設定 → 直接回 sent=false 附說明(不視為錯誤);③ 組一支臨時 handler(forwarder.build_target_handler)直接 emit——刻意不走現有轉發鏈,因為測試要能在總開關關著時驗連通(客戶通常先測通再打開),且走佇列的話 emit() 立刻返回、測不出任何東西;④ 發稽核事件 6101;⑤ 依 transport 決定 delivery_confirmed。
7. 前端檔案地圖(compliance-manager-fe/)¶
| 檔案 | 角色 |
|---|---|
src/views/log-forwarding/LogForwardingForm.vue |
唯一頁面元件(表單 + 測試按鈕 + 離開確認 + 髒值比對) |
src/config/router/index.js |
route log-forwarding → /system/log-forwarding |
src/config/api/api.js |
LOG_FORWARDING / LOG_FORWARDING_TEST 端點常數 |
src/config/locales/i18n/zh-tw/log-forwarding.json、.../en/log-forwarding.json |
本頁 i18n(獨立檔,非併入 pages.json) |
src/composables/useLicenseReadonly.js |
授權唯讀期間的寫入類 UI 反灰+原因提示 |
src/utils/userUtil.js(hasCap) |
能力點判定 |
src/components/common/LoadingState.vue |
載入中骨架 |
8. 後端檔案地圖¶
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| 設定 CRUD / 測試 | api/log_forwarding/routes/log_forwarding_route.py |
app/log_forwarding/service/log_forwarding_app_service.py(@transaction) |
domain/log_forwarding/service/log_forwarding_setting_domain_service.py → ILogForwardingSettingRepo → infra/log_forwarding/repository/log_forwarding_setting_repo_impl.py |
| Handler 鏈掛載 / 熱生效 | —(無 HTTP 入口) | — | common/log_forwarding/forwarder.py(組裝/卸載/watcher)、gelf_handler.py(GELF 1.1 自寫)、settings_reader.py(背景唯讀讀取器) |
| 啟動掛載點 | — | — | core/app_factory.py(init_database 之後 forwarder.start())、main.py::_post_fork(restart_after_fork()) |
DDD 提醒(兩處刻意偏離,理由已在檔頭寫明,勿當通例照抄):
common/log_forwarding/settings_reader.py自開 session 讀 DB,未走 repo/@transaction。原因:handler 鏈組裝發生在沒有 request 脈絡的地方(啟動期 DI 尚未 wire 完、watcher 背景執行緒無session_scope與 RLS 變數)。它比照infra/system_config/system_config_root_reader.py的既有樣板(繞 RLS 的獨立唯讀 session,讀完就還連線),沒有長出第二套真相——API 端的 CRUD 一律走完整四層路徑。- 檔案放
common/而非infra/:它必須能被「DI 尚未就緒」的啟動路徑 import(比照common/integrity/)。新增其他背景設定讀取需求時可比照此樣板;一般業務查詢一律走 repo,不要引用本頁當先例。
9. DB¶
9.1 資料表總清單(本頁讀寫的全部表)¶
| 表 | 讀/寫 | 說明 | 詳述 |
|---|---|---|---|
config.log_forwarding_settings |
讀 / 寫 | 轉發設定本體(tenant_id IS NULL 全域列,第一版僅此一列) |
§9.3 |
public.capabilities / public.role_capabilities / public.route_capabilities / public.ui_routes / public.roles |
讀 | 能力點守門與選單可見性(log-forwarding.{read,update}) |
_overview §6 |
public.users |
讀 | 審計欄位 enrich(login_name → nickname),走 jedi-auth UserDomainService |
— |
public.system_logs |
寫(間接) | 稽核事件 6100 / 6101 由既有 DBLogHandler 寫入,非本頁直接操作 |
§11 |
9.2 ER 圖¶
本頁只有一張獨立設定表(無 FK、無關聯),RBAC 唯讀依賴沿用 _overview §6 核心資料模型圖,不另畫 ER 圖。轉發鏈的執行期結構見 §11 的 handler 鏈圖。
9.3 核心表欄位¶
config.log_forwarding_settings(表 comment:「FR-068 系統 log 轉發設定(tenant_id NULL = 全域;第一版僅此一列)」;以下取自 DEV 實庫 \d):
| 欄位 | 型別 | 說明 |
|---|---|---|
| id | bigint | 主鍵(BIGSERIAL) |
| uid | varchar(36) | UUID,UNIQUE |
| tenant_id | bigint(nullable) | NULL=全域設定;D3 預留租戶層覆寫,第一版不使用 |
| enabled | boolean NOT NULL(預設 false) | 總開關 |
| protocol | varchar(20) NOT NULL(預設 syslog) |
syslog(RFC 5424)/gelf(Graylog 原生結構化) |
| transport | varchar(10) NOT NULL(預設 udp) |
udp=fire-and-forget/tcp=非阻塞,斷線靜默降級不重試 |
| host | varchar(255)(nullable) | log 伺服器位址 |
| port | integer(nullable) | 連接埠 |
| forward_app_log | boolean NOT NULL(預設 true) | D2 流一:一般應用 log(無 event_code) |
| forward_audit_events | boolean NOT NULL(預設 true) | D2 流二:稽核事件(帶 event_code,SIEM 客戶主要訴求) |
| masking_enabled | boolean NOT NULL(預設 false) | D5 預留欄——第一版無任何行為,UI 顯示為 disabled |
| created_user / updated_user | varchar(255)(nullable) | 操作者 login_name |
| created_at / updated_at | timestamptz NOT NULL(預設 now()) |
updated_at 亦是多 worker 熱生效比對的參考 |
約束與索引:
| 名稱 | 內容 |
|---|---|
uq_lfs_global |
UNIQUE ((1)) WHERE tenant_id IS NULL — 部分唯一索引保證全域列只有一筆(一般 UNIQUE 下 NULL 互不相等,擋不住重複) |
uq_lfs_tenant |
UNIQUE (tenant_id) WHERE tenant_id IS NOT NULL — D3 預留,第一版無列落在此 |
chk_lfs_protocol / chk_lfs_transport / chk_lfs_port |
值域檢查(對應 §4 的第三層防線) |
chk_lfs_enabled_needs_target |
enabled IS FALSE OR (host IS NOT NULL AND host <> '' AND port IS NOT NULL) — 讓 DB 擋在寫入當下,避免「存成功但轉發靜默不動」這種最難查的狀態 |
不掛 RLS(與本群多數租戶表不同):這是平台層設定(比照
config.detection_tools),且轉發 handler 掛載發生在沒有 request 脈絡的背景執行緒——RLS 需要的app.allowed_tenant_paths當下不存在,掛了只會讓背景讀取恆空。存取控制由應用層能力點負責。選單登記在
public.ui_routes:name='log-forwarding'、url='/system/log-forwarding'、pid=47(group-system-config)、sort=35——DEV 實庫確認落在notify-config(30) 與user-log(40) 之間。route_capabilities綁 read=ALL/ update=ANY。
10. 頁面邏輯與資料對應¶
關鍵欄位對應:
| 畫面元素 | FE state | API 欄位 |
|---|---|---|
| 啟用 log 轉發 | config.enabled |
enabled |
| 協定 / 傳輸方式 | config.protocol / config.transport |
同名 |
| Log 伺服器位址 / 連接埠 | config.host / config.port |
host / port(送出前 trim,空值轉 null) |
| 應用程式 log / 稽核事件勾選 | config.forward_app_log / config.forward_audit_events |
同名 |
| 敏感資料遮罩是否可用 | maskingAvailable(ref,預設 false;v-if 條件) |
masking_available(唯讀旗標) |
| 生效延遲秒數 | appliesWithinSeconds(ref) |
applies_within_seconds(僅 PUT 回傳) |
載入流程:onMounted → GET /log-forwarding → applyResponse() 只取表單用得到的欄位(uid/審計欄不進表單狀態)→ 不論成功或失敗都拍一次快照(originalConfig),離開時才不會誤判為有未存變更。
髒值判定:isDirty = JSON.stringify(config) !== originalConfig——用快照比對而非單向 hasChange 旗標,這樣「改了又改回原值」不會被誤判為髒(否則測試按鈕會無故反灰)。
儲存流程:configLoadFailed → 直接擋;FE 必填檢查 → 擋;PUT → 以 BE 回傳的實際存檔結果回填(例如 host 被 trim)再重拍快照——用送出的 payload 拍會讓「BE 沒照收」的欄位被誤認為已存;BE 未回完整設定時退而重讀一次 getConfig()。啟用狀態下額外跳一則 info toast 說明生效延遲。
錯誤對應:BE error envelope 由 BaseService 統一轉 toast;port 用 ?? null 而非 || null 回填(0 不是合法埠,但要區分「使用者剛清空」與「BE 沒給」,統一以 null 表示未設定)。
11. 背景行為與外部依賴¶
| 類型 | 內容 |
|---|---|
| 通知 | 無站內通知 |
| Job / 排程 | log-forwarding-watcher daemon 執行緒(每個 gunicorn worker 各一條):每 LOG_FORWARDING_WATCH_INTERVAL(預設 30s)重讀設定,指紋有變才重掛。輪詢成本以 4 worker 計約每秒 0.13 次查詢 |
| Socket | 無(頁面層);轉發本身持有對外 UDP/TCP socket |
| jedi-* 套件 | jedi-common(@transaction / CustomFormatter / DBLogHandler / 例外類別)、jedi-auth(JWT context、UserDomainService 補 nickname) |
| 系統參數 | LOG_FORWARDING_WATCH_INTERVAL(熱生效輪詢秒數,預設 30)、LOG_FORWARDING_APP_NAME(syslog APP-NAME,預設 guidant-ai)、LOG_FORWARDING_HOSTNAME(syslog HOSTNAME 覆寫;未設則推導自 SYSTEM_URL 的 host,再退回 gethostname()) |
| 事件碼 | 6100 LOG_FORWARDING_SETTING_UPDATED(設定變更)、6101 LOG_FORWARDING_TEST_SENT(測試送出)——轉發目標本身就是稽核關切點:改掉 log server 位址等於改變「稽核軌跡送去哪裡」,這個動作自己必須留下紀錄 |
執行期的 handler 鏈:
設計要點(common/log_forwarding/forwarder.py / gelf_handler.py):
- 掛載目標是 7 個頂層 logger(
api/app/infra/domain/common/middleware/error_handler),與 jedi-common logging config 宣告的一致——它們是所有 module logger 的父節點。 - 兩流分流靠
_StreamFilter看 record 有無event_code,不另外接一條稽核專用鏈(稽核事件本來就走同一組 logger,另接等於同一筆送兩次)。 - 主執行緒零阻塞:
QueueHandler.emit()只做put_nowait,所有 socket I/O 在QueueListener背景執行緒。佇列上限 10000,滿了靜默丟棄新 record(每累積 1000 筆記一行本地 warning)——stdlib 預設會往 stderr 印 traceback,log server 慢時等於每丟一筆印一段,比丟 log 本身更糟。 - 失敗語意:UDP fire-and-forget;TCP 連線失敗後置降級旗標、不逐筆重連(不製造重試風暴),直到下次設定變更重建 handler 才再試。任何組不出 handler 的情況只記一筆本地 warning,本機 log 不受影響。
- syslog 送出格式:
<PRI>1 <UTC 時戳>Z <HOSTNAME> <APP-NAME> <PID> <MSGID> - <訊息>,facility 為local0。MSGID帶事件碼(一般 log 為-),是接收端分流兩種紀錄最省事的判準。 - GELF 自寫不引套件:
pygelf/graypy皆不在專案依賴內,引入等於多一個要進打包清單、離線安裝包與授權盤點的第三方依賴;且兩者的失敗語意(重試/往上拋)與 D4 要求的「靜默降級」相反,用套件反而要再包一層壓制。UDP 走 gzip、TCP 走 NUL 分隔不壓縮(GELF 規格),不做 chunking,單筆超過 8 KiB 截斷並標…[truncated]。 - 稽核事件的結構化欄位:
common/util/audit_log.audit()額外把event_type與各欄位掛在 record 上供 GELF 拆成獨立欄位(Graylog 上可_project_id:88直接查),message格式一個字都沒改——本地system_logs與既有 SQL runbook 都靠它。
12. 邊界情況與已知坑¶
- 測試按鈕測的是「已儲存」的設定,不是畫面上的值:BE 的 test 端點讀 DB。FE 因此在
isDirty時把按鈕反灰。若日後有人「順手」讓測試改吃表單當前值(比照 SMTP 伺服器設定 與 LDAP 設定 的做法),必須同步拿掉這個反灰,否則兩邊語意打架。兩種設計各有道理,但 SMTP/LDAP 測的是即時連線、log 轉發測的是「現在真的在轉的那組設定」,不宜草率對齊。 - UDP 的
sent=true只代表送出,不代表送達:delivery_confirmed在 UDP 恆false,FE 三種措辭不可簡化成一律「測試成功」。這不是保守措辭而是真實限制——CM-1408 即是活例:封包確實飛到了 Graylog,但因格式不合被整批丟棄,而當時的驗收(純落檔的 socket)看起來一切正常。 - 敏感資料遮罩是預留位,且 UI 上完全看不到:
masking_enabled欄位存在於 DB 與 API,但第一版無任何遮罩行為,強行送true會被 domain 擋(GRC_400120)。FE 自 CM-1410 起整塊不渲染(v-if="maskingAvailable",而masking_available由 BE 恆回false)——決策者裁定未上的功能不在 UI 曝光,避免客戶反覆詢問何時可用;早期版本曾以「反灰+即將推出 Tag」呈現,已移除。未來 BE 回true時 FE 自動顯示、不用改前端;i18n key 保留未刪。log 目前原樣轉發,信任邊界假設在客戶內網(design D5)。 - 熱生效是輪詢不是廣播,最長 30 秒:本專案沒有任何跨 worker 訊號機制(無 SIGHUP handler、無 Redis pub/sub 訂閱端、無 worker 名冊),故採「每 worker 自己每 30 秒重讀」。API 誠實回傳
applies_within_seconds,FE 也如實顯示——不要把它包裝成即時生效。想調整改環境變數LOG_FORWARDING_WATCH_INTERVAL。 restart_after_fork()非有不可:create_app()跑在 gunicorn master,fork 後子進程繼承的是「模組全域看起來已啟動」的假象,但執行緒不跨 fork 存活。不重建的話每個 worker 的 log 都會被丟進沒人消費的佇列、且設定改了永遠不生效。掛載點在main.py::_post_fork,動 gunicorn 啟動流程時勿移除。settings_reader自開 session 是刻意偏離,不是通例:見 §8 的 DDD 提醒。新增一般業務查詢請走 repo。- 選單登記與能力點是兩支不同的 migration:
2026-08-25-fr068-1建表+能力點+授權,2026-08-27-fr068-2補ui_routes+route_capabilities。只做前者的話整頁會被前端當成「無權限」而全部反灰——FE 的能力判定來自/user/web-menu回的選單樹,capability 掛在選單節點底下一起回,沒有節點就沒有能力陣列。日後新增系統設定頁務必兩支都做。 - 本頁三個端點都有能力點守門,與本群多數頁相反:SMTP 伺服器設定、LDAP 設定 的寫入端點僅
@jwt_required。新增設定頁請比照本頁,不要比照那兩頁。 - 兩個能力點刻意
is_platform=false:落地部署版客戶的系統管理員必須自己改得動 log server 位址。設成平台層會被trg_role_capabilities_platform_guard擋住而無法下放。 config.log_forwarding_settings只會有一列:uq_lfs_global這個部分唯一索引保證了這件事。程式端沒有列表 API、沒有新增按鈕;save_global()內的「不存在則建一列」是防守路徑(migration 已預建),不是給人拿來建第二列的入口。- 佇列滿時會靜默丟棄轉發副本:接收端跟不上時,本地 log 完整無缺、丟的只是送出去的那份。維運判斷依據是
log/app.log內的「log 轉發佇列已滿,累計丟棄 N 筆」——只在累積每 1000 筆時記一行(刻意不逐筆記,否則變成另一場洗版)。 - STG / POC 尚未套 migration:兩支 migration 目前只套 DEV。上版前需依環境異動規範由決策者放行後,走 init image 的 migrate 模式套用。
13. 開發與驗證¶
- 跑起來:BE
python main.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload)。 - 測試帳號:需具
log-forwarding.*能力(DEV 上為 Administrator 角色;比照 SMTP 設定頁的權限範圍)。帳號見 memoryreference_dev_login/.env(憑證不寫入本文件)。 - 導航路徑:登入 → 側選單「系統設定」→「Log 轉發設定」(
/system/log-forwarding)。 - 前置資料:DEV 需已套
scripts/sql/2026-08-25-fr068-1-log-forwarding-settings.sql與2026-08-27-fr068-2-log-forwarding-menu-route.sql。 - 本機驗證假接收端:
nc -ul <port>(UDP)/nc -l <port>(TCP)可驗「送得出去」;但驗不到「收得下來」——純落檔的 socket 不解析 RFC 5424,CM-1408 的三個格式缺陷就是這樣漏掉的。對外協定類的驗收請用會解析的接收端(真 log server,或至少照規格 regex 解析並斷言時戳、結尾位元組的腳本)。 - E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁尚無 feature 檔。 - 使用手冊:三家(rsyslog / Graylog / ELK)接收端對接範例、送出內容格式與排錯對照見
docs/user-manual/log-forwarding-guide.md;落地部署版的選配設定入口見該手冊 §10.6。 - 相關文件:設計決策(D1–D6)見
docs/features/FR-068-2608-log-forwarding/design.md;同群設定頁對照 SMTP 伺服器設定、LDAP 設定、通知頻道設定;站內操作紀錄查詢見 操作日誌。