mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-23 00:46:42 +08:00
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:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -187,10 +187,14 @@ const (
|
||||
TokenNoACK
|
||||
TokenAnalyze
|
||||
TokenSummary
|
||||
TokenStats
|
||||
TokenStorage
|
||||
TokenQuota
|
||||
TokenTree
|
||||
TokenOrphan
|
||||
TokenHour
|
||||
TokenDay
|
||||
TokenMonth
|
||||
TokenDays
|
||||
TokenWindow
|
||||
TokenActivity
|
||||
|
||||
Reference in New Issue
Block a user