跳轉到

Log 轉發設定(Log Forwarding)

功能群:系統管理|三層 RBAC 模型與角色權限基調先讀 功能群總覽 §2 / §5

事實基準:2026-08-27 從 FE src/views/log-forwarding/LogForwardingForm.vue + BE api/log_forwarding/ / app/log_forwarding/ / domain/log_forwarding/ / common/log_forwarding/ + DEV 實庫 config.log_forwarding_settingspublic.ui_routes 掃出(FR-068 為新表新頁,db_schema.json / routes.json dump 早於本功能,欄位與選單位置改以 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_settingstenant_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_availablefalse 故整塊不出現;未上的功能不在 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_settingstenant_id IS NULL)該列更新;發稽核事件 6100本進程立即重掛 handler 鏈,其餘 worker 於 applies_within_seconds(預設 30)內跟上

UC-LF-02 發送測試 log

項目 內容
角色 log-forwarding.update 能力者(測試會實際送出網路封包,屬副作用非查詢)
前置條件 已登入;表單無未儲存變更(否則 FE 反灰);DB 已存有 host + port
產出 / 後置條件 不寫設定表;對外送出一筆測試訊息;發稽核事件 6101;回傳 sentdelivery_confirmedUDP 恆 false

UC-LF-01/02 流程:載入回填→填寫→FE 必填驗證→PUT(capability→domain 驗證→寫入→發稽核→本進程重掛)/POST test(讀已存設定→臨時 handler 直送→依 TCP/UDP 分流回報)

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.pyLogForwardingSettingRoute.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=truehostport 為空 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、無列表):

Log 轉發設定版面:說明訊息+表單欄位(總開關/協定/傳輸/位址/埠/兩流勾選,灰)+儲存(連 UC-LF-01)與發送測試 log(連 UC-LF-02)按鈕

📸 實機截圖待補(亮色模式)。待補清單:初次進頁(總開關關閉、位址空白)、啟用後填妥設定、測試成功(UDP 版 info toast)、測試失敗(帶原因的 error toast)、必填欄位紅框。

狀態呈現

  • loading 期間以 LoadingStatesize="page")取代整張表單。
  • 頁首固定一則 PrimeVue Message(severity=info,不可關閉)說明「轉發為旁路、log 伺服器異常不影響系統運作」。
  • 位址/連接埠的 required-label 樣式與紅框隨總開關動態切換:class="{ 'required-label': config.enabled }")——關閉時它們不是必填。
  • 儲存與測試按鈕在送出期間各自顯示 loading spinner;反灰時以 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 dataLogForwardingSettingResponseapi/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(LogForwardingSettingUpdateRequestunknown = 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 展開後兩者都是 Nonelog_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 dataLogForwardingTestResponse):

欄位 型別 說明
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 附說明(不視為錯誤);③ 組一支臨時 handlerforwarder.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.jshasCap 能力點判定
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.pyILogForwardingSettingRepoinfra/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.pyinit_database 之後 forwarder.start())、main.py::_post_forkrestart_after_fork()

DDD 提醒(兩處刻意偏離,理由已在檔頭寫明,勿當通例照抄)

  1. 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 一律走完整四層路徑。
  2. 檔案放 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_namenickname),走 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_routesname='log-forwarding'url='/system/log-forwarding'pid=47group-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,預設 falsev-if 條件) masking_available(唯讀旗標)
生效延遲秒數 appliesWithinSeconds(ref) applies_within_seconds(僅 PUT 回傳)

