Explorar o código

API: Add user online stats (#3637)

* add statsUserOnline bool to policy

* add OnlineMap struct to stats

* apply UserOnline functionality to dispatcher

* add statsonline api command

* fix comments

* Update app/stats/online_map.go

Co-authored-by: mmmray <[email protected]>

* improve AddIP

* regenerate pb

---------

Co-authored-by: mmmray <[email protected]>
Hossin Asaadi hai 11 meses
pai
achega
2c72864935

+ 12 - 0
app/dispatcher/default.go

@@ -181,6 +181,18 @@ func (d *DefaultDispatcher) getLink(ctx context.Context) (*transport.Link, *tran
 				}
 			}
 		}
+
+		if p.Stats.UserOnline {
+			name := "user>>>" + user.Email + ">>>online"
+			if om, _ := stats.GetOrRegisterOnlineMap(d.stats, name); om != nil {
+				sessionInbounds := session.InboundFromContext(ctx)
+				userIP := sessionInbounds.Source.Address.String()
+				om.AddIP(userIP)
+				// log Online user with ips
+				// errors.LogDebug(ctx, "user>>>" + user.Email + ">>>online", om.Count(), om.List())
+
+			}
+		}
 	}
 
 	return inboundLink, outboundLink

+ 1 - 0
app/policy/config.go

