Go CLI: add admin stats commands (#17090)

### Summary

```
RAGFlow(admin)> STATS USERS TOP 5 FROM '2026-01-01' TO '2026-02-01';
+-----------------------+----------------------------------------------+------------+---------------------+-----+
| command               | error                                        | from_date  | to_date             | top |
+-----------------------+----------------------------------------------+------------+---------------------+-----+
| get_token_users_stats | 'Get API token users stats' is not supported | 2026-01-01 | 2026-02-01 23:59:59 | 5   |
+-----------------------+----------------------------------------------+------------+---------------------+-----+
RAGFlow(admin)> STATS USER 'aaa@aaa.com' FROM '2026-01-01' TO '2026-02-01' MONTH;
+-----------------+----------------------------------------+------------+-------------+---------------------+-------------+
| command         | error                                  | from_date  | granularity | to_date             | user_name   |
+-----------------+----------------------------------------+------------+-------------+---------------------+-------------+
| get_token_stats | 'Get API token stats' is not supported | 2026-01-01 | month       | 2026-02-01 23:59:59 | aaa@aaa.com |
+-----------------+----------------------------------------+------------+-------------+---------------------+-------------+
RAGFlow(admin)> STATS SUMMARY FROM '2026-01-01' TO '2026-02-01' MONTH;
+-----------+------------------------------------------------+
| field     | value                                          |
+-----------+------------------------------------------------+
| to_date   | 2026-02-01 23:59:59                            |
| command   | get_token_stats_summary                        |
| error     | 'Get API token stats summary' is not supported |
| from_date | 2026-01-01                                     |
+-----------+------------------------------------------------+
```

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-07-20 14:24:28 +08:00
committed by GitHub
parent 27b6cd1a65
commit 7ad27adecb
9 changed files with 383 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ package cli
import (
"encoding/json"
"fmt"
"net/url"
"ragflow/internal/common"
)
@@ -1733,6 +1734,102 @@ func (c *CLI) AdminShowUsersPlanQuotaCommand(cmd *Command) (ResponseIf, error) {
return HandleCommonDataResponse(resp, "get users plan quota")
}
// AdminStatsUserCommand stats user token usage
func (c *CLI) AdminStatsUserCommand(cmd *Command) (ResponseIf, error) {
if c.Config.CLIMode != AdminMode || c.AdminServerClient.LoginToken == nil {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode or already login")
}
userName, ok := cmd.Params["user_name"].(string)
if !ok {
return nil, fmt.Errorf("user name not provided")
}
fromTime, _ := cmd.Params["from"].(string)
toTime, _ := cmd.Params["to"].(string)
granularity, ok := cmd.Params["granularity"].(string)
if !ok {
return nil, fmt.Errorf("granularity not provided")
}
q := url.Values{}
q.Set("user_name", userName)
if fromTime != "" {
q.Set("from", fromTime)
}
if toTime != "" {
q.Set("to", toTime)
}
q.Set("granularity", granularity)
baseUrl := fmt.Sprintf("/admin/stats/token?%s", q.Encode())
resp, err := c.AdminServerClient.Request("GET", baseUrl, "admin", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get stats user: %w", err)
}
return HandleCommonResponse(resp, "get stats user")
}
// AdminStatsUsersCommand stats users token usage
func (c *CLI) AdminStatsUsersCommand(cmd *Command) (ResponseIf, error) {
if c.Config.CLIMode != AdminMode || c.AdminServerClient.LoginToken == nil {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode or already login")
}
top, ok := cmd.Params["top"].(int)
if !ok {
return nil, fmt.Errorf("top not provided")
}
fromTime, _ := cmd.Params["from"].(string)
toTime, _ := cmd.Params["to"].(string)
q := url.Values{}
q.Set("top", fmt.Sprintf("%d", top))
if fromTime != "" {
q.Set("from", fromTime)
}
if toTime != "" {
q.Set("to", toTime)
}
url := fmt.Sprintf("/admin/stats/token/users?%s", q.Encode())
resp, err := c.AdminServerClient.Request("GET", url, "web", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get stats: %w", err)
}
return HandleCommonResponse(resp, "get stats")
}
// AdminStatsSummaryCommand stats summary token usage
func (c *CLI) AdminStatsSummaryCommand(cmd *Command) (ResponseIf, error) {
if c.Config.CLIMode != AdminMode || c.AdminServerClient.LoginToken == nil {
return nil, fmt.Errorf("this command is only allowed in ADMIN mode or already login")
}
fromTime, _ := cmd.Params["from"].(string)
toTime, _ := cmd.Params["to"].(string)
q := url.Values{}
if fromTime != "" {
q.Set("from", fromTime)
}
if toTime != "" {
q.Set("to", toTime)
}
baseUrl := fmt.Sprintf("/admin/stats/token/summary?%s", q.Encode())
resp, err := c.AdminServerClient.Request("GET", baseUrl, "admin", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get stats summary: %w", err)
}
return HandleCommonDataResponse(resp, "get stats summary")
}
// ListUsers lists all users (admin mode only)
// Returns (result_map, error) - result_map is non-nil for benchmark mode
func (c *CLI) AdminListUsersCommand(cmd *Command) (ResponseIf, error) {

View File

@@ -949,6 +949,151 @@ func (p *Parser) parseCommonShowPoolModel() (*Command, error) {
// endregion SHOW commands
// region STATS commands
// STATS USER 'user_name' FROM <start_time> TO <end_time> HOUR, DAY, MONTH
// STATS USER 'user_name' FROM <start_time> TO <end_time>
// STATS USERS TOP <n> FROM <start_time> TO <end_time>
// STATS SUMMARY FROM <start_time> TO <end_time>
func (p *Parser) parseAdminStatsCommands() (*Command, error) {
p.nextToken() // consume STATS
switch p.curToken.Type {
case TokenUser:
return p.parseAdminStatsUser()
case TokenUsers:
return p.parseAdminStatsUsers()
case TokenSummary:
return p.parseAdminStatsSummary()
default:
return nil, fmt.Errorf("expected USER or MODEL or SUMMARY after STATS")
}
}
// STATS USER 'user_name' FROM <start_time> TO <end_time> HOUR, DAY, MONTH
// STATS USER 'user_name' FROM <start_time> TO <end_time>
func (p *Parser) parseAdminStatsUser() (*Command, error) {
p.nextToken() // consume USER
cmd := NewCommand("admin_stats_user")
userName, err := p.parseQuotedString()
if err != nil {
return nil, err
}
cmd.Params["user_name"] = userName
p.nextToken()
// Parse time range
if p.curToken.Type == TokenFrom {
p.nextToken()
cmd.Params["from"], err = p.parseQuotedString()
if err != nil {
return nil, err
}
p.nextToken()
}
if p.curToken.Type == TokenTo {
p.nextToken()
cmd.Params["to"], err = p.parseQuotedString()
if err != nil {
return nil, err
}
p.nextToken()
}
switch p.curToken.Type {
case TokenHour:
p.nextToken()
cmd.Params["granularity"] = "HOUR"
case TokenDay:
p.nextToken()
cmd.Params["granularity"] = "DAY"
case TokenMonth:
p.nextToken()
cmd.Params["granularity"] = "MONTH"
default:
p.nextToken()
cmd.Params["granularity"] = "HOUR"
}
// Semicolon is optional
if p.curToken.Type == TokenSemicolon {
p.nextToken()
}
return cmd, nil
}
// STATS USERS TOP <n> FROM <start_time> TO <end_time>
func (p *Parser) parseAdminStatsUsers() (*Command, error) {
p.nextToken() // consume USERS
cmd := NewCommand("admin_stats_users")
cmd.Params["top"] = 10
var err error
if p.curToken.Type == TokenTop {
p.nextToken()
cmd.Params["top"], err = p.parseNumber()
if err != nil {
return nil, err
}
p.nextToken()
}
if p.curToken.Type == TokenFrom {
p.nextToken()
cmd.Params["from"], err = p.parseQuotedString()
if err != nil {
return nil, err
}
p.nextToken()
}
if p.curToken.Type == TokenTo {
p.nextToken()
cmd.Params["to"], err = p.parseQuotedString()
if err != nil {
return nil, err
}
p.nextToken()
}
if p.curToken.Type == TokenSemicolon {
p.nextToken()
}
return cmd, nil
}
// STATS SUMMARY FROM <start_time> TO <end_time>
func (p *Parser) parseAdminStatsSummary() (*Command, error) {
p.nextToken() // consume SUMMARY
cmd := NewCommand("admin_stats_summary")
var err error
if p.curToken.Type == TokenFrom {
p.nextToken()
cmd.Params["from"], err = p.parseQuotedString()
if err != nil {
return nil, err
}
p.nextToken()
}
if p.curToken.Type == TokenTo {
p.nextToken()
cmd.Params["to"], err = p.parseQuotedString()
if err != nil {
return nil, err
}
p.nextToken()
}
if p.curToken.Type == TokenSemicolon {
p.nextToken()
}
return cmd, nil
}
// endregion STATS commands
// CHECK LICENSE
// CHECK PROVIDER 'provider_name' REGION 'region_name' KEY 'api_key' [URL 'base_url'];
// CHECK PROVIDER 'provider_name' INSTANCE 'instance_name';

View File

@@ -207,6 +207,12 @@ func (c *CLI) ExecuteAdminCommand(cmd *Command) (ResponseIf, error) {
return c.AdminShowUsersPlanSummaryCommand(cmd)
case "admin_show_users_plan_quota":
return c.AdminShowUsersPlanQuotaCommand(cmd)
case "admin_stats_user":
return c.AdminStatsUserCommand(cmd)
case "admin_stats_users":
return c.AdminStatsUsersCommand(cmd)
case "admin_stats_summary":
return c.AdminStatsSummaryCommand(cmd)
case "admin_list_users_command":
return c.AdminListUsersCommand(cmd)
case "admin_list_users_condition_command":

View File

@@ -513,6 +513,8 @@ func (l *Lexer) lookupIdent(ident string) Token {
return Token{Type: TokenAnalyze, Value: ident}
case "SUMMARY":
return Token{Type: TokenSummary, Value: ident}
case "STATS":
return Token{Type: TokenStats, Value: ident}
case "STORAGE":
return Token{Type: TokenStorage, Value: ident}
case "QUOTA":
@@ -523,6 +525,12 @@ func (l *Lexer) lookupIdent(ident string) Token {
return Token{Type: TokenOrphan, Value: ident}
case "DAYS":
return Token{Type: TokenDays, Value: ident}
case "HOUR":
return Token{Type: TokenHour, Value: ident}
case "DAY":
return Token{Type: TokenDay, Value: ident}
case "MONTH":
return Token{Type: TokenMonth, Value: ident}
case "WINDOW":
return Token{Type: TokenWindow, Value: ident}
case "ACTIVITY":

View File

@@ -94,6 +94,8 @@ func (p *Parser) parseAdminCommand() (*Command, error) {
return p.parseAdminListCommands()
case TokenShow:
return p.parseAdminShowCommands()
case TokenStats:
return p.parseAdminStatsCommands()
case TokenCheck:
return p.parseAdminCheck()
case TokenCreate:

View File

@@ -187,10 +187,14 @@ const (
TokenNoACK
TokenAnalyze
TokenSummary
TokenStats
TokenStorage
TokenQuota
TokenTree
TokenOrphan
TokenHour
TokenDay
TokenMonth
TokenDays
TokenWindow
TokenActivity