|
1 | 1 | package main |
2 | 2 |
|
3 | | -import ( |
4 | | - "context" |
5 | | - "log" |
6 | | - "strings" |
7 | | - "sync" |
8 | | -) |
| 3 | +import "strings" |
9 | 4 |
|
10 | | -// UserTraffic holds accumulated uplink/downlink bytes for a user |
| 5 | +// UserTraffic holds uplink/downlink bytes. |
| 6 | +// For users: Tx = client uplink (client -> server), Rx = client downlink (server -> client). |
| 7 | +// For the node: Tx/Rx are the sum of outbound uplink/downlink across all non-API outbounds, |
| 8 | +// which is the real traffic that actually traversed Xray. |
11 | 9 | type UserTraffic struct { |
12 | 10 | Tx int64 `json:"tx"` // uplink bytes |
13 | 11 | Rx int64 `json:"rx"` // downlink bytes |
14 | 12 | } |
15 | 13 |
|
16 | | -// StatsStore accumulates traffic stats between panel polls |
17 | | -type StatsStore struct { |
18 | | - mu sync.Mutex |
19 | | - traffic map[string]*UserTraffic // keyed by email |
| 14 | +// Snapshot is the parsed result of a single Xray QueryStats call with an empty |
| 15 | +// pattern. Users is keyed by email (which equals the panel userId). Node is the |
| 16 | +// aggregated node-level traffic derived from outbound stats. |
| 17 | +type Snapshot struct { |
| 18 | + Users map[string]*UserTraffic |
| 19 | + Node UserTraffic |
20 | 20 | } |
21 | 21 |
|
22 | | -func NewStatsStore() *StatsStore { |
23 | | - return &StatsStore{ |
24 | | - traffic: make(map[string]*UserTraffic), |
25 | | - } |
26 | | -} |
| 22 | +// apiOutboundTag is the tag of the internal Xray API outbound; its counters |
| 23 | +// represent gRPC control-plane traffic and must not be attributed to the node. |
| 24 | +const apiOutboundTag = "API" |
27 | 25 |
|
28 | | -// CollectFromXray fetches stats from Xray (with reset) and adds to local accumulator. |
29 | | -// Xray resets its own counters after each query with reset=true. |
30 | | -func (s *StatsStore) CollectFromXray(ctx context.Context, xc *XrayClient) error { |
31 | | - rawStats, err := xc.QueryStats(ctx, true) |
32 | | - if err != nil { |
33 | | - return err |
| 26 | +// ParseSnapshot converts a flat map of Xray stat names to a structured Snapshot. |
| 27 | +// Expected stat name formats: |
| 28 | +// - user>>>{email}>>>traffic>>>{uplink|downlink} |
| 29 | +// - outbound>>>{tag}>>>traffic>>>{uplink|downlink} |
| 30 | +// - inbound>>>{tag}>>>traffic>>>{uplink|downlink} (ignored here; reserved for future metrics) |
| 31 | +func ParseSnapshot(rawStats map[string]int64) Snapshot { |
| 32 | + snap := Snapshot{ |
| 33 | + Users: make(map[string]*UserTraffic, len(rawStats)/2), |
34 | 34 | } |
35 | 35 |
|
36 | | - s.mu.Lock() |
37 | | - defer s.mu.Unlock() |
38 | | - |
39 | 36 | for name, value := range rawStats { |
40 | | - // Name format: user>>>email>>>traffic>>>uplink or downlink |
41 | 37 | parts := strings.Split(name, ">>>") |
42 | | - if len(parts) != 4 || parts[0] != "user" || parts[2] != "traffic" { |
| 38 | + if len(parts) != 4 || parts[2] != "traffic" { |
43 | 39 | continue |
44 | 40 | } |
45 | | - email := parts[1] |
46 | | - direction := parts[3] |
| 41 | + kind, id, direction := parts[0], parts[1], parts[3] |
47 | 42 |
|
48 | | - if s.traffic[email] == nil { |
49 | | - s.traffic[email] = &UserTraffic{} |
50 | | - } |
51 | | - switch direction { |
52 | | - case "uplink": |
53 | | - s.traffic[email].Tx += value |
54 | | - case "downlink": |
55 | | - s.traffic[email].Rx += value |
| 43 | + switch kind { |
| 44 | + case "user": |
| 45 | + ut := snap.Users[id] |
| 46 | + if ut == nil { |
| 47 | + ut = &UserTraffic{} |
| 48 | + snap.Users[id] = ut |
| 49 | + } |
| 50 | + switch direction { |
| 51 | + case "uplink": |
| 52 | + ut.Tx += value |
| 53 | + case "downlink": |
| 54 | + ut.Rx += value |
| 55 | + } |
| 56 | + case "outbound": |
| 57 | + if id == apiOutboundTag { |
| 58 | + continue |
| 59 | + } |
| 60 | + switch direction { |
| 61 | + case "uplink": |
| 62 | + snap.Node.Tx += value |
| 63 | + case "downlink": |
| 64 | + snap.Node.Rx += value |
| 65 | + } |
56 | 66 | } |
57 | 67 | } |
58 | 68 |
|
59 | | - if len(rawStats) > 0 { |
60 | | - log.Printf("[stats] Collected %d stat entries from Xray", len(rawStats)) |
61 | | - } |
62 | | - return nil |
63 | | -} |
64 | | - |
65 | | -// GetAndReset returns all accumulated stats and resets the local store. |
66 | | -// Called when the panel polls /stats. |
67 | | -func (s *StatsStore) GetAndReset() map[string]*UserTraffic { |
68 | | - s.mu.Lock() |
69 | | - defer s.mu.Unlock() |
70 | | - |
71 | | - result := s.traffic |
72 | | - s.traffic = make(map[string]*UserTraffic) |
73 | | - return result |
| 69 | + return snap |
74 | 70 | } |
0 commit comments