@@ -73,6 +73,7 @@ func (p *Policy) ToCorePolicy() policy.Session {
 	if p.Stats != nil {
 		cp.Stats.UserUplink = p.Stats.UserUplink
 		cp.Stats.UserDownlink = p.Stats.UserDownlink
+		cp.Stats.UserOnline = p.Stats.UserOnline
 	}
 	if p.Buffer != nil {
 		cp.Buffer.PerConnection = p.Buffer.Connection

+ 150 - 42
app/policy/config.pb.go

@@ -1,7 +1,7 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.35.1
-// 	protoc        v5.28.2
+// 	protoc-gen-go v1.34.2
+// 	protoc        v4.25.3
 // source: app/policy/config.proto
 
 package policy
@@ -301,6 +301,7 @@ type Policy_Stats struct {
 
 	UserUplink   bool `protobuf:"varint,1,opt,name=user_uplink,json=userUplink,proto3" json:"user_uplink,omitempty"`
 	UserDownlink bool `protobuf:"varint,2,opt,name=user_downlink,json=userDownlink,proto3" json:"user_downlink,omitempty"`
+	UserOnline   bool `protobuf:"varint,3,opt,name=user_online,json=userOnline,proto3" json:"user_online,omitempty"`
 }
 
 func (x *Policy_Stats) Reset() {
@@ -347,6 +348,13 @@ func (x *Policy_Stats) GetUserDownlink() bool {
 	return false
 }
 
+func (x *Policy_Stats) GetUserOnline() bool {
+	if x != nil {
+		return x.UserOnline
+	}
+	return false
+}
+
 type Policy_Buffer struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -469,7 +477,7 @@ var file_app_policy_config_proto_rawDesc = []byte{
 	0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x78, 0x72, 0x61, 0x79, 0x2e,
 	0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x1e, 0x0a, 0x06, 0x53, 0x65,
 	0x63, 0x6f, 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xa6, 0x04, 0x0a, 0x06, 0x50,
+	0x01, 0x28, 0x0d, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xc7, 0x04, 0x0a, 0x06, 0x50,
 	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x39, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74,
 	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
 	0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e,
@@ -496,49 +504,51 @@ var file_app_policy_config_proto_rawDesc = []byte{
 	0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x04, 0x20,
 	0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70,
 	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x52, 0x0c, 0x64, 0x6f,
-	0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x4f, 0x6e, 0x6c, 0x79, 0x1a, 0x4d, 0x0a, 0x05, 0x53, 0x74,
+	0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x4f, 0x6e, 0x6c, 0x79, 0x1a, 0x6e, 0x0a, 0x05, 0x53, 0x74,
 	0x61, 0x74, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x75, 0x70, 0x6c, 0x69,
 	0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x55, 0x70,
 	0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x6f, 0x77,
 	0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73, 0x65,
-	0x72, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x1a, 0x28, 0x0a, 0x06, 0x42, 0x75, 0x66,
-	0x66, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f,
-	0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74,
-	0x69, 0x6f, 0x6e, 0x22, 0xfb, 0x01, 0x0a, 0x0c, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x6f,
-	0x6c, 0x69, 0x63, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20,
-	0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70,
-	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x6f, 0x6c, 0x69,
-	0x63, 0x79, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x1a,
-	0xaf, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, 0x62,
-	0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28,
-	0x08, 0x52, 0x0d, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x70, 0x6c, 0x69, 0x6e, 0x6b,
-	0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x64, 0x6f, 0x77, 0x6e,
-	0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x62, 0x6f,
-	0x75, 0x6e, 0x64, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x27, 0x0a, 0x0f, 0x6f,
-	0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x03,
-	0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x70,
-	0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64,
-	0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52,
-	0x10, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e,
-	0x6b, 0x22, 0xcc, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x38, 0x0a, 0x05,
-	0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x78, 0x72,
-	0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43, 0x6f,
-	0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
-	0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x35, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
-	0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50,
-	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x1a, 0x51, 0x0a,
-	0x0a, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
-	0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a,
-	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x78,
-	0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x50,
-	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
-	0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
-	0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x50, 0x01, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75,
-	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d,
-	0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0xaa,
-	0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63,
-	0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x72, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65,
+	0x72, 0x5f, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a,
+	0x75, 0x73, 0x65, 0x72, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x1a, 0x28, 0x0a, 0x06, 0x42, 0x75,
+	0x66, 0x66, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69,
+	0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
+	0x74, 0x69, 0x6f, 0x6e, 0x22, 0xfb, 0x01, 0x0a, 0x0c, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50,
+	0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e,
+	0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x50, 0x6f, 0x6c,
+	0x69, 0x63, 0x79, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73,
+	0x1a, 0xaf, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e,
+	0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x0d, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x55, 0x70, 0x6c, 0x69, 0x6e,
+	0x6b, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x64, 0x6f, 0x77,
+	0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x6e, 0x62,
+	0x6f, 0x75, 0x6e, 0x64, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x27, 0x0a, 0x0f,
+	0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x75, 0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x55,
+	0x70, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e,
+	0x64, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08,
+	0x52, 0x10, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x69,
+	0x6e, 0x6b, 0x22, 0xcc, 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x38, 0x0a,
+	0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x78,
+	0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x43,
+	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x79,
+	0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x35, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65,
+	0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
+	0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d,
+	0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x1a, 0x51,
+	0x0a, 0x0a, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03,
+	0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d,
+	0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e,
+	0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e,
+	0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
+	0x01, 0x42, 0x4f, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70,
+	0x70, 0x2e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x50, 0x01, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68,
+	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79,
+	0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79,
+	0xaa, 0x02, 0x0f, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50, 0x6f, 0x6c, 0x69,
+	0x63, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -589,6 +599,104 @@ func file_app_policy_config_proto_init() {
 	if File_app_policy_config_proto != nil {
 		return
 	}
+	if !protoimpl.UnsafeEnabled {
+		file_app_policy_config_proto_msgTypes[0].Exporter = func(v any, i int) any {
+			switch v := v.(*Second); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_app_policy_config_proto_msgTypes[1].Exporter = func(v any, i int) any {
+			switch v := v.(*Policy); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_app_policy_config_proto_msgTypes[2].Exporter = func(v any, i int) any {
+			switch v := v.(*SystemPolicy); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_app_policy_config_proto_msgTypes[3].Exporter = func(v any, i int) any {
+			switch v := v.(*Config); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_app_policy_config_proto_msgTypes[4].Exporter = func(v any, i int) any {
+			switch v := v.(*Policy_Timeout); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_app_policy_config_proto_msgTypes[5].Exporter = func(v any, i int) any {
+			switch v := v.(*Policy_Stats); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_app_policy_config_proto_msgTypes[6].Exporter = func(v any, i int) any {
+			switch v := v.(*Policy_Buffer); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_app_policy_config_proto_msgTypes[7].Exporter = func(v any, i int) any {
+			switch v := v.(*SystemPolicy_Stats); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{

+ 1 - 0
app/policy/config.proto

@@ -22,6 +22,7 @@ message Policy {
   message Stats {
     bool user_uplink = 1;
     bool user_downlink = 2;
+    bool user_online = 3;
   }
 
   message Buffer {

+ 14 - 0
app/stats/command/command.go

@@ -46,6 +46,20 @@ func (s *statsServer) GetStats(ctx context.Context, request *GetStatsRequest) (*
 	}, nil
 }
 
+func (s *statsServer) GetStatsOnline(ctx context.Context, request *GetStatsRequest) (*GetStatsResponse, error) {
+	c := s.stats.GetOnlineMap(request.Name)
+	if c == nil {
+		return nil, errors.New(request.Name, " not found.")
+	}
+	value := int64(c.Count())
+	return &GetStatsResponse{
+		Stat: &Stat{
+			Name:  request.Name,
+			Value: value,
+		},
+	}, nil
+}
+
 func (s *statsServer) QueryStats(ctx context.Context, request *QueryStatsRequest) (*QueryStatsResponse, error) {
 	matcher, err := strmatcher.Substr.New(request.Pattern)
 	if err != nil {

+ 130 - 123
app/stats/command/command.pb.go

@@ -1,8 +1,8 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
 // 	protoc-gen-go v1.35.1
-// 	protoc        v5.28.2
-// source: app/stats/command/command.proto
+// 	protoc        v5.28.3
+// source: command.proto
 
 package command
 
@@ -33,7 +33,7 @@ type GetStatsRequest struct {
 
 func (x *GetStatsRequest) Reset() {
 	*x = GetStatsRequest{}
-	mi := &file_app_stats_command_command_proto_msgTypes[0]
+	mi := &file_command_proto_msgTypes[0]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -45,7 +45,7 @@ func (x *GetStatsRequest) String() string {
 func (*GetStatsRequest) ProtoMessage() {}
 
 func (x *GetStatsRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_app_stats_command_command_proto_msgTypes[0]
+	mi := &file_command_proto_msgTypes[0]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -58,7 +58,7 @@ func (x *GetStatsRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use GetStatsRequest.ProtoReflect.Descriptor instead.
 func (*GetStatsRequest) Descriptor() ([]byte, []int) {
-	return file_app_stats_command_command_proto_rawDescGZIP(), []int{0}
+	return file_command_proto_rawDescGZIP(), []int{0}
 }
 
 func (x *GetStatsRequest) GetName() string {
@@ -86,7 +86,7 @@ type Stat struct {
 
 func (x *Stat) Reset() {
 	*x = Stat{}
-	mi := &file_app_stats_command_command_proto_msgTypes[1]
+	mi := &file_command_proto_msgTypes[1]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -98,7 +98,7 @@ func (x *Stat) String() string {
 func (*Stat) ProtoMessage() {}
 
 func (x *Stat) ProtoReflect() protoreflect.Message {
-	mi := &file_app_stats_command_command_proto_msgTypes[1]
+	mi := &file_command_proto_msgTypes[1]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -111,7 +111,7 @@ func (x *Stat) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Stat.ProtoReflect.Descriptor instead.
 func (*Stat) Descriptor() ([]byte, []int) {
-	return file_app_stats_command_command_proto_rawDescGZIP(), []int{1}
+	return file_command_proto_rawDescGZIP(), []int{1}
 }
 
 func (x *Stat) GetName() string {
@@ -138,7 +138,7 @@ type GetStatsResponse struct {
 
 func (x *GetStatsResponse) Reset() {
 	*x = GetStatsResponse{}
-	mi := &file_app_stats_command_command_proto_msgTypes[2]
+	mi := &file_command_proto_msgTypes[2]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -150,7 +150,7 @@ func (x *GetStatsResponse) String() string {
 func (*GetStatsResponse) ProtoMessage() {}
 
 func (x *GetStatsResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_app_stats_command_command_proto_msgTypes[2]
+	mi := &file_command_proto_msgTypes[2]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -163,7 +163,7 @@ func (x *GetStatsResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use GetStatsResponse.ProtoReflect.Descriptor instead.
 func (*GetStatsResponse) Descriptor() ([]byte, []int) {
-	return file_app_stats_command_command_proto_rawDescGZIP(), []int{2}
+	return file_command_proto_rawDescGZIP(), []int{2}
 }
 
 func (x *GetStatsResponse) GetStat() *Stat {
@@ -184,7 +184,7 @@ type QueryStatsRequest struct {
 
 func (x *QueryStatsRequest) Reset() {
 	*x = QueryStatsRequest{}
-	mi := &file_app_stats_command_command_proto_msgTypes[3]
+	mi := &file_command_proto_msgTypes[3]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -196,7 +196,7 @@ func (x *QueryStatsRequest) String() string {
 func (*QueryStatsRequest) ProtoMessage() {}
 
 func (x *QueryStatsRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_app_stats_command_command_proto_msgTypes[3]
+	mi := &file_command_proto_msgTypes[3]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -209,7 +209,7 @@ func (x *QueryStatsRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use QueryStatsRequest.ProtoReflect.Descriptor instead.
 func (*QueryStatsRequest) Descriptor() ([]byte, []int) {
-	return file_app_stats_command_command_proto_rawDescGZIP(), []int{3}
+	return file_command_proto_rawDescGZIP(), []int{3}
 }
 
 func (x *QueryStatsRequest) GetPattern() string {
@@ -236,7 +236,7 @@ type QueryStatsResponse struct {
 
 func (x *QueryStatsResponse) Reset() {
 	*x = QueryStatsResponse{}
-	mi := &file_app_stats_command_command_proto_msgTypes[4]
+	mi := &file_command_proto_msgTypes[4]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -248,7 +248,7 @@ func (x *QueryStatsResponse) String() string {
 func (*QueryStatsResponse) ProtoMessage() {}
 
 func (x *QueryStatsResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_app_stats_command_command_proto_msgTypes[4]
+	mi := &file_command_proto_msgTypes[4]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -261,7 +261,7 @@ func (x *QueryStatsResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use QueryStatsResponse.ProtoReflect.Descriptor instead.
 func (*QueryStatsResponse) Descriptor() ([]byte, []int) {
-	return file_app_stats_command_command_proto_rawDescGZIP(), []int{4}
+	return file_command_proto_rawDescGZIP(), []int{4}
 }
 
 func (x *QueryStatsResponse) GetStat() []*Stat {
@@ -279,7 +279,7 @@ type SysStatsRequest struct {
 
 func (x *SysStatsRequest) Reset() {
 	*x = SysStatsRequest{}
-	mi := &file_app_stats_command_command_proto_msgTypes[5]
+	mi := &file_command_proto_msgTypes[5]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -291,7 +291,7 @@ func (x *SysStatsRequest) String() string {
 func (*SysStatsRequest) ProtoMessage() {}
 
 func (x *SysStatsRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_app_stats_command_command_proto_msgTypes[5]
+	mi := &file_command_proto_msgTypes[5]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -304,7 +304,7 @@ func (x *SysStatsRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use SysStatsRequest.ProtoReflect.Descriptor instead.
 func (*SysStatsRequest) Descriptor() ([]byte, []int) {
-	return file_app_stats_command_command_proto_rawDescGZIP(), []int{5}
+	return file_command_proto_rawDescGZIP(), []int{5}
 }
 
 type SysStatsResponse struct {
@@ -326,7 +326,7 @@ type SysStatsResponse struct {
 
 func (x *SysStatsResponse) Reset() {
 	*x = SysStatsResponse{}
-	mi := &file_app_stats_command_command_proto_msgTypes[6]
+	mi := &file_command_proto_msgTypes[6]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -338,7 +338,7 @@ func (x *SysStatsResponse) String() string {
 func (*SysStatsResponse) ProtoMessage() {}
 
 func (x *SysStatsResponse) ProtoReflect() protoreflect.Message {
-	mi := &file_app_stats_command_command_proto_msgTypes[6]
+	mi := &file_command_proto_msgTypes[6]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -351,7 +351,7 @@ func (x *SysStatsResponse) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use SysStatsResponse.ProtoReflect.Descriptor instead.
 func (*SysStatsResponse) Descriptor() ([]byte, []int) {
-	return file_app_stats_command_command_proto_rawDescGZIP(), []int{6}
+	return file_command_proto_rawDescGZIP(), []int{6}
 }
 
 func (x *SysStatsResponse) GetNumGoroutine() uint32 {
@@ -432,7 +432,7 @@ type Config struct {
 
 func (x *Config) Reset() {
 	*x = Config{}
-	mi := &file_app_stats_command_command_proto_msgTypes[7]
+	mi := &file_command_proto_msgTypes[7]
 	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 	ms.StoreMessageInfo(mi)
 }
@@ -444,7 +444,7 @@ func (x *Config) String() string {
 func (*Config) ProtoMessage() {}
 
 func (x *Config) ProtoReflect() protoreflect.Message {
-	mi := &file_app_stats_command_command_proto_msgTypes[7]
+	mi := &file_command_proto_msgTypes[7]
 	if x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -457,99 +457,104 @@ func (x *Config) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use Config.ProtoReflect.Descriptor instead.
 func (*Config) Descriptor() ([]byte, []int) {
-	return file_app_stats_command_command_proto_rawDescGZIP(), []int{7}
+	return file_command_proto_rawDescGZIP(), []int{7}
 }
 
-var File_app_stats_command_command_proto protoreflect.FileDescriptor
+var File_command_proto protoreflect.FileDescriptor
 
-var file_app_stats_command_command_proto_rawDesc = []byte{
-	0x0a, 0x1f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x6d,
-	0x61, 0x6e, 0x64, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74,
-	0x6f, 0x12, 0x16, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74,
-	0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74,
-	0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04,
+var file_command_proto_rawDesc = []byte{
+	0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
+	0x16, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e,
+	0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x3b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x53, 0x74,
+	0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14,
+	0x0a, 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72,
+	0x65, 0x73, 0x65, 0x74, 0x22, 0x30, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x12, 0x0a, 0x04,
 	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
-	0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
-	0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x22, 0x30, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x12,
-	0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
-	0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
-	0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x44, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53,
-	0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x04,
-	0x73, 0x74, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61,
-	0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
-	0x61, 0x6e, 0x64, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x43,
-	0x0a, 0x11, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x01,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x14, 0x0a,
-	0x05, 0x72, 0x65, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65,
-	0x73, 0x65, 0x74, 0x22, 0x46, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74,
-	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x73, 0x74, 0x61,
-	0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61,
-	0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
-	0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x53,
-	0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa2,
-	0x02, 0x0a, 0x10, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
-	0x6e, 0x73, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74,
-	0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f,
-	0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43,
-	0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43, 0x12, 0x14, 0x0a,
-	0x05, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x41, 0x6c,
-	0x6c, 0x6f, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f,
-	0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c,
-	0x6c, 0x6f, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x53, 0x79, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04,
-	0x52, 0x03, 0x53, 0x79, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73,
-	0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73, 0x12,
-	0x14, 0x0a, 0x05, 0x46, 0x72, 0x65, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05,
-	0x46, 0x72, 0x65, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x4f, 0x62, 0x6a,
-	0x65, 0x63, 0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x4c, 0x69, 0x76, 0x65,
-	0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65,
-	0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x50,
-	0x61, 0x75, 0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55,
-	0x70, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, 0x70, 0x74,
-	0x69, 0x6d, 0x65, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0xba, 0x02,
-	0x0a, 0x0c, 0x53, 0x74, 0x61, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f,
-	0x0a, 0x08, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61,
-	0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
-	0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73,
-	0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74,
-	0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12,
-	0x65, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x29, 0x2e,
-	0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63,
-	0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74,
-	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
+	0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52,
+	0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x44, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61,
+	0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x73, 0x74,
+	0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
 	0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
-	0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
-	0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x79, 0x73,
-	0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
+	0x64, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x43, 0x0a, 0x11,
+	0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x07, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x72,
+	0x65, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x72, 0x65, 0x73, 0x65,
+	0x74, 0x22, 0x46, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52,
+	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x73, 0x74, 0x61, 0x74, 0x18,
+	0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
 	0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53,
-	0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28,
-	0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e,
-	0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73,
-	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x64, 0x0a, 0x1a, 0x63, 0x6f,
-	0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73,
-	0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68,
-	0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79,
-	0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2f,
-	0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x16, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41,
-	0x70, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
-	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x74, 0x61, 0x74, 0x52, 0x04, 0x73, 0x74, 0x61, 0x74, 0x22, 0x11, 0x0a, 0x0f, 0x53, 0x79, 0x73,
+	0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa2, 0x02, 0x0a,
+	0x10, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e,
+	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0c, 0x4e, 0x75, 0x6d, 0x47, 0x6f, 0x72, 0x6f,
+	0x75, 0x74, 0x69, 0x6e, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43, 0x18, 0x02,
+	0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x4e, 0x75, 0x6d, 0x47, 0x43, 0x12, 0x14, 0x0a, 0x05, 0x41,
+	0x6c, 0x6c, 0x6f, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x41, 0x6c, 0x6c, 0x6f,
+	0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x63, 0x18,
+	0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f,
+	0x63, 0x12, 0x10, 0x0a, 0x03, 0x53, 0x79, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03,
+	0x53, 0x79, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73, 0x18, 0x06,
+	0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x4d, 0x61, 0x6c, 0x6c, 0x6f, 0x63, 0x73, 0x12, 0x14, 0x0a,
+	0x05, 0x46, 0x72, 0x65, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x46, 0x72,
+	0x65, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63,
+	0x74, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x4c, 0x69, 0x76, 0x65, 0x4f, 0x62,
+	0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x50, 0x61, 0x75, 0x73, 0x65, 0x54, 0x6f,
+	0x74, 0x61, 0x6c, 0x4e, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x50, 0x61, 0x75,
+	0x73, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x4e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x70, 0x74,
+	0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x55, 0x70, 0x74, 0x69, 0x6d,
+	0x65, 0x22, 0x08, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x32, 0xa1, 0x03, 0x0a, 0x0c,
+	0x53, 0x74, 0x61, 0x74, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x08,
+	0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
+	0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
+	0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+	0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61,
+	0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74,
+	0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a,
+	0x0e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x12,
+	0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73,
+	0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74,
+	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e,
+	0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
+	0x64, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+	0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x0a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61,
+	0x74, 0x73, 0x12, 0x29, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74,
+	0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72,
+	0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e,
+	0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63,
+	0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x53, 0x74, 0x61, 0x74,
+	0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x62, 0x0a, 0x0b, 0x47,
+	0x65, 0x74, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x78, 0x72, 0x61,
+	0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
+	0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x73,
+	0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x53, 0x79, 0x73,
+	0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42,
+	0x64, 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e,
+	0x73, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x50, 0x01, 0x5a,
+	0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73,
+	0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x73,
+	0x74, 0x61, 0x74, 0x73, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0xaa, 0x02, 0x16, 0x58,
+	0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x43, 0x6f,
+	0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
-	file_app_stats_command_command_proto_rawDescOnce sync.Once
-	file_app_stats_command_command_proto_rawDescData = file_app_stats_command_command_proto_rawDesc
+	file_command_proto_rawDescOnce sync.Once
+	file_command_proto_rawDescData = file_command_proto_rawDesc
 )
 
-func file_app_stats_command_command_proto_rawDescGZIP() []byte {
-	file_app_stats_command_command_proto_rawDescOnce.Do(func() {
-		file_app_stats_command_command_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_stats_command_command_proto_rawDescData)
+func file_command_proto_rawDescGZIP() []byte {
+	file_command_proto_rawDescOnce.Do(func() {
+		file_command_proto_rawDescData = protoimpl.X.CompressGZIP(file_command_proto_rawDescData)
 	})
-	return file_app_stats_command_command_proto_rawDescData
+	return file_command_proto_rawDescData
 }
 
-var file_app_stats_command_command_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
-var file_app_stats_command_command_proto_goTypes = []any{
+var file_command_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
+var file_command_proto_goTypes = []any{
 	(*GetStatsRequest)(nil),    // 0: xray.app.stats.command.GetStatsRequest
 	(*Stat)(nil),               // 1: xray.app.stats.command.Stat
 	(*GetStatsResponse)(nil),   // 2: xray.app.stats.command.GetStatsResponse
@@ -559,43 +564,45 @@ var file_app_stats_command_command_proto_goTypes = []any{
 	(*SysStatsResponse)(nil),   // 6: xray.app.stats.command.SysStatsResponse
 	(*Config)(nil),             // 7: xray.app.stats.command.Config
 }
-var file_app_stats_command_command_proto_depIdxs = []int32{
+var file_command_proto_depIdxs = []int32{
 	1, // 0: xray.app.stats.command.GetStatsResponse.stat:type_name -> xray.app.stats.command.Stat
 	1, // 1: xray.app.stats.command.QueryStatsResponse.stat:type_name -> xray.app.stats.command.Stat
 	0, // 2: xray.app.stats.command.StatsService.GetStats:input_type -> xray.app.stats.command.GetStatsRequest
-	3, // 3: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest
-	5, // 4: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest
-	2, // 5: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse
-	4, // 6: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse
-	6, // 7: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse
-	5, // [5:8] is the sub-list for method output_type
-	2, // [2:5] is the sub-list for method input_type
+	0, // 3: xray.app.stats.command.StatsService.GetStatsOnline:input_type -> xray.app.stats.command.GetStatsRequest
+	3, // 4: xray.app.stats.command.StatsService.QueryStats:input_type -> xray.app.stats.command.QueryStatsRequest
+	5, // 5: xray.app.stats.command.StatsService.GetSysStats:input_type -> xray.app.stats.command.SysStatsRequest
+	2, // 6: xray.app.stats.command.StatsService.GetStats:output_type -> xray.app.stats.command.GetStatsResponse
+	2, // 7: xray.app.stats.command.StatsService.GetStatsOnline:output_type -> xray.app.stats.command.GetStatsResponse
+	4, // 8: xray.app.stats.command.StatsService.QueryStats:output_type -> xray.app.stats.command.QueryStatsResponse
+	6, // 9: xray.app.stats.command.StatsService.GetSysStats:output_type -> xray.app.stats.command.SysStatsResponse
+	6, // [6:10] is the sub-list for method output_type
+	2, // [2:6] is the sub-list for method input_type
 	2, // [2:2] is the sub-list for extension type_name
 	2, // [2:2] is the sub-list for extension extendee
 	0, // [0:2] is the sub-list for field type_name
 }
 
-func init() { file_app_stats_command_command_proto_init() }
-func file_app_stats_command_command_proto_init() {
-	if File_app_stats_command_command_proto != nil {
+func init() { file_command_proto_init() }
+func file_command_proto_init() {
+	if File_command_proto != nil {
 		return
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
 		File: protoimpl.DescBuilder{
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: file_app_stats_command_command_proto_rawDesc,
+			RawDescriptor: file_command_proto_rawDesc,
 			NumEnums:      0,
 			NumMessages:   8,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
-		GoTypes:           file_app_stats_command_command_proto_goTypes,
-		DependencyIndexes: file_app_stats_command_command_proto_depIdxs,
-		MessageInfos:      file_app_stats_command_command_proto_msgTypes,
+		GoTypes:           file_command_proto_goTypes,
+		DependencyIndexes: file_command_proto_depIdxs,
+		MessageInfos:      file_command_proto_msgTypes,
 	}.Build()
-	File_app_stats_command_command_proto = out.File
-	file_app_stats_command_command_proto_rawDesc = nil
-	file_app_stats_command_command_proto_goTypes = nil
-	file_app_stats_command_command_proto_depIdxs = nil
+	File_command_proto = out.File
+	file_command_proto_rawDesc = nil
+	file_command_proto_goTypes = nil
+	file_command_proto_depIdxs = nil
 }

+ 1 - 0
app/stats/command/command.proto

@@ -48,6 +48,7 @@ message SysStatsResponse {
 
 service StatsService {
   rpc GetStats(GetStatsRequest) returns (GetStatsResponse) {}
+  rpc GetStatsOnline(GetStatsRequest) returns (GetStatsResponse) {}
   rpc QueryStats(QueryStatsRequest) returns (QueryStatsResponse) {}
   rpc GetSysStats(SysStatsRequest) returns (SysStatsResponse) {}
 }

+ 44 - 6
app/stats/command/command_grpc.pb.go

@@ -1,8 +1,8 @@
 // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
 // versions:
 // - protoc-gen-go-grpc v1.5.1
-// - protoc             v5.28.2
-// source: app/stats/command/command.proto
+// - protoc             v5.28.3
+// source: command.proto
 
 package command
 
@@ -19,9 +19,10 @@ import (
 const _ = grpc.SupportPackageIsVersion9
 
 const (
-	StatsService_GetStats_FullMethodName    = "/xray.app.stats.command.StatsService/GetStats"
-	StatsService_QueryStats_FullMethodName  = "/xray.app.stats.command.StatsService/QueryStats"
-	StatsService_GetSysStats_FullMethodName = "/xray.app.stats.command.StatsService/GetSysStats"
+	StatsService_GetStats_FullMethodName       = "/xray.app.stats.command.StatsService/GetStats"
+	StatsService_GetStatsOnline_FullMethodName = "/xray.app.stats.command.StatsService/GetStatsOnline"
+	StatsService_QueryStats_FullMethodName     = "/xray.app.stats.command.StatsService/QueryStats"
+	StatsService_GetSysStats_FullMethodName    = "/xray.app.stats.command.StatsService/GetSysStats"
 )
 
 // StatsServiceClient is the client API for StatsService service.
@@ -29,6 +30,7 @@ const (
 // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
 type StatsServiceClient interface {
 	GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error)
+	GetStatsOnline(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error)
 	QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error)
 	GetSysStats(ctx context.Context, in *SysStatsRequest, opts ...grpc.CallOption) (*SysStatsResponse, error)
 }
@@ -51,6 +53,16 @@ func (c *statsServiceClient) GetStats(ctx context.Context, in *GetStatsRequest,
 	return out, nil
 }
 
+func (c *statsServiceClient) GetStatsOnline(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(GetStatsResponse)
+	err := c.cc.Invoke(ctx, StatsService_GetStatsOnline_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *statsServiceClient) QueryStats(ctx context.Context, in *QueryStatsRequest, opts ...grpc.CallOption) (*QueryStatsResponse, error) {
 	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
 	out := new(QueryStatsResponse)
@@ -76,6 +88,7 @@ func (c *statsServiceClient) GetSysStats(ctx context.Context, in *SysStatsReques
 // for forward compatibility.
 type StatsServiceServer interface {
 	GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error)
+	GetStatsOnline(context.Context, *GetStatsRequest) (*GetStatsResponse, error)
 	QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error)
 	GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error)
 	mustEmbedUnimplementedStatsServiceServer()
@@ -91,6 +104,9 @@ type UnimplementedStatsServiceServer struct{}
 func (UnimplementedStatsServiceServer) GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
 }
+func (UnimplementedStatsServiceServer) GetStatsOnline(context.Context, *GetStatsRequest) (*GetStatsResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetStatsOnline not implemented")
+}
 func (UnimplementedStatsServiceServer) QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method QueryStats not implemented")
 }
@@ -136,6 +152,24 @@ func _StatsService_GetStats_Handler(srv interface{}, ctx context.Context, dec fu
 	return interceptor(ctx, in, info, handler)
 }
 
+func _StatsService_GetStatsOnline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetStatsRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(StatsServiceServer).GetStatsOnline(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: StatsService_GetStatsOnline_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(StatsServiceServer).GetStatsOnline(ctx, req.(*GetStatsRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _StatsService_QueryStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(QueryStatsRequest)
 	if err := dec(in); err != nil {
@@ -183,6 +217,10 @@ var StatsService_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "GetStats",
 			Handler:    _StatsService_GetStats_Handler,
 		},
+		{
+			MethodName: "GetStatsOnline",
+			Handler:    _StatsService_GetStatsOnline_Handler,
+		},
 		{
 			MethodName: "QueryStats",
 			Handler:    _StatsService_QueryStats_Handler,
@@ -193,5 +231,5 @@ var StatsService_ServiceDesc = grpc.ServiceDesc{
 		},
 	},
 	Streams:  []grpc.StreamDesc{},
-	Metadata: "app/stats/command/command.proto",
+	Metadata: "command.proto",
 }

+ 80 - 0
app/stats/online_map.go

@@ -0,0 +1,80 @@
+package stats
+
+import (
+	"sync"
+	"time"
+)
+
+// OnlineMap is an implementation of stats.OnlineMap.
+type OnlineMap struct {
+	value         int
+	ipList        map[string]time.Time
+	access        sync.RWMutex
+	lastCleanup   time.Time
+	cleanupPeriod time.Duration
+}
+
+// NewOnlineMap creates a new instance of OnlineMap.
+func NewOnlineMap() *OnlineMap {
+	return &OnlineMap{
+		ipList:        make(map[string]time.Time),
+		lastCleanup:   time.Now(),
+		cleanupPeriod: 10 * time.Second,
+	}
+}
+
+// Count implements stats.OnlineMap.
+func (c *OnlineMap) Count() int {
+	return c.value
+}
+
+// List implements stats.OnlineMap.
+func (c *OnlineMap) List() []string {
+	return c.GetKeys()
+}
+
+// AddIP implements stats.OnlineMap.
+func (c *OnlineMap) AddIP(ip string) {
+	list := c.ipList
+
+	if ip == "127.0.0.1" {
+		return
+	}
+	if _, ok := list[ip]; !ok {
+		c.access.Lock()
+		list[ip] = time.Now()
+		c.access.Unlock()
+	}
+	if time.Since(c.lastCleanup) > c.cleanupPeriod {
+		list = c.RemoveExpiredIPs(list)
+		c.lastCleanup = time.Now()
+	}
+
+	c.value = len(list)
+	c.ipList = list
+}
+
+func (c *OnlineMap) GetKeys() []string {
+	c.access.RLock()
+	defer c.access.RUnlock()
+
+	keys := []string{}
+	for k := range c.ipList {
+		keys = append(keys, k)
+	}
+	return keys
+}
+
+func (c *OnlineMap) RemoveExpiredIPs(list map[string]time.Time) map[string]time.Time {
+	c.access.Lock()
+	defer c.access.Unlock()
+
+	now := time.Now()
+	for k, t := range list {
+		diff := now.Sub(t)
+		if diff.Seconds() > 20 {
+			delete(list, k)
+		}
+	}
+	return list
+}

+ 45 - 6
app/stats/stats.go

@@ -11,17 +11,19 @@ import (
 
 // Manager is an implementation of stats.Manager.
 type Manager struct {
-	access   sync.RWMutex
-	counters map[string]*Counter
-	channels map[string]*Channel
-	running  bool
+	access    sync.RWMutex
+	counters  map[string]*Counter
+	onlineMap map[string]*OnlineMap
+	channels  map[string]*Channel
+	running   bool
 }
 
 // NewManager creates an instance of Statistics Manager.
 func NewManager(ctx context.Context, config *Config) (*Manager, error) {
 	m := &Manager{
-		counters: make(map[string]*Counter),
-		channels: make(map[string]*Channel),
+		counters:  make(map[string]*Counter),
+		onlineMap: make(map[string]*OnlineMap),
+		channels:  make(map[string]*Channel),
 	}
 
 	return m, nil
@@ -81,6 +83,43 @@ func (m *Manager) VisitCounters(visitor func(string, stats.Counter) bool) {
 	}
 }
 
+// RegisterOnlineMap implements stats.Manager.
+func (m *Manager) RegisterOnlineMap(name string) (stats.OnlineMap, error) {
+	m.access.Lock()
+	defer m.access.Unlock()
+
+	if _, found := m.onlineMap[name]; found {
+		return nil, errors.New("onlineMap ", name, " already registered.")
+	}
+	errors.LogDebug(context.Background(), "create new onlineMap ", name)
+	om := NewOnlineMap()
+	m.onlineMap[name] = om
+	return om, nil
+}
+
+// UnregisterOnlineMap implements stats.Manager.
+func (m *Manager) UnregisterOnlineMap(name string) error {
+	m.access.Lock()
+	defer m.access.Unlock()
+
+	if _, found := m.onlineMap[name]; found {
+		errors.LogDebug(context.Background(), "remove onlineMap ", name)
+		delete(m.onlineMap, name)
+	}
+	return nil
+}
+
+// GetOnlineMap implements stats.Manager.
+func (m *Manager) GetOnlineMap(name string) stats.OnlineMap {
+	m.access.RLock()
+	defer m.access.RUnlock()
+
+	if om, found := m.onlineMap[name]; found {
+		return om
+	}
+	return nil
+}
+
 // RegisterChannel implements stats.Manager.
 func (m *Manager) RegisterChannel(name string) (stats.Channel, error) {
 	m.access.Lock()

+ 3 - 0
features/policy/policy.go

@@ -27,6 +27,8 @@ type Stats struct {
 	UserUplink bool
 	// Whether or not to enable stat counter for user downlink traffic.
 	UserDownlink bool
+	// Whether or not to enable online map for user.
+	UserOnline bool
 }
 
 // Buffer contains settings for internal buffer.
@@ -123,6 +125,7 @@ func SessionDefault() Session {
 		Stats: Stats{
 			UserUplink:   false,
 			UserDownlink: false,
+			UserOnline:   false,
 		},
 		Buffer: defaultBufferPolicy(),
 	}

+ 44 - 0
features/stats/stats.go

@@ -20,6 +20,18 @@ type Counter interface {
 	Add(int64) int64
 }
 
+// OnlineMap is the interface for stats.
+//
+// xray:api:stable
+type OnlineMap interface {
+	// Count is the current value of the OnlineMap.
+	Count() int
+	// AddIP adds a ip to the current OnlineMap.
+	AddIP(string)
+	// List is the current OnlineMap ip list.
+	List() []string
+}
+
 // Channel is the interface for stats channel.
 //
 // xray:api:stable
@@ -70,6 +82,13 @@ type Manager interface {
 	// GetCounter returns a counter by its identifier.
 	GetCounter(string) Counter
 
+	// RegisterOnlineMap registers a new onlinemap to the manager. The identifier string must not be empty, and unique among other onlinemaps.
+	RegisterOnlineMap(string) (OnlineMap, error)
+	// UnregisterOnlineMap unregisters a onlinemap from the manager by its identifier.
+	UnregisterOnlineMap(string) error
+	// GetOnlineMap returns a onlinemap by its identifier.
+	GetOnlineMap(string) OnlineMap
+
 	// RegisterChannel registers a new channel to the manager. The identifier string must not be empty, and unique among other channels.
 	RegisterChannel(string) (Channel, error)
 	// UnregisterChannel unregisters a channel from the manager by its identifier.
@@ -88,6 +107,16 @@ func GetOrRegisterCounter(m Manager, name string) (Counter, error) {
 	return m.RegisterCounter(name)
 }
 
+// GetOrRegisterOnlineMap tries to get the OnlineMap first. If not exist, it then tries to create a new onlinemap.
+func GetOrRegisterOnlineMap(m Manager, name string) (OnlineMap, error) {
+	onlineMap := m.GetOnlineMap(name)
+	if onlineMap != nil {
+		return onlineMap, nil
+	}
+
+	return m.RegisterOnlineMap(name)
+}
+
 // GetOrRegisterChannel tries to get the StatChannel first. If not exist, it then tries to create a new channel.
 func GetOrRegisterChannel(m Manager, name string) (Channel, error) {
 	channel := m.GetChannel(name)
@@ -128,6 +157,21 @@ func (NoopManager) GetCounter(string) Counter {
 	return nil
 }
 
+// RegisterOnlineMap implements Manager.
+func (NoopManager) RegisterOnlineMap(string) (OnlineMap, error) {
+	return nil, errors.New("not implemented")
+}
+
+// UnregisterOnlineMap implements Manager.
+func (NoopManager) UnregisterOnlineMap(string) error {
+	return nil
+}
+
+// GetOnlineMap implements Manager.
+func (NoopManager) GetOnlineMap(string) OnlineMap {
+	return nil
+}
+
 // RegisterChannel implements Manager.
 func (NoopManager) RegisterChannel(string) (Channel, error) {
 	return nil, errors.New("not implemented")

+ 2 - 0
infra/conf/policy.go

@@ -11,6 +11,7 @@ type Policy struct {
 	DownlinkOnly      *uint32 `json:"downlinkOnly"`
 	StatsUserUplink   bool    `json:"statsUserUplink"`
 	StatsUserDownlink bool    `json:"statsUserDownlink"`
+	StatsUserOnline   bool    `json:"statsUserOnline"`
 	BufferSize        *int32  `json:"bufferSize"`
 }
 
@@ -34,6 +35,7 @@ func (t *Policy) Build() (*policy.Policy, error) {
 		Stats: &policy.Policy_Stats{
 			UserUplink:   t.StatsUserUplink,
 			UserDownlink: t.StatsUserDownlink,
+			UserOnline:   t.StatsUserOnline,
 		},
 	}
 

+ 1 - 0
main/commands/all/api/api.go

@@ -26,5 +26,6 @@ var CmdAPI = &base.Command{
 		cmdAddRules,
 		cmdRemoveRules,
 		cmdSourceIpBlock,
+		cmdOnlineStats,
 	},
 }

+ 47 - 0
main/commands/all/api/stats_online.go

@@ -0,0 +1,47 @@
+package api
+
+import (
+	statsService "github.com/xtls/xray-core/app/stats/command"
+	"github.com/xtls/xray-core/main/commands/base"
+)
+
+var cmdOnlineStats = &base.Command{
+	CustomFlags: true,
+	UsageLine:   "{{.Exec}} api statsonline [--server=127.0.0.1:8080] [-name '']",
+	Short:       "Get online user",
+	Long: `
+Get statistics from Xray.
+Arguments:
+	-s, -server 
+		The API server address. Default 127.0.0.1:8080
+	-t, -timeout
+		Timeout seconds to call API. Default 3
+	-email
+		email of the user.
+	-reset
+		Reset the counter to fetching its value.
+Example:
+	{{.Exec}} {{.LongName}} --server=127.0.0.1:8080 -email "[email protected]"
+`,
+	Run: executeOnlineStats,
+}
+
+func executeOnlineStats(cmd *base.Command, args []string) {
+	setSharedFlags(cmd)
+	email := cmd.Flag.String("email", "", "")
+	cmd.Flag.Parse(args)
+	statName := "user>>>" + *email + ">>>online"
+	conn, ctx, close := dialAPIServer()
+	defer close()
+
+	client := statsService.NewStatsServiceClient(conn)
+	r := &statsService.GetStatsRequest{
+		Name:   statName,
+		Reset_: false,
+	}
+	resp, err := client.GetStatsOnline(ctx, r)
+	if err != nil {
+		base.Fatalf("failed to get stats: %s", err)
+	}
+	showJSONResponse(resp)
+}