載入流程onMountedGET /log-forwardingapplyResponse() 只取表單用得到的欄位(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 鏈

log 轉發 handler 鏈:七個頂層 logger →兩流 filter→補操作者欄位→丟棄式 QueueHandler→佇列→背景 QueueListener 上的 SysLogHandler/GelfHandler→客戶 log 伺服器;watcher 執行緒每 30 秒重讀設定重掛

設計要點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 為 local0MSGID 帶事件碼(一般 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. 邊界情況與已知坑

  1. 測試按鈕測的是「已儲存」的設定,不是畫面上的值:BE 的 test 端點讀 DB。FE 因此在 isDirty 時把按鈕反灰。若日後有人「順手」讓測試改吃表單當前值(比照 SMTP 伺服器設定LDAP 設定 的做法),必須同步拿掉這個反灰,否則兩邊語意打架。兩種設計各有道理,但 SMTP/LDAP 測的是即時連線、log 轉發測的是「現在真的在轉的那組設定」,不宜草率對齊。
  2. UDP 的 sent=true 只代表送出,不代表送達delivery_confirmed 在 UDP 恆 false,FE 三種措辭不可簡化成一律「測試成功」。這不是保守措辭而是真實限制——CM-1408 即是活例:封包確實飛到了 Graylog,但因格式不合被整批丟棄,而當時的驗收(純落檔的 socket)看起來一切正常。
  3. 敏感資料遮罩是預留位,且 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)。
  4. 熱生效是輪詢不是廣播,最長 30 秒:本專案沒有任何跨 worker 訊號機制(無 SIGHUP handler、無 Redis pub/sub 訂閱端、無 worker 名冊),故採「每 worker 自己每 30 秒重讀」。API 誠實回傳 applies_within_seconds,FE 也如實顯示——不要把它包裝成即時生效。想調整改環境變數 LOG_FORWARDING_WATCH_INTERVAL
  5. restart_after_fork() 非有不可create_app() 跑在 gunicorn master,fork 後子進程繼承的是「模組全域看起來已啟動」的假象,但執行緒不跨 fork 存活。不重建的話每個 worker 的 log 都會被丟進沒人消費的佇列、且設定改了永遠不生效。掛載點在 main.py::_post_fork動 gunicorn 啟動流程時勿移除
  6. settings_reader 自開 session 是刻意偏離,不是通例:見 §8 的 DDD 提醒。新增一般業務查詢請走 repo。
  7. 選單登記與能力點是兩支不同的 migration2026-08-25-fr068-1 建表+能力點+授權,2026-08-27-fr068-2ui_routes + route_capabilities只做前者的話整頁會被前端當成「無權限」而全部反灰——FE 的能力判定來自 /user/web-menu 回的選單樹,capability 掛在選單節點底下一起回,沒有節點就沒有能力陣列。日後新增系統設定頁務必兩支都做。
  8. 本頁三個端點都有能力點守門,與本群多數頁相反SMTP 伺服器設定LDAP 設定 的寫入端點僅 @jwt_required新增設定頁請比照本頁,不要比照那兩頁。
  9. 兩個能力點刻意 is_platform=false:落地部署版客戶的系統管理員必須自己改得動 log server 位址。設成平台層會被 trg_role_capabilities_platform_guard 擋住而無法下放。
  10. config.log_forwarding_settings 只會有一列uq_lfs_global 這個部分唯一索引保證了這件事。程式端沒有列表 API、沒有新增按鈕;save_global() 內的「不存在則建一列」是防守路徑(migration 已預建),不是給人拿來建第二列的入口
  11. 佇列滿時會靜默丟棄轉發副本:接收端跟不上時,本地 log 完整無缺、丟的只是送出去的那份。維運判斷依據是 log/app.log 內的「log 轉發佇列已滿,累計丟棄 N 筆」——只在累積每 1000 筆時記一行(刻意不逐筆記,否則變成另一場洗版)。
  12. 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 設定頁的權限範圍)。帳號見 memory reference_dev_login / .env(憑證不寫入本文件)。
  • 導航路徑:登入 → 側選單「系統設定」→「Log 轉發設定」(/system/log-forwarding)。
  • 前置資料:DEV 需已套 scripts/sql/2026-08-25-fr068-1-log-forwarding-settings.sql2026-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 解析並斷言時戳、結尾位元組的腳本)。
  • E2Ecompliance-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 設定通知頻道設定;站內操作紀錄查詢見 操作日誌