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

@@ -23,6 +23,7 @@ import (
"ragflow/internal/dao"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
@@ -1893,3 +1894,77 @@ func (h *Handler) BatchDeleteWhiteList(c *gin.Context) {
common.SuccessWithData(c, result, "Batch delete white list successfully")
}
// GetTokenStats returns API token statistics for the current user's tenant.
func (h *Handler) GetTokenStats(c *gin.Context) {
userName := c.Query("user_name")
if userName == "" {
common.ErrorWithCode(c, common.CodeBadRequest, "User name is required")
return
}
now := time.Now()
fromDate := c.DefaultQuery("from", now.AddDate(0, 0, -7).Format("2006-01-02 00:00:00"))
toDate := c.DefaultQuery("to", now.Format("2006-01-02 15:04:05"))
if len(toDate) == 10 {
toDate += " 23:59:59"
}
granularity := c.Query("granularity")
if granularity == "" {
granularity = "hour"
}
granularity = strings.ToLower(granularity)
stats, err := h.service.GetTokenStats(userName, fromDate, toDate, granularity)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, stats, "success")
}
// GetTokenUsersStats returns API token statistics summary for the current user's tenant.
func (h *Handler) GetTokenUsersStats(c *gin.Context) {
now := time.Now()
fromDate := c.DefaultQuery("from", now.AddDate(0, 0, -7).Format("2006-01-02 00:00:00"))
toDate := c.DefaultQuery("to", now.Format("2006-01-02 15:04:05"))
if len(toDate) == 10 {
toDate += " 23:59:59"
}
topStr := c.Query("top")
top, err := strconv.Atoi(topStr)
if err != nil {
common.ErrorWithCode(c, common.CodeBadRequest, "Invalid top")
return
}
stats, err := h.service.GetTokenUsersStats(fromDate, toDate, top)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, stats, "success")
}
// GetTokenStatsSummary returns API token statistics summary for the current user's tenant.
func (h *Handler) GetTokenStatsSummary(c *gin.Context) {
now := time.Now()
fromDate := c.DefaultQuery("from", now.AddDate(0, 0, -7).Format("2006-01-02 00:00:00"))
toDate := c.DefaultQuery("to", now.Format("2006-01-02 15:04:05"))
if len(toDate) == 10 {
toDate += " 23:59:59"
}
stats, err := h.service.GetTokenStatsSummary(fromDate, toDate)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, stats, "success")
}

View File

@@ -238,6 +238,11 @@ func (r *Router) Setup(engine *gin.Engine) {
protected.GET("/system/license", r.handler.ShowSystemLicense)
protected.PUT("/system/license/config", r.handler.UpdateSystemLicenseConfig)
// Token statistics
protected.GET("/stats/token", r.handler.GetTokenStats)
protected.GET("/stats/token/users", r.handler.GetTokenUsersStats)
protected.GET("/stats/token/summary", r.handler.GetTokenStatsSummary)
// Fingerprint
protected.GET("/fingerprint", r.handler.GetFingerprint)
// License

View File

@@ -1287,3 +1287,44 @@ func (s *Service) BatchDeleteWhiteList(ids []int) (map[string]interface{}, error
}
return result, nil
}
// GetTokenStats returns API token statistics for the user.
func (s *Service) GetTokenStats(userName, fromDate, toDate, granularity string) ([]map[string]interface{}, error) {
result := []map[string]interface{}{
{
"command": "get_token_stats",
"user_name": userName,
"from_date": fromDate,
"to_date": toDate,
"granularity": granularity,
"error": "'Get API token stats' is not supported",
},
}
return result, nil
}
// GetTokenUsersStats returns API token statistics for all users.
func (s *Service) GetTokenUsersStats(fromDate, toDate string, top int) ([]map[string]interface{}, error) {
result := []map[string]interface{}{
{
"command": "get_token_users_stats",
"from_date": fromDate,
"to_date": toDate,
"top": top,
"error": "'Get API token users stats' is not supported",
},
}
return result, nil
}
// GetTokenStatsSummary returns API token statistics summary for all users.
func (s *Service) GetTokenStatsSummary(fromDate, toDate string) (map[string]interface{}, error) {
result := map[string]interface{}{
"command": "get_token_stats_summary",
"from_date": fromDate,
"to_date": toDate,
"error": "'Get API token stats summary' is not supported",
}
return result, nil
}

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