跳转至

审计与可观察性

三个信号源

  1. 常规日志 —— tracing 输出到 stderr / journald,DEBUG/INFO/WARN/ERROR 级别,用于开发和故障诊断
  2. 审计 JSONL —— 只含 target=futu_audit 的事件,用于合规、事后追溯、攻击调查
  3. Prometheus metrics —— counter 形式的聚合数据,用于告警 + dashboard

审计 JSONL

开启:

futu-opend --audit-log /var/log/futu-audit.jsonl
# 或
futu-opend --audit-log /var/log/futu/       # 目录 → 每日滚动 futu-audit.log.YYYY-MM-DD

futu-mcp / futucli 同名 flag。

事件 schema

{
  "timestamp": "2026-04-15T10:23:45.123Z",
  "level": "WARN",                      // reject=WARN, allow=INFO, trade=WARN
  "target": "futu_audit",
  "iface": "rest" | "grpc" | "ws" | "mcp" | "cli",
  "endpoint": "/api/order" | "proto_id=2202" | "futu_place_order",
  "key_id": "bot_a" | "<missing>" | "<invalid>" | "<none>",
  "outcome": "allow" | "reject" | "success" | "failure",
  "reason": "limit: rate limit exceeded: 5 in 60s (cap 3)",
  "scope": "trade:real",                // allow 时
  "args_hash": "8a3f2b9c"               // 交易事件:SHA-256 前 8 hex
}

常见 jq 查询

# 最近的 reject
jq 'select(.outcome=="reject")' /var/log/futu-audit.jsonl | tail -20

# 某把 key 的下单记录
jq 'select(.key_id=="bot_a" and .endpoint|test("order|place|modify"))' \
  /var/log/futu-audit.jsonl

# 按拒绝原因统计
jq -r 'select(.outcome=="reject") | .reason' /var/log/futu-audit.jsonl \
  | awk -F': ' '{print $1}' \
  | sort | uniq -c | sort -rn

# per-iface 的请求分布
jq -r '.iface' /var/log/futu-audit.jsonl | sort | uniq -c

DuckDB 批量分析

-- 加载 JSONL
CREATE TABLE audit AS SELECT * FROM read_json_auto('/var/log/futu-audit.jsonl');

-- 某 key 的日成交次数
SELECT DATE(timestamp) AS day, COUNT(*) AS orders
FROM audit
WHERE key_id = 'bot_a' AND endpoint LIKE '%order%'
GROUP BY day;

Prometheus metrics

抓取:

prometheus.yml
scrape_configs:
  - job_name: futu-opend
    static_configs: [{ targets: ['opend:22222'] }]
  - job_name: futu-mcp
    static_configs: [{ targets: ['mcp:38765'] }]

三个 counter

# HELP futu_auth_events_total Auth / trade events by iface, outcome, key_id
# TYPE futu_auth_events_total counter
futu_auth_events_total{iface="rest",outcome="allow",key_id="bot_a"} 1234

# HELP futu_auth_limit_rejects_total Limit-check rejects by iface, key_id, reason
# TYPE futu_auth_limit_rejects_total counter
futu_auth_limit_rejects_total{iface="grpc",key_id="bot_b",reason="rate"} 7

# HELP futu_ws_filtered_pushes_total Pushes filtered out for client lacking scope
# TYPE futu_ws_filtered_pushes_total counter
futu_ws_filtered_pushes_total{required_scope="trade",key_id="bot_c"} 42

reason 分桶

futu_auth_limit_rejects_totalreason 标签是有限集合:

reason 含义
rate 速率超限
daily 日累计超限
per_order 单笔超限
market 市场白名单
symbol 品种白名单
side 方向白名单
hours 时段窗口
other 其他(classify_limit_reason 没覆盖的)

告警规则示例

alerts.yml
groups:
  - name: futu-opend
    rules:
      - alert: FutuAuthRejectSpike
        expr: rate(futu_auth_events_total{outcome="reject"}[5m]) > 10
        for: 5m
        annotations:
          summary: "Auth reject rate high ({{ $value }}/s)"
          description: "可能是攻击或 key 配置错"

      - alert: FutuRateLimitFrequent
        expr: rate(futu_auth_limit_rejects_total{reason="rate"}[15m]) > 1
        for: 15m
        annotations:
          summary: "Key {{ $labels.key_id }} 长期触发 rate limit"

      - alert: FutuDailyCapNearLimit
        expr: futu_auth_limit_rejects_total{reason="daily"} > 5
        for: 5m
        annotations:
          summary: "Key {{ $labels.key_id }} 今天多次触发日累计上限"

Grafana dashboard

curl -LO https://futuapi.com/deploy/grafana-dashboard.json
# Grafana UI: Dashboards → Import → 上传 JSON

(文件会跟随网站一起发布)

  • Request rate by ifacesum by (iface) (rate(futu_auth_events_total[5m]))
  • Allow vs Reject — 两条线叠加
  • Top rejected keystopk(10, sum by (key_id) (rate(futu_auth_events_total{outcome="reject"}[1h])))
  • Limit reject breakdown — pie by reason
  • WS filter droppedsum by (required_scope) (rate(futu_ws_filtered_pushes_total[5m]))

协作模式

  • 日常监控 → Grafana dashboard
  • 告警触发 → 查 audit JSONL 具体事件
  • 深挖调查 → 审计 JSONL + DuckDB / jq

三者互补:metrics 做趋势(数字聚合),audit 做溯源(具体事件),日志做调试(为什么错了)。