Server Metrics 連線人數 Integration Guide — Frontend
Backend branch: feat/connected-user-metrics → cortex/dev,語意修正 fixed/connected-counts-monotonicity(#1242)
API base: SIT(admin Bearer token 必要)
生效時間:merge 後 cortex-api --reload 立即生效,無 migration、無需重啟 scheduler
Consumer:cortex-management「系統監控 / 伺服器狀態」頁(同一支端點的既有下鑽契約見 server-metrics-access-log-drilldown.md)。
1. 欄位改名對照表(Breaking)
無。本次是純 additive,既有欄位的名稱、型別、語意一律未動。
2. 新欄位(additive)
| Endpoint | 新欄位 | 型別 | 語意 |
|---|---|---|---|
GET /api/admin/server-metrics |
users.connected_members_in_range |
number |
查詢區間內留下請求紀錄的 distinct 登入會員數 |
GET /api/admin/server-metrics |
users.connected_anonymous_ips_in_range |
number |
查詢區間內未登入請求的 distinct client IP 數 |
GET /api/admin/server-metrics |
users.connected_total_in_range |
number |
上述兩者相加。人數的上界,非精確人數,見 §5 |
三個欄位都必定存在(非 optional、不會是 null),無資料時為 0。
與既有 users.* 欄位的關係
既有欄位全部源自 refresh_tokens,結構上看不到未登入者。新欄位源自 api_access_log,兩組數字不可互相推導、也不該相加:
| 資料來源 | 時間語意 | 涵蓋範圍 | |
|---|---|---|---|
online_now / valid_access_token_sessions |
refresh_tokens |
呼叫當下 | 僅登入者,且 members.status = 'active' |
logged_in_today / _this_week / _this_month / _in_range |
refresh_tokens.created_at |
對應期間 | 僅登入者 |
connected_*_in_range |
api_access_log |
query_range(上界截到 generated_at) |
登入 + 未登入,不檢查會員現況 |
3. 行為改變(Status code / shape)
| 情境 | 修前 | 修後 |
|---|---|---|
| 任何成功查詢 | 200,users 有 6 個 key |
200,users 有 9 個 key(多 3 個,既有 6 個不變) |
from_date > to_date |
422 |
422(不變) |
| 區間超過 31 天 | 422 |
422(不變) |
| 非 admin | 403 |
403(不變) |
沒有任何 status code 改變。前端若用嚴格模式解析(例如 zod 的 .strict()),新欄位會讓舊 schema 驗證失敗——這是唯一需要動的地方。
4. 六情境 response(connected_* 三欄由 committed 測試斷言)
以下三個 connected_* 欄位的值不是手寫、也不是一次性截圖,而是由
investment-agent/backend/tests/integration/test_admin_server_metrics_connections_pg.py::test_connection_counts_match_seeded_rows_in_each_window
對真實 PostgreSQL 逐項 assert 的值。查詢語意若改變,該測試會紅,這份文件不會靜默過期。
其餘六個 token-based 欄位未被斷言——它們在這個 scratch DB 恆為 0,因為該庫沒有 refresh_tokens 資料。那是環境結果,不是新行為。
fixture 落在 2001 年(api_access_log 由 migration 051 於 2026 建立,真實列不可能落在該區間),匿名位址用 RFC 5737 保留段。
fixture 內容(day 1 = 2001-01-01,day 2 = 01-02,day 3 = 01-03):
- 會員 A(day 1 兩個位址
203.0.113.1/.2)、會員 B(day 1203.0.113.3)、會員 C(day 1 與 day 2 皆203.0.113.4) - 匿名
192.0.2.10(day 1 兩筆)、192.0.2.11(day 1)、192.0.2.30(day 3) 203.0.113.7:day 1 同時有 B 的登入請求與一筆匿名請求(重疊位址)198.51.100.5:day 1 匿名、day 2 由 A 登入(漂移位址)- 一筆
client_ip IS NULL的匿名請求
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"$API_BASE/api/admin/server-metrics?from_date=2001-01-01&to_date=2001-01-01"
4-1 登入與未登入都有,且含重疊位址(happy path)
from_date=2001-01-01&to_date=2001-01-01
{
"online_now": 0,
"valid_access_token_sessions": 0,
"logged_in_today": 0,
"logged_in_this_week": 0,
"logged_in_this_month": 0,
"logged_in_in_range": 0,
"connected_members_in_range": 3,
"connected_anonymous_ips_in_range": 4,
"connected_total_in_range": 7
}
匿名 4 = 192.0.2.10(兩筆收斂成 1)、192.0.2.11、203.0.113.7、198.51.100.5。client_ip IS NULL 那筆不算。
203.0.113.7 同時被算進兩半——會員 B 當天從該位址登入過。這是刻意的:見 §5。
4-2 放大區間(單調性)
from_date=2001-01-01&to_date=2001-01-02
{
"connected_members_in_range": 3,
"connected_anonymous_ips_in_range": 4,
"connected_total_in_range": 7
}
day 2 沒有帶進任何 day 1 沒有的呼叫端,所以三個數字都持平;198.51.100.5 仍然計入匿名半邊,即使它在 day 2 帶著登入身分出現。舊版在這裡會把匿名從 3 降到 2、total 從 6 降到 5(實測值)。放大區間絕不會讓任何一個數字下降(test_widening_the_range_never_lowers_a_count 釘住)。
4-3 只有登入流量
from_date=2001-01-02&to_date=2001-01-02
{
"connected_members_in_range": 2,
"connected_anonymous_ips_in_range": 0,
"connected_total_in_range": 2
}
4-4 只有未登入流量
from_date=2001-01-03&to_date=2001-01-03
{
"connected_members_in_range": 0,
"connected_anonymous_ips_in_range": 1,
"connected_total_in_range": 1
}
4-5 區間內無任何流量
from_date=2001-01-04&to_date=2001-01-04
{
"connected_members_in_range": 0,
"connected_anonymous_ips_in_range": 0,
"connected_total_in_range": 0
}
無資料是 200 + 全零,不是 404。
4-6 錯誤情境:起日晚於訖日
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"$API_BASE/api/admin/server-metrics?from_date=2001-02-01&to_date=2001-01-01"
{
"detail": "from_date must be on or before to_date"
}
HTTP 422。錯誤 shape 與既有行為一致。註:只有 status code 被測試釘住(tests/test_admin_server_metrics_route.py),detail 字串目前無測試斷言,可能隨實作調整。
5. 這些數字是上界,不是精確人數
兩半可能重疊。public 端點無 token 即可呼叫,已登入會員的呼叫端若在這些請求上省略 Authorization header,同一人會同時出現在兩半(見 §4-1 的 203.0.113.7)。
早期版本曾用 anti-join 扣除重疊,已於 #1242 移除,原因有二:
- 它讓聚合非單調。 anti-join 只看得到查詢區間,所以某位址在區間頭幾天匿名、後幾天登入,放大區間會讓它從匿名半邊消失——超集區間報出比子集更小的數字,逐日長條圖與週月彙總互相矛盾。
- 它只在同一 IP 成立。 同一人換網路(4G ↔ Wi-Fi、多裝置)仍然被算成兩個,去重根本沒接到最常見的情況。
以壞掉的聚合換取部分去重不划算,所以現在保留重疊並如實標示。前端不需要、也無法自行修正這個重疊——後端沒有匿名身分可供比對。
6. Consumer 端 type diff
export interface ServerMetricsUsers {
online_now: number;
valid_access_token_sessions: number;
logged_in_today: number;
logged_in_this_week: number;
logged_in_this_month: number;
logged_in_in_range: number;
+ /** 區間內留下請求紀錄的 distinct 登入會員數。 */
+ connected_members_in_range: number;
+ /** 區間內未登入請求的 distinct client IP 數。
+ * 單位是 IP 不是人:同一 NAT 後方多人算 1,同一人換網路算 2。
+ * 可能與 connected_members_in_range 重疊,見 §5。 */
+ connected_anonymous_ips_in_range: number;
+ /** 兩者相加。人數的上界,不是精確人數。 */
+ connected_total_in_range: number;
}
7. UI 呈現注意事項
- 不要標成「訪客人數」。
connected_anonymous_ips_in_range是 IP 數,欄位名帶ips就是為了不在名字上失真。建議標成「未登入連線端(以 IP 計)」並附 tooltip。 connected_total_in_range混合單位:人 + 位址。一位會員用三台裝置貢獻 1,一位匿名者換三個網路貢獻 3,所以登入比例改變時,總數的移動方向取決於呼叫端組成:同一人原本用 N 個位址、登入後收斂成 1,總數下降;但 NAT 後方多人共用一個位址時,其中一人登入會把原本合併的 1 拆成「1 位會員 + 該位址其餘的人」而使總數上升。不可當趨勢指標。- 不要和
online_now並列成同一組指標。前者是區間累計、後者是當下快照,並排會讓人誤以為可比較。 - 逐日數字不可相加。
COUNT(DISTINCT)本質不可加,七天各自查詢的和不低於一次七天區間查詢的結果(各日呼叫端完全不重疊時才相等)。要「本週不重複」就直接查該區間。 connected_*不做「現在在線」判定。若 UI 需要「目前同時在線」,那仍然只有online_now(且僅涵蓋登入者)。
8. 已知限制(需在 UI 或文件向管理員說明)
logging.access_log.exclude_paths(/health、/、/docs、/openapi.json、/redoc)不寫入 access log,只造訪這些路徑的呼叫端不會被計入。- 保留天數依
DEPLOY_ENV而異:dev 7 天、SIT/UAT 14 天、prod 180 天。在 SIT 上只能回溯兩週,超出區間會回全零而非錯誤——UI 應避免讓管理員以為「兩週前真的沒人來」。 client_ip的真實性依部署而定:SIT 經FORWARDED_ALLOW_IPS解析 Cloudflare Tunnel 的X-Forwarded-For,記到真實客戶端 IP;本機 dev compose 未設該變數,所有請求塌成同一個 docker gateway 位址,匿名計數在 dev 無參考價值。- 刪除會員會改變歷史數字:
api_access_log.member_id是ON DELETE SET NULL,該員過去的請求會脫離 member 半邊。其中client_ip非 NULL 的列改由匿名半邊計(若該位址尚未被計過),client_ip IS NULL的列則兩半都不計,因此總數可能淨減。同一組查詢參數在刪除前後會得到不同結果。停權(status改變)則不影響。
9. Consumer 遷移 checklist
- [ ] grep
ServerMetricsUsers(或等價的 response type 定義),依 §6 補上三個欄位 - [ ] 若使用
zod/io-ts等執行期驗證且為 strict 模式,更新 schema 否則新欄位會驗證失敗 - [ ] 「系統監控 / 伺服器狀態」頁加上連線人數區塊,文案依 §7 避免把 IP 數說成人數
- [ ] 為 SIT 的 14 天保留期加上區間提示
- [ ] 確認既有
online_now的顯示位置與新指標視覺上分開(快照 vs 區間累計) - [ ] 若要做趨勢圖,確認未把逐日數字相加當成期間總計(§7)
10. 參考
- 後端規格:
docs/api-reference.md§26B「連線人數定義」 - OpenAPI:
docs/assets/openapi.json(ServerMetricsResponse→MetricsUsers) - 數值來源與真實 DB 行為測試:
investment-agent/backend/tests/integration/test_admin_server_metrics_connections_pg.py - 去重移除的裁決:issue #